diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx index aa81161c2..32ad99590 100644 --- a/app/(auth)/casting-player.tsx +++ b/app/(auth)/casting-player.tsx @@ -172,14 +172,21 @@ export default function CastingPlayerScreen() { }); // Episode/season cluster: episode list, next episode, season data, loader - const { episodes, nextEpisode, seasonData, loadEpisode } = useCastEpisodes({ - api, - user, - currentItem, - remoteMediaClient, - castDevice, - settings, - }); + const { episodes, nextEpisode, seasonData, loadEpisode, loadingEpisodeId } = + useCastEpisodes({ + api, + user, + currentItem, + remoteMediaClient, + castDevice, + settings, + }); + + // True while a `loadEpisode` is in flight and `currentItem` (derived from the + // cast customData) still describes the previous episode. Used to suppress + // episode-dependent secondary UI that would otherwise flash stale data. + const isEpisodeTransitioning = + loadingEpisodeId != null && loadingEpisodeId !== currentItem?.Id; // Expose this player to the app-wide remote-control surface while a cast // session is connected. `castingControls` is the live useCasting result. @@ -442,12 +449,15 @@ export default function CastingPlayerScreen() { onPressSettings={() => setShowSettings(true)} /> - {/* Title Area */} - + {/* Title Area — hidden during an episode change to avoid flashing + the previous episode's title/season-episode numbers. */} + {!isEpisodeTransitioning && ( + + )} {/* Scrollable content area */} - {/* Poster with buffering overlay */} + {/* Poster with buffering overlay — force the overlay during an + episode change so the loading state covers the stale poster. */} applySelection({ maxBitrate: value })} - audioTracks={availableAudioTracks} + audioTracks={isEpisodeTransitioning ? [] : availableAudioTracks} selectedAudioIndex={currentSelection?.audioStreamIndex ?? -1} onAudioChange={(index) => applySelection({ audioStreamIndex: index }) } - subtitleTracks={availableSubtitleTracks} + subtitleTracks={ + isEpisodeTransitioning ? [] : availableSubtitleTracks + } selectedSubtitleIndex={currentSelection?.subtitleStreamIndex ?? -1} onSubtitleChange={(index) => applySelection({ subtitleStreamIndex: index }) diff --git a/hooks/useCastEpisodes.ts b/hooks/useCastEpisodes.ts index 3ef7afd56..ed93a4fe8 100644 --- a/hooks/useCastEpisodes.ts +++ b/hooks/useCastEpisodes.ts @@ -20,6 +20,13 @@ interface UseCastEpisodesResult { nextEpisode: BaseItemDto | null; seasonData: BaseItemDto | null; loadEpisode: (episode: BaseItemDto) => Promise; + /** + * Id of the episode currently being loaded onto the cast device, or null + * when no load is pending. The cast `customData` (and thus `currentItem`) + * lags behind the load, so consumers use this to detect the stale window + * between a `loadEpisode` call and the cast reporting the new episode. + */ + loadingEpisodeId: string | null; } export function useCastEpisodes({ @@ -33,12 +40,15 @@ export function useCastEpisodes({ const [episodes, setEpisodes] = useState([]); const [nextEpisode, setNextEpisode] = useState(null); const [seasonData, setSeasonData] = useState(null); + // Target episode id while a load is in flight; cleared once it resolves. + const [loadingEpisodeId, setLoadingEpisodeId] = useState(null); // Load a different episode on the Chromecast const loadEpisode = useCallback( async (episode: BaseItemDto) => { if (!api || !user?.Id || !episode.Id || !remoteMediaClient) return; + setLoadingEpisodeId(episode.Id); try { const startPositionMs = (episode.UserData?.PlaybackPositionTicks ?? 0) / 10000; @@ -63,6 +73,10 @@ export function useCastEpisodes({ } } catch (error) { console.error("[Casting Player] Failed to load episode:", error); + } finally { + // Clear regardless of outcome: on success `currentItem` catches up via + // customData; on failure the stale guard must not stay stuck. + setLoadingEpisodeId(null); } }, [ @@ -143,5 +157,5 @@ export function useCastEpisodes({ api, ]); - return { episodes, nextEpisode, seasonData, loadEpisode }; + return { episodes, nextEpisode, seasonData, loadEpisode, loadingEpisodeId }; }