From 7b2f093d85f6542c79f330a9d09bf34ff688057f Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Mon, 1 Jun 2026 11:15:21 +0200 Subject: [PATCH] mobile: recover from mid-playback decode/seek errors (doply NAL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug f6c86847/b1b5e1a2: doply/playmogo plays fine but seeking throws "source error, invalid NAL length" in ExoPlayer. Investigation (cross-IP, 2026-06-01) showed the stream is well-formed — faststart MP4 (moov before mdat) on cloudatacdn.com which fully supports HTTP range (206, correct content-range, repeatable token, no redirect). So it is an ExoPlayer-internal seek failure, not an HTTP/container problem, and expo-video exposes no extractor/MIME hint to influence it. Mitigation: when the native player errors *after* it had already loaded (i.e. a mid-playback/seek failure, not an initial-load failure) and the error is not a 404/410, recreate the source via player.replace() and resume at the last known position — this opens a fresh connection and re-parses moov, which typically clears the transient decode error. Hard-capped at 2 attempts per mount to avoid any auto-reload loop; if it still fails it falls through to the existing proxy/WebView fallback and error UI. Initial-load errors are untouched, so the resolver and the ~59k working doply sources are unaffected. Also thread playbackId/entityKind through the resolved-hoster and proxy/WebView nav.replace calls so those paths get the 404 "Mark broken" affordance too, and complete the local RouteParams type with headers/fallbackProxyUrl. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/src/screens/PlayerScreen.tsx | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index 2f305a9..e8cdb2e 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -39,6 +39,8 @@ interface RouteParams { refererHost?: string; title?: string; mode?: 'video' | 'webview'; + headers?: Record; + fallbackProxyUrl?: string; fallbackEmbedUrl?: string; } @@ -179,14 +181,53 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { // Każdy step ma osobną ref żeby nie loopować. const didFallbackProxyRef = React.useRef(false); const didFallbackWebViewRef = React.useRef(false); + // Seek/decode recovery (bug f6c86847: doply/playmogo „invalid NAL length” przy + // przewijaniu). Stream jest poprawny — faststart MP4, CDN wspiera Range 206 + // (zweryfikowane 2026-06-01 cross-IP) — więc to wewnętrzny błąd seeka ExoPlayera, + // nie HTTP. `replace(source)` otwiera świeże połączenie + re-parsuje moov; resume + // na ostatniej dobrej pozycji ratuje playback. Twardy limit 2 prób / mount żeby + // NIE wpaść w auto-loop. Tylko gdy player JUŻ grał (post-load, czyli seek-error, + // nie initial-load) i błąd nie jest 404/410. + const loadedOnceRef = React.useRef(false); + const lastGoodPositionRef = React.useRef(0); + const seekRecoveryRef = React.useRef(0); + React.useEffect(() => { + if (status === 'readyToPlay') loadedOnceRef.current = true; + }, [status]); React.useEffect(() => { if (status !== 'error') return; + // Step 0: post-load decode/seek error → recover in-place (przed proxy/WebView, + // które są dla INITIAL-load errorów IP-bound CDN). + if ( + loadedOnceRef.current && + seekRecoveryRef.current < 2 && + !isGoneError(playerError?.message) + ) { + seekRecoveryRef.current += 1; + const resumeAt = lastGoodPositionRef.current; + try { + player.replace(source); + setTimeout(() => { + try { + if (resumeAt > 0) player.currentTime = resumeAt; + player.play(); + } catch { + // disposed/za wcześnie — następny error tick spróbuje znów (do limitu) + } + }, 700); + } catch { + // replace failed — przepuść do fallback chain niżej + } + return; + } // Step 1 → 2: direct fail (403/410/etc), spróbuj proxy URL. if (fallbackProxyUrl && !didFallbackProxyRef.current && url !== fallbackProxyUrl) { didFallbackProxyRef.current = true; nav.replace('Player', { url: fallbackProxyUrl, sceneId, + playbackId: params.playbackId, + entityKind, durationSec, refererHost, title, @@ -203,13 +244,15 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { nav.replace('Player', { url: fallbackEmbedUrl, sceneId, + playbackId: params.playbackId, + entityKind, durationSec, refererHost, title, mode: 'webview', }); } - }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title]); + }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title, player, source, playerError]); const lastReportedRef = React.useRef(0); // Lokalny tick co 500ms — driver dla custom scrubber + time labels. expo-video @@ -221,6 +264,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { try { const pos = player.currentTime || 0; setPosition(pos); + // Ostatnia „dobra” pozycja dla seek-recovery (tylko gdy faktycznie gra). + if (player.playing && pos > 0) lastGoodPositionRef.current = pos; const dur = player.duration || 0; if (dur > 0 && Math.abs(dur - knownDuration) > 0.5) setKnownDuration(dur); const posInt = Math.floor(pos); @@ -1124,6 +1169,8 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) { nav.replace('Player', { url: result.url, sceneId, + playbackId: params.playbackId, + entityKind, durationSec, refererHost, headers: result.headers,