diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index bde74ad..a6f0049 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-08-20b', + date: 'August 2026', + items: [ + 'Fixed a crash that replaced the video with an error screen. If you left the app while a scene was still opening, or Android restored the player by itself, the player refused to start. It now waits until the app is back on screen and then plays normally.', + ], + }, { id: '2026-08-20', date: 'August 2026', diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index 7b6a852..39d9e34 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -15,6 +15,7 @@ import { ActivityIndicator, Alert, Animated, + AppState, LayoutChangeEvent, Pressable, StatusBar, @@ -133,7 +134,47 @@ function detectMode(url: string): 'video' | 'webview' { return 'video'; } +/** Bramka na konstruowanie playera. + * + * `new VideoPlayer(...)` w expo-video BEZWARUNKOWO wola w `init` startService dla + * androidx.media3 MediaSessionService (VideoPlayer.kt:233), a Android od 8.0 zabrania + * startu zwyklego serwisu z tla. Efekt: gdy ekran playera zamontuje sie, kiedy apka + * jest juz w tle (user wyszedl w trakcie resolve albo system wznowil stos ekranow), + * konstruktor rzuca IllegalStateException i uzytkownik dostaje ekran bledu zamiast + * wideo. GOON-12: 63 wystapienia, 28 osob, wszystkie z in_foreground=false. + * + * Nie da sie tego wylaczyc konfiguracja, wiec po prostu nie tworzymy playera, dopoki + * nie jestesmy na wierzchu. Hookow nie da sie warunkowac, stad osobny komponent. + * Bramka dziala TYLKO przy montowaniu: gdy player juz istnieje, zejscie w tlo jest + * bezpieczne (serwis jest zbindowany) i odtwarzania nie przerywamy. + */ export function PlayerScreen() { + const [foreground, setForeground] = React.useState( + () => AppState.currentState === 'active', + ); + React.useEffect(() => { + if (foreground) return; + const sub = AppState.addEventListener('change', (next) => { + if (next === 'active') setForeground(true); + }); + return () => sub.remove(); + }, [foreground]); + + if (!foreground) { + return ( + + + + ); + } + return ; +} + +const gateStyles = StyleSheet.create({ + wait: { flex: 1, backgroundColor: '#000', alignItems: 'center', justifyContent: 'center' }, +}); + +function PlayerScreenActive() { const route = useRoute>(); const params = route.params as RouteParams; const mode = params.mode ?? detectMode(params.url);