diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx index e16111e7..0234f5a2 100644 --- a/app/(auth)/casting-player.tsx +++ b/app/(auth)/casting-player.tsx @@ -242,6 +242,7 @@ export default function CastingPlayerScreen() { const [seasonData, setSeasonData] = useState(null); // Track selection states + // null = not yet initialized (use server default), -1 = subtitles off, >= 0 = specific track const [selectedAudioTrackIndex, setSelectedAudioTrackIndex] = useState< number | null >(null); @@ -250,6 +251,34 @@ export default function CastingPlayerScreen() { >(null); const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1); + // Initialize track selection from server defaults when item data arrives + useEffect(() => { + if (!fetchedItem) return; + const source = fetchedItem.MediaSources?.[0]; + if (source) { + if (source.DefaultAudioStreamIndex != null) { + setSelectedAudioTrackIndex(source.DefaultAudioStreamIndex); + } + // Jellyfin uses -1 for "no subtitles", >= 0 for a specific track + const defaultSub = source.DefaultSubtitleStreamIndex; + setSelectedSubtitleTrackIndex(defaultSub ?? -1); + return; + } + // Fallback: scan MediaStreams for IsDefault flags + if (fetchedItem.MediaStreams) { + const defaultAudio = fetchedItem.MediaStreams.find( + (s) => s.Type === "Audio" && s.IsDefault, + ); + if (defaultAudio?.Index != null) { + setSelectedAudioTrackIndex(defaultAudio.Index); + } + const defaultSub = fetchedItem.MediaStreams.find( + (s) => s.Type === "Subtitle" && s.IsDefault, + ); + setSelectedSubtitleTrackIndex(defaultSub?.Index ?? -1); + } + }, [fetchedItem?.Id]); + // Function to reload media with new audio/subtitle/quality settings const reloadWithSettings = useCallback( async (options: { @@ -268,6 +297,19 @@ export default function CastingPlayerScreen() { // Get new stream URL with updated settings const enableH265 = settings.enableH265ForChromecast; + + // Resolve subtitle index: + // - options.subtitleIndex is explicitly provided: null = disable (-1), number = specific track + // - options.subtitleIndex is undefined: preserve current selection + let resolvedSubtitleIndex: number | undefined; + if (options.subtitleIndex === undefined) { + resolvedSubtitleIndex = selectedSubtitleTrackIndex ?? undefined; + } else if (options.subtitleIndex === null) { + resolvedSubtitleIndex = -1; + } else { + resolvedSubtitleIndex = options.subtitleIndex; + } + const data = await getStreamUrl({ api, item: currentItem, @@ -276,9 +318,7 @@ export default function CastingPlayerScreen() { userId: user.Id, audioStreamIndex: options.audioIndex ?? selectedAudioTrackIndex ?? undefined, - // null = subtitles off (omit from request), number = specific track - subtitleStreamIndex: - options.subtitleIndex === null ? undefined : options.subtitleIndex, + subtitleStreamIndex: resolvedSubtitleIndex, maxStreamingBitrate: options.bitrateValue, }); @@ -308,6 +348,7 @@ export default function CastingPlayerScreen() { mediaStatus?.streamPosition, settings.enableH265ForChromecast, selectedAudioTrackIndex, + selectedSubtitleTrackIndex, ], ); @@ -1422,15 +1463,17 @@ export default function CastingPlayerScreen() { }} subtitleTracks={availableSubtitleTracks} selectedSubtitleTrack={ - selectedSubtitleTrackIndex === null + selectedSubtitleTrackIndex == null || + selectedSubtitleTrackIndex < 0 ? null : availableSubtitleTracks.find( (t) => t.index === selectedSubtitleTrackIndex, ) || null } onSubtitleTrackChange={(track) => { - setSelectedSubtitleTrackIndex(track?.index ?? null); - // Reload stream with new subtitle track + // -1 = disabled, >= 0 = specific track + setSelectedSubtitleTrackIndex(track?.index ?? -1); + // Reload stream: null signals disable, number selects track reloadWithSettings({ subtitleIndex: track?.index ?? null }); }} playbackSpeed={currentPlaybackSpeed} diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx index 7f69637e..1948de2e 100644 --- a/components/Chromecast.tsx +++ b/components/Chromecast.tsx @@ -114,16 +114,15 @@ export function Chromecast({ // Generate a new PlaySessionId when the content changes if (contentId !== lastContentIdRef.current) { - const randomBytes = new Uint8Array(16); - crypto.getRandomValues(randomBytes); - // Format as UUID v4 - randomBytes[6] = (randomBytes[6] & 0x0f) | 0x40; // Version 4 - randomBytes[8] = (randomBytes[8] & 0x3f) | 0x80; // Variant 10 - const uuid = Array.from(randomBytes, (b, i) => { - const hex = b.toString(16).padStart(2, "0"); - return [4, 6, 8, 10].includes(i) ? `-${hex}` : hex; - }).join(""); - playSessionIdRef.current = uuid; + // Use Math.random()-based UUID v4 (React Native lacks global crypto) + playSessionIdRef.current = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ); lastContentIdRef.current = contentId; } diff --git a/utils/casting/mediaInfo.ts b/utils/casting/mediaInfo.ts index c0eae0a1..fcc83b9e 100644 --- a/utils/casting/mediaInfo.ts +++ b/utils/casting/mediaInfo.ts @@ -102,6 +102,8 @@ export const buildCastMediaInfo = ({ Bitrate: src.Bitrate, Container: src.Container, Name: src.Name, + DefaultAudioStreamIndex: src.DefaultAudioStreamIndex, + DefaultSubtitleStreamIndex: src.DefaultSubtitleStreamIndex, })), UserData: item.UserData ? { PlaybackPositionTicks: item.UserData.PlaybackPositionTicks }