feat(player): start Plex transcodes at the resume position (#1817)
A Plex transcode session always starts producing at zero: the decision request never sent offset=, so any non-zero open - resuming a transcoded title, or switching from Direct Play to a transcoded quality mid-playback - opened a session whose produced window begins at the start of the file and seeked it. mpv immediately requests a segment the transcoder has not produced, PMS answers 404 for it and every subsequent segment, and playback buffers forever. Send offset=<seconds> (6dp) with the decision and start request - the view offset on initial open, the resolved resume position on every in-place reload - so the session begins producing at the position the player consumes first. The playlist timeline is unchanged: an offset session's media playlist still covers the full title from segment zero, so the player keeps opening with start: at the resume position and in-stream seeks work as before. Before a native player opens an offset playlist, waitForTranscodeReady walks the master playlist, the media playlist, and the segment containing the offset, because PMS can publish a manifest before that segment is fetchable and mpv treats the 404 as an HLS error. The probe is best-effort: it never fails an open, hands off immediately on HTTP 500 (on the response and exception paths alike) so the server-limit dialog stays prompt, stops on cancellation, skips itself when the playlist durations never reach the offset, and stays out of the endpoint-failover cascade. In-place reloads resolve the replacement source only after the old stop report has gone out, so Plex cannot use that stop to terminate the replacement transcode. close #1840
This commit is contained in:
@@ -177,6 +177,45 @@ class MediaServerHttpClient {
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue a GET and return only status and headers, draining the body
|
||||
/// unread — the shape for probes that ask "does this answer?" rather than
|
||||
/// "what does it say?".
|
||||
///
|
||||
/// Unlike [getBytes] the status code is surfaced instead of only logged.
|
||||
/// Unlike [get] nothing is ever decoded, so a body that fails decoding
|
||||
/// cannot convert a status into an exception, and — because
|
||||
/// [FailoverHttpClient] overrides [get] alone — this method structurally
|
||||
/// never enters the endpoint-failover cascade. Non-2xx is returned, not
|
||||
/// thrown, matching [get].
|
||||
Future<MediaServerResponse> getStatus(
|
||||
String url, {
|
||||
Map<String, String>? headers,
|
||||
Duration? timeout,
|
||||
AbortController? abort,
|
||||
}) {
|
||||
return _perform<MediaServerResponse>(
|
||||
'GET',
|
||||
url,
|
||||
headers: headers,
|
||||
timeout: timeout,
|
||||
abort: abort,
|
||||
consume: (streamed, scope) async {
|
||||
final effectiveUri = switch (streamed) {
|
||||
http.BaseResponseWithUrl(:final url) => url,
|
||||
_ => scope.uri,
|
||||
};
|
||||
await scope.receive(streamed.stream.drain<void>());
|
||||
scope.logResponse(streamed.statusCode);
|
||||
return MediaServerResponse(
|
||||
statusCode: streamed.statusCode,
|
||||
headers: streamed.headers,
|
||||
requestUri: scope.uri,
|
||||
effectiveUri: effectiveUri,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Stream-download a URL directly into a file.
|
||||
Future<void> downloadFile(
|
||||
String url,
|
||||
|
||||
Reference in New Issue
Block a user