Files
plezy/lib/screens/video_player/playback_failure_action.dart
T
edde746 e6be5f9fef fix(player): surface a persistent HTTP 503 at open instead of retrying forever
ffmpeg's reconnect loop deliberately retries 503 without bound (#1520), so a
server that keeps refusing the stream at open time left a silent black screen:
ExoPlayer fell back to MPV, MPV reconnected forever, and no error ever reached
the screen. A new open-phase watchdog arms on the first 503 seen before any
frame renders and, after 20s without one, synthesizes a server-http-503 error
that shows an actionable dialog. Mid-stream 503s and live TV keep their
existing ride-out paths.

close #1830
2026-08-08 12:06:32 +02:00

72 lines
2.6 KiB
Dart

import '../../mpv/models.dart';
/// Highest fallback level the live-TV ladder will climb before giving up.
const int maxLiveFallbackLevel = 2;
/// What the player screen should do about one playback error.
enum PlaybackFailureAction {
/// Server rejected the session with HTTP 500 — a bandwidth/transcoding limit.
serverLimitDialog,
/// Server could not read the file behind the item (HTTP 404).
mediaUnreadableDialog,
/// Server kept refusing the stream with HTTP 503 for the whole open phase.
serverBusyDialog,
/// A live retry already owns the player and its error UI.
ignore,
/// Climb one rung of the live-TV fallback ladder.
liveRetry,
/// Live ladder is exhausted and its last retry failed.
liveInterrupted,
/// Show the raw player error and leave the route.
fatal,
}
/// Decides what [cause] plus the statuses seen on this open mean for playback.
///
/// Pure so the policy is testable without a live player screen, mirroring
/// [runLiveStreamRetry]. [fatalHttpStatuses] is the set of
/// [fatalPlaybackHttpStatuses] entries the player's log stream reported.
///
/// Live TV deliberately diverges on 404: an HLS segment that has rolled off the
/// playlist, or a transcode session restarting under us, answers 404 mid-stream,
/// and the bounded ladder exists to ride that out. Only on-demand playback
/// treats 404 as terminal, where it does mean the file is unreadable. 500 stays
/// terminal for both — a limit rejection is not something a retry clears. 503
/// arrives only as the open-phase watchdog's cause tag (it never latches into
/// [fatalHttpStatuses]); by then the reconnect loop has had its chances, so
/// on-demand playback surfaces it while live TV keeps its ladder.
PlaybackFailureAction resolvePlaybackFailureAction({
required String? cause,
required Set<int> fatalHttpStatuses,
required bool isLive,
required bool liveRetrying,
required int liveFallbackLevel,
required bool liveRetryFailed,
}) {
if (cause == PlayerError.serverHttp500 || fatalHttpStatuses.contains(500)) {
return PlaybackFailureAction.serverLimitDialog;
}
if (!isLive && (cause == PlayerError.serverHttp404 || fatalHttpStatuses.contains(404))) {
return PlaybackFailureAction.mediaUnreadableDialog;
}
if (!isLive && cause == PlayerError.serverHttp503) {
return PlaybackFailureAction.serverBusyDialog;
}
if (isLive) {
if (liveRetrying) return PlaybackFailureAction.ignore;
if (liveFallbackLevel < maxLiveFallbackLevel) return PlaybackFailureAction.liveRetry;
if (liveRetryFailed) return PlaybackFailureAction.liveInterrupted;
}
return PlaybackFailureAction.fatal;
}