Publish file/device quality updates to sync app state
This commit is contained in:
25
src/api.rs
25
src/api.rs
@@ -17,11 +17,17 @@ type Aes128CbcDec = cbc::Decryptor<Aes128>;
|
|||||||
pub enum TrackStream {
|
pub enum TrackStream {
|
||||||
DirectUrl {
|
DirectUrl {
|
||||||
url: String,
|
url: String,
|
||||||
|
sampling_rate_hz: Option<u32>,
|
||||||
|
bit_depth: Option<u32>,
|
||||||
|
channels: Option<u32>,
|
||||||
},
|
},
|
||||||
Segmented {
|
Segmented {
|
||||||
url_template: String,
|
url_template: String,
|
||||||
n_segments: u32,
|
n_segments: u32,
|
||||||
encryption_key_hex: Option<String>,
|
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 {
|
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) {
|
if let (Some(url_template), Some(n_segments)) = (file.url_template, file.n_segments) {
|
||||||
@@ -490,6 +501,9 @@ impl QobuzApi {
|
|||||||
url_template,
|
url_template,
|
||||||
n_segments,
|
n_segments,
|
||||||
encryption_key_hex: file.key,
|
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
|
let url = self
|
||||||
.get_track_url_legacy(access_token, track_id, format_id)
|
.get_track_url_legacy(access_token, track_id, format_id)
|
||||||
.await?;
|
.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)
|
.get_track_stream(access_token, track_id, format_id)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
TrackStream::DirectUrl { url } => Ok(url),
|
TrackStream::DirectUrl { url, .. } => Ok(url),
|
||||||
TrackStream::Segmented { .. } => Err(QobuzError::ApiError(
|
TrackStream::Segmented { .. } => Err(QobuzError::ApiError(
|
||||||
"Track uses segmented stream; use get_track_stream instead".to_string(),
|
"Track uses segmented stream; use get_track_stream instead".to_string(),
|
||||||
)),
|
)),
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -179,11 +179,14 @@ async fn main() -> Result<()> {
|
|||||||
drop(guard);
|
drop(guard);
|
||||||
let api = QobuzApi::new(&config);
|
let api = QobuzApi::new(&config);
|
||||||
match api.get_track_stream(&token, &track_id, format_id).await {
|
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 {
|
Ok(TrackStream::Segmented {
|
||||||
url_template,
|
url_template,
|
||||||
n_segments,
|
n_segments,
|
||||||
encryption_key_hex,
|
encryption_key_hex,
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
..
|
||||||
}) => {
|
}) => {
|
||||||
println!("Segmented stream template: {}", url_template);
|
println!("Segmented stream template: {}", url_template);
|
||||||
println!("Segments: {}", n_segments);
|
println!("Segments: {}", n_segments);
|
||||||
@@ -195,6 +198,12 @@ async fn main() -> Result<()> {
|
|||||||
"no"
|
"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) => {
|
Err(e) => {
|
||||||
error!("Failed: {}", e);
|
error!("Failed: {}", e);
|
||||||
|
|||||||
130
src/qconnect.rs
130
src/qconnect.rs
@@ -392,6 +392,30 @@ fn msg_max_audio_quality_changed(quality: u64) -> Vec<u8> {
|
|||||||
build_qconnect_message(28, &payload)
|
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.
|
/// RNDR_SRVR_VOLUME_MUTED (29): renderer confirms mute state.
|
||||||
fn msg_volume_muted(muted: bool) -> Vec<u8> {
|
fn msg_volume_muted(muted: bool) -> Vec<u8> {
|
||||||
let payload = encode_field_varint(1, if muted { 1 } else { 0 });
|
let payload = encode_field_varint(1, if muted { 1 } else { 0 });
|
||||||
@@ -1045,25 +1069,43 @@ async fn run_connection(
|
|||||||
current_duration_ms = duration_ms;
|
current_duration_ms = duration_ms;
|
||||||
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
|
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
let player_stream = match stream {
|
let (player_stream, stream_sr, stream_bits, stream_ch) = match stream {
|
||||||
TrackStream::DirectUrl { url } => {
|
TrackStream::DirectUrl {
|
||||||
|
url,
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
} => {
|
||||||
info!("[STATE] Got direct stream URL (duration={}ms)", duration_ms);
|
info!("[STATE] Got direct stream URL (duration={}ms)", duration_ms);
|
||||||
StreamSource::DirectUrl(url)
|
(
|
||||||
|
StreamSource::DirectUrl(url),
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
TrackStream::Segmented {
|
TrackStream::Segmented {
|
||||||
url_template,
|
url_template,
|
||||||
n_segments,
|
n_segments,
|
||||||
encryption_key_hex,
|
encryption_key_hex,
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
} => {
|
} => {
|
||||||
info!(
|
info!(
|
||||||
"[STATE] Got segmented stream (segments={}, duration={}ms)",
|
"[STATE] Got segmented stream (segments={}, duration={}ms)",
|
||||||
n_segments, duration_ms
|
n_segments, duration_ms
|
||||||
);
|
);
|
||||||
StreamSource::Segmented {
|
(
|
||||||
url_template,
|
StreamSource::Segmented {
|
||||||
n_segments,
|
url_template,
|
||||||
encryption_key_hex,
|
n_segments,
|
||||||
}
|
encryption_key_hex,
|
||||||
|
},
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
player.send(PlayerCommand::Play {
|
player.send(PlayerCommand::Play {
|
||||||
@@ -1073,6 +1115,24 @@ async fn run_connection(
|
|||||||
duration_ms,
|
duration_ms,
|
||||||
start_position_ms: requested_pos.unwrap_or(0),
|
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
|
current_buffer_state = 2; // OK
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -1271,17 +1331,39 @@ async fn run_connection(
|
|||||||
current_duration_ms = duration_ms;
|
current_duration_ms = duration_ms;
|
||||||
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
|
match api.get_track_stream(auth_token, &track_id_str, format_id).await {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
let player_stream = match stream {
|
let (player_stream, stream_sr, stream_bits, stream_ch) = match stream {
|
||||||
TrackStream::DirectUrl { url } => StreamSource::DirectUrl(url),
|
TrackStream::DirectUrl {
|
||||||
|
url,
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
} => {
|
||||||
|
(
|
||||||
|
StreamSource::DirectUrl(url),
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
|
}
|
||||||
TrackStream::Segmented {
|
TrackStream::Segmented {
|
||||||
url_template,
|
url_template,
|
||||||
n_segments,
|
n_segments,
|
||||||
encryption_key_hex,
|
encryption_key_hex,
|
||||||
} => StreamSource::Segmented {
|
sampling_rate_hz,
|
||||||
url_template,
|
bit_depth,
|
||||||
n_segments,
|
channels,
|
||||||
encryption_key_hex,
|
} => {
|
||||||
},
|
(
|
||||||
|
StreamSource::Segmented {
|
||||||
|
url_template,
|
||||||
|
n_segments,
|
||||||
|
encryption_key_hex,
|
||||||
|
},
|
||||||
|
sampling_rate_hz,
|
||||||
|
bit_depth,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
player.send(PlayerCommand::Play {
|
player.send(PlayerCommand::Play {
|
||||||
stream: player_stream,
|
stream: player_stream,
|
||||||
@@ -1290,6 +1372,24 @@ async fn run_connection(
|
|||||||
duration_ms,
|
duration_ms,
|
||||||
start_position_ms: 0,
|
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)
|
current_buffer_state = 2; // OK(2)
|
||||||
info!("Restarted at format_id={}", format_id);
|
info!("Restarted at format_id={}", format_id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user