From ab90a1a52e40557ccd1f42191eb15b5b57cf9198 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Mon, 6 Jul 2026 21:05:24 +0200 Subject: [PATCH] fix(tv): re-negotiate the stream when changing audio track while transcoding --- app/(auth)/player/direct-player.tsx | 25 ++++++++++++++++++++++++- app/(auth)/tv-option-modal.tsx | 7 ++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 877dd163..0b5f0839 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -893,6 +893,22 @@ export default function DirectPlayerPage() { // Check if we're transcoding const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl); + // A transcoded stream only carries the audio track the server encoded + // into it — switching requires re-negotiating the stream with the new + // index (like the mobile menu's replacePlayer), not an mpv aid change. + if (isTranscoding) { + const queryParams = new URLSearchParams({ + itemId: item?.Id ?? "", + audioIndex: String(index), + subtitleIndex: String(currentSubtitleIndex), + mediaSourceId: stream?.mediaSource?.Id ?? "", + bitrateValue: bitrateValue?.toString() ?? "", + playbackPosition: msToTicks(progress.get()).toString(), + }).toString(); + router.replace(`player/direct-player?${queryParams}` as any); + return; + } + // Convert Jellyfin index to MPV track ID const mpvTrackId = getMpvAudioId( stream?.mediaSource, @@ -904,7 +920,14 @@ export default function DirectPlayerPage() { await videoRef.current?.setAudioTrack?.(mpvTrackId); } }, - [stream?.mediaSource], + [ + stream?.mediaSource, + item?.Id, + currentSubtitleIndex, + bitrateValue, + router, + progress, + ], ); // TV subtitle track change handler diff --git a/app/(auth)/tv-option-modal.tsx b/app/(auth)/tv-option-modal.tsx index 180228e3..b4c66b33 100644 --- a/app/(auth)/tv-option-modal.tsx +++ b/app/(auth)/tv-option-modal.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Animated, Easing, + InteractionManager, ScrollView, StyleSheet, TVFocusGuideView, @@ -75,9 +76,13 @@ export default function TVOptionModal() { }, [isReady]); const handleSelect = (value: any) => { - modalState?.onSelect(value); + // Close FIRST, run the callback after this modal route is dismissed: an + // onSelect that navigates (e.g. the transcode audio switch replacing the + // player) would otherwise target the MODAL route and be swallowed. + const onSelect = modalState?.onSelect; store.set(tvOptionModalAtom, null); router.back(); + InteractionManager.runAfterInteractions(() => onSelect?.(value)); }; const handleClose = useCallback(() => {