mobile: recover from mid-playback decode/seek errors (doply NAL)
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) <noreply@anthropic.com>
This commit is contained in:
parent
967123d5d6
commit
7b2f093d85
1 changed files with 48 additions and 1 deletions
|
|
@ -39,6 +39,8 @@ interface RouteParams {
|
||||||
refererHost?: string;
|
refererHost?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
mode?: 'video' | 'webview';
|
mode?: 'video' | 'webview';
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
fallbackProxyUrl?: string;
|
||||||
fallbackEmbedUrl?: string;
|
fallbackEmbedUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,14 +181,53 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
// Każdy step ma osobną ref żeby nie loopować.
|
// Każdy step ma osobną ref żeby nie loopować.
|
||||||
const didFallbackProxyRef = React.useRef(false);
|
const didFallbackProxyRef = React.useRef(false);
|
||||||
const didFallbackWebViewRef = 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(() => {
|
React.useEffect(() => {
|
||||||
if (status !== 'error') return;
|
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.
|
// Step 1 → 2: direct fail (403/410/etc), spróbuj proxy URL.
|
||||||
if (fallbackProxyUrl && !didFallbackProxyRef.current && url !== fallbackProxyUrl) {
|
if (fallbackProxyUrl && !didFallbackProxyRef.current && url !== fallbackProxyUrl) {
|
||||||
didFallbackProxyRef.current = true;
|
didFallbackProxyRef.current = true;
|
||||||
nav.replace('Player', {
|
nav.replace('Player', {
|
||||||
url: fallbackProxyUrl,
|
url: fallbackProxyUrl,
|
||||||
sceneId,
|
sceneId,
|
||||||
|
playbackId: params.playbackId,
|
||||||
|
entityKind,
|
||||||
durationSec,
|
durationSec,
|
||||||
refererHost,
|
refererHost,
|
||||||
title,
|
title,
|
||||||
|
|
@ -203,13 +244,15 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
nav.replace('Player', {
|
nav.replace('Player', {
|
||||||
url: fallbackEmbedUrl,
|
url: fallbackEmbedUrl,
|
||||||
sceneId,
|
sceneId,
|
||||||
|
playbackId: params.playbackId,
|
||||||
|
entityKind,
|
||||||
durationSec,
|
durationSec,
|
||||||
refererHost,
|
refererHost,
|
||||||
title,
|
title,
|
||||||
mode: 'webview',
|
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);
|
const lastReportedRef = React.useRef(0);
|
||||||
// Lokalny tick co 500ms — driver dla custom scrubber + time labels. expo-video
|
// Lokalny tick co 500ms — driver dla custom scrubber + time labels. expo-video
|
||||||
|
|
@ -221,6 +264,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
try {
|
try {
|
||||||
const pos = player.currentTime || 0;
|
const pos = player.currentTime || 0;
|
||||||
setPosition(pos);
|
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;
|
const dur = player.duration || 0;
|
||||||
if (dur > 0 && Math.abs(dur - knownDuration) > 0.5) setKnownDuration(dur);
|
if (dur > 0 && Math.abs(dur - knownDuration) > 0.5) setKnownDuration(dur);
|
||||||
const posInt = Math.floor(pos);
|
const posInt = Math.floor(pos);
|
||||||
|
|
@ -1124,6 +1169,8 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
|
||||||
nav.replace('Player', {
|
nav.replace('Player', {
|
||||||
url: result.url,
|
url: result.url,
|
||||||
sceneId,
|
sceneId,
|
||||||
|
playbackId: params.playbackId,
|
||||||
|
entityKind,
|
||||||
durationSec,
|
durationSec,
|
||||||
refererHost,
|
refererHost,
|
||||||
headers: result.headers,
|
headers: result.headers,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue