/** * Przycisk Chromecasta i wysyłka materiału na telewizor. * * Dlaczego to osobny komponent, a nie kilka linijek w `PlayerScreen`: hooki biblioteki * (`useRemoteMediaClient` → `useCastSession`) **synchronicznie subskrybują natywne * zdarzenia** w `useEffect`. Na urządzeniu bez Google Play Services natywnego modułu nie * ma i taka subskrypcja wywraca cały ekran. Hooka nie da się wywołać warunkowo, więc * jedynym czystym wyjściem jest zamontowanie tego komponentu dopiero wtedy, gdy wiemy, * że Cast działa. To nie jest teoria: mamy użytkownika na LineageOS (GOON-3B) i sideload * na zegarku (GOON-51), oba bez pełnego GMS. * * Sprawdzenie dostępności siedzi w `useCastAvailable` i jest asynchroniczne, więc samo * w sobie niczego nie subskrybuje i jest bezpieczne wszędzie. */ import React from 'react'; import { Alert, Platform } from 'react-native'; import { CastButton, CastContext, useRemoteMediaClient } from 'react-native-google-cast'; import type { CastTarget } from '../lib/cast'; import { theme } from '../theme'; /** * Huawei z EMUI i uslugami Google (Honor 20, YAL-L21, Android 10): nakladka systemowa daje * polprzezroczysty kolor motywu, a MediaRouteButton przy tworzeniu liczy na nim kontrast * (androidx ColorUtils.calculateContrast) i rzuca "background can not be translucent". * Aplikacja padala przy KAZDYM otwarciu filmu (GOON-82, 2026-10-08). Bez przycisku Cast * film gra normalnie; na nowszych Huawei bez uslug Google Cast i tak nie dziala. */ function castCrashesOnThisDevice(): boolean { if (Platform.OS !== 'android') return false; const c = Platform.constants as { Manufacturer?: string; Brand?: string }; const maker = `${c.Manufacturer ?? ''} ${c.Brand ?? ''}`.toLowerCase(); return maker.includes('huawei'); } /** Czy na tym urządzeniu w ogóle da się castować. Null = jeszcze nie wiadomo. */ export function useCastAvailable(): boolean { const [available, setAvailable] = React.useState(false); React.useEffect(() => { let alive = true; if (castCrashesOnThisDevice()) return; (async () => { try { const state = await CastContext.getPlayServicesState(); // `success` to jedyny stan, w którym SDK jest gotowe. Reszta (brak usług, // wersja do aktualizacji, wyłączone) znaczy dla nas tyle samo: bez castowania. if (alive) setAvailable(state === 'success'); } catch { // Natywnego modułu nie ma albo rzucił. Zostajemy przy braku castowania. if (alive) setAvailable(false); } })(); return () => { alive = false; }; }, []); return available; } type Props = { target: CastTarget; title?: string; /** Pozycja w sekundach, od której ma zacząć telewizor. */ startTimeSec: number; /** Wołane po udanym przejęciu odtwarzania, żeby telefon przestał pobierać. */ onCastStarted: () => void; }; export function CastControl({ target, title, startTimeSec, onCastStarted }: Props) { const client = useRemoteMediaClient(); const sentRef = React.useRef(null); React.useEffect(() => { if (!client) return; // Jedno wysłanie na adres. Bez tej blokady każdy re-render ładowałby materiał od // nowa i telewizor wracałby do początku. if (sentRef.current === target.contentUrl) return; sentRef.current = target.contentUrl; client .loadMedia({ autoplay: true, startTime: Math.max(0, Math.floor(startTimeSec)), mediaInfo: { contentUrl: target.contentUrl, contentType: target.contentType, metadata: { type: 'movie', title: title || 'Goon' }, }, }) .then(onCastStarted) .catch((e: unknown) => { sentRef.current = null; Alert.alert('Cast failed', e instanceof Error ? e.message : 'unknown error'); }); }, [client, target, title, startTimeSec, onCastStarted]); // Sam przycisk chowa się, gdy w sieci nie widać żadnego urządzenia, więc u kogoś bez // Chromecasta pasek playera wygląda dokładnie jak dotąd. return ; }