Connected Devices: added remote control
This commit is contained in:
+107
-12
@@ -523,6 +523,8 @@ pub(crate) fn transfer_active_to_this_device(state: &mut AppState, runtime: &mut
|
|||||||
request_urgent_device_sync(runtime);
|
request_urgent_device_sync(runtime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
extrapolate_control_position(state);
|
||||||
|
let previous_active_id = state.device_playback.active_device_id.clone();
|
||||||
let should_start = state.player.current.is_some() || !state.player.queue.is_empty();
|
let should_start = state.player.current.is_some() || !state.player.queue.is_empty();
|
||||||
if state.player.current.is_none() && !state.player.queue.is_empty() {
|
if state.player.current.is_none() && !state.player.queue.is_empty() {
|
||||||
state.player.current = state.player.queue.get(state.player.queue_pos).cloned();
|
state.player.current = state.player.queue.get(state.player.queue_pos).cloned();
|
||||||
@@ -539,9 +541,32 @@ pub(crate) fn transfer_active_to_this_device(state: &mut AppState, runtime: &mut
|
|||||||
push_media_update(state, runtime, true);
|
push_media_update(state, runtime, true);
|
||||||
}
|
}
|
||||||
publish_playback_snapshot(state, runtime);
|
publish_playback_snapshot(state, runtime);
|
||||||
|
record_active_handoff(state, runtime, previous_active_id);
|
||||||
request_urgent_device_sync(runtime);
|
request_urgent_device_sync(runtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_active_handoff(
|
||||||
|
state: &mut AppState,
|
||||||
|
runtime: &Runtime,
|
||||||
|
previous_active_id: Option<String>,
|
||||||
|
) {
|
||||||
|
let Some(target) = previous_active_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if target == state.device_playback.self_device_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let command = crate::devices::PlaybackCommand::ActiveChanged {
|
||||||
|
active_device_id: state.device_playback.self_device_id.clone(),
|
||||||
|
active_device_name: state.device_playback.self_device_name.clone(),
|
||||||
|
state: playback_state_from_ui(state),
|
||||||
|
};
|
||||||
|
if let Err(err) = runtime.devices.record_playback_command(&target, command) {
|
||||||
|
tracing::warn!(%err, target, "recording active handoff command failed");
|
||||||
|
state.status_message = Some(format!("device handoff failed: {err:#}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn record_control_playback_state(state: &mut AppState, runtime: &Runtime) {
|
fn record_control_playback_state(state: &mut AppState, runtime: &Runtime) {
|
||||||
if !state.device_playback.is_control() {
|
if !state.device_playback.is_control() {
|
||||||
return;
|
return;
|
||||||
@@ -2050,6 +2075,30 @@ fn handle_playback_command(
|
|||||||
push_media_update(state, runtime, true);
|
push_media_update(state, runtime, true);
|
||||||
publish_playback_snapshot(state, runtime);
|
publish_playback_snapshot(state, runtime);
|
||||||
}
|
}
|
||||||
|
crate::devices::PlaybackCommand::ActiveChanged {
|
||||||
|
active_device_id,
|
||||||
|
active_device_name,
|
||||||
|
state: wire,
|
||||||
|
} => {
|
||||||
|
if active_device_id == state.device_playback.self_device_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let was_active = state.device_playback.role == state::DevicePlaybackRole::Active;
|
||||||
|
let snapshot = crate::devices::PlaybackSnapshot {
|
||||||
|
device_id: active_device_id,
|
||||||
|
device_name: active_device_name,
|
||||||
|
active: true,
|
||||||
|
updated_at_ms: unix_time_ms(),
|
||||||
|
state: wire,
|
||||||
|
};
|
||||||
|
become_control_device(state, runtime, snapshot);
|
||||||
|
runtime.player_start_pending = false;
|
||||||
|
push_media_metadata(state, runtime);
|
||||||
|
push_media_update(state, runtime, true);
|
||||||
|
if was_active {
|
||||||
|
state.status_message = Some("active playback moved to another device".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2167,6 +2216,42 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let placeholder_key = state
|
||||||
|
.player
|
||||||
|
.queue
|
||||||
|
.iter()
|
||||||
|
.find(|track| track.id == placeholder_id)
|
||||||
|
.or_else(|| {
|
||||||
|
state
|
||||||
|
.player
|
||||||
|
.current
|
||||||
|
.as_ref()
|
||||||
|
.filter(|track| track.id == placeholder_id)
|
||||||
|
})
|
||||||
|
.map(track_playback_key);
|
||||||
|
let queue_pos_waiting =
|
||||||
|
state
|
||||||
|
.player
|
||||||
|
.queue
|
||||||
|
.get(state.player.queue_pos)
|
||||||
|
.is_some_and(|track| {
|
||||||
|
track.id == placeholder_id
|
||||||
|
|| placeholder_key
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|key| track_playback_key(track) == key)
|
||||||
|
});
|
||||||
|
let current_waiting = state.player.current.as_ref().is_some_and(|current| {
|
||||||
|
current.id == placeholder_id
|
||||||
|
|| placeholder_key
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|key| track_playback_key(current) == key)
|
||||||
|
});
|
||||||
|
let waiting = queue_pos_waiting || current_waiting;
|
||||||
|
let resume_position_secs = if waiting {
|
||||||
|
state.player.position_secs
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(playable) => {
|
Ok(playable) => {
|
||||||
if playable.imported {
|
if playable.imported {
|
||||||
@@ -2180,33 +2265,43 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let resolved = playable.track.clone();
|
let resolved = playable.track.clone();
|
||||||
|
let resolved_key = track_playback_key(&resolved);
|
||||||
// Swap the placeholder for the real track everywhere it
|
// Swap the placeholder for the real track everywhere it
|
||||||
// sits in the queue.
|
// sits in the queue.
|
||||||
for slot in &mut state.player.queue {
|
for slot in &mut state.player.queue {
|
||||||
if slot.id == placeholder_id {
|
if slot.id == placeholder_id
|
||||||
|
|| placeholder_key
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|key| track_playback_key(slot) == key)
|
||||||
|
{
|
||||||
*slot = resolved.clone();
|
*slot = resolved.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let waiting = state
|
|
||||||
.player
|
|
||||||
.current
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|current| current.id == placeholder_id);
|
|
||||||
if waiting {
|
if waiting {
|
||||||
// Playback was parked on this track; start it now.
|
// Playback was parked on this track; start it now.
|
||||||
let paused = state.player.paused;
|
let paused = state.player.paused;
|
||||||
start_current_audio(state, runtime, 0.0, paused);
|
if state
|
||||||
|
.player
|
||||||
|
.queue
|
||||||
|
.get(state.player.queue_pos)
|
||||||
|
.is_none_or(|track| track_playback_key(track) != resolved_key)
|
||||||
|
&& let Some(pos) = state
|
||||||
|
.player
|
||||||
|
.queue
|
||||||
|
.iter()
|
||||||
|
.position(|track| track_playback_key(track) == resolved_key)
|
||||||
|
{
|
||||||
|
state.player.queue_pos = pos;
|
||||||
|
}
|
||||||
|
state.player.current =
|
||||||
|
state.player.queue.get(state.player.queue_pos).cloned();
|
||||||
|
start_current_audio(state, runtime, resume_position_secs, paused);
|
||||||
push_media_metadata(state, runtime);
|
push_media_metadata(state, runtime);
|
||||||
push_media_update(state, runtime, true);
|
push_media_update(state, runtime, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
state.status_message = Some(format!("federation: {message}"));
|
state.status_message = Some(format!("federation: {message}"));
|
||||||
let waiting = state
|
|
||||||
.player
|
|
||||||
.current
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|current| current.id == placeholder_id);
|
|
||||||
if waiting {
|
if waiting {
|
||||||
// Skip the failed track instead of stalling the queue.
|
// Skip the failed track instead of stalling the queue.
|
||||||
if state.player.queue_pos + 1 < state.player.queue.len() {
|
if state.player.queue_pos + 1 < state.player.queue.len() {
|
||||||
|
|||||||
+8
-1
@@ -253,7 +253,14 @@ pub struct PlaybackSnapshot {
|
|||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum PlaybackCommand {
|
pub enum PlaybackCommand {
|
||||||
SetState { state: PlaybackStateWire },
|
SetState {
|
||||||
|
state: PlaybackStateWire,
|
||||||
|
},
|
||||||
|
ActiveChanged {
|
||||||
|
active_device_id: String,
|
||||||
|
active_device_name: String,
|
||||||
|
state: PlaybackStateWire,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
|||||||
Reference in New Issue
Block a user