An EOF-driven advance does one cold metadata fetch with a single endpoint
failover and no transient retry. When connectivity to the server drops for
the ~20s that fetch needs (issue log: both plex.direct endpoints connect
timed out, then the running stream's own TLS socket died), the reload
rolled back to the finished episode's last frame: black screen, progress
bar parked at the end, no way forward but the transport controls - while
pressing Next by hand seconds later succeeded. The per-item metadata cache
row could not absorb the blip either, because adjacency comes from queue
containers, so the next episode's row is cold at the exact moment the
transition needs it.
Three changes:
- A failed in-place reload now records its classified failure reason, and
an advance that ran with the completion latch set re-presents the Play
Next prompt when that reason is serverUnavailable. With auto-play
enabled the countdown re-fires the advance up to two times before the
prompt goes manual-only; Watch Together sessions and mid-episode Next
presses (whose rolled-back stream is still valid) keep the existing
handling. playNextRetryPresentation owns the decision and is unit-tested.
- Committing adjacency now best-effort prefetches the next episode's full
metadata row through fetchItem, which writes the exact row playback
initialization falls back to on both backends (Plex: same cache key and
full playback query shape; Jellyfin: the /Users/{uid}/Items/{id} row the
playback bundle reads). A warm row turns a blip at the transition into a
normal start.
- JellyfinClient.fetchItem's documented "pure transport error -> cached
row" fallback was dead code: the HTTP layer wraps transport errors into
MediaServerHttpException, which the first catch rethrew unconditionally.
Status-less, non-cancelled failures now take the fallback; answered
requests (401/403/5xx) and cancellations surface unchanged.
Verified with new contract tests (Plex: cold row fails transiently ->
fetchItem primes -> the same failing fetch serves playback from cache;
Jellyfin: primed row survives a transport failure into fetchPlaybackBundle)
plus the full test/screens/video_player and test/services suites and
analyzer parity.
close#1867
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).
Since a1b6a8971 only the selected sidecar attached at open, so mpv's
track-list carried one external subtitle and the track sheet could only
offer the rest as primary source switches - tap-and-hold on a
non-selected external track selected it as primary instead of secondary.
Real external files are cheap static fetches, so Jellyfin, Plex direct
play, and offline discovery now mark them preload and they ride along in
sub-files at open, keeping every external track selectable as a
secondary subtitle without a reopen. Embedded rows extracted on a
transcode stay lazy: extraction can stall behind the transcoder, which
is exactly what used to trip the sidecar open guard.
close#1860
Non-Original presets advertised hevc inside the mpegts HLS target; a Plex
Pass server with HEVC encoding enabled obliges, and its HEVC encode -> TS
segmenter path emits parameter sets mpv rejects ("PPS changed between
slices"). The VOD target now requests fragmented MP4 (verified against
PMS 1.22-1.43), retrying once with an H.264-only TS profile when a
server's decision does not echo the mp4 container back, and falling back
to direct play when neither is honoured. Live TV keeps its own TS target:
live sessions copy broadcast hevc/mpeg2video streams, a path the encoder
bug does not touch.
Presets also now send the videoResolution/videoQuality caps their labels
promise; previously only the bitrate limitation went out, so a "1080p
8 Mbps" preset delivered 2160p at a starved 8 Mbps.
close#1859
Two regressions since 2.9.1 broke subtitles on transcoded playback. Since
a1b6a8971 sidecars load with the media behind a 10s open guard, so a
subtitle URL the server is slow to serve — Jellyfin extracting an
embedded stream while its transcoder spins up — tripped the guard: stop,
reopen without subtitles, "Selected subtitles could not be loaded"
snackbar, and an emptied subtitle menu. Since 2b3853a88 every embedded
Plex subtitle was handed to the player as a sidecar whose URL is the
original container, so a transcode also range-read and demuxed the
source over HTTP — for a 40 GB remux, purely to find a subtitle track —
which is also why PGS never appeared: the client was handed a container
to demux rather than a rendition to play.
Delivery is the server's job again, backported from the AVPlayer branch
(42ba01440, the subtitle subset of 6852ac274, and a3da81e83) and adapted
to main's mpv backend:
Plex burns every embedded track (subtitles=burn); only a real external
file with a /library/streams key stays a client-fetched sidecar. A burn
is a re-encode, so directPlay is withdrawn — a real PMS answers HTTP 400
to directPlay=1 with burn — and the burn is aimed by selecting the
stream on the part first via the selectStreams PUT, because the decision
endpoint ignores subtitleStreamID alongside subtitles=burn. An
unaimable or undeliverable burn (dvb_teletext) refuses the transcode and
falls back to warned direct play rather than welding the wrong language
in or silently dropping the caption. Main's per-preset
directPlay/directStream pinning is kept; verified against a live PMS
that burn works under directStream=0.
Jellyfin never offers image formats as External, so bitmaps fall through
to Encode and are burned; text External is withheld per request when the
effective selection — including the server's DefaultSubtitleStreamIndex —
is embedded, and offered when it is a real file, so a file is delivered
as a file and never fetched twice. The burned row is excluded from the
sidecars; remaining text rows stay extractable, which is how a secondary
track still renders over a transcode. Sidecar URLs now use the format
extension the endpoint expects instead of the reported codec name.
The controls and selection layers learn what burning means: burn
eligibility is the codec's property, so burned rows stay selectable in
the menu; any change away from a burned selection renegotiates with the
server instead of pretending a local switch worked; the visibility
shortcut explains itself instead of doing nothing; and the track manager
is told when the primary is server-rendered so it stops waiting out a
thirty-second deadline for a native track that is already pixels.
Verified: analyzer parity, clean_translations --check --strict, full
flutter test (5749), and decision-level runs against live Plex and
Jellyfin servers — text and PGS burn decisions, the directPlay=1+burn
400, External file delivery, an unchanged no-burn baseline, and a real
burn session serving its playlist. The pre-commit aggregate was bypassed
for pre-existing main-state findings outside this diff: 21 format-drifted
files and three unused test seams in lib/main.dart.
close#1738
Refs #1815, #1622.
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
Episode advance carried live player state, so the viewer's choice only
survived while every episode could serve it: one episode without the
picked audio or subtitle fell back, and the fallback became the carry for
the rest of the session. The screen now keeps the last explicit audio,
subtitle, and secondary-subtitle choices for its lifetime; automatic
outcomes never overwrite them, so the choice retries on every following
episode and reattaches as soon as a catalog can serve it again.
Audio catches up with the subtitle carry from #1785. The old matcher
required raw language equality (a 'sv' pick never found a 'swe' row) and
otherwise took the first same-language track, flipping a commentary or
alternate-mix pick back to the main mix on every episode. Audio now uses
the same evidence bands as subtitles: bridged language parity is
authoritative, a unique title match vouches for untagged tracks, codec
and channel-count parity only break ties, and an ambiguous catalog
declines to the server's own choice instead of guessing. The synthesized
source descriptor also prefers the row's own title over the display title
that collapses to the bare language.
Episode advance previously sent no audio hint to negotiation at all, so a
transcode baked in the server's default audio no matter what was playing.
Both backends now resolve the carried semantics against the new episode's
streams: Jellyfin sends the resolved AudioStreamIndex, Plex feeds the
transcode decision, an explicit per-part stream id always wins, and a
failed match falls back to the server's pick.
close#1785
* fix(plex): show PGS subtitles when transcoding
Bitmap subtitles (PGS/VOBSUB) disappeared from the menu while transcoding on Plex, leaving only "Off". The MKV transcode already supports them, so embed them in the stream like text subs. Now they show up and can be turned on.
* fix(plex): give bitmap subtitle codecs the right sidecar extension
getSubtitleExtension fell back to .srt for vobsub, pgssub and dvb subs. Map them to .sub/.sup so it stays consistent with isImageSubtitleCodec and any sidecar URL or download uses the correct extension.