fix(player): keep live TV on its retry ladder when a stream 404s
The 404 branch added in 16668be5 ran ahead of the live-TV fallback chain, so a transient live 404 — an HLS segment rolled off the playlist, or a transcode session restarting under us — showed "file unavailable" and killed a stream the bounded ladder would have recovered. Only on-demand playback can read a 404 as terminal, where it really does mean the file is unreadable. 500 stays terminal for both, since a limit rejection is not something a retry clears. The dispatch lived in a private extension on the screen state, where no test could reach the decision. Extract it as resolvePlaybackFailureAction next to runLiveStreamRetry, which already sets that precedent, and cover both the live and on-demand paths plus the ladder's rungs.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/mpv/models.dart';
|
||||
import 'package:plezy/screens/video_player/playback_failure_action.dart';
|
||||
|
||||
PlaybackFailureAction resolve({
|
||||
String? cause,
|
||||
Set<int> statuses = const <int>{},
|
||||
bool isLive = false,
|
||||
bool liveRetrying = false,
|
||||
int liveFallbackLevel = 0,
|
||||
bool liveRetryFailed = false,
|
||||
}) {
|
||||
return resolvePlaybackFailureAction(
|
||||
cause: cause,
|
||||
fatalHttpStatuses: statuses,
|
||||
isLive: isLive,
|
||||
liveRetrying: liveRetrying,
|
||||
liveFallbackLevel: liveFallbackLevel,
|
||||
liveRetryFailed: liveRetryFailed,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('HTTP 404', () {
|
||||
test('on-demand playback treats it as terminal', () {
|
||||
// The file behind the item is unreadable server-side (#1750); no retry,
|
||||
// quality change, or backend switch recovers it.
|
||||
expect(resolve(statuses: {404}), PlaybackFailureAction.mediaUnreadableDialog);
|
||||
expect(resolve(cause: PlayerError.serverHttp404), PlaybackFailureAction.mediaUnreadableDialog);
|
||||
});
|
||||
|
||||
test('live TV rides it out on the fallback ladder instead', () {
|
||||
// An HLS segment that rolled off the playlist, or a transcode session
|
||||
// restarting, answers 404 mid-stream. Showing "file unavailable" there
|
||||
// would kill a recoverable stream.
|
||||
expect(resolve(statuses: {404}, isLive: true), PlaybackFailureAction.liveRetry);
|
||||
expect(resolve(cause: PlayerError.serverHttp404, isLive: true), PlaybackFailureAction.liveRetry);
|
||||
});
|
||||
|
||||
test('live TV still exhausts the ladder before giving up', () {
|
||||
expect(
|
||||
resolve(statuses: {404}, isLive: true, liveFallbackLevel: maxLiveFallbackLevel, liveRetryFailed: true),
|
||||
PlaybackFailureAction.liveInterrupted,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('HTTP 500', () {
|
||||
test('is terminal for on-demand and live alike', () {
|
||||
// A bandwidth/transcoding limit rejection is not something a retry
|
||||
// clears, so live TV gets the same modal.
|
||||
expect(resolve(statuses: {500}), PlaybackFailureAction.serverLimitDialog);
|
||||
expect(resolve(statuses: {500}, isLive: true), PlaybackFailureAction.serverLimitDialog);
|
||||
expect(resolve(cause: PlayerError.serverHttp500, isLive: true), PlaybackFailureAction.serverLimitDialog);
|
||||
});
|
||||
|
||||
test('outranks a 404 latched on the same open', () {
|
||||
expect(resolve(statuses: {404, 500}), PlaybackFailureAction.serverLimitDialog);
|
||||
});
|
||||
});
|
||||
|
||||
group('live fallback ladder', () {
|
||||
test('climbs every rung below the bound', () {
|
||||
for (var level = 0; level < maxLiveFallbackLevel; level++) {
|
||||
expect(resolve(isLive: true, liveFallbackLevel: level), PlaybackFailureAction.liveRetry);
|
||||
}
|
||||
});
|
||||
|
||||
test('an in-flight retry owns the error', () {
|
||||
expect(resolve(isLive: true, liveRetrying: true), PlaybackFailureAction.ignore);
|
||||
// Even a latched 404 must not preempt the running retry.
|
||||
expect(resolve(statuses: {404}, isLive: true, liveRetrying: true), PlaybackFailureAction.ignore);
|
||||
});
|
||||
|
||||
test('a failed last retry reports an interruption', () {
|
||||
expect(
|
||||
resolve(isLive: true, liveFallbackLevel: maxLiveFallbackLevel, liveRetryFailed: true),
|
||||
PlaybackFailureAction.liveInterrupted,
|
||||
);
|
||||
});
|
||||
|
||||
test('an exhausted ladder that never failed falls through to the raw error', () {
|
||||
expect(resolve(isLive: true, liveFallbackLevel: maxLiveFallbackLevel), PlaybackFailureAction.fatal);
|
||||
});
|
||||
});
|
||||
|
||||
test('an error with no server status is fatal for on-demand playback', () {
|
||||
expect(resolve(), PlaybackFailureAction.fatal);
|
||||
expect(resolve(statuses: {503}), PlaybackFailureAction.fatal);
|
||||
expect(resolve(cause: 'some-decoder-fault'), PlaybackFailureAction.fatal);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user