Add mobile chunked streaming with segmented playback fallback

This commit is contained in:
joren
2026-03-31 22:14:15 +02:00
parent bb362686b4
commit 749b0c1aaf
8 changed files with 923 additions and 22 deletions

View File

@@ -6,9 +6,9 @@ use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, error, info, warn};
use crate::api::QobuzApi;
use crate::api::{QobuzApi, TrackStream};
use crate::config::Config;
use crate::player::{AudioPlayer, PlayerCommand, PlayerState};
use crate::player::{AudioPlayer, PlayerCommand, PlayerState, StreamSource};
// ---------------------------------------------------------------------------
// Protobuf helpers (hand-rolled, matching the qconnect.proto schema)
@@ -503,8 +503,25 @@ fn parse_incoming_commands(data: &[u8]) -> Vec<QConnectCommand> {
get_varint_field(&qvf, 1).unwrap_or(0) as u32
})
.unwrap_or(0);
let current_track = get_bytes_field(&fields, 4).map(parse_queue_track);
let next_track = get_bytes_field(&fields, 5).map(parse_queue_track);
let current_track = get_bytes_field(&fields, 4)
.map(parse_queue_track)
.and_then(|t| {
if t.track_id <= 0 || t.queue_item_id < 0 {
None
} else {
Some(t)
}
});
let next_track =
get_bytes_field(&fields, 5)
.map(parse_queue_track)
.and_then(|t| {
if t.track_id <= 0 || t.queue_item_id < 0 {
None
} else {
Some(t)
}
});
info!("[RECV] SET_STATE: playing_state={:?}, position_ms={:?}, current_track={:?}, next_track={:?}, queue_ver={}",
playing_state, position_ms, current_track, next_track, queue_version_major);
@@ -1026,11 +1043,31 @@ async fn run_connection(
Err(e) => { warn!("get_track failed: {}", e); 0 }
};
current_duration_ms = duration_ms;
match api.get_track_url(auth_token, &track_id_str, format_id).await {
Ok(url) => {
info!("[STATE] Got URL, playing (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 } => {
info!("[STATE] Got direct stream URL (duration={}ms)", duration_ms);
StreamSource::DirectUrl(url)
}
TrackStream::Segmented {
url_template,
n_segments,
encryption_key_hex,
} => {
info!(
"[STATE] Got segmented stream (segments={}, duration={}ms)",
n_segments, duration_ms
);
StreamSource::Segmented {
url_template,
n_segments,
encryption_key_hex,
}
}
};
player.send(PlayerCommand::Play {
url,
stream: player_stream,
track_id: track.track_id,
queue_item_id: track.queue_item_id,
duration_ms,
@@ -1216,10 +1253,22 @@ async fn run_connection(
Err(e) => { warn!("get_track failed: {}", e); current_duration_ms }
};
current_duration_ms = duration_ms;
match api.get_track_url(auth_token, &track_id_str, format_id).await {
Ok(url) => {
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),
TrackStream::Segmented {
url_template,
n_segments,
encryption_key_hex,
} => StreamSource::Segmented {
url_template,
n_segments,
encryption_key_hex,
},
};
player.send(PlayerCommand::Play {
url,
stream: player_stream,
track_id: current_track_id,
queue_item_id: current_queue_item_id,
duration_ms,