fix(player): tell the user when the server cannot read the media file

A 404 on the media stream means the server resolved the item but could not
open the file behind it — moved, deleted, or on storage that went away.
Jellyfin maps the resulting FileNotFoundException to 404, and PlaybackInfo
never stats the file, so negotiation succeeds and only the stream request
fails. Playback then died with a snackbar reading "Failed to open
[REDACTED_URL]" before popping the route, which tells the user nothing and
leaves nothing useful in a bug report.

Generalize the HTTP-500 log probe into PlayerError.httpStatusFromLog and
latch every status in fatalPlaybackHttpStatuses. Each latches on its own so
the 503 that stream-lavf-o deliberately retries cannot mask the fatal status
behind it. A 404 now raises a dedicated modal naming the cause and the fix.

On Android a 404 previously failed the "Response code: 500" string test and
fell through to the ExoPlayer→MPV fallback, showing "switching to compatible
player" before failing again on the same request. Read the real status off
HttpDataSource.InvalidResponseCodeException instead and skip the fallback:
an HTTP status is not a codec problem.
This commit is contained in:
edde746
2026-07-31 21:45:33 +02:00
parent 6f9edd3e93
commit 86c8011b72
35 changed files with 290 additions and 45 deletions
@@ -105,7 +105,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
_errorSubscription = currentPlayer.streams.error.listen(_onPlayerError);
// warn is included so we can catch ffmpeg's "HTTP error 500" line in
// warn is included so we can catch ffmpeg's "HTTP error 4xx/5xx" line in
// _onPlayerLog — the error-level log that follows omits the status code.
_logSubscription = currentPlayer.streams.log
.where((log) => const {PlayerLogLevel.fatal, PlayerLogLevel.error, PlayerLogLevel.warn}.contains(log.level))
@@ -147,7 +147,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
_playbackRestartSubscription = currentPlayer.streams.playbackRestart.listen((_) async {
if (!mounted || player != currentPlayer) return;
_lastLogError = null;
_sawServer500 = false;
_fatalHttpStatuses.clear();
_live.fallbackLevel = 0;
_live.retryFailed = false;
final markFirstFrameReady = _markFirstFrameReady(currentPlayer, settingsService);