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 };
}