fix(remote playback): Move audio between remote players
This commit is contained in:
parent
6836ce79c8
commit
4634ed595c
@ -215,8 +215,16 @@ impl PlaybackModeManager {
|
|||||||
log::info!("[PlaybackMode] transfer_to_remote_inner ENTERED");
|
log::info!("[PlaybackMode] transfer_to_remote_inner ENTERED");
|
||||||
debug!("[PlaybackMode] transfer_to_remote_inner: session_id={}", session_id);
|
debug!("[PlaybackMode] transfer_to_remote_inner: session_id={}", session_id);
|
||||||
|
|
||||||
|
// If we're already controlling a remote session, that *old* session — not
|
||||||
|
// the idle local player — is the source of truth for the current track and
|
||||||
|
// position. Capture it so we can resume there and stop it afterwards.
|
||||||
|
let previous_remote_session = match self.get_mode() {
|
||||||
|
PlaybackMode::Remote { session_id: prev } if prev != session_id => Some(prev),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
// Get current player state and queue context
|
// Get current player state and queue context
|
||||||
let (queue_ids, current_index, position_seconds, queue_context) = {
|
let (queue_ids, mut current_index, mut position_seconds, queue_context) = {
|
||||||
log::info!("[PlaybackMode] Acquiring player controller lock...");
|
log::info!("[PlaybackMode] Acquiring player controller lock...");
|
||||||
debug!("[PlaybackMode] Acquiring player controller lock...");
|
debug!("[PlaybackMode] Acquiring player controller lock...");
|
||||||
let player = self.player_controller.lock().await;
|
let player = self.player_controller.lock().await;
|
||||||
@ -313,6 +321,46 @@ impl PlaybackModeManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Remote -> remote switch: take the current track and position from the
|
||||||
|
// session we're leaving, since the local player is idle and reports 0.
|
||||||
|
if let Some(ref prev_session_id) = previous_remote_session {
|
||||||
|
log::info!(
|
||||||
|
"[PlaybackMode] Remote->remote switch; reading state from previous session {}",
|
||||||
|
prev_session_id
|
||||||
|
);
|
||||||
|
match client.get_session(prev_session_id).await {
|
||||||
|
Ok(Some(session)) => {
|
||||||
|
// Resume at the previous session's position.
|
||||||
|
if let Some(ticks) = session.play_state.as_ref().and_then(|ps| ps.position_ticks) {
|
||||||
|
position_seconds = ticks as f64 / TICKS_PER_SECOND;
|
||||||
|
log::info!(
|
||||||
|
"[PlaybackMode] Using previous remote position: {:.2}s",
|
||||||
|
position_seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Resume on whichever track the previous session reached.
|
||||||
|
if let Some(now_id) = session.now_playing_item.as_ref().and_then(|i| i.id.as_deref()) {
|
||||||
|
if let Some(idx) = queue_ids.iter().position(|id| id == now_id) {
|
||||||
|
log::info!(
|
||||||
|
"[PlaybackMode] Previous session is on track {} (queue index {})",
|
||||||
|
now_id,
|
||||||
|
idx
|
||||||
|
);
|
||||||
|
current_index = idx;
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"[PlaybackMode] Previous session's track {} not found in queue; keeping index {}",
|
||||||
|
now_id,
|
||||||
|
current_index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => log::warn!("[PlaybackMode] Previous remote session not found while reading state"),
|
||||||
|
Err(e) => log::warn!("[PlaybackMode] Failed to read previous remote session: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate position in ticks (from the live position read above)
|
// Calculate position in ticks (from the live position read above)
|
||||||
let start_position_ticks = start_position_ticks_from_seconds(position_seconds);
|
let start_position_ticks = start_position_ticks_from_seconds(position_seconds);
|
||||||
|
|
||||||
@ -446,6 +494,16 @@ impl PlaybackModeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remote -> remote switch: stop the session we just left so we don't end
|
||||||
|
// up with two devices playing at once. Do this only after the new session
|
||||||
|
// is confirmed playing, so a failure here doesn't leave us with silence.
|
||||||
|
if let Some(prev_session_id) = previous_remote_session {
|
||||||
|
log::info!("[PlaybackMode] Stopping previous remote session {}", prev_session_id);
|
||||||
|
if let Err(e) = client.send_session_command(prev_session_id, "Stop").await {
|
||||||
|
log::warn!("[PlaybackMode] Failed to stop previous remote session: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop local playback (queue should remain intact for remote session)
|
// Stop local playback (queue should remain intact for remote session)
|
||||||
log::info!("[PlaybackMode] Stopping local playback - queue should NOT be cleared");
|
log::info!("[PlaybackMode] Stopping local playback - queue should NOT be cleared");
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,6 +28,7 @@ vi.mock("$lib/stores/queue", () => ({
|
|||||||
setQueue: vi.fn(),
|
setQueue: vi.fn(),
|
||||||
addToQueue: vi.fn(),
|
addToQueue: vi.fn(),
|
||||||
},
|
},
|
||||||
|
currentQueueItem: { subscribe: vi.fn((fn: any) => { fn(null); return () => {}; }) },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./DownloadButton.svelte", () => ({
|
vi.mock("./DownloadButton.svelte", () => ({
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import type { MediaItem, ItemType } from "$lib/api/types";
|
|||||||
import type { NowPlayingItem } from "$lib/api/bindings";
|
import type { NowPlayingItem } from "$lib/api/bindings";
|
||||||
import { isRemoteMode } from "./playbackMode";
|
import { isRemoteMode } from "./playbackMode";
|
||||||
import { selectedSession } from "./sessions";
|
import { selectedSession } from "./sessions";
|
||||||
|
import { currentQueueItem } from "./queue";
|
||||||
import { ticksToSeconds } from "$lib/utils/playbackUnits";
|
import { ticksToSeconds } from "$lib/utils/playbackUnits";
|
||||||
|
|
||||||
// Merged media item from backend (matches Rust MergedMediaItem)
|
// Merged media item from backend (matches Rust MergedMediaItem)
|
||||||
@ -257,21 +258,24 @@ export const mergedVolume = derived(
|
|||||||
* AND current media is audio (not video: Movie or Episode)
|
* AND current media is audio (not video: Movie or Episode)
|
||||||
*/
|
*/
|
||||||
export const shouldShowAudioMiniPlayer = derived(
|
export const shouldShowAudioMiniPlayer = derived(
|
||||||
[player, currentMedia, isRemoteMode, selectedSession],
|
[player, currentMedia, currentQueueItem, isRemoteMode, selectedSession],
|
||||||
([$player, $media, $isRemote, $session]) => {
|
([$player, $media, $queueItem, $isRemote, $session]) => {
|
||||||
// In remote mode, show if the remote session has a now-playing item
|
// In remote mode, show if the remote session has a now-playing item
|
||||||
if ($isRemote && $session?.nowPlayingItem) {
|
if ($isRemote && $session?.nowPlayingItem) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local mode: only show when actively playing or paused
|
// Local mode: hide only when there is genuinely nothing loaded
|
||||||
|
// (idle/stopped/error). Keep showing through loading/seeking transitions
|
||||||
|
// so the mini player doesn't blink out when advancing between tracks.
|
||||||
const state = $player.state;
|
const state = $player.state;
|
||||||
if (state.kind !== "playing" && state.kind !== "paused") {
|
if (state.kind === "idle" || state.kind === "error") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show for video content
|
// Determine media type from the player state, falling back to the queue
|
||||||
const mediaType = $media?.type;
|
// item (the player store can momentarily lack media during transitions).
|
||||||
|
const mediaType = $media?.type ?? $queueItem?.type;
|
||||||
if (mediaType === "Movie" || mediaType === "Episode") {
|
if (mediaType === "Movie" || mediaType === "Episode") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,10 +136,10 @@
|
|||||||
<BottomNav />
|
<BottomNav />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Mini Player - show everywhere except on full player page and login -->
|
<!-- Mini Player - show everywhere except on full player page, login and settings -->
|
||||||
<!-- Android: Show on all routes (except player/login) -->
|
<!-- Android: Show on all routes (except player/login/settings) -->
|
||||||
<!-- Desktop: Show on non-library routes (library layout has its own MiniPlayer) -->
|
<!-- Desktop: Show on non-library routes (library layout has its own MiniPlayer) -->
|
||||||
{#if !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login')}
|
{#if !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login') && !$page.url.pathname.startsWith('/settings')}
|
||||||
{#if $isAndroid || !$page.url.pathname.startsWith('/library')}
|
{#if $isAndroid || !$page.url.pathname.startsWith('/library')}
|
||||||
<MiniPlayer
|
<MiniPlayer
|
||||||
media={$currentMedia}
|
media={$currentMedia}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user