Files
plezy/lib/screens/video_player/open_http_503_watchdog.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

52 lines
1.9 KiB
Dart

import 'dart:async';
/// How long an open gets to produce a first frame after the server starts
/// answering the stream with HTTP 503 before the failure is surfaced.
///
/// ffmpeg's reconnect loop retries 503 with exponential backoff (0s, 1s, 3s,
/// 7s, ...), so this window covers several attempts — enough for the short
/// server restart #1520 rides out mid-stream, while a server still refusing
/// after all of them is treated as down for this item (#1830).
const Duration openHttp503Patience = Duration(seconds: 20);
/// Bounds a media open the server answers with HTTP 503.
///
/// Mid-stream, ffmpeg deliberately reconnects on 503 forever (#1520) and mpv
/// just reports buffering. At open time — before any frame has rendered —
/// that same loop is a silent black screen with no error event (#1830):
/// nothing client-side ever fails. This watchdog arms on the first 503
/// observed while the open is still frameless and calls [onPersistent] if
/// [disarm] has not been called within [patience].
///
/// The caller decides which opens are eligible (live TV stays out — its
/// fallback ladder owns retries there) and owns the synthesized error. Only
/// the timer bookkeeping lives here so it is testable with `fakeAsync`.
class OpenHttp503Watchdog {
OpenHttp503Watchdog({required this.onPersistent, this.patience = openHttp503Patience});
final Duration patience;
final void Function() onPersistent;
Timer? _timer;
bool get isArmed => _timer != null;
/// A 503 was observed while the current open has no frame yet. Arms once;
/// later 503s from the same reconnect loop keep the original deadline.
void onOpenPhase503() {
_timer ??= Timer(patience, _fire);
}
/// The open produced output, was replaced, or the screen is going away:
/// any further 503s belong to the reconnect path (or the next open).
void disarm() {
_timer?.cancel();
_timer = null;
}
void _fire() {
_timer = null;
onPersistent();
}
}