Tapping Repair on the startup-failure screen always threw: the repair flow received the gate State's own context, which has no Navigator, MaterialLocalizations, or ScaffoldMessenger, so both the confirm dialog and the fallback error snackbar raised instead of rendering. Thread the bootstrap home builder's context (below the MaterialApp) into the repair flow and its failure snackbar.
With two profiles bound to two Jellyfin/Emby users on one server, a legacy bare machine-id lookup (offline downloads, cached playback metadata) could resolve another user's cache row, serving their resume position and token-stamped image URLs. Mirror the write-path guard: when the bare scope matches more than one user, log and return nothing.
A 2-letter preferred language (es, ar, ru, ...) prefix-matched unrelated 3-letter track codes (est, arm, rum), so profile-based audio/subtitle selection could pick a wrong-language track whenever it preceded the wanted one in container order. Route the matcher through languageMatches, which compares exact codes, region variants, and ISO-639 variations.
While a Skip Intro / Skip Credits prompt is up and the chrome is down,
Back walked the screen's staged chain and exited playback. On a remote
nothing else means "no thanks": left/right seek, OK takes the skip,
up/down raises the OSD. Declining an intro cost you the episode.
Adds a stage for it alongside the sheet and content-strip stages that
already run locally in the player controls. Gated on the same condition
as Select's skip path, off on phones (#1938), off without canControl,
and off while a screen-level prompt owns the key.
The claim is latched for the whole press: handleBackKeyAction acts on
the key-up, and the button's own 7s auto-dismiss can fire in between.
Co-authored-by: cajunflavoredbob <cajunflavoredbob@users.noreply.github.com>
Recording indicators never appeared in the EPG and the program sheet kept
offering "Record" for already-subscribed programmes, so rules were created
repeatedly (nine duplicate subscriptions in the linked report). Two defects,
both confirmed against the official web client bundle:
- The sheet's scheduled-state check called
/media/providers/<identifier>/media/subscriptions/mapping/<ratingKey>,
which 404s: PMS mounts that route under the numeric MediaProvider id from
/media/providers. The check failed instantly and unconditionally on every
platform, so the button always read "Record".
- The guide derived its dot solely from cross-matching grab metadata, and
the grab refresh issued right after scheduling raced the server
materializing the grab, wiping the optimistic key before one frame showed.
The grid response already tags subscribed airings with subscriptionID /
grandparentSubscriptionID - the signal the official client renders from.
LiveTvProgram now carries those attributes; the guide checks them first
(grab matching stays as the secondary signal) and keeps local
schedule/cancel actions authoritative until the next grid load. The sheet
resolves Record vs Manage from the tagged rule key without a network call,
falling back to the fixed numeric-id mapping route for untagged airings.
Grab parsing also accepts the nested airing under "Video", which PMS uses
for every non-scheduled grab status in JSON.
close#2009
Opening Plezy on a TV box resumes the resident process rather than
cold-starting it, so the Libraries grid kept showing the in-memory
content from the previous session -- hours stale -- until the user
switched libraries and back. Nothing on the resume path refreshed tab
content.
MainScreen now latches genuine backgrounding and, on a resume more than
five minutes later, refreshes LibrariesProvider and sweeps the content
tabs through each screen's in-place Refreshable.refresh() -- skipped
while offline, before startup priming, during playback, or with no
connected servers. LibrariesScreen.refresh() now refetches the selected
library's loaded tabs (the toolbar refresh action) instead of
re-selecting the saved library, which never reloaded them because tab
widgets only reload when the library's globalKey changes.
close#2043
A continue-watching episode card was 180dp on home but 232dp in the
grid behind "see all" at 480dp: hub rows widened the resolved poster
cell (x1.5) while grids widen the max extent (x1.8) before the integral
column packing, and the TV shelf multiplied the rail's tall card by the
same 1.5. Rows now adopt the grid's packed wide cell via
MediaGridDelegate.wideCellWidth, the TV shelf passes the hub's wide
flag to TvBrowseRailLayout.cardWidthFor, and the single widening
scheme is stated in MediaGridDelegate. Grids are untouched.
Live TV "What's On" and catalog related rows are the two TV surfaces
without a TvBrowseRail path; HubSection's own clamp ([210, 340],
unscaled) rendered ~40% larger cards than every neighboring rail and
gave 720p TVs the same 210dp floor as 4K. Delegate the shelf card
width to TvBrowseRailLayout.cardWidthFor so the clamps scale and match
the rails.
Home hub rows showed ~3 large posters per row while the hub's "see all"
page packed 5 small ones on the same 360dp phone. Hub detail was the
only surface on the padding-aware target-count formula, which resolves
ceil(lerp(5, 2, f)) columns regardless of screen width (and 9+ columns
on TV). Drop the flag so hub detail shares the fixed-extent formula
with every other grid, and delete the now-unused
getMaxCrossAxisExtentWithPadding and its usePaddingAware plumbing.
close#2039
Transport from the controls layer ran unawaited with no error handling:
a tap racing player teardown (NOT_INITIALIZED on Android) or a wedged
mpv event queue (SET_PROPERTY_FAILED on Windows) crashed the app. The
controls path now tolerates failures the same way the remote transport
path one function above already does.
The most frequent Dart crash in production: CompanionRemoteProvider's
async crypto rebuild can call stopListening() on a discovery service
that was already disposed, and _emitHosts() then threw 'Bad state:
Cannot add new events after calling close'. Emitting after dispose is
now a no-op.
Backgrounding the Android app or locking the screen near-always dumped the
companion remote back to the device-selection page, forcing a manual
re-connect. Three defects combined: the peer's trailing disconnected status
event knocked the session out of reconnecting and broke the retry chain, the
bounded backoff budget (~31s) burned out against restricted background
networking before the user returned, and nothing retried on resume.
The provider now ignores stale peer status/error events while a reconnect
cycle is active, defers retries while backgrounded instead of consuming the
budget, and on resume retries immediately with a fresh budget - or pings a
nominally-connected session so a dead socket fails into the reconnect path
right away. Authenticated socket errors in the peer now surface as
disconnects like clean closes, keeping both terminal signals on the
reconnect path.
close#2035
Plex "Other Videos" libraries returned items as type=movie with
subtype=clip, which mapped to MediaKind.movie and rendered 2:3 poster
cards that cropped the generated 16:9 video-frame thumbs.
Items keep the movie kind so downloads, add-to, delete-from-server, and
detail navigation stay available, but subtype=clip now renders wide with
the thumb-first artwork clips already use. Sections marked subtype=clip
in /media/providers map to the clip library kind, giving Plex home-video
libraries the same folder-first grouping and wide grid cells as
MediaBrowser homevideos views. Wide-only hubs also keep 16:9 cards in
the poster episode modes, matching the TV rail's gate.
close#2036
Pressing Cmd+, on macOS did nothing: the stock Flutter template left a
disabled Preferences menu item holding the key equivalent, and nothing
in the app handled the chord.
MainScreen now handles Cmd+, (macOS) / Ctrl+, (Windows/Linux) beside
the existing Cmd+F search shortcut, reusing its tab-aware open path. A
SettingsShortcut fallback above the profile navigator covers pushed
content routes, pushing a named settings route; a navigator observer
blocks the chord while settings is already in the stack, and the video
player route keeps ownership of the keyboard. The dead xib menu item
is removed.
close#1909
The controller extraction left a bare $_mediaControls.suspendedForTvBackground
in the lifecycle diagnostic string, so the line logged the controller
instance instead of the boolean it exists to surface during TV
background-suspend debugging.
The Android TV background suspend (#1911) kept its grace timer, the
suspended latch, and four pre-stop position/track fields loose on the
State, with the latch/rollback/consume invariants enforced only by
convention across three lifecycle methods and the redelivery loop.
Move them into TvBackgroundSuspendState: latch() sets the snapshot and
the suspended flag together, clear() rolls both back when the native
stop fails, and consumeForRestore() drops the suspended latch before
handing the snapshot to the restore reload — the ordering the bounded
stop-report redelivery depends on. Pure eligibility predicates stay in
tv_background_suspend_policy.dart; orchestration stays in the
lifecycle part.
The live fallback ladder, retry latch, timeline suspend/resume, and
exit-on-resume flags were poked directly from the error handler, the
play-intent path, the playing-state listener, and lifecycle handling.
Give the live part named operations (_beginLiveLadderRetry,
_retryLiveStreamForPlayIntent, _resetLiveLadderOnPlaybackRestart,
_stopLiveSessionForTvBackground, _consumeLiveExitOnResume) and move
the timeline suspend/resume helpers over from lifecycle, so
LiveTvSessionState is mutated only by live_tv.dart and the initial
tune in the start composition root.
Adjacency, loading flags, the Play Next prompt/countdown, the
transient-retry budget (#1867), the completion latch, and the per-
screen adjacency loader were thirteen loose State fields written by
the episode parts, the reload engine, and the stream listeners. Move
them into EpisodeSessionState — the episode analog of
LiveTvSessionState — so the vertical's mutable state has one home and
the reload engine's touch points (clear prompt on open, reset
adjacency and retry budget on swap, record failure reason) are visible
as writes to one object. Logic is unchanged; the screen disposes the
state object instead of the raw timer.
The dead-stream recovery budget, its progress-based refill, and the
parked latch (#1520) were four State fields written from the commit
chokepoint, the position listener, the reload engine, seeking, and the
transport-intent handlers. Move the state machine into
SpuriousEofRecovery with explicit verbs (interceptEof, retry,
clearPark, resetBudget, onPositionAdvanced); the reload call rides one
injected callback that fills the common in-place-reload arguments. The
reload outcome enum becomes the public MediaReloadOutcome so owned
helpers outside the screen library can consume it.
The mutually-exclusive playback transition state, its lease identity,
the idle completer, and the playback generation counter were four State
fields manipulated by centralized screen methods. Move them verbatim
into PlaybackTransitionGate (tryAcquire/owns/advance/release/forceIdle/
waitForIdle/beginGeneration) so lease discipline is enforced by one
type and parts can no longer touch the raw fields. The transition enum
and lease type become public in the gate's library; leases are only
mintable through tryAcquire.
_hasRenderedFirstFrame had five writer files with hand-rolled
snapshot/restore pairs in the reload and channel-zap rollbacks, and the
paired _hasFirstFrame ValueNotifier was reset independently along the
open path. Move both flags into FirstFrameGate with explicit verbs —
markReady, resetUiForOpen, resetRenderedForAttempt, reset,
forceUiReadyOnFailure, snapshot/restore — so the UI-vs-reporting
asymmetry and the transactional rollbacks are enforced by one type.
The uiReady notifier keeps its identity across attempts; the video
surface, controls, and buffering overlay hold it by reference.
Unit-tests the gate's verb semantics.
Shader preset application, ambient lighting enable/restore/toggle, and
video zoom/boxfit lived as an extension reading per-attempt services
off the player State. Move them into VisualEffectsController, a plain
State-owned helper; the shader, ambient-lighting, and video-filter
services are injected as late-bound getters because all three are
re-created per playback attempt and nulled in teardown. Rebuilds keep
flowing through the single requestRebuild callback.
The CompanionRemoteReceiver callback install/uninstall, home-fallback
save/restore, provider sync handle, and receipt-time volume/seek
dispatch lived on the player State. Move them into
CompanionRemoteBinding, a plain State-owned helper: the binding
instance replaces the State as the receiver's playerOwner token with
identical identical()-guard semantics, and the provider is captured at
bind time because context.read can fail during dispose. Subtitle/audio
cycling stays on the State — the drain loop is bound to the playback
transition lease — and is passed in as callbacks.
The Android TV background suspension latch, availability policy, and
resume/rewind restore logic lived as an extension on the 149-field
player State, with the suspension flag read and reset from three other
parts. Move them into MediaControlsScreenController, a plain
State-owned helper following the established player pattern: the
manager, player, and current item are injected as late-bound getters
because they are re-created per playback attempt. The controller now
solely owns the suspension latch; teardown clears it via
resetSuspension() instead of writing the State field directly.
episode_navigation.dart fused two responsibilities: the episode
next/previous vertical and the screen's generic in-place media
transition engine (_switchPlaybackSource, _performPlaybackSourceSwitch,
_selectSourceSubtitleLocally, _reloadMediaInPlace,
_reapplyScopedPlayerPrefsForItemChange), which lifecycle restore,
spurious-EOF recovery, Watch Together, and the companion remote also
drive. Move the engine and deferTranscodeSubtitleSelection verbatim
into playback_reload.dart so the episode part contains only the
episode feature. No code changes beyond the file split.
Two private formatters mapped the canonical server resolution label to
display text (media_quality_labels, media_version) with edge-case
drift, and several single-consumer helpers added indirection without
callers:
- add shared resolutionDisplayLabel in resolution_label.dart and
migrate MediaVersion.displayLabel and the quality-label builder to it
- drop the resolutionLabelFromHeight compat re-export from
jellyfin_mappers (no remaining consumers) and its stale doc note
- delete TraktCatalogSource.membershipKeysFor, byte-identical to the
CatalogWatchlistMachinery default
- derive the rating sheet backend label from
MediaBrowserDialect.productName instead of a hardcoded switch
- inline EndpointFailoverManager into failover_http_client.dart, its
only importer, and remove endpoint_failover_interceptor.dart
MAL token refresh waited on the general 20s request timeout while the
Trakt and MDBList refresh paths use the dedicated 15s refresh timeout,
so a hung refresh held the 401 retry longer on MAL than on its
siblings. Align MAL with the shared refreshTimeout.
Google Play rejected 2.16.0 (versionCode 142) for an Android Automotive
navigation dead end: on a car, "Sign in with Plex" resolves to the
in-app QR wait, which offered only Retry — and the AAOS system bar has
no back button, so a touch user could not leave the screen without
killing the app. Add a Cancel action to the QR and browser-polling
states that aborts the attempt and returns to the initial sign-in
actions on every platform.
Verified on an API 34 Automotive emulator: Cancel returns to the
landing with the Jellyfin/Emby paths reachable.
Audio Normalization on the mpv path made every video start with stuttering
audio on Linux: dynamic-mode loudnorm always outputs float64 at 192 kHz, so
the AO opened at f64/192k and PipeWire had to convert/resample on the
deadline-critical path (~4x the per-cycle DSP work) while playback startup
load was still settling. Appending mpv's native format filter pins the
chain back to 48 kHz float, so the conversion happens once on the buffered
decode side and the AO opens a normal stream. Integrated loudness output is
unchanged (-14 LUFS, verified via ebur128). mpv's own format filter is used
instead of lavfi aformat so the fix does not depend on which lavfi filters
each platform's bundled ffmpeg compiles in.
Addresses the normalization half of #1720; the residual startup
micro-stutter with normalization disabled is a separate regression.
Sleeping the device mid-video with "Ask for profile on app open" enabled
left the app wedged on an unresponsive Choose Profile screen (Nvidia
Shield). The picker is pushed on the root navigator, but the player's
focus self-heal only consulted its own nested profile-session route, so
it yanked D-pad focus back behind the picker whenever it lost it.
Route currency now walks every enclosing navigator (isRouteChainCurrent)
before the player reclaims focus, primes loading-phase navigation focus,
or claims the surface on window focus. The resume-time prompt is also
skipped entirely while a video player is active: waking mid-stream
resumes the stream instead of stacking the picker over the session.
close#2034
Closing an episode context menu (or its Rate / File Info sheets) on a
show opened from a Home section scrolled the detail page back to the
top, and on shows with many seasons jumped the season selector back to
the entered-from season. The Home path parks invisible focus on the
initial episode/season target; dismissing the menu restored focus to
that parked node, whose focus-gain auto-scroll then yanked the viewport.
Focus chrome is already keyboard-mode-only, so make the focus-gain
reveal match: FocusableWrapper, FocusableChipStateMixin, and
FocusableTileStateMixin now scroll into view only during keyboard/D-pad
sessions. The gate reads the tracker's live state (new
InputModeTracker.currentMode) because the inherited provider is one
frame stale on the first navigation key of a session. The touch OSK
search submit keeps its jump-to-results via an explicit reveal, matching
the existing pointer-mode convention of pairing requestFocus with an
explicit scroll.
close#2031
Plex Discover's hub, search, and related endpoints ignore includeGuids,
so items opened from those rows carried no tmdb id and the Request
action never appeared — only watchlist items (whose endpoint does
return Guids) could be requested. The gate was frozen in initState from
the row item; it now reads the detail-enriched item, whose metadata
fetch does bring the tmdb id.
close#1959
When mpv bitstreams (audio-spdif), audio-params describe the IEC 61937
carrier - 192 kHz "stereo" for E-AC-3 - so the performance overlay told
users their 5.1 track was playing as 2ch stereo. Every recent "EAC3
downgraded to PCM" report in #1300 reads back this string while the
receiver route stays healthy.
Show the source track's channel layout and sample rate instead, plus an
explicit Passthrough row naming the bitstreamed codec, on both the
mpv-channel and Android mpv-fallback stats paths.
Finishing an episode of a show set to rewatching stomped the entry back
to watching. The scrobble write now reads the entry's status and rewatch
count in the same request as the episode count, keeps REPEATING /
is_rewatching entries rewatching, starts a rewatch when progress lands on
a completed entry, and bumps the rewatch count when a rewatch completes.
close#2026
Swipe navigation felt sluggish next to native tvOS apps: Plezy priced a
focus step at 0.55x the focused item's extent, throttled repeats to 140ms,
and stopped dead on finger lift - the inverse of the native engine's feel.
Retuned AppleTvRemoteTouchService to on-device measurements of the native
focus engine (issue #2006):
- one focus step per fixed 400pt of touch travel; drop item-extent scaling
(measured step distance was identical across a 230pt and a 345pt axis)
- repeat cadence 140ms -> 60ms (native median)
- fast lifts glide one or two extra steps (>=2000 / >=8000 pt/s) within
~130ms of the lift; reversal pivots and new touches cancel the glide
Posters and episode thumbnails looked noticeably soft on Fire TV sticks and
similar boxes: the auto-detected reduced tier capped artwork DPR at 1.5 and
tightened tile decode caps, so every tile was fetched at 56% of its pixels
and upscaled twice on large 4K panels.
Drop the reduced-tier DPR cap and the poster/square/thumb decode caps so
tiles fetch and decode at full TV density on every tier; raise the reduced
image-cache budget to the 64MB TV baseline to absorb the larger tiles.
Backdrops keep their scrim-masked ~720p reduced caps, and the display budget
factor stays pinned to 1.0 there, so the low-RAM art ceiling is unchanged.
close#2020
The custom sidebar read as an ad-hoc widget: collapsed rows were the
expanded layout clipped to the strip, the libraries tree used a
staircase of indents and mixed row heights, and hover expansion pushed
the whole content area sideways.
Rebuild the rail on M3 Expressive geometry: collapsed destinations are
icon-only 56x32 pills centered in an 80px strip (48px icon-only on TV),
expanded rows are fixed-height destinations with a full-width stadium
indicator, and the two layouts crossfade/lerp during the width morph
instead of clipping. The libraries section is flattened into full-width
destination rows under a chevron header at standard destination
metrics, hover/touch expansion overlays content as a rounded, shadowed
panel over a dimmed scrim instead of pushing it, and rail width, item
morphs, content translate, and bleed all share one duration/curve.
HEVC files played through the mpv backend on the Nvidia Shield show a
solid blue screen with audio: under hwdec=mediacodec, gpu-next samples
the decoder output as a samplerExternalOES that libplacebo declares in
both shader stages, and the Tegra GLES linker rejects the pair ("struct
type mismatch between shaders for uniform"), failing every frame. The
in-chain gpu fallback never engages because gpu-next initializes fine,
and mpv/libplacebo master reproduce it unchanged.
DV reshaping — the reason gpu-next exists on Android — only happens
under software decode, so offer gpu-next exactly there and keep the
legacy gpu VO for hardware sessions on both the primary mpv backend and
the ExoPlayer fallback core.
close#2010
Searching a name shared by a movie, a show, and audio drowns the wanted
item in episode and track rows. The search screen now offers kind chips
(All, Movies, TV Shows, ...) above the results; chips derive from the
pre-rank candidate pool, and selecting one re-ranks that kind's
candidates with the full display budget, so a kind crowded out of the
trimmed All view still surfaces everything the servers returned for it.
Chip switches never refetch, and the filter falls back to All when a
refined query no longer returns its kind.
close#2001
Removing an item from Continue Watching on Emby posts HideFromResume,
which only the dedicated /Users/{uid}/Items/Resume route honors: the
IsResumable /Items query and the per-series /Shows/NextUp lookups kept
returning hidden rows, so removed items blipped back on every refresh.
Emby's dedicated route conflates both shelves (in-progress items plus
one zero-position next episode per started series), so every playback
surface now reads one hide-aware window from it and splits the rows by
PlaybackPositionTicks: positive rows feed Continue Watching, zero-
position episode rows feed Next Up. The 1+N per-series Next Up
reconstruction is deleted; one recency scan still supplies the play
dates and the NextUpDateCutoff window. Jellyfin request strings are
unchanged.
close#2003
Browsing artwork showed PMS PhotoTranscoder log lines flagged
"upscaled: 1" on every request. The flag merely echoes the upscale=1
query param Plezy sent on every cover request (PMS prints it before
locating the source, including on cache hits and pure downscales), but
the param had a real cost: whenever the requested box exceeded the
source — hero art at the 4K display budget (3840x2160 requests against
the near-universal 1920x1080 agent art), portrait backdrops on >1080p
phones — PMS genuinely enlarged the image server-side. Verified against
PMS 1.43: ~2.5x the transcode time and transfer bytes for zero rendered
detail, since the client covers the slot on the GPU regardless and the
fit-policy decode bounds never enlarge.
Send upscale=0 on cover requests too: downscale output is byte-identical
and oversized requests now return the native image. Jellyfin needs no
change — MaxWidth/MaxHeight never enlarge.
close#1975
Closing the window now runs a graceful exit (so trackers get their
terminal scrobble), but the teardown kept the window on screen for
seconds while screens visibly emptied as servers disconnected.
Hide the window as the first step of exit teardown, bounded so a
stalled platform channel cannot hold an already-accepted exit open, and
cover every graceful-exit entry (window X, Cmd+Q, OS-initiated), not
just the close button. Add a terminal MultiServerManager.shutdown() that
closes the status streams before draining clients: the still-mounted
widget tree must never observe an "all servers gone" snapshot mid-exit,
which would flip the app into offline UI and dismantle screens (and
with it the teardown-driven AppLifecycleListener used-after-dispose
assert). A health result landing after shutdown is a silent no-op.
Supersedes the flag-based approach in #1999.
Exiting the player after enough progress moved the played item to the front of Continue Watching left D-pad focus at its old position, now occupied by a completely different item.
TvBrowseRail and HubSection now remap the focused index to follow the focused item identity when a hub reorders underneath it, falling back to the same series replacement entry when a finished episode is swapped for the next one, and HubSection focus memory is remapped so re-entering the row lands on the same item.
close#1987
Connected MDBList accounts were omitted from Explore, so their watchlists could not be selected or displayed. Register the profile-scoped MDBList client as a catalog source with watchlist mutations and search.
close#1888
A relay connection can win the phase-1 race (plex.tv edge answers fast),
gets saved as the preferred endpoint, and the cached URL then wins the
deterministic head-start probe at every bind — pinning future sessions to
relay's 2 Mbps cap even after direct connectivity returns, surfacing as
HTTP 500s on Original quality. A session that lands on relay legitimately
(direct down at connect time) stays there all evening: re-optimization
only fired on connectivity events.
Relay is now a fallback tier in the race: all probes still start at once,
but a relay success is held until every direct candidate has failed, and a
cached relay URL gets no head start. A single persist gate refuses relay
at all four endpoint-save sites, classified against both the live server
and the connect-time capture so rotated relay URIs cannot slip through.
While the active endpoint is relay, a bounded-backoff re-probe (30s/60s/
120s) re-races the candidates so a returning direct endpoint promotes
away without a restart.
There was no way to tell from the detail screen that a movie or episode
has multiple versions (theatrical/extended cuts, 1080p/4K encodes); the
only entry point was the hidden Play Version item in the overflow menu.
The Play button now becomes a Material 3 Expressive split button when
the item carries more than one version and its server is reachable: the
main segment keeps plain Play (which already resumes the remembered
version), and a narrower chevron segment runs the existing Play Version
flow (version picker, quality picker when the backend can transcode,
preference save). The flow itself is extracted from the context menu
into a shared promptAndPlayVersion helper, and FocusableActionBar gains
per-action spacingBefore so the joined pair can sit tighter than the
rest of the row. Transcode-only quality picking stays in the overflow
menu so the chevron keeps signaling a real version choice.
close#1881
Changing playback speed, shader preset, aspect ratio, or a sync offset in
the player always rewrote the global default, so a show watched at 1.5x
forced 1.5x onto movies, an Anime4K pick followed the user into
live-action libraries, and a file-specific subtitle offset leaked onto the
next title.
Each of those settings now has a scope under Settings > Video Playback >
Remember Player Changes: don't save, everywhere (default, pre-existing
behavior), per library, or per show/movie. Player writes route through the
configured scope; playback start and in-place item changes resolve the
value for the current item, falling back to the global default. Items
without the needed identity (live TV placeholders, Jellyfin/Emby downloads
without a stamped library) keep the global behavior. Resolution consults
only the configured scope, so entries saved under a previous scope become
inert instead of shadowing the new one.
close#1984close#1322close#1616
On iOS and Apple TV the lock screen, Control Center, and iPhone remote card only offered previous/next track for video, so the transport buttons restarted the episode or jumped to the next one instead of skipping a few seconds.
MediaControlsManager suppressed the MPRemoteCommandCenter skip commands on all Darwin platforms because they displace the next/previous buttons — the right call for music, the wrong one for video. The suppression is now a per-surface policy: the video player opts in with preferSkipOverTrackButtons and advertises the in-player small-skip step (seekTimeSmall, default 10s) via setSkipIntervals, while music keeps next/previous as its lock-screen transport. Skip events already routed through MediaControlRouter to _seekRelative on every platform, so the OS-echoed interval drives the actual seek.
close#1994