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
+23 -4
View File
@@ -15,13 +15,27 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
if (!mounted || _isExiting.value) return;
// Fatal, unrecoverable until server-side fix — show modal instead of a snackbar.
if (err.cause == PlayerError.serverHttp500 || _sawServer500) {
//
// A sidecar subtitle fetch can also log a status, but it never raises the
// end-file error this handler is wired to, so the status observed here
// belongs to the primary media open.
if (err.cause == PlayerError.serverHttp500 || _fatalHttpStatuses.contains(500)) {
_hasFatalPlaybackError = true;
_progressTracker?.stopTracking();
unawaited(_showServerLimitDialog());
return;
}
// The server resolved the item but could not read the file behind it. No
// retry, quality change, or backend switch recovers that, and the raw mpv
// line ("Failed to open <redacted url>") tells the user nothing actionable.
if (err.cause == PlayerError.serverHttp404 || _fatalHttpStatuses.contains(404)) {
_hasFatalPlaybackError = true;
_progressTracker?.stopTracking();
unawaited(_showMediaUnreadableDialog());
return;
}
// Live TV: retry with progressively degraded stream settings
// (mirrors Plex web client fallback chain).
if (widget.isLive) {
@@ -48,9 +62,8 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
}
void _onPlayerLog(PlayerLog log) {
if (!_sawServer500 && VideoPlayerScreenState._server500Pattern.hasMatch(log.text)) {
_sawServer500 = true;
}
final status = PlayerError.httpStatusFromLog(log.text);
if (status != null && fatalPlaybackHttpStatuses.contains(status)) _fatalHttpStatuses.add(status);
if (log.level == PlayerLogLevel.error || log.level == PlayerLogLevel.fatal) {
appLogger.e('[Player LOG ERROR] [${log.prefix}] ${log.text}');
_lastLogError = _redactPlayerError(log.text.trim());
@@ -65,6 +78,12 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
if (mounted) unawaited(_handleBackButton());
}
Future<void> _showMediaUnreadableDialog() async {
if (!mounted) return;
await showMediaUnreadableDialog(context);
if (mounted) unawaited(_handleBackButton());
}
/// Handle notification when native player switched from ExoPlayer to MPV
Future<void> _onBackendSwitched() async {
_playerBackendLabel = 'mpv';