From a2d0df8b30bfbdbf76865bd5c1251ef5b857ef67 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:39:25 -0600 Subject: [PATCH] refactor(qconnect): hoist audio-quality report dedup into qconnect-app (slice 6) Step 7 of the renderer-engine extraction. Moves report_file/device_audio_ quality_if_changed (dedup-against-last-reported + build RendererReport + send) out of the Tauri service into QconnectApp methods, so the Slint report-back loop reuses them. The Tauri service methods are now thin delegates that extract runtime.app and forward. Byte-identical: runtime.sync_state and the Arc passed to QconnectApp::new (the one sync_handle() returns) are clones of the SAME Mutex (service.rs:232/242/277), so the dedup state and the report build/send are unchanged; only the owner of the code moved. send_renderer_report_command + the last_reported_* fields were already in qconnect-app. Verified: cargo check -p qbz-nix clean (no warnings); qbz-nix qconnect 42/42 + qconnect-app 38/38 green. cc --- crates/qconnect-app/src/app.rs | 87 +++++++++++++++++++++++++++++-- src-tauri/src/qconnect/service.rs | 64 +++-------------------- 2 files changed, 91 insertions(+), 60 deletions(-) diff --git a/crates/qconnect-app/src/app.rs b/crates/qconnect-app/src/app.rs index 328d633ef..02c2ebf98 100644 --- a/crates/qconnect-app/src/app.rs +++ b/crates/qconnect-app/src/app.rs @@ -5,7 +5,7 @@ use std::{ use qconnect_core::{ apply_event, apply_renderer_command, telemetry, PendingCorrelation, PendingQueueAction, - QConnectQueueState, QConnectRendererState, QueueEvent, QueueItem, RendererCommand, + QConnectQueueState, QConnectRendererState, QueueEvent, QueueItem, QueueVersion, RendererCommand, }; use qconnect_protocol::{ build_qconnect_outbound_envelope, build_qconnect_renderer_outbound_envelope, @@ -23,9 +23,10 @@ use async_trait::async_trait; use crate::session::{ compute_connection_state, deferred_join_reason, is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id, refresh_local_renderer_id, - should_arm_renderer_watchdog, should_reask_queue_state, LocalIdentity, QconnectLifecycleState, - QconnectRendererInfo, RendererStatus, ServerActiveState, PLAYING_STATE_PLAYING, - PLAYING_STATE_UNKNOWN, QCONNECT_RENDERER_LOST_TIMEOUT_MS, + should_arm_renderer_watchdog, should_reask_queue_state, LocalIdentity, + QconnectFileAudioQualitySnapshot, QconnectLifecycleState, QconnectRendererInfo, RendererStatus, + ServerActiveState, PLAYING_STATE_PLAYING, PLAYING_STATE_UNKNOWN, + QCONNECT_RENDERER_LOST_TIMEOUT_MS, }; use crate::{ ensure_session_renderer_state, sync_session_renderer_active_flags, QconnectAppError, @@ -210,6 +211,84 @@ where self.send_renderer_report(report).await } + /// Emit a RndrSrvrFileAudioQualityChanged report describing the decoded file + /// format, deduped against the last reported value. Returns Ok(true) when a + /// report was sent. Frontend-agnostic (slice 6, step 7): both the Tauri and + /// Slint report loops feed this; the snapshot is produced from the engine's + /// `current_output_format()`. + pub async fn report_file_audio_quality_if_changed( + &self, + queue_version: QueueVersion, + audio_quality: QconnectFileAudioQualitySnapshot, + ) -> Result { + let sync_state = self.sync_handle(); + { + let state = sync_state.lock().await; + if state.last_reported_file_audio_quality == Some(audio_quality) { + return Ok(false); + } + } + + let report = RendererReport::new( + RendererReportType::RndrSrvrFileAudioQualityChanged, + Uuid::new_v4().to_string(), + queue_version, + serde_json::json!({ + "sampling_rate": audio_quality.sampling_rate, + "bit_depth": audio_quality.bit_depth, + "nb_channels": audio_quality.nb_channels, + "audio_quality": audio_quality.audio_quality + }), + ); + + self.send_renderer_report_command(report) + .await + .map_err(|err| format!("send file audio quality report failed: {err}"))?; + + let mut state = sync_state.lock().await; + state.last_reported_file_audio_quality = Some(audio_quality); + 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 async fn report_device_audio_quality_if_changed( + &self, + queue_version: QueueVersion, + sampling_rate: i32, + bit_depth: i32, + nb_channels: i32, + ) -> Result { + let sync_state = self.sync_handle(); + 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 + }), + ); + + self.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 state's position from the frontend's playback position. /// This keeps the internal state in sync with the actual audio playback, so that /// renderer reports triggered by server commands (pause/resume) include the real position. diff --git a/src-tauri/src/qconnect/service.rs b/src-tauri/src/qconnect/service.rs index 53e65e73c..98a2326e5 100644 --- a/src-tauri/src/qconnect/service.rs +++ b/src-tauri/src/qconnect/service.rs @@ -595,40 +595,17 @@ impl QconnectServiceState { queue_version: qconnect_app::QueueVersion, audio_quality: QconnectFileAudioQualitySnapshot, ) -> Result { - let (app, sync_state) = { + let app = { 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)) + Arc::clone(&runtime.app) }; - - { - let state = sync_state.lock().await; - if state.last_reported_file_audio_quality == Some(audio_quality) { - return Ok(false); - } - } - - let report = RendererReport::new( - RendererReportType::RndrSrvrFileAudioQualityChanged, - Uuid::new_v4().to_string(), - queue_version, - serde_json::json!({ - "sampling_rate": audio_quality.sampling_rate, - "bit_depth": audio_quality.bit_depth, - "nb_channels": audio_quality.nb_channels, - "audio_quality": audio_quality.audio_quality - }), - ); - - app.send_renderer_report_command(report) + // Dedup + report build + send live in qconnect-app (slice 6, step 7); + // `runtime.app` shares the very sync Mutex this used to lock directly. + app.report_file_audio_quality_if_changed(queue_version, audio_quality) .await - .map_err(|err| format!("send file audio quality report failed: {err}"))?; - - let mut state = sync_state.lock().await; - state.last_reported_file_audio_quality = Some(audio_quality); - Ok(true) } /// Emit a RndrSrvrDeviceAudioQualityChanged(27) report describing the actual @@ -641,40 +618,15 @@ impl QconnectServiceState { bit_depth: i32, nb_channels: i32, ) -> Result { - let (app, sync_state) = { + let app = { 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)) + Arc::clone(&runtime.app) }; - - 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) + app.report_device_audio_quality_if_changed(queue_version, sampling_rate, bit_depth, nb_channels) .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.