refactor(qconnect): move session mutators to qconnect-app with injected identity (slice 2+4)
Relocate the pure session mutators (refresh_local_renderer_id, is_peer/local_renderer_active, find_unique_renderer_id, normalize_active_renderer_id, renderer_allows_remote_volume) and the QconnectFileAudioQualitySnapshot type into qconnect-app::session. refresh_local_renderer_id now takes an injected LocalIdentity (device uuid + device-info) so qconnect-app stays free of the persistence / transport crates (ADR-006). A thin Tauri wrapper resolves the identity from default_qconnect_device_info() and delegates, keeping the three event_sink call sites and the existing tests unchanged. The eager device-info build is idempotent and side-effect-free, so renderer-id resolution is byte-identical. qconnect-app + src-tauri qconnect tests green (33 / 42). cc
This commit is contained in:
3 files changed
+146
-100
No files matched your search
@@ -15,10 +15,12 @@ pub use startup::{compute_effective_startup, QconnectStartupMode};
|
|||||||
pub use error::QconnectAppError;
|
pub use error::QconnectAppError;
|
||||||
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
||||||
pub use session::{
|
pub use session::{
|
||||||
compute_connection_state, deferred_join_reason, quality_from_max_audio_quality,
|
compute_connection_state, deferred_join_reason, find_unique_renderer_id,
|
||||||
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision,
|
is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id,
|
||||||
QconnectLifecycleState, QconnectRendererInfo, QconnectSessionRendererState,
|
quality_from_max_audio_quality, refresh_local_renderer_id, renderer_allows_remote_volume,
|
||||||
QconnectSessionState, RendererStatus, ServerActiveState,
|
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision, LocalIdentity,
|
||||||
|
QconnectFileAudioQualitySnapshot, QconnectLifecycleState, QconnectRendererInfo,
|
||||||
|
QconnectSessionRendererState, QconnectSessionState, RendererStatus, ServerActiveState,
|
||||||
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
||||||
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -237,6 +237,123 @@ pub struct QconnectRendererInfo {
|
|||||||
pub volume_remote_control: Option<i32>,
|
pub volume_remote_control: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolved local-device identity injected by the frontend adapter into the
|
||||||
|
/// session layer. Lets `refresh_local_renderer_id` stay frontend-agnostic: the
|
||||||
|
/// Tauri adapter resolves it from its persisted device uuid + device-info
|
||||||
|
/// builder, the Slint adapter from the shared qbz-app identity — qconnect-app
|
||||||
|
/// never depends on the persistence/transport crates.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LocalIdentity {
|
||||||
|
pub device_uuid: String,
|
||||||
|
pub friendly_name: Option<String>,
|
||||||
|
pub brand: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub device_type: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of the last reported file (stream) audio quality, used to dedup
|
||||||
|
/// outbound RndrSrvrFileAudioQualityChanged reports. Moved here with the
|
||||||
|
/// session topology so the relocated sync accumulator is self-contained.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct QconnectFileAudioQualitySnapshot {
|
||||||
|
pub sampling_rate: i32,
|
||||||
|
pub bit_depth: i32,
|
||||||
|
pub nb_channels: i32,
|
||||||
|
pub audio_quality: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the active renderer permits remote volume control. Absent capability
|
||||||
|
/// (None) defaults to allowed; only an explicit non-ALLOWED value disables.
|
||||||
|
pub fn renderer_allows_remote_volume(info: &QconnectRendererInfo) -> bool {
|
||||||
|
matches!(info.volume_remote_control, None | Some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the local renderer id within the session by matching the injected
|
||||||
|
/// local identity: first by exact device_uuid, then by a unique device
|
||||||
|
/// fingerprint (name+brand+model+type), then a looser name+type fingerprint.
|
||||||
|
/// Identical resolution order to the prior Tauri-side fn; identity is now data.
|
||||||
|
pub fn refresh_local_renderer_id(session: &mut QconnectSessionState, identity: &LocalIdentity) {
|
||||||
|
if let Some(renderer_id) = session
|
||||||
|
.renderers
|
||||||
|
.iter()
|
||||||
|
.find(|renderer| renderer.device_uuid.as_deref() == Some(identity.device_uuid.as_str()))
|
||||||
|
.map(|renderer| renderer.renderer_id)
|
||||||
|
{
|
||||||
|
session.local_renderer_id = Some(renderer_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_friendly_name = identity.friendly_name.as_deref();
|
||||||
|
let local_brand = identity.brand.as_deref();
|
||||||
|
let local_model = identity.model.as_deref();
|
||||||
|
let local_device_type = identity.device_type;
|
||||||
|
|
||||||
|
// Some server ADD_RENDERER payloads omit device_uuid for the local renderer.
|
||||||
|
// Fall back to a unique device fingerprint so controller-side handoff logic
|
||||||
|
// can still distinguish local vs peer renderers.
|
||||||
|
if let Some(renderer_id) = find_unique_renderer_id(session, |renderer| {
|
||||||
|
renderer.friendly_name.as_deref() == local_friendly_name
|
||||||
|
&& renderer.brand.as_deref() == local_brand
|
||||||
|
&& renderer.model.as_deref() == local_model
|
||||||
|
&& renderer.device_type == local_device_type
|
||||||
|
}) {
|
||||||
|
session.local_renderer_id = Some(renderer_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(renderer_id) = find_unique_renderer_id(session, |renderer| {
|
||||||
|
renderer.friendly_name.as_deref() == local_friendly_name
|
||||||
|
&& renderer.device_type == local_device_type
|
||||||
|
}) {
|
||||||
|
session.local_renderer_id = Some(renderer_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.local_renderer_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_active_renderer_id(value: Option<i64>) -> Option<i32> {
|
||||||
|
value
|
||||||
|
.filter(|renderer_id| *renderer_id >= 0)
|
||||||
|
.and_then(|renderer_id| i32::try_from(renderer_id).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_peer_renderer_active(session: &QconnectSessionState) -> bool {
|
||||||
|
match (session.active_renderer_id, session.local_renderer_id) {
|
||||||
|
(Some(active_renderer_id), Some(local_renderer_id)) => {
|
||||||
|
active_renderer_id != local_renderer_id
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_local_renderer_active(session: &QconnectSessionState) -> bool {
|
||||||
|
match (session.active_renderer_id, session.local_renderer_id) {
|
||||||
|
(Some(active_renderer_id), Some(local_renderer_id)) => {
|
||||||
|
active_renderer_id == local_renderer_id
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_unique_renderer_id(
|
||||||
|
session: &QconnectSessionState,
|
||||||
|
predicate: impl Fn(&QconnectRendererInfo) -> bool,
|
||||||
|
) -> Option<i32> {
|
||||||
|
let mut matches = session
|
||||||
|
.renderers
|
||||||
|
.iter()
|
||||||
|
.filter(|renderer| predicate(renderer))
|
||||||
|
.map(|renderer| renderer.renderer_id);
|
||||||
|
|
||||||
|
let first = matches.next()?;
|
||||||
|
if matches.next().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(first)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use super::queue_resolution::{
|
|||||||
find_cursor_index_by_queue_item_id, normalize_current_queue_item_id_from_queue_state,
|
find_cursor_index_by_queue_item_id, normalize_current_queue_item_id_from_queue_state,
|
||||||
ordered_queue_cursors, QconnectOrderedQueueCursor,
|
ordered_queue_cursors, QconnectOrderedQueueCursor,
|
||||||
};
|
};
|
||||||
use super::transport::{default_qconnect_device_info, resolve_qconnect_device_uuid};
|
use super::transport::default_qconnect_device_info;
|
||||||
use super::{
|
use super::{
|
||||||
QconnectQueueVersionPayload, QconnectRemoteSyncState, QconnectVisibleQueueProjection,
|
QconnectQueueVersionPayload, QconnectRemoteSyncState, QconnectVisibleQueueProjection,
|
||||||
};
|
};
|
||||||
@@ -31,6 +31,14 @@ pub(super) use qconnect_app::{compute_connection_state, ServerActiveState};
|
|||||||
/// command surface keeps the same serialized shape.
|
/// command surface keeps the same serialized shape.
|
||||||
pub use qconnect_app::{QconnectRendererInfo, QconnectSessionRendererState, QconnectSessionState};
|
pub use qconnect_app::{QconnectRendererInfo, QconnectSessionRendererState, QconnectSessionState};
|
||||||
|
|
||||||
|
/// Pure session mutators + the file-audio-quality snapshot type also moved to
|
||||||
|
/// qconnect-app (slice 2+4). Re-exported so existing `super::session::…` /
|
||||||
|
/// `super::…` references compile unchanged.
|
||||||
|
pub(super) use qconnect_app::{
|
||||||
|
find_unique_renderer_id, is_local_renderer_active, is_peer_renderer_active,
|
||||||
|
normalize_active_renderer_id, renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) fn queue_item_snapshot_for_cursor(
|
pub(super) fn queue_item_snapshot_for_cursor(
|
||||||
queue: &QConnectQueueState,
|
queue: &QConnectQueueState,
|
||||||
cursor: QconnectOrderedQueueCursor,
|
cursor: QconnectOrderedQueueCursor,
|
||||||
@@ -46,14 +54,6 @@ pub(super) fn queue_item_snapshot_for_cursor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) struct QconnectFileAudioQualitySnapshot {
|
|
||||||
pub(super) sampling_rate: i32,
|
|
||||||
pub(super) bit_depth: i32,
|
|
||||||
pub(super) nb_channels: i32,
|
|
||||||
pub(super) audio_quality: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub(crate) struct QconnectRendererReportDebugEvent {
|
pub(crate) struct QconnectRendererReportDebugEvent {
|
||||||
pub(super) requested_current_queue_item_id: Option<i32>,
|
pub(super) requested_current_queue_item_id: Option<i32>,
|
||||||
@@ -71,76 +71,21 @@ pub(crate) struct QconnectRendererReportDebugEvent {
|
|||||||
pub(super) resolution_strategy: String,
|
pub(super) resolution_strategy: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the active renderer permits remote volume control. Absent capability
|
/// Thin Tauri wrapper: resolves this device's identity (uuid + device-info)
|
||||||
/// (None) defaults to allowed; only an explicit non-ALLOWED value disables.
|
/// adapter-side, then delegates to the frontend-agnostic resolver in
|
||||||
pub(super) fn renderer_allows_remote_volume(info: &QconnectRendererInfo) -> bool {
|
/// qconnect-app. qconnect-app stays identity-free. The eager device-info build
|
||||||
matches!(info.volume_remote_control, None | Some(1))
|
/// is idempotent and side-effect-free (env reads + the cached uuid), so the
|
||||||
}
|
/// resolved renderer id is identical to the prior in-place implementation.
|
||||||
|
|
||||||
pub(super) fn refresh_local_renderer_id(session: &mut QconnectSessionState) {
|
pub(super) fn refresh_local_renderer_id(session: &mut QconnectSessionState) {
|
||||||
let local_device_uuid = resolve_qconnect_device_uuid();
|
let info = default_qconnect_device_info();
|
||||||
if let Some(renderer_id) = session
|
let identity = qconnect_app::LocalIdentity {
|
||||||
.renderers
|
device_uuid: info.device_uuid.unwrap_or_default(),
|
||||||
.iter()
|
friendly_name: info.friendly_name,
|
||||||
.find(|renderer| renderer.device_uuid.as_deref() == Some(local_device_uuid.as_str()))
|
brand: info.brand,
|
||||||
.map(|renderer| renderer.renderer_id)
|
model: info.model,
|
||||||
{
|
device_type: info.device_type,
|
||||||
session.local_renderer_id = Some(renderer_id);
|
};
|
||||||
return;
|
qconnect_app::refresh_local_renderer_id(session, &identity);
|
||||||
}
|
|
||||||
|
|
||||||
let local_device_info = default_qconnect_device_info();
|
|
||||||
let local_friendly_name = local_device_info.friendly_name.as_deref();
|
|
||||||
let local_brand = local_device_info.brand.as_deref();
|
|
||||||
let local_model = local_device_info.model.as_deref();
|
|
||||||
let local_device_type = local_device_info.device_type;
|
|
||||||
|
|
||||||
// Some server ADD_RENDERER payloads omit device_uuid for the local renderer.
|
|
||||||
// Fall back to a unique device fingerprint so controller-side handoff logic
|
|
||||||
// can still distinguish local vs peer renderers.
|
|
||||||
if let Some(renderer_id) = find_unique_renderer_id(session, |renderer| {
|
|
||||||
renderer.friendly_name.as_deref() == local_friendly_name
|
|
||||||
&& renderer.brand.as_deref() == local_brand
|
|
||||||
&& renderer.model.as_deref() == local_model
|
|
||||||
&& renderer.device_type == local_device_type
|
|
||||||
}) {
|
|
||||||
session.local_renderer_id = Some(renderer_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(renderer_id) = find_unique_renderer_id(session, |renderer| {
|
|
||||||
renderer.friendly_name.as_deref() == local_friendly_name
|
|
||||||
&& renderer.device_type == local_device_type
|
|
||||||
}) {
|
|
||||||
session.local_renderer_id = Some(renderer_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
session.local_renderer_id = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn normalize_active_renderer_id(value: Option<i64>) -> Option<i32> {
|
|
||||||
value
|
|
||||||
.filter(|renderer_id| *renderer_id >= 0)
|
|
||||||
.and_then(|renderer_id| i32::try_from(renderer_id).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn is_peer_renderer_active(session: &QconnectSessionState) -> bool {
|
|
||||||
match (session.active_renderer_id, session.local_renderer_id) {
|
|
||||||
(Some(active_renderer_id), Some(local_renderer_id)) => {
|
|
||||||
active_renderer_id != local_renderer_id
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn is_local_renderer_active(session: &QconnectSessionState) -> bool {
|
|
||||||
match (session.active_renderer_id, session.local_renderer_id) {
|
|
||||||
(Some(active_renderer_id), Some(local_renderer_id)) => {
|
|
||||||
active_renderer_id == local_renderer_id
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn sync_session_renderer_active_flags(state: &mut QconnectRemoteSyncState) {
|
pub(super) fn sync_session_renderer_active_flags(state: &mut QconnectRemoteSyncState) {
|
||||||
@@ -169,24 +114,6 @@ pub(super) fn ensure_session_renderer_state(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn find_unique_renderer_id(
|
|
||||||
session: &QconnectSessionState,
|
|
||||||
predicate: impl Fn(&QconnectRendererInfo) -> bool,
|
|
||||||
) -> Option<i32> {
|
|
||||||
let mut matches = session
|
|
||||||
.renderers
|
|
||||||
.iter()
|
|
||||||
.filter(|renderer| predicate(renderer))
|
|
||||||
.map(|renderer| renderer.renderer_id);
|
|
||||||
|
|
||||||
let first = matches.next()?;
|
|
||||||
if matches.next().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(first)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn build_session_renderer_snapshot(
|
pub(super) fn build_session_renderer_snapshot(
|
||||||
queue: &QConnectQueueState,
|
queue: &QConnectQueueState,
|
||||||
renderer_state: Option<&QconnectSessionRendererState>,
|
renderer_state: Option<&QconnectSessionRendererState>,
|
||||||
|
|||||||
Reference in new issue
Block a user