Connected Devices: added remote control
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ unicode-width = "0.2.2"
|
|||||||
core-foundation = "0.10.1"
|
core-foundation = "0.10.1"
|
||||||
|
|
||||||
[target."cfg(windows)".dependencies]
|
[target."cfg(windows)".dependencies]
|
||||||
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_Graphics_Gdi"] }
|
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_Pipes"] }
|
||||||
|
|
||||||
[target."cfg(unix)".dependencies]
|
[target."cfg(unix)".dependencies]
|
||||||
libc = "0.2.186"
|
libc = "0.2.186"
|
||||||
|
|||||||
+41
-6
@@ -251,7 +251,9 @@ fn sync_player_shared(state: &mut AppState, runtime: &Runtime) {
|
|||||||
} else {
|
} else {
|
||||||
runtime.player.shared.audio_analysis()
|
runtime.player.shared.audio_analysis()
|
||||||
};
|
};
|
||||||
publish_playback_snapshot(state, runtime);
|
if state.device_playback.role == state::DevicePlaybackRole::Active {
|
||||||
|
publish_playback_snapshot(state, runtime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unix_time_ms() -> i64 {
|
fn unix_time_ms() -> i64 {
|
||||||
@@ -310,6 +312,27 @@ fn playback_track_to_ui(
|
|||||||
wire.to_track_item()
|
wire.to_track_item()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn track_playback_key(track: &crate::library::models::TrackItem) -> String {
|
||||||
|
if let Some(content_id) = track
|
||||||
|
.content_id
|
||||||
|
.as_deref()
|
||||||
|
.and_then(music_dht::normalize_content_id)
|
||||||
|
.or_else(|| {
|
||||||
|
track
|
||||||
|
.fed
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|fed| fed.content_id.as_deref())
|
||||||
|
.and_then(music_dht::normalize_content_id)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return format!("content:{content_id}");
|
||||||
|
}
|
||||||
|
if let Some(fed) = &track.fed {
|
||||||
|
return format!("fed:{}:{}", fed.owner, fed.item_id);
|
||||||
|
}
|
||||||
|
format!("local:{}", track.id)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_playback_state_to_ui(
|
fn apply_playback_state_to_ui(
|
||||||
state: &mut AppState,
|
state: &mut AppState,
|
||||||
wire: &crate::devices::PlaybackStateWire,
|
wire: &crate::devices::PlaybackStateWire,
|
||||||
@@ -352,20 +375,31 @@ fn apply_playback_state_to_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn publish_playback_snapshot(state: &mut AppState, runtime: &Runtime) {
|
fn publish_playback_snapshot(state: &mut AppState, runtime: &Runtime) {
|
||||||
|
if state.device_playback.role != state::DevicePlaybackRole::Active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publish_playback_snapshot_with_active(state, runtime, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_inactive_playback_snapshot(state: &mut AppState, runtime: &Runtime) {
|
||||||
|
publish_playback_snapshot_with_active(state, runtime, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_playback_snapshot_with_active(state: &mut AppState, runtime: &Runtime, active: bool) {
|
||||||
let Ok((device_id, device_name)) = runtime.devices.identity_summary() else {
|
let Ok((device_id, device_name)) = runtime.devices.identity_summary() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
update_local_idle_since(state);
|
update_local_idle_since(state);
|
||||||
state.device_playback.self_device_id = device_id.clone();
|
state.device_playback.self_device_id = device_id.clone();
|
||||||
state.device_playback.self_device_name = device_name.clone();
|
state.device_playback.self_device_name = device_name.clone();
|
||||||
if state.device_playback.role == state::DevicePlaybackRole::Active {
|
if active {
|
||||||
state.device_playback.active_device_id = Some(device_id.clone());
|
state.device_playback.active_device_id = Some(device_id.clone());
|
||||||
state.device_playback.active_device_name = Some(device_name.clone());
|
state.device_playback.active_device_name = Some(device_name.clone());
|
||||||
}
|
}
|
||||||
let snapshot = crate::devices::PlaybackSnapshot {
|
let snapshot = crate::devices::PlaybackSnapshot {
|
||||||
device_id,
|
device_id,
|
||||||
device_name,
|
device_name,
|
||||||
active: state.device_playback.role == state::DevicePlaybackRole::Active,
|
active,
|
||||||
updated_at_ms: unix_time_ms(),
|
updated_at_ms: unix_time_ms(),
|
||||||
state: playback_state_from_ui(state),
|
state: playback_state_from_ui(state),
|
||||||
};
|
};
|
||||||
@@ -445,6 +479,7 @@ pub(crate) fn become_control_device(
|
|||||||
) {
|
) {
|
||||||
if state.device_playback.role == state::DevicePlaybackRole::Active {
|
if state.device_playback.role == state::DevicePlaybackRole::Active {
|
||||||
runtime.player.stop();
|
runtime.player.stop();
|
||||||
|
publish_inactive_playback_snapshot(state, runtime);
|
||||||
}
|
}
|
||||||
state.device_playback.role = state::DevicePlaybackRole::Control;
|
state.device_playback.role = state::DevicePlaybackRole::Control;
|
||||||
state.device_playback.active_device_id = Some(snapshot.device_id.clone());
|
state.device_playback.active_device_id = Some(snapshot.device_id.clone());
|
||||||
@@ -1978,7 +2013,7 @@ fn handle_playback_command(
|
|||||||
) {
|
) {
|
||||||
match command {
|
match command {
|
||||||
crate::devices::PlaybackCommand::SetState { state: wire } => {
|
crate::devices::PlaybackCommand::SetState { state: wire } => {
|
||||||
let old_current_id = state.player.current.as_ref().map(|track| track.id);
|
let old_current_key = state.player.current.as_ref().map(track_playback_key);
|
||||||
let old_playing = state.player.playing;
|
let old_playing = state.player.playing;
|
||||||
let old_paused = state.player.paused;
|
let old_paused = state.player.paused;
|
||||||
become_active_device(state, runtime, false);
|
become_active_device(state, runtime, false);
|
||||||
@@ -1993,8 +2028,8 @@ fn handle_playback_command(
|
|||||||
publish_playback_snapshot(state, runtime);
|
publish_playback_snapshot(state, runtime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let current_id = state.player.current.as_ref().map(|track| track.id);
|
let current_key = state.player.current.as_ref().map(track_playback_key);
|
||||||
if !old_playing || old_current_id != current_id {
|
if !old_playing || old_current_key != current_key {
|
||||||
start_current_audio(
|
start_current_audio(
|
||||||
state,
|
state,
|
||||||
runtime,
|
runtime,
|
||||||
|
|||||||
+34
-1
@@ -123,6 +123,37 @@ pub struct PlaybackTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PlaybackTrack {
|
impl PlaybackTrack {
|
||||||
|
fn portable_placeholder_id(&self) -> i64 {
|
||||||
|
let key = self
|
||||||
|
.content_id
|
||||||
|
.as_deref()
|
||||||
|
.and_then(music_dht::normalize_content_id)
|
||||||
|
.map(|content_id| format!("content:{content_id}"))
|
||||||
|
.or_else(|| {
|
||||||
|
self.fed
|
||||||
|
.as_ref()
|
||||||
|
.map(|fed| fed.content_id.as_str())
|
||||||
|
.and_then(music_dht::normalize_content_id)
|
||||||
|
.map(|content_id| format!("content:{content_id}"))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
self.fed
|
||||||
|
.as_ref()
|
||||||
|
.map(|fed| format!("fed:{}:{}", fed.owner, fed.item_id))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"remote:{}:{}:{}:{}",
|
||||||
|
self.id, self.title, self.release_title, self.duration_seconds
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let hash = blake3::hash(key.as_bytes());
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
bytes.copy_from_slice(&hash.as_bytes()[..8]);
|
||||||
|
let positive = (i64::from_be_bytes(bytes) & i64::MAX).max(1);
|
||||||
|
-positive
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_track(track: &TrackItem) -> Self {
|
pub fn from_track(track: &TrackItem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: track.id,
|
id: track.id,
|
||||||
@@ -169,7 +200,7 @@ impl PlaybackTrack {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
TrackItem {
|
TrackItem {
|
||||||
id: self.id,
|
id: self.portable_placeholder_id(),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
track_number: self.track_number,
|
track_number: self.track_number,
|
||||||
disc_number: self.disc_number,
|
disc_number: self.disc_number,
|
||||||
@@ -3453,6 +3484,8 @@ mod tests {
|
|||||||
let mut legacy_wire = wire.clone();
|
let mut legacy_wire = wire.clone();
|
||||||
legacy_wire.file_path = "/Users/me/Music/song.mp3".to_string();
|
legacy_wire.file_path = "/Users/me/Music/song.mp3".to_string();
|
||||||
let restored = legacy_wire.to_track_item();
|
let restored = legacy_wire.to_track_item();
|
||||||
|
assert!(restored.id < 0);
|
||||||
|
assert_ne!(restored.id, source.id);
|
||||||
assert!(restored.file_path.is_empty());
|
assert!(restored.file_path.is_empty());
|
||||||
assert_eq!(restored.content_id, source.content_id);
|
assert_eq!(restored.content_id, source.content_id);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -126,7 +126,42 @@ fn capture_stderr() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(windows)]
|
||||||
|
fn capture_stderr() {
|
||||||
|
use std::io::BufRead as _;
|
||||||
|
use std::os::windows::io::FromRawHandle as _;
|
||||||
|
|
||||||
|
use windows_sys::Win32::Foundation::CloseHandle;
|
||||||
|
use windows_sys::Win32::System::Console::{STD_ERROR_HANDLE, SetStdHandle};
|
||||||
|
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut read = core::ptr::null_mut();
|
||||||
|
let mut write = core::ptr::null_mut();
|
||||||
|
if CreatePipe(&mut read, &mut write, core::ptr::null(), 0) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if SetStdHandle(STD_ERROR_HANDLE, write) == 0 {
|
||||||
|
CloseHandle(read);
|
||||||
|
CloseHandle(write);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let reader = std::fs::File::from_raw_handle(read);
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("stderr".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
for line in std::io::BufReader::new(reader).lines() {
|
||||||
|
let Ok(line) = line else { break };
|
||||||
|
if !line.trim().is_empty() {
|
||||||
|
tracing::warn!(target: "stderr", "{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
fn capture_stderr() {}
|
fn capture_stderr() {}
|
||||||
|
|
||||||
/// Kitty keyboard protocol, where supported, disambiguates Esc from alt-keys
|
/// Kitty keyboard protocol, where supported, disambiguates Esc from alt-keys
|
||||||
|
|||||||
Reference in New Issue
Block a user