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.
57 lines
2.5 KiB
Dart
57 lines
2.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/mpv/models.dart';
|
|
|
|
void main() {
|
|
group('PlayerError.httpStatusFromLog', () {
|
|
test('reads the status out of the ffmpeg warn line that precedes a failed open', () {
|
|
// Verbatim from the #1750 report: the only line in the whole failure that
|
|
// names the status. The error-level "Failed to open ..." that follows
|
|
// omits it.
|
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 404 Not Found'), 404);
|
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 500 Internal Server Error'), 500);
|
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 503 Service Unavailable'), 503);
|
|
});
|
|
|
|
test('reads the status media3 stringifies into its exception chain', () {
|
|
expect(
|
|
PlayerError.httpStatusFromLog(
|
|
'androidx.media3.datasource.HttpDataSource\$InvalidResponseCodeException: Response code: 404',
|
|
),
|
|
404,
|
|
);
|
|
expect(PlayerError.httpStatusFromLog('Response code: 500'), 500);
|
|
});
|
|
|
|
test('returns null for player logs that name no status', () {
|
|
expect(PlayerError.httpStatusFromLog('Failed to open https://jf.example.com/Videos/item-1/stream.'), isNull);
|
|
expect(PlayerError.httpStatusFromLog('loading failed'), isNull);
|
|
expect(PlayerError.httpStatusFromLog(''), isNull);
|
|
});
|
|
|
|
test('does not mistake an unrelated number for a status', () {
|
|
// A bare code with no HTTP context must not reach the fatal dialogs.
|
|
expect(PlayerError.httpStatusFromLog('Set property: stream-buffer-size="404"'), isNull);
|
|
expect(PlayerError.httpStatusFromLog('audio/aac 500 kbps'), isNull);
|
|
// Adjacent digits are not a 3-digit status.
|
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 4040 Nope'), isNull);
|
|
});
|
|
|
|
test('reports the first status when a line names several', () {
|
|
expect(PlayerError.httpStatusFromLog('HTTP error 404 after HTTP error 500'), 404);
|
|
});
|
|
});
|
|
|
|
group('fatalPlaybackHttpStatuses', () {
|
|
test('covers exactly the statuses no client-side retry can recover', () {
|
|
expect(fatalPlaybackHttpStatuses, {404, 500});
|
|
});
|
|
|
|
test('excludes the 503 the reconnect path deliberately retries', () {
|
|
// stream-lavf-o sets reconnect_on_http_error=503, so a 503 is expected
|
|
// mid-playback and must not latch as fatal.
|
|
expect(fatalPlaybackHttpStatuses.contains(503), isFalse);
|
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 503 Service Unavailable'), 503);
|
|
});
|
|
});
|
|
}
|