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
72 lines
2.6 KiB
Dart
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;
|
|
}
|