feat(qconnect): real duration/queue_item_ids in join report + DeviceAudioQualityChanged(27)

Add the RndrSrvrDeviceAudioQualityChanged(27) renderer report end to end:
the report-type variant + message string in qconnect-protocol renderer.rs,
the RendererDeviceAudioQualityChangedMessage proto struct plus the genuinely
free tag-27 field on QConnectMessage (26=file AQ, 28=max AQ), and the mapper
arm. Wire the adapter emitter report_device_audio_quality_if_changed (deduped
via a new last_reported_device_audio_quality sync-state field) next to the
file-AQ report, sourcing the real output stream config. Fix the deferred-join
StateUpdated report to resolve the real current track duration (via CoreBridge)
and current/next queue_item_ids instead of hardcoding nulls.

cc
This commit is contained in:
vicrodh committed 2026-06-01 04:29:18 -06:00
1 parent 0d5eb50889
commit 03a551e6ff
6 files changed
+155 -4

No files matched your search

+16
View File
@@ -750,6 +750,22 @@ pub async fn v2_qconnect_report_playback_state(
{
log::warn!("[QConnect] Failed to report file audio quality: {err}");
}
// P1-7: also report the actual DAC output format (DeviceAudioQualityChanged,
// tag 27). In QBZ's bit-perfect pipeline the output config matches the track
// under passthrough; the snapshot above already carries the real output
// stream sample_rate / bit_depth / channels.
if let Err(err) = service
.report_device_audio_quality_if_changed(
queue_version,
audio_quality.sampling_rate,
audio_quality.bit_depth,
audio_quality.nb_channels,
)
.await
{
log::warn!("[QConnect] Failed to report device audio quality: {err}");
}
}
if let Err(err) = app_handle.emit(
+3
View File
@@ -85,6 +85,9 @@ pub(super) struct QconnectRemoteSyncState {
pub(super) last_materialized_start_index: Option<usize>,
pub(super) last_materialized_core_shuffle_order: Option<Vec<usize>>,
pub(super) last_reported_file_audio_quality: Option<QconnectFileAudioQualitySnapshot>,
/// Last reported device (DAC output) audio quality: (sampling_rate, bit_depth, nb_channels).
/// Used to dedup outbound RndrSrvrDeviceAudioQualityChanged(27) reports.
pub(super) last_reported_device_audio_quality: Option<(i32, i32, i32)>,
pub(super) last_applied_queue_state: Option<QConnectQueueState>,
pub(super) last_remote_queue_state: Option<QConnectQueueState>,
pub(super) session_loop_mode: Option<i32>,
+81 -3
View File
@@ -379,6 +379,7 @@ impl QconnectServiceState {
deferred_renderer_join(
&app_for_loop,
&sync_state_for_loop,
&app_handle_for_status,
session_uuid,
deferred_join_reason(has_disconnected),
)
@@ -1040,6 +1041,52 @@ impl QconnectServiceState {
Ok(true)
}
/// Emit a RndrSrvrDeviceAudioQualityChanged(27) report describing the actual
/// DAC output format (sampling_rate / bit_depth / nb_channels), deduped against
/// the last reported value. Returns Ok(true) when a report was sent.
pub(super) async fn report_device_audio_quality_if_changed(
&self,
queue_version: qconnect_app::QueueVersion,
sampling_rate: i32,
bit_depth: i32,
nb_channels: i32,
) -> Result<bool, String> {
let (app, sync_state) = {
let guard = self.inner.lock().await;
let Some(runtime) = guard.runtime.as_ref() else {
return Err("QConnect service is not running".to_string());
};
(Arc::clone(&runtime.app), Arc::clone(&runtime.sync_state))
};
let key = (sampling_rate, bit_depth, nb_channels);
{
let state = sync_state.lock().await;
if state.last_reported_device_audio_quality == Some(key) {
return Ok(false);
}
}
let report = RendererReport::new(
RendererReportType::RndrSrvrDeviceAudioQualityChanged,
Uuid::new_v4().to_string(),
queue_version,
serde_json::json!({
"sampling_rate": sampling_rate,
"bit_depth": bit_depth,
"nb_channels": nb_channels
}),
);
app.send_renderer_report_command(report)
.await
.map_err(|err| format!("send device audio quality report failed: {err}"))?;
let mut state = sync_state.lock().await;
state.last_reported_device_audio_quality = Some(key);
Ok(true)
}
/// Update the renderer's internal position from the frontend's actual playback position.
/// This keeps the QConnect app's renderer state in sync with audio playback so that
/// subsequent renderer reports (e.g. after pause/resume) include the correct position.
@@ -1887,6 +1934,7 @@ async fn bootstrap_remote_presence(
async fn deferred_renderer_join(
app: &Arc<QconnectApp<NativeWsTransport, TauriQconnectEventSink>>,
sync_state: &Arc<Mutex<QconnectRemoteSyncState>>,
app_handle: &AppHandle,
session_uuid: &str,
join_reason: i32,
) {
@@ -1945,17 +1993,47 @@ async fn deferred_renderer_join(
return;
}
// 2. Send initial StateUpdated report
let state_report_payload = serde_json::json!({
// 2. Send initial StateUpdated report. At join time (e.g. reconnect
// mid-playback) we may already have a current track, so resolve the real
// duration + current/next queue_item_ids instead of hardcoding nulls.
let renderer = app.renderer_state_snapshot().await;
let queue = app.queue_state_snapshot().await;
let current_track_id = renderer.current_track.as_ref().map(|item| item.track_id);
let (current_qid, next_qid, _) = current_track_id
.map(|tid| resolve_queue_item_ids_from_queue_state(&queue, tid))
.unwrap_or((None, None, None));
let duration_secs = match current_track_id {
Some(track_id) => {
let mut resolved: u64 = 0;
if let Some(bridge_state) = app_handle.try_state::<CoreBridgeState>() {
if let Some(bridge) = bridge_state.try_get().await {
resolved = bridge
.get_track(track_id)
.await
.map(|track| u64::from(track.duration))
.unwrap_or(0);
}
}
resolved
}
None => 0,
};
let mut state_report_payload = serde_json::json!({
"playing_state": PLAYING_STATE_STOPPED,
"buffer_state": BUFFER_STATE_OK,
"current_position": 0,
"duration": 0,
"duration": duration_secs,
"queue_version": {
"major": queue_version_ref.major,
"minor": queue_version_ref.minor
}
});
if let Some(qid) = current_qid {
state_report_payload["current_queue_item_id"] = serde_json::json!(qid);
}
if let Some(qid) = next_qid {
state_report_payload["next_queue_item_id"] = serde_json::json!(qid);
}
let state_report = RendererReport::new(
RendererReportType::RndrSrvrStateUpdated,
Uuid::new_v4().to_string(),