fix(player): seek Plex transcodes in-band instead of pre-warming at the resume offset

A quality switch or resumed open at a nonzero position sent offset=T on the
HLS start URL, waited for the readiness probe to touch the segment at T, and
then had mpv seek to T anyway. mpv's stream probing always reads segment zero
first, and a Plex segment request is a seek, so the transcoder was dragged
through seek(T) -> seek(0) -> seek(T) within seconds of the open. Measured
against PMS 1.43, a segment response that races such a restart can be left
open with headers sent and no data or error, and ffmpeg's HLS segment reads
have no default timeout, so playback buffered forever after the first frame
(issue #1859). Starting the session plain and letting the player's start=T
request the resume segment performs the one unavoidable transcoder seek.

The offset request parameter, the readiness probe, and the probe-only
getStatus HTTP helper are removed; live TV time-shift keeps its own offset
path. Transcode opens now also set an explicit network-timeout with
demuxer-level reconnect options: mpv's stream-layer reconnect settings never
reach ffmpeg's HLS segment fetches, so a silently hung segment response now
times out after 20s and is re-requested on a fresh connection instead of
buffering indefinitely. Verified against a live PMS (resume plays from the
requested position) and a stall harness (hung segment re-requested at 20s
with no content skip).
This commit is contained in:
edde746
2026-08-10 23:04:35 +02:00
parent aff6b6576f
commit 3a704a2b9b
8 changed files with 36 additions and 665 deletions
-12
View File
@@ -75,7 +75,6 @@ import '../providers/shader_provider.dart';
import '../providers/user_profile_provider.dart';
import '../utils/app_logger.dart';
import '../utils/dialogs.dart';
import '../utils/media_server_http_client.dart' show AbortController;
import '../utils/log_redaction_manager.dart';
import '../utils/live_tv_player_navigation.dart';
import '../utils/player_utils.dart';
@@ -473,9 +472,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Completer<void>? _playbackTransitionIdleCompleter;
bool _playbackIntentShouldPlay = true;
/// In-flight transcode readiness probe, aborted by the next probe or by
/// dispose so a superseded open never leaves it polling out its window.
AbortController? _transcodeReadinessAbort;
int _pendingSubtitleCycleCount = 0;
bool _subtitleCycleDrainActive = false;
@@ -1307,13 +1303,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
preferredSubtitleTrack: _preferredSubtitleTrack,
sessionIdentifier: _playbackSessionIdentifier,
transcodeSessionId: _playbackTranscodeSessionId,
// The initial resume position is the server view offset (the
// online open resolves the same value later), so a resumed
// transcode starts producing at the resume point instead of
// seeking a stream that begins at zero.
transcodeOffset: _currentMetadata.viewOffsetMs != null
? Duration(milliseconds: _currentMetadata.viewOffsetMs!)
: null,
),
offlineLibraryMode: false,
);
@@ -1828,7 +1817,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
@override
void dispose() {
unawaited(AndroidExitDiagnostics.markUiState(AndroidUiState.mainScreen));
_transcodeReadinessAbort?.abort();
_playerInitializationGeneration++;
_frameRate.dispose();
WidgetsBinding.instance.removeObserver(this);