fix: retry live tv tune, clamp timeline duration
This commit is contained in:
@@ -2602,8 +2602,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
|
||||
// For live TV, player position/duration are unreliable (often 0).
|
||||
// Use playbackTime as time, and program duration from tune metadata.
|
||||
// Plex rejects timeline pings where time > duration; grow duration to
|
||||
// match — otherwise Tunarr-style short synthetic programs 400 mid-stream.
|
||||
final time = playbackTime;
|
||||
final duration = _liveDurationMs ?? 0;
|
||||
final duration = max(_liveDurationMs ?? 0, time);
|
||||
|
||||
final updatedBuffer = await client.updateLiveTimeline(
|
||||
ratingKey: ratingKey,
|
||||
|
||||
@@ -262,11 +262,21 @@ class PlexClient {
|
||||
}
|
||||
|
||||
bool _shouldAttemptFailover(PlexHttpException e) {
|
||||
if (e.isTransient) return true;
|
||||
final sc = e.statusCode;
|
||||
return e.type == PlexHttpErrorType.connectionTimeout ||
|
||||
e.type == PlexHttpErrorType.receiveTimeout ||
|
||||
e.type == PlexHttpErrorType.connectionError ||
|
||||
(sc != null && sc >= 500 && sc <= 599);
|
||||
return sc != null && sc >= 500 && sc <= 599;
|
||||
}
|
||||
|
||||
/// POST the tune endpoint with one retry on transient HTTP failure.
|
||||
Future<PlexResponse> _postTuneWithRetry(String path, String sessionIdentifier) async {
|
||||
final query = {'X-Plex-Session-Identifier': sessionIdentifier};
|
||||
try {
|
||||
return await _http.post(path, queryParameters: query, timeout: ConnectionTimeouts.tune);
|
||||
} on PlexHttpException catch (e) {
|
||||
if (!e.isTransient) rethrow;
|
||||
appLogger.w('Tune channel: transient failure, retrying once', error: e);
|
||||
return await _http.post(path, queryParameters: query, timeout: ConnectionTimeouts.tune);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch /media/providers and parse libraries + EPG providers from the response.
|
||||
@@ -2658,9 +2668,9 @@ class PlexClient {
|
||||
try {
|
||||
final sessionIdentifier = generateSessionIdentifier();
|
||||
|
||||
final response = await _http.post(
|
||||
final response = await _postTuneWithRetry(
|
||||
'/livetv/dvrs/$dvrKey/channels/$channelIdentifier/tune',
|
||||
queryParameters: {'X-Plex-Session-Identifier': sessionIdentifier},
|
||||
sessionIdentifier,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
|
||||
@@ -11,6 +11,10 @@ class ConnectionTimeouts {
|
||||
/// HTTP connect timeout for individual HTTP requests to a Plex server.
|
||||
static const connect = Duration(seconds: 10);
|
||||
|
||||
/// HTTP timeout for the live-TV tune POST. Matches Plex web's value — the
|
||||
/// default 10s connect budget is too tight on Fire-TV cold starts.
|
||||
static const tune = Duration(seconds: 30);
|
||||
|
||||
/// Per-server connection budget: preferred probe + race + HTTPS upgrade attempt + 1s buffer.
|
||||
static const perServerConnect = Duration(milliseconds: 1500 + 2000 + 2000 + 1000);
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ class PlexHttpException implements Exception {
|
||||
return PlexHttpException(type: PlexHttpErrorType.unknown, message: error.toString(), requestUri: uri);
|
||||
}
|
||||
|
||||
/// Whether the error looks transient (network/timeout) and worth retrying.
|
||||
bool get isTransient =>
|
||||
type == PlexHttpErrorType.connectionTimeout ||
|
||||
type == PlexHttpErrorType.connectionError ||
|
||||
type == PlexHttpErrorType.receiveTimeout;
|
||||
|
||||
@override
|
||||
String toString() => 'PlexHttpException(${type.name}: $message)';
|
||||
}
|
||||
|
||||
@@ -84,4 +84,28 @@ void main() {
|
||||
expect(e.toString(), 'PlexHttpException(cancelled: halt)');
|
||||
});
|
||||
});
|
||||
|
||||
group('PlexHttpException.isTransient', () {
|
||||
PlexHttpException ex(PlexHttpErrorType t) => PlexHttpException(type: t);
|
||||
|
||||
test('connectionTimeout is transient', () {
|
||||
expect(ex(PlexHttpErrorType.connectionTimeout).isTransient, isTrue);
|
||||
});
|
||||
|
||||
test('receiveTimeout is transient', () {
|
||||
expect(ex(PlexHttpErrorType.receiveTimeout).isTransient, isTrue);
|
||||
});
|
||||
|
||||
test('connectionError is transient', () {
|
||||
expect(ex(PlexHttpErrorType.connectionError).isTransient, isTrue);
|
||||
});
|
||||
|
||||
test('cancelled is NOT transient (user-driven abort)', () {
|
||||
expect(ex(PlexHttpErrorType.cancelled).isTransient, isFalse);
|
||||
});
|
||||
|
||||
test('unknown is NOT transient', () {
|
||||
expect(ex(PlexHttpErrorType.unknown).isTransient, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user