diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx
index d1735bd4d..ac40646b2 100644
--- a/app/(auth)/casting-player.tsx
+++ b/app/(auth)/casting-player.tsx
@@ -37,6 +37,7 @@ import Animated, {
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BITRATES } from "@/components/BitrateSelector";
import { CastPlayerHeader } from "@/components/casting/player/CastPlayerHeader";
+import { CastPlayerPoster } from "@/components/casting/player/CastPlayerPoster";
import { CastPlayerTitle } from "@/components/casting/player/CastPlayerTitle";
import { ChromecastDeviceSheet } from "@/components/chromecast/ChromecastDeviceSheet";
import { ChromecastEpisodeList } from "@/components/chromecast/ChromecastEpisodeList";
@@ -695,130 +696,18 @@ export default function CastingPlayerScreen() {
showsVerticalScrollIndicator={false}
>
{/* Poster with buffering overlay */}
-
-
- {posterUrl ? (
-
- ) : (
-
-
-
- )}
-
- {/* Skip intro/credits bar at bottom of poster */}
- {currentSegment && (
- {
- if (!remoteMediaClient) return;
- try {
- const seekFn = async (positionMs: number) => {
- if (
- mediaStatus?.playerState ===
- MediaPlayerState.PLAYING ||
- mediaStatus?.playerState === MediaPlayerState.PAUSED
- ) {
- await remoteMediaClient.seek({
- position: positionMs / 1000,
- });
- }
- };
- if (currentSegment.type === "intro") {
- await skipIntro(seekFn);
- } else if (currentSegment.type === "credits") {
- await skipCredits(seekFn);
- } else {
- await skipSegment(seekFn);
- }
- } catch (error) {
- console.error("[Casting Player] Skip error:", error);
- }
- }}
- style={{
- position: "absolute",
- bottom: 0,
- left: 0,
- right: 0,
- backgroundColor: protocolColor,
- paddingVertical: 12,
- paddingHorizontal: 16,
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- gap: 8,
- }}
- >
-
-
- {t(
- `player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`,
- )}
-
-
- )}
-
- {/* Buffering overlay */}
- {isBuffering && (
-
-
-
- {t("casting_player.buffering")}
-
-
- )}
-
-
+
{/* Fixed 4-button control row for episodes - positioned independently */}
diff --git a/components/casting/player/CastPlayerPoster.tsx b/components/casting/player/CastPlayerPoster.tsx
new file mode 100644
index 000000000..aa5e8c3d5
--- /dev/null
+++ b/components/casting/player/CastPlayerPoster.tsx
@@ -0,0 +1,176 @@
+/**
+ * Casting Player Poster
+ * Poster image with empty-state fallback, skip intro/credits bar, and buffering overlay.
+ */
+
+import { Ionicons } from "@expo/vector-icons";
+import { Image } from "expo-image";
+import type { TFunction } from "i18next";
+import { ActivityIndicator, Pressable, View } from "react-native";
+import {
+ MediaPlayerState,
+ type MediaStatus,
+ type RemoteMediaClient,
+} from "react-native-google-cast";
+import type { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
+import { Text } from "@/components/common/Text";
+
+type ChromecastSegments = ReturnType;
+
+interface CastPlayerPosterProps {
+ /** Poster image URL, or null when unavailable. */
+ posterUrl: string | null;
+ /** Whether the cast media is currently buffering. */
+ isBuffering: boolean;
+ /** The current playback segment (intro/credits/etc.), or null. */
+ currentSegment: ChromecastSegments["currentSegment"];
+ /** Skip the intro segment. */
+ skipIntro: ChromecastSegments["skipIntro"];
+ /** Skip the credits segment. */
+ skipCredits: ChromecastSegments["skipCredits"];
+ /** Skip the current generic segment. */
+ skipSegment: ChromecastSegments["skipSegment"];
+ /** The remote media client, or null when no session. */
+ remoteMediaClient: RemoteMediaClient | null;
+ /** Raw Chromecast media status. */
+ mediaStatus: MediaStatus | null;
+ /** Theme accent color. */
+ protocolColor: string;
+ /** Translation function. */
+ t: TFunction;
+}
+
+export function CastPlayerPoster({
+ posterUrl,
+ isBuffering,
+ currentSegment,
+ skipIntro,
+ skipCredits,
+ skipSegment,
+ remoteMediaClient,
+ mediaStatus,
+ protocolColor,
+ t,
+}: CastPlayerPosterProps) {
+ return (
+
+
+ {posterUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+ {/* Skip intro/credits bar at bottom of poster */}
+ {currentSegment && (
+ {
+ if (!remoteMediaClient) return;
+ try {
+ const seekFn = async (positionMs: number) => {
+ if (
+ mediaStatus?.playerState === MediaPlayerState.PLAYING ||
+ mediaStatus?.playerState === MediaPlayerState.PAUSED
+ ) {
+ await remoteMediaClient.seek({
+ position: positionMs / 1000,
+ });
+ }
+ };
+ if (currentSegment.type === "intro") {
+ await skipIntro(seekFn);
+ } else if (currentSegment.type === "credits") {
+ await skipCredits(seekFn);
+ } else {
+ await skipSegment(seekFn);
+ }
+ } catch (error) {
+ console.error("[Casting Player] Skip error:", error);
+ }
+ }}
+ style={{
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: protocolColor,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 8,
+ }}
+ >
+
+
+ {t(
+ `player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`,
+ )}
+
+
+ )}
+
+ {/* Buffering overlay */}
+ {isBuffering && (
+
+
+
+ {t("casting_player.buffering")}
+
+
+ )}
+
+
+ );
+}