fix(player): keep paused server sessions alive
Report the paused timeline every tick (~10s, matching official clients) instead of every ~60s, and ping the Plex transcoder keepalive endpoint alongside it while transcoding — PMS reaps idle transcode sessions that timeline reports alone historically have not kept alive. Prevents the transcode-variant of the #1520 stream death; the reporter's direct-play case is covered by EOF classification and reconnect.
This commit is contained in:
@@ -256,15 +256,19 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
||||
// play queue holding the siblings is created fire-and-forget and may
|
||||
// not exist yet when the tracker is wired.
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
final effectivePlayMethod = playMethod ?? (_isTranscoding ? 'Transcode' : 'DirectPlay');
|
||||
_progressTracker = PlaybackProgressTracker(
|
||||
client: mediaClient,
|
||||
metadata: metadata,
|
||||
player: currentPlayer,
|
||||
offlineWatchService: offlineWatchService,
|
||||
queueOnOnlineFailure: _playbackContext?.shouldQueueOnReportFailure ?? _usesLocalPlaybackSource,
|
||||
playMethod: playMethod ?? (_isTranscoding ? 'Transcode' : 'DirectPlay'),
|
||||
playMethod: effectivePlayMethod,
|
||||
playSessionId: playSessionId,
|
||||
mediaInfo: mediaInfo,
|
||||
onPausedKeepalive: mediaClient is PlexClient && effectivePlayMethod == 'Transcode'
|
||||
? () => mediaClient.pingTranscodeSession(_playbackTranscodeSessionId)
|
||||
: null,
|
||||
onScrobbled: () async {
|
||||
// Other episodes of a Plex multi-episode file share this item's
|
||||
// part — watching the file watched them too (#1500). Reusing
|
||||
|
||||
@@ -63,6 +63,13 @@ class PlaybackProgressTracker {
|
||||
/// un-scrobble the primary item.
|
||||
final Future<void> Function()? onScrobbled;
|
||||
|
||||
/// Invoked on every paused progress tick. The player wires this to the
|
||||
/// Plex transcoder keepalive ping (`/video/:/transcode/universal/ping`) —
|
||||
/// timeline reports alone historically have not been enough to stop PMS
|
||||
/// from reaping an idle transcode, so official Plex clients send both
|
||||
/// while paused. Best-effort; failures are the callee's to swallow.
|
||||
final Future<void> Function()? onPausedKeepalive;
|
||||
|
||||
/// Timer for periodic progress updates
|
||||
Timer? _progressTimer;
|
||||
|
||||
@@ -77,9 +84,6 @@ class PlaybackProgressTracker {
|
||||
/// Timer ticks to skip before retrying after failures (exponential backoff).
|
||||
int _ticksToSkip = 0;
|
||||
|
||||
/// Counts timer ticks while paused to send periodic "paused" heartbeats.
|
||||
int _pausedTickCounter = 0;
|
||||
|
||||
/// Whether we've already scrobbled (marked as watched) for this playback session.
|
||||
bool _scrobbled = false;
|
||||
|
||||
@@ -105,6 +109,7 @@ class PlaybackProgressTracker {
|
||||
this.playSessionId,
|
||||
this.mediaInfo,
|
||||
this.onScrobbled,
|
||||
this.onPausedKeepalive,
|
||||
this.updateInterval = const Duration(seconds: 10),
|
||||
}) : assert(!isOffline || offlineWatchService != null, 'offlineWatchService is required when isOffline is true'),
|
||||
assert(isOffline || client != null, 'client is required when isOffline is false'),
|
||||
@@ -137,27 +142,22 @@ class PlaybackProgressTracker {
|
||||
}
|
||||
|
||||
_progressTimer = Timer.periodic(updateInterval, (timer) {
|
||||
// Skip ticks when backing off after consecutive failures to avoid
|
||||
// flooding the network with doomed requests during an outage.
|
||||
if (_ticksToSkip > 0) {
|
||||
_ticksToSkip--;
|
||||
return;
|
||||
}
|
||||
if (player.state.isActive) {
|
||||
_pausedTickCounter = 0;
|
||||
// Skip ticks when backing off after consecutive failures to avoid
|
||||
// flooding the network with doomed requests during an outage.
|
||||
if (_ticksToSkip > 0) {
|
||||
_ticksToSkip--;
|
||||
return;
|
||||
}
|
||||
_sendProgress('playing');
|
||||
} else {
|
||||
// Send periodic "paused" updates to keep the server session alive
|
||||
// (~60s with default 10s interval)
|
||||
_pausedTickCounter++;
|
||||
if (_pausedTickCounter >= 6) {
|
||||
_pausedTickCounter = 0;
|
||||
if (_ticksToSkip > 0) {
|
||||
_ticksToSkip--;
|
||||
return;
|
||||
}
|
||||
_sendProgress('paused');
|
||||
}
|
||||
// Report every tick while paused too — official clients do the
|
||||
// same (~10s); the timeline heartbeat is what keeps the server
|
||||
// session and its transcoder from being reaped during a long
|
||||
// pause (#1520).
|
||||
_sendProgress('paused');
|
||||
final keepalive = onPausedKeepalive;
|
||||
if (keepalive != null) unawaited(keepalive());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1682,6 +1682,20 @@ class PlexClient
|
||||
throwIfHttpError(response);
|
||||
}
|
||||
|
||||
/// Keep a paused transcode session alive. Timeline updates alone have not
|
||||
/// historically stopped PMS from reaping an idle transcoder, so Plex
|
||||
/// clients send this alongside every paused timeline (see OpenPHT's
|
||||
/// SendTranscoderPing). [transcodeSessionId] is the `session` param the
|
||||
/// transcode was started with. Best-effort: a failed ping must never
|
||||
/// disturb playback, so errors are logged and swallowed.
|
||||
Future<void> pingTranscodeSession(String transcodeSessionId) async {
|
||||
try {
|
||||
await _http.get('/video/:/transcode/universal/ping', queryParameters: {'session': transcodeSessionId});
|
||||
} catch (e) {
|
||||
appLogger.d('Transcode keepalive ping failed', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove item from Continue Watching (On Deck) without affecting watch status or progress
|
||||
/// This uses the same endpoint Plex Web uses to hide items from Continue Watching
|
||||
Future<void> removeFromOnDeck(String ratingKey) async {
|
||||
|
||||
Reference in New Issue
Block a user