Publish file/device quality updates to sync app state

This commit is contained in:
joren
2026-03-31 22:32:44 +02:00
parent bacb40af58
commit 7b882a727a
3 changed files with 147 additions and 19 deletions

View File

@@ -17,11 +17,17 @@ type Aes128CbcDec = cbc::Decryptor<Aes128>;
pub enum TrackStream {
DirectUrl {
url: String,
sampling_rate_hz: Option<u32>,
bit_depth: Option<u32>,
channels: Option<u32>,
},
Segmented {
url_template: String,
n_segments: u32,
encryption_key_hex: Option<String>,
sampling_rate_hz: Option<u32>,
bit_depth: Option<u32>,
channels: Option<u32>,
},
}
@@ -482,7 +488,12 @@ impl QobuzApi {
}
if let Some(url) = file.url {
return Ok(TrackStream::DirectUrl { url });
return Ok(TrackStream::DirectUrl {
url,
sampling_rate_hz: file.sampling_rate.map(|v| v.round() as u32),
bit_depth: file.bit_depth.map(|v| v as u32),
channels: Some(2),
});
}
if let (Some(url_template), Some(n_segments)) = (file.url_template, file.n_segments) {
@@ -490,6 +501,9 @@ impl QobuzApi {
url_template,
n_segments,
encryption_key_hex: file.key,
sampling_rate_hz: file.sampling_rate.map(|v| v.round() as u32),
bit_depth: file.bit_depth.map(|v| v as u32),
channels: Some(2),
});
}
@@ -566,7 +580,12 @@ impl QobuzApi {
let url = self
.get_track_url_legacy(access_token, track_id, format_id)
.await?;
Ok(TrackStream::DirectUrl { url })
Ok(TrackStream::DirectUrl {
url,
sampling_rate_hz: None,
bit_depth: None,
channels: None,
})
}
}
}
@@ -581,7 +600,7 @@ impl QobuzApi {
.get_track_stream(access_token, track_id, format_id)
.await?
{
TrackStream::DirectUrl { url } => Ok(url),
TrackStream::DirectUrl { url, .. } => Ok(url),
TrackStream::Segmented { .. } => Err(QobuzError::ApiError(
"Track uses segmented stream; use get_track_stream instead".to_string(),
)),

View File

@@ -179,11 +179,14 @@ async fn main() -> Result<()> {
drop(guard);
let api = QobuzApi::new(&config);
match api.get_track_stream(&token, &track_id, format_id).await {
Ok(TrackStream::DirectUrl { url }) => println!("Stream URL: {}", url),
Ok(TrackStream::DirectUrl { url, .. }) => println!("Stream URL: {}", url),
Ok(TrackStream::Segmented {
url_template,
n_segments,
encryption_key_hex,
sampling_rate_hz,
bit_depth,
..
}) => {
println!("Segmented stream template: {}", url_template);
println!("Segments: {}", n_segments);
@@ -195,6 +198,12 @@ async fn main() -> Result<()> {
"no"
}
);
if let Some(sr) = sampling_rate_hz {
println!("Sampling rate: {} Hz", sr);
}
if let Some(bits) = bit_depth {
println!("Bit depth: {}", bits);
}
}
Err(e) => {
error!("Failed: {}", e);

View File

@@ -392,6 +392,30 @@ fn msg_max_audio_quality_changed(quality: u64) -> Vec<u8> {
build_qconnect_message(28, &payload)
}
fn msg_file_audio_quality_changed(
sampling_rate_hz: u64,
bit_depth: u64,
channels: u64,
audio_quality: u64,
) -> Vec<u8> {
let mut payload = encode_field_varint(1, sampling_rate_hz);
payload.extend(encode_field_varint(2, bit_depth));
payload.extend(encode_field_varint(3, channels));
payload.extend(encode_field_varint(4, audio_quality));
build_qconnect_message(26, &payload)
}
fn msg_device_audio_quality_changed(
sampling_rate_hz: u64,
bit_depth: u64,
channels: u64,
) -> Vec<u8> {
let mut payload = encode_field_varint(1, sampling_rate_hz);
payload.extend(encode_field_varint(2, bit_depth));
payload.extend(encode_field_varint(3, channels));
build_qconnect_message(27, &payload)
}
/// RNDR_SRVR_VOLUME_MUTED (29): renderer confirms mute state.
fn msg_volume_muted(muted: bool) -> Vec<u8> {
let payload = encode_field_varint(1, if muted { 1 } else { 0 });
@@ -1045,25 +1069,43 @@ async fn run_connection(
current_duration_ms = duration_ms;
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
Ok(stream) => {
let player_stream = match stream {
TrackStream::DirectUrl { url } => {
let (player_stream, stream_sr, stream_bits, stream_ch) = match stream {
TrackStream::DirectUrl {
url,
sampling_rate_hz,
bit_depth,
channels,
} => {
info!("[STATE] Got direct stream URL (duration={}ms)", duration_ms);
StreamSource::DirectUrl(url)
(
StreamSource::DirectUrl(url),
sampling_rate_hz,
bit_depth,
channels,
)
}
TrackStream::Segmented {
url_template,
n_segments,
encryption_key_hex,
sampling_rate_hz,
bit_depth,
channels,
} => {
info!(
"[STATE] Got segmented stream (segments={}, duration={}ms)",
n_segments, duration_ms
);
(
StreamSource::Segmented {
url_template,
n_segments,
encryption_key_hex,
}
},
sampling_rate_hz,
bit_depth,
channels,
)
}
};
player.send(PlayerCommand::Play {
@@ -1073,6 +1115,24 @@ async fn run_connection(
duration_ms,
start_position_ms: requested_pos.unwrap_or(0),
});
if let (Some(sr), Some(bits)) = (stream_sr, stream_bits) {
let ch = stream_ch.unwrap_or(2).max(1);
let file_msg = msg_file_audio_quality_changed(
sr as u64,
bits as u64,
ch as u64,
max_audio_quality as u64,
);
ws_tx.send(Message::Binary(build_payload_frame(msg_id, &file_msg).into())).await?;
msg_id += 1;
let dev_msg = msg_device_audio_quality_changed(
sr as u64,
bits as u64,
ch as u64,
);
ws_tx.send(Message::Binary(build_payload_frame(msg_id, &dev_msg).into())).await?;
msg_id += 1;
}
current_buffer_state = 2; // OK
}
Err(e) => {
@@ -1271,17 +1331,39 @@ async fn run_connection(
current_duration_ms = duration_ms;
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
Ok(stream) => {
let player_stream = match stream {
TrackStream::DirectUrl { url } => StreamSource::DirectUrl(url),
let (player_stream, stream_sr, stream_bits, stream_ch) = match stream {
TrackStream::DirectUrl {
url,
sampling_rate_hz,
bit_depth,
channels,
} => {
(
StreamSource::DirectUrl(url),
sampling_rate_hz,
bit_depth,
channels,
)
}
TrackStream::Segmented {
url_template,
n_segments,
encryption_key_hex,
} => StreamSource::Segmented {
sampling_rate_hz,
bit_depth,
channels,
} => {
(
StreamSource::Segmented {
url_template,
n_segments,
encryption_key_hex,
},
sampling_rate_hz,
bit_depth,
channels,
)
}
};
player.send(PlayerCommand::Play {
stream: player_stream,
@@ -1290,6 +1372,24 @@ async fn run_connection(
duration_ms,
start_position_ms: 0,
});
if let (Some(sr), Some(bits)) = (stream_sr, stream_bits) {
let ch = stream_ch.unwrap_or(2).max(1);
let file_msg = msg_file_audio_quality_changed(
sr as u64,
bits as u64,
ch as u64,
*quality as u64,
);
ws_tx.send(Message::Binary(build_payload_frame(msg_id, &file_msg).into())).await?;
msg_id += 1;
let dev_msg = msg_device_audio_quality_changed(
sr as u64,
bits as u64,
ch as u64,
);
ws_tx.send(Message::Binary(build_payload_frame(msg_id, &dev_msg).into())).await?;
msg_id += 1;
}
current_buffer_state = 2; // OK(2)
info!("Restarted at format_id={}", format_id);
}