fix(qconnect): align renderer state with official queue semantics
cx
This commit is contained in:
5 files changed
+277
-17
No files matched your search
@@ -1290,4 +1290,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(commands[0].payload["value"], true);
|
assert_eq!(commands[0].payload["value"], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_official_renderer_state_updated_fixed64_timestamp_frame() {
|
||||||
|
let raw_message = vec![
|
||||||
|
0x08, 0x17, 0xba, 0x01, 0x21, 0x0a, 0x1f, 0x08, 0x02, 0x10, 0x02, 0x1a, 0x0d,
|
||||||
|
0x09, 0xe1, 0x08, 0x7e, 0xe5, 0x9c, 0x01, 0x00, 0x00, 0x10, 0xa1, 0xec, 0x01,
|
||||||
|
0x20, 0xa8, 0x9e, 0x14, 0x2a, 0x04, 0x08, 0x09, 0x10, 0x02, 0x38, 0x0c,
|
||||||
|
];
|
||||||
|
|
||||||
|
let message = QConnectMessage::decode(raw_message.as_slice()).expect("decode message");
|
||||||
|
let state = message
|
||||||
|
.rndr_srvr_state_updated
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|payload| payload.state.as_ref())
|
||||||
|
.expect("renderer state");
|
||||||
|
let position = state.current_position.as_ref().expect("playback position");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
message.message_type,
|
||||||
|
Some(QConnectMessageType::MessageTypeRndrSrvrStateUpdated as i32)
|
||||||
|
);
|
||||||
|
assert_eq!(position.timestamp, Some(1_773_376_768_225));
|
||||||
|
assert_eq!(position.value, Some(30_241));
|
||||||
|
assert_eq!(state.current_queue_item_id, None);
|
||||||
|
assert_eq!(state.next_queue_item_id, Some(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -661,7 +661,7 @@ pub struct RendererMuteVolumeMessage {
|
|||||||
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct PlaybackPositionMessage {
|
pub struct PlaybackPositionMessage {
|
||||||
#[prost(uint64, optional, tag = "1")]
|
#[prost(fixed64, optional, tag = "1")]
|
||||||
pub timestamp: Option<u64>,
|
pub timestamp: Option<u64>,
|
||||||
#[prost(int32, optional, tag = "2")]
|
#[prost(int32, optional, tag = "2")]
|
||||||
pub value: Option<i32>,
|
pub value: Option<i32>,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use qconnect_transport_ws::{NativeWsTransport, WsTransportConfig};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tauri::async_runtime::JoinHandle;
|
use tauri::async_runtime::JoinHandle;
|
||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, Manager, State};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -39,8 +39,13 @@ const DEFAULT_QCONNECT_DEVICE_TYPE: i32 = 5; // computer
|
|||||||
const DEFAULT_QCONNECT_SOFTWARE_PREFIX: &str = "qbz";
|
const DEFAULT_QCONNECT_SOFTWARE_PREFIX: &str = "qbz";
|
||||||
const QCONNECT_REMOTE_QUEUE_SOURCE: &str = "qobuz_connect_remote";
|
const QCONNECT_REMOTE_QUEUE_SOURCE: &str = "qobuz_connect_remote";
|
||||||
// AudioQuality enum: 0=unknown, 1=mp3, 2=cd, 3=hires_l1, 4=hires_l2(192k), 5=hires_l3(384k)
|
// AudioQuality enum: 0=unknown, 1=mp3, 2=cd, 3=hires_l1, 4=hires_l2(192k), 5=hires_l3(384k)
|
||||||
|
const AUDIO_QUALITY_UNKNOWN: i32 = 0;
|
||||||
const AUDIO_QUALITY_MP3: i32 = 1;
|
const AUDIO_QUALITY_MP3: i32 = 1;
|
||||||
|
const AUDIO_QUALITY_CD: i32 = 2;
|
||||||
|
const AUDIO_QUALITY_HIRES_LEVEL1: i32 = 3;
|
||||||
const AUDIO_QUALITY_HIRES_LEVEL2: i32 = 4;
|
const AUDIO_QUALITY_HIRES_LEVEL2: i32 = 4;
|
||||||
|
const AUDIO_QUALITY_HIRES_LEVEL3: i32 = 5;
|
||||||
|
const DEFAULT_QCONNECT_CHANNEL_COUNT: i32 = 2;
|
||||||
// VolumeRemoteControl enum: 0=unknown, 1=not_allowed, 2=allowed
|
// VolumeRemoteControl enum: 0=unknown, 1=not_allowed, 2=allowed
|
||||||
const VOLUME_REMOTE_CONTROL_ALLOWED: i32 = 2;
|
const VOLUME_REMOTE_CONTROL_ALLOWED: i32 = 2;
|
||||||
// JoinSessionReason: 0=unknown, 1=controller_request, 2=reconnection
|
// JoinSessionReason: 0=unknown, 1=controller_request, 2=reconnection
|
||||||
@@ -304,11 +309,20 @@ struct QconnectRemoteSyncState {
|
|||||||
last_renderer_track_id: Option<u64>,
|
last_renderer_track_id: Option<u64>,
|
||||||
last_renderer_next_track_id: Option<u64>,
|
last_renderer_next_track_id: Option<u64>,
|
||||||
last_renderer_playing_state: Option<i32>,
|
last_renderer_playing_state: Option<i32>,
|
||||||
|
last_reported_file_audio_quality: Option<QconnectFileAudioQualitySnapshot>,
|
||||||
last_applied_queue_version: Option<(u64, u64)>,
|
last_applied_queue_version: Option<(u64, u64)>,
|
||||||
/// Session topology — stored from session management events (types 81-87).
|
/// Session topology — stored from session management events (types 81-87).
|
||||||
session: QconnectSessionState,
|
session: QconnectSessionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct QconnectFileAudioQualitySnapshot {
|
||||||
|
sampling_rate: i32,
|
||||||
|
bit_depth: i32,
|
||||||
|
nb_channels: i32,
|
||||||
|
audio_quality: i32,
|
||||||
|
}
|
||||||
|
|
||||||
fn queue_payload_track_preview(payload: &Value, key: &str) -> Vec<i64> {
|
fn queue_payload_track_preview(payload: &Value, key: &str) -> Vec<i64> {
|
||||||
payload
|
payload
|
||||||
.get(key)
|
.get(key)
|
||||||
@@ -819,6 +833,47 @@ impl QconnectServiceState {
|
|||||||
.map_err(|err| format!("send renderer report failed: {err}"))
|
.map_err(|err| format!("send renderer report failed: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn report_file_audio_quality_if_changed(
|
||||||
|
&self,
|
||||||
|
queue_version: qconnect_app::QueueVersion,
|
||||||
|
audio_quality: QconnectFileAudioQualitySnapshot,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let (app, sync_state) = {
|
||||||
|
let guard = self.inner.lock().await;
|
||||||
|
let Some(runtime) = guard.runtime.as_ref() else {
|
||||||
|
return Err("QConnect service is not running".to_string());
|
||||||
|
};
|
||||||
|
(Arc::clone(&runtime.app), Arc::clone(&runtime.sync_state))
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let state = sync_state.lock().await;
|
||||||
|
if state.last_reported_file_audio_quality == Some(audio_quality) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = RendererReport::new(
|
||||||
|
RendererReportType::RndrSrvrFileAudioQualityChanged,
|
||||||
|
Uuid::new_v4().to_string(),
|
||||||
|
queue_version,
|
||||||
|
serde_json::json!({
|
||||||
|
"sampling_rate": audio_quality.sampling_rate,
|
||||||
|
"bit_depth": audio_quality.bit_depth,
|
||||||
|
"nb_channels": audio_quality.nb_channels,
|
||||||
|
"audio_quality": audio_quality.audio_quality
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.send_renderer_report_command(report)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("send file audio quality report failed: {err}"))?;
|
||||||
|
|
||||||
|
let mut state = sync_state.lock().await;
|
||||||
|
state.last_reported_file_audio_quality = Some(audio_quality);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the renderer's internal position from the frontend's actual playback position.
|
/// Update the renderer's internal position from the frontend's actual playback position.
|
||||||
/// This keeps the QConnect app's renderer state in sync with audio playback so that
|
/// This keeps the QConnect app's renderer state in sync with audio playback so that
|
||||||
/// subsequent renderer reports (e.g. after pause/resume) include the correct position.
|
/// subsequent renderer reports (e.g. after pause/resume) include the correct position.
|
||||||
@@ -923,7 +978,7 @@ fn resolve_queue_item_ids_from_queue_state(
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|item| item.track_id == track_id)
|
.position(|item| item.track_id == track_id)
|
||||||
{
|
{
|
||||||
let current_item = &queue.queue_items[current_index];
|
let current_qid = normalize_current_queue_item_id_from_queue_state(queue, current_index);
|
||||||
let next_item = if queue.shuffle_mode {
|
let next_item = if queue.shuffle_mode {
|
||||||
queue
|
queue
|
||||||
.shuffle_order
|
.shuffle_order
|
||||||
@@ -945,7 +1000,7 @@ fn resolve_queue_item_ids_from_queue_state(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Some(current_item.queue_item_id),
|
Some(current_qid),
|
||||||
next_item.map(|item| item.queue_item_id),
|
next_item.map(|item| item.queue_item_id),
|
||||||
next_item.map(|item| item.track_id),
|
next_item.map(|item| item.track_id),
|
||||||
);
|
);
|
||||||
@@ -1402,6 +1457,90 @@ fn normalize_volume_to_fraction(volume: i32) -> f32 {
|
|||||||
volume.clamp(0, 100) as f32 / 100.0
|
volume.clamp(0, 100) as f32 / 100.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_cloud_placeholder_current_queue_item(
|
||||||
|
queue: &QConnectQueueState,
|
||||||
|
current_index: usize,
|
||||||
|
) -> bool {
|
||||||
|
let Some(current_item) = queue.queue_items.get(current_index) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
current_index == 0
|
||||||
|
&& current_item.queue_item_id == current_item.track_id
|
||||||
|
&& queue
|
||||||
|
.queue_items
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.any(|item| item.queue_item_id < current_item.queue_item_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_current_queue_item_id_from_queue_state(
|
||||||
|
queue: &QConnectQueueState,
|
||||||
|
current_index: usize,
|
||||||
|
) -> u64 {
|
||||||
|
if is_cloud_placeholder_current_queue_item(queue, current_index) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
queue.queue_items[current_index].queue_item_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_qconnect_audio_quality(sample_rate: u32, bit_depth: u32) -> i32 {
|
||||||
|
if sample_rate == 0 || bit_depth == 0 {
|
||||||
|
AUDIO_QUALITY_UNKNOWN
|
||||||
|
} else if sample_rate >= 384_000 {
|
||||||
|
AUDIO_QUALITY_HIRES_LEVEL3
|
||||||
|
} else if sample_rate >= 192_000 {
|
||||||
|
AUDIO_QUALITY_HIRES_LEVEL2
|
||||||
|
} else if bit_depth > 16 || sample_rate > 48_000 {
|
||||||
|
AUDIO_QUALITY_HIRES_LEVEL1
|
||||||
|
} else if sample_rate >= 44_100 {
|
||||||
|
AUDIO_QUALITY_CD
|
||||||
|
} else {
|
||||||
|
AUDIO_QUALITY_MP3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_qconnect_file_audio_quality_snapshot(
|
||||||
|
sample_rate: u32,
|
||||||
|
bit_depth: u32,
|
||||||
|
nb_channels: i32,
|
||||||
|
) -> Option<QconnectFileAudioQualitySnapshot> {
|
||||||
|
if sample_rate == 0 || bit_depth == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(QconnectFileAudioQualitySnapshot {
|
||||||
|
sampling_rate: sample_rate as i32,
|
||||||
|
bit_depth: bit_depth as i32,
|
||||||
|
nb_channels,
|
||||||
|
audio_quality: classify_qconnect_audio_quality(sample_rate, bit_depth),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_active_playback_audio_quality(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
) -> Option<QconnectFileAudioQualitySnapshot> {
|
||||||
|
if let Some(bridge_state) = app_handle.try_state::<CoreBridgeState>() {
|
||||||
|
if let Some(bridge) = bridge_state.try_get().await {
|
||||||
|
if let Some(snapshot) = build_qconnect_file_audio_quality_snapshot(
|
||||||
|
bridge.player().state.get_sample_rate(),
|
||||||
|
bridge.player().state.get_bit_depth(),
|
||||||
|
DEFAULT_QCONNECT_CHANNEL_COUNT,
|
||||||
|
) {
|
||||||
|
return Some(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_state = app_handle.try_state::<AppState>()?;
|
||||||
|
build_qconnect_file_audio_quality_snapshot(
|
||||||
|
app_state.player.state.get_sample_rate(),
|
||||||
|
app_state.player.state.get_bit_depth(),
|
||||||
|
DEFAULT_QCONNECT_CHANNEL_COUNT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn bootstrap_remote_presence(
|
async fn bootstrap_remote_presence(
|
||||||
app: &Arc<QconnectApp<NativeWsTransport, TauriQconnectEventSink>>,
|
app: &Arc<QconnectApp<NativeWsTransport, TauriQconnectEventSink>>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -2057,6 +2196,15 @@ pub async fn v2_qconnect_report_playback_state(
|
|||||||
log::warn!("[QConnect] Failed to report playback state: {err}");
|
log::warn!("[QConnect] Failed to report playback state: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(audio_quality) = resolve_active_playback_audio_quality(&app_handle).await {
|
||||||
|
if let Err(err) = service
|
||||||
|
.report_file_audio_quality_if_changed(queue_version, audio_quality)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::warn!("[QConnect] Failed to report file audio quality: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = app_handle.emit(
|
if let Err(err) = app_handle.emit(
|
||||||
"qconnect:renderer_report_debug",
|
"qconnect:renderer_report_debug",
|
||||||
&QconnectRendererReportDebugEvent {
|
&QconnectRendererReportDebugEvent {
|
||||||
@@ -2380,6 +2528,9 @@ fn decode_hex_channel(raw: &str) -> Result<Vec<u8>, String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
|
QconnectFileAudioQualitySnapshot,
|
||||||
|
AUDIO_QUALITY_HIRES_LEVEL1,
|
||||||
|
build_qconnect_file_audio_quality_snapshot, classify_qconnect_audio_quality,
|
||||||
decode_hex_channel, determine_queue_lookup_report_strategy,
|
decode_hex_channel, determine_queue_lookup_report_strategy,
|
||||||
normalize_volume_to_fraction, parse_subscribe_channels,
|
normalize_volume_to_fraction, parse_subscribe_channels,
|
||||||
resolve_queue_item_ids_from_queue_state,
|
resolve_queue_item_ids_from_queue_state,
|
||||||
@@ -2518,7 +2669,7 @@ mod tests {
|
|||||||
Some(123452387),
|
Some(123452387),
|
||||||
Some(123452387_i32),
|
Some(123452387_i32),
|
||||||
Some(1),
|
Some(1),
|
||||||
Some(123452387_i32),
|
Some(0),
|
||||||
Some(12),
|
Some(12),
|
||||||
),
|
),
|
||||||
Some("queue_lookup_queue_drift"),
|
Some("queue_lookup_queue_drift"),
|
||||||
@@ -2565,6 +2716,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_placeholder_current_queue_item_id_to_zero() {
|
||||||
|
let queue: QConnectQueueState = serde_json::from_value(json!({
|
||||||
|
"version": { "major": 8, "minor": 2 },
|
||||||
|
"queue_items": [
|
||||||
|
{ "track_context_uuid": "ctx", "track_id": 126886853, "queue_item_id": 126886853 },
|
||||||
|
{ "track_context_uuid": "ctx", "track_id": 123452387, "queue_item_id": 10 },
|
||||||
|
{ "track_context_uuid": "ctx", "track_id": 126886854, "queue_item_id": 1 }
|
||||||
|
],
|
||||||
|
"shuffle_mode": false,
|
||||||
|
"shuffle_order": null,
|
||||||
|
"autoplay_mode": false,
|
||||||
|
"autoplay_loading": false,
|
||||||
|
"autoplay_items": [],
|
||||||
|
"updated_at_ms": 0
|
||||||
|
}))
|
||||||
|
.expect("queue state");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_queue_item_ids_from_queue_state(&queue, 126886853),
|
||||||
|
(Some(0), Some(10), Some(123452387)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_next_queue_item_id_from_shuffle_order() {
|
fn resolves_next_queue_item_id_from_shuffle_order() {
|
||||||
let queue: QConnectQueueState = serde_json::from_value(json!({
|
let queue: QConnectQueueState = serde_json::from_value(json!({
|
||||||
@@ -2588,4 +2763,18 @@ mod tests {
|
|||||||
(Some(100), Some(200), Some(20)),
|
(Some(100), Some(200), Some(20)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_24_bit_streams_as_hires_level1() {
|
||||||
|
assert_eq!(classify_qconnect_audio_quality(44_100, 24), AUDIO_QUALITY_HIRES_LEVEL1);
|
||||||
|
assert_eq!(
|
||||||
|
build_qconnect_file_audio_quality_snapshot(96_000, 24, 2),
|
||||||
|
Some(QconnectFileAudioQualitySnapshot {
|
||||||
|
sampling_rate: 96_000,
|
||||||
|
bit_depth: 24,
|
||||||
|
nb_channels: 2,
|
||||||
|
audio_quality: AUDIO_QUALITY_HIRES_LEVEL1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,23 @@ describe('resolveQconnectPlayNextInsertAfter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes the placeholder first queue item id to zero for play next', () => {
|
||||||
|
const queueSnapshot = buildQueueSnapshot([126886853, 123452387, 126886854], [126886853, 10, 1]);
|
||||||
|
const rendererSnapshot: QconnectRendererSnapshot = {
|
||||||
|
current_track: { track_id: 126886853, queue_item_id: 126886853 },
|
||||||
|
next_track: { track_id: 126886854, queue_item_id: 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(resolveQconnectPlayNextInsertAfter(queueSnapshot, rendererSnapshot)).toEqual({
|
||||||
|
insertAfter: 0,
|
||||||
|
strategy: 'renderer_current_queue_item_id_verified',
|
||||||
|
queueIndex: 0,
|
||||||
|
nextQueueIndex: 2,
|
||||||
|
matchedTrackId: 126886853,
|
||||||
|
matchedQueueItemId: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('prefers the authoritative current track when renderer current is stale but still present in queue', () => {
|
it('prefers the authoritative current track when renderer current is stale but still present in queue', () => {
|
||||||
const queueSnapshot = buildQueueSnapshot([123452387, 123452388, 123452389], [123452387, 1, 2]);
|
const queueSnapshot = buildQueueSnapshot([123452387, 123452388, 123452389], [123452387, 1, 2]);
|
||||||
const rendererSnapshot: QconnectRendererSnapshot = {
|
const rendererSnapshot: QconnectRendererSnapshot = {
|
||||||
|
|||||||
@@ -108,6 +108,28 @@ function findTrackIndex(
|
|||||||
return index >= 0 ? index : null;
|
return index >= 0 ? index : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCloudPlaceholderCurrentQueueItem(
|
||||||
|
queueItems: QconnectQueueItemSnapshot[],
|
||||||
|
queueIndex: number | null
|
||||||
|
): queueIndex is number {
|
||||||
|
if (queueIndex !== 0) return false;
|
||||||
|
|
||||||
|
const currentItem = queueItems[queueIndex];
|
||||||
|
if (!currentItem) return false;
|
||||||
|
if (currentItem.queue_item_id !== currentItem.track_id) return false;
|
||||||
|
|
||||||
|
return queueItems.slice(1).some((item) => isNonNegativeNumber(item.queue_item_id) && item.queue_item_id < currentItem.queue_item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvedQueueItemId(
|
||||||
|
queueItems: QconnectQueueItemSnapshot[],
|
||||||
|
queueIndex: number
|
||||||
|
): number {
|
||||||
|
return isCloudPlaceholderCurrentQueueItem(queueItems, queueIndex)
|
||||||
|
? 0
|
||||||
|
: queueItems[queueIndex].queue_item_id;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveQconnectPlayNextInsertAfter(
|
export function resolveQconnectPlayNextInsertAfter(
|
||||||
queueSnapshot: QconnectQueueSnapshot | null | undefined,
|
queueSnapshot: QconnectQueueSnapshot | null | undefined,
|
||||||
rendererSnapshot: QconnectRendererSnapshot | null | undefined,
|
rendererSnapshot: QconnectRendererSnapshot | null | undefined,
|
||||||
@@ -139,35 +161,38 @@ export function resolveQconnectPlayNextInsertAfter(
|
|||||||
&& authoritativeCurrentTrackId !== currentTrack?.track_id;
|
&& authoritativeCurrentTrackId !== currentTrack?.track_id;
|
||||||
|
|
||||||
if (authoritativeTrackDisagreesWithRenderer && authoritativeTrackIndexBeforeNext !== null) {
|
if (authoritativeTrackDisagreesWithRenderer && authoritativeTrackIndexBeforeNext !== null) {
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, authoritativeTrackIndexBeforeNext);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[authoritativeTrackIndexBeforeNext].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'authoritative_track_id_before_renderer_next',
|
strategy: 'authoritative_track_id_before_renderer_next',
|
||||||
queueIndex: authoritativeTrackIndexBeforeNext,
|
queueIndex: authoritativeTrackIndexBeforeNext,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[authoritativeTrackIndexBeforeNext].track_id,
|
matchedTrackId: queueItems[authoritativeTrackIndexBeforeNext].track_id,
|
||||||
matchedQueueItemId: queueItems[authoritativeTrackIndexBeforeNext].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authoritativeTrackDisagreesWithRenderer && authoritativeCurrentTrackIndex !== null) {
|
if (authoritativeTrackDisagreesWithRenderer && authoritativeCurrentTrackIndex !== null) {
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, authoritativeCurrentTrackIndex);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[authoritativeCurrentTrackIndex].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'authoritative_track_id_match',
|
strategy: 'authoritative_track_id_match',
|
||||||
queueIndex: authoritativeCurrentTrackIndex,
|
queueIndex: authoritativeCurrentTrackIndex,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[authoritativeCurrentTrackIndex].track_id,
|
matchedTrackId: queueItems[authoritativeCurrentTrackIndex].track_id,
|
||||||
matchedQueueItemId: queueItems[authoritativeCurrentTrackIndex].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentQueueIndex !== null) {
|
if (currentQueueIndex !== null) {
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, currentQueueIndex);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[currentQueueIndex].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'renderer_current_queue_item_id_verified',
|
strategy: 'renderer_current_queue_item_id_verified',
|
||||||
queueIndex: currentQueueIndex,
|
queueIndex: currentQueueIndex,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[currentQueueIndex].track_id,
|
matchedTrackId: queueItems[currentQueueIndex].track_id,
|
||||||
matchedQueueItemId: queueItems[currentQueueIndex].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,37 +200,40 @@ export function resolveQconnectPlayNextInsertAfter(
|
|||||||
? findTrackIndexBefore(queueItems, currentTrack?.track_id, nextQueueIndex)
|
? findTrackIndexBefore(queueItems, currentTrack?.track_id, nextQueueIndex)
|
||||||
: null;
|
: null;
|
||||||
if (trackIndexBeforeNext !== null) {
|
if (trackIndexBeforeNext !== null) {
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, trackIndexBeforeNext);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[trackIndexBeforeNext].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'queue_track_id_before_renderer_next',
|
strategy: 'queue_track_id_before_renderer_next',
|
||||||
queueIndex: trackIndexBeforeNext,
|
queueIndex: trackIndexBeforeNext,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[trackIndexBeforeNext].track_id,
|
matchedTrackId: queueItems[trackIndexBeforeNext].track_id,
|
||||||
matchedQueueItemId: queueItems[trackIndexBeforeNext].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTrackIndex = findTrackIndex(queueItems, currentTrack?.track_id);
|
const currentTrackIndex = findTrackIndex(queueItems, currentTrack?.track_id);
|
||||||
if (currentTrackIndex !== null) {
|
if (currentTrackIndex !== null) {
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, currentTrackIndex);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[currentTrackIndex].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'queue_track_id_match',
|
strategy: 'queue_track_id_match',
|
||||||
queueIndex: currentTrackIndex,
|
queueIndex: currentTrackIndex,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[currentTrackIndex].track_id,
|
matchedTrackId: queueItems[currentTrackIndex].track_id,
|
||||||
matchedQueueItemId: queueItems[currentTrackIndex].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextQueueIndex !== null && nextQueueIndex > 0) {
|
if (nextQueueIndex !== null && nextQueueIndex > 0) {
|
||||||
const fallbackIndex = nextQueueIndex - 1;
|
const fallbackIndex = nextQueueIndex - 1;
|
||||||
|
const queueItemId = resolvedQueueItemId(queueItems, fallbackIndex);
|
||||||
return {
|
return {
|
||||||
insertAfter: queueItems[fallbackIndex].queue_item_id,
|
insertAfter: queueItemId,
|
||||||
strategy: 'queue_item_before_renderer_next',
|
strategy: 'queue_item_before_renderer_next',
|
||||||
queueIndex: fallbackIndex,
|
queueIndex: fallbackIndex,
|
||||||
nextQueueIndex,
|
nextQueueIndex,
|
||||||
matchedTrackId: queueItems[fallbackIndex].track_id,
|
matchedTrackId: queueItems[fallbackIndex].track_id,
|
||||||
matchedQueueItemId: queueItems[fallbackIndex].queue_item_id
|
matchedQueueItemId: queueItemId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user