fix(qbzd): enforce quality cap for QConnect

cx
This commit is contained in:
vicrodh committed 2026-08-27 19:37:00 -06:00
1 parent 251f153a6a
commit 5bae442864
3 files changed
+61 -3

No files matched your search

+1
View File
@@ -234,6 +234,7 @@ pub async fn run(roots: ProfileRoots, cfg: QbzdConfig, warns: Vec<String>) -> Re
booted.runtime.clone(),
booted.shared.clone(),
&roots,
quality_cell.clone(),
report_notify,
booted.bus.subscribe(),
);
+49 -2
View File
@@ -84,13 +84,22 @@ pub struct DaemonRendererEngine {
runtime: Arc<AppRuntime<DaemonAdapter>>,
/// T10 (OD4): resolved volume policy for this session (from the KV at connect).
volume_mode: VolumeMode,
/// Live daemon `playback.quality` preference. The settings reload route
/// updates this same cell, so an active QConnect session honors changes
/// without reconnecting (#693).
quality_cap: Arc<std::sync::Mutex<Quality>>,
}
impl DaemonRendererEngine {
pub fn new(runtime: Arc<AppRuntime<DaemonAdapter>>, volume_mode: VolumeMode) -> Self {
pub fn new(
runtime: Arc<AppRuntime<DaemonAdapter>>,
volume_mode: VolumeMode,
quality_cap: Arc<std::sync::Mutex<Quality>>,
) -> Self {
Self {
runtime,
volume_mode,
quality_cap,
}
}
@@ -98,6 +107,15 @@ impl DaemonRendererEngine {
self.runtime.core()
}
fn effective_quality(&self, requested: Quality) -> Quality {
let cap = self
.quality_cap
.lock()
.map(|quality| *quality)
.unwrap_or_else(|poisoned| *poisoned.into_inner());
clamp_quality(requested, cap)
}
/// Last-resort load for tracks the raw-URL path cannot fetch (the CDN
/// header flood defeats every reqwest attempt — see
/// `remote_stream::is_header_flood_error`): the CMAF path is unaffected by
@@ -209,10 +227,18 @@ impl QconnectRendererEngine for DaemonRendererEngine {
async fn start_track_stream(
&self,
track_id: u64,
quality: Quality,
requested_quality: Quality,
duration_secs: u64,
start_position_secs: u64,
) -> Result<(), String> {
let quality = self.effective_quality(requested_quality);
if quality != requested_quality {
log::info!(
"[QConnect] playback.quality capped track {track_id}: controller requested {:?}, using {:?}",
requested_quality,
quality
);
}
let stream_url = self
.core()
.get_stream_url(track_id, quality)
@@ -280,6 +306,9 @@ impl QconnectRendererEngine for DaemonRendererEngine {
}
}
fn clamp_quality(requested: Quality, cap: Quality) -> Quality {
Quality::min_tier(requested, cap)
}
async fn download_remote_audio(url: &str) -> Result<Vec<u8>, String> {
let response = reqwest::Client::new()
@@ -349,4 +378,22 @@ mod tests {
// Whitespace around the real value is tolerated.
assert_eq!(VolumeMode::from_kv(Some(" locked ")), VolumeMode::Locked);
}
#[test]
fn qconnect_quality_never_exceeds_daemon_preference() {
let tiers = [
Quality::Mp3,
Quality::Lossless,
Quality::HiRes,
Quality::UltraHiRes,
];
for requested in tiers {
for cap in tiers {
let effective = clamp_quality(requested, cap);
assert!(effective <= requested);
assert!(effective <= cap);
assert_eq!(effective, requested.min(cap));
}
}
}
}
+11 -1
View File
@@ -147,6 +147,10 @@ pub struct DaemonQconnectService {
#[allow(dead_code)] // T11 (settings reload) re-reads the KV through this path.
settings_db: PathBuf,
custom_device_name: Arc<tokio::sync::RwLock<Option<String>>>,
/// Shared with the playback driver and settings reload route; the engine
/// reads it for every remote stream start so QConnect cannot bypass the
/// daemon's configured maximum quality (#693).
quality_cap: Arc<std::sync::Mutex<qbz_models::Quality>>,
}
impl DaemonQconnectService {
@@ -197,7 +201,11 @@ impl DaemonQconnectService {
// next connect. Unset/unknown -> Software (the OD4 default).
let volume_mode =
engine::VolumeMode::from_kv(transport::load_volume_mode_at(&self.settings_db).as_deref());
let engine = DaemonRendererEngine::new(Arc::clone(&self.runtime), volume_mode);
let engine = DaemonRendererEngine::new(
Arc::clone(&self.runtime),
volume_mode,
Arc::clone(&self.quality_cap),
);
let sink = Arc::new(DaemonEventSink::new(engine, Arc::clone(&sync_state)));
let app = Arc::new(QconnectApp::new(
Arc::clone(&transport),
@@ -466,6 +474,7 @@ pub fn start(
runtime: Runtime,
shared: SharedState,
roots: &ProfileRoots,
quality_cap: Arc<std::sync::Mutex<qbz_models::Quality>>,
report_notify: Arc<tokio::sync::Notify>,
core_events: broadcast::Receiver<CoreEvent>,
) -> QconnectHandle {
@@ -496,6 +505,7 @@ pub fn start(
shared,
settings_db,
custom_device_name: Arc::new(tokio::sync::RwLock::new(custom_name)),
quality_cap,
});
let watcher = if should_auto_connect {