Commit Graph
2957 Commits
Author SHA1 Message Date
edde746 eaccef33dc fix(subtitles): clear a displayed ASS cue immediately when subtitles are disabled
Turning subtitles off (or hiding them) while an ASS/SSA cue was on screen left
that cue painted until its natural end time on the ExoPlayer backend: AssHandler
nulls the libass track, after which every render returns null, no payload ever
reaches the GL thread again, and the overlay keeps its last swapped atlas. The
existing invalidateSubtitles() call from the #1387 fix could never repaint it.

The atlas pipeline now detects a trackless render request and hands the GL
thread one zero-quad payload, which clears and swaps a transparent frame. The
clear is keyed on the renderer state generation so one dropped as stale is
retried, and it self-heals from per-video-frame requests while playing; the
existing invalidate on the disable transition covers the paused case.

close #1884
2026-08-16 12:42:04 +02:00
edde746 2ff3b350ae feat(player): give sync delay sliders 50ms steps over a ±10s range
The sync delay slider spanned ±60s with 100ms steps, making fine
adjustment between e.g. 100ms and 200ms impractical on touch and mouse.
The slider now covers ±10s at 50ms per step (taps and D-pad included),
while the +/- step buttons still reach the ±60s absolute limit via
long-press, so existing large saved offsets keep working and display
their true value.

close #1907
2026-08-16 09:18:49 +02:00
edde746 bb642fce03 fix(plex): send Live TV favorite updates as JSON so they persist again
Adding or removing a Plex Live TV favorite channel never persisted: the PUT
to epg.provider.plex.tv/settings/favoriteChannels answered 400. The dio ->
package:http migration (15b22f75) lost dio's implicit JSON content type, so
the JSON-encoded favorites list went out as text/plain and the Plex cloud
refused to parse the body. Verified live: the identical payload succeeds
with application/json and the mutation persists.

MediaServerHttpClient now defaults content-type to application/json for
structured bodies. Callers and client defaults still win (the headers map
is case-insensitive and already populated), so Jellyfin's pinned default
and the octet-stream artwork upload are unaffected; favorites is the only
Plex request with a JSON body.

close #1878
2026-08-16 09:14:28 +02:00
edde746 8069683bd3 fix(ios): give music playback lock-screen and headphone controls
Music on iOS played through a mixable audio session: mpv's audiounit
output requests mixWithOthers unless audio-exclusive is set, and a
mixable session disqualifies the app from Now Playing. iOS ignored the
published metadata and remote-command targets, so the lock screen showed
no controls, headphone buttons drove the previous media app, and other
apps kept playing alongside plezy.

Set audio-exclusive on the audio-only core at init on iOS — the same
contract the video player already applies at playback start. Its only
effect there is dropping mixWithOthers.

close #1921
2026-08-16 09:00:49 +02:00
edde746 d1d393eec0 fix(plex): stop timeline heartbeats once the server terminates the session
Pausing past the server's paused-session limit (or an admin stop) removed
the session on PMS, but Plezy kept sending paused timeline heartbeats,
re-registering it as a zombie session the server could no longer clear.

Plex signals the termination with terminationCode/terminationText on the
MediaContainer of the next timeline reply. Detect it, close the reporting
session with one final stopped report at the current playhead (which
removes the session row), and suppress paused heartbeats plus the
transcode keepalive until playback actually resumes, which opens a fresh
server session.

close #1916
2026-08-16 08:58:57 +02:00
edde746 972b62f6fa fix(watch-together): tell lobby guests the room's control mode
Guests joining an "Anyone" room saw "Host controls playback" and a
locked room until the host actually started something. Control mode
only travelled inside the host's PlaybackState broadcast, and every
broadcast path requires an active media epoch, so an idle lobby had no
carrier at all: guests sat on the joinAsGuest hostOnly default. The v1
protocol's sessionConfig message covered this; the v3 rewrite lost it.

Carry the mode on the host's join messages instead: the directed join
reply every participant already sends to a new peer, and the host's
reconnect re-announce. The field is optional on the wire ('cm'), so
older clients ignore it and rooms with older hosts degrade to the
previous behavior. Guests apply it only from the relay-derived host
peer ID, never from a join's own spoofable isHost flag.

close #1950
2026-08-16 08:48:58 +02:00
edde746 ea528b5213 fix(macos): use CoreAudio for every audio format
The AVFoundation/CoreAudio split still left PCM on AVFoundation and maintained two macOS timing paths. Select CoreAudio as the sole macOS audio output so PCM and compressed streams share the HAL-backed implementation. This intentionally drops macOS AVFoundation spatialization; iOS and tvOS remain unchanged.
2026-08-15 20:09:35 +02:00
edde746 5a0b595466 fix(android): play MKV files with late track metadata
MKV files whose Tracks element follows media clusters could direct-play with audio but no video. Upgrade Media3 to 1.11.0 and cover the extractor regression.

close #1947
2026-08-15 19:08:00 +02:00
edde746 35b3e97730 fix(macos): route compressed audio through CoreAudio
AC3/EAC3 playback could stutter after AVFoundation began handling compressed streams. Decline compressed formats in macOS AVFoundation so mpv falls through to CoreAudio, while retaining AVFoundation for PCM and the tvOS Dolby path.

close #1940
2026-08-15 17:52:51 +02:00
edde746 2f0a6a6bf5 fix(android): preserve PGS subtitle display-plane aspect
PGS cues use their own composition plane, but ExoPlayer sized bitmap subtitles from the cropped video aspect, stretching and displacing them. Infer the plane aspect from Media3 cue geometry and fit it inside the visible video bounds.

close #1945
2026-08-15 15:05:14 +02:00
edde746 d623c2164f fix(player): exit on phone back even with the chrome up (#1938)
4443b761 staged the system-back path (strip/fullscreen/hide/exit), so on
Android a back with the controls visible hid them instead of closing the
player. The PlayerNavigationCoordinator now takes an exitPlayerBeforeChrome
policy; the player enables it on mobile, restoring immediate exit on Back
while TV/desktop keep the staged chrome handling.
2026-08-15 14:02:07 +02:00
Aldo BarrerasandGitHub 8383c7b73b chore: bump go server version, run formatter & regenerate podfile lock file checksum (#1944)
* Bump go server and run formatter.

* Regenerate Podfile lock file checksum.
2026-08-15 13:44:56 +02:00
edde746 be863a0c1c fix(linux): remove the Flutter-texture fallback permanently
The display-agnostic texture renderer restored by 9cdfe759 is deleted
again, this time for good: it is a second, SDR-only rendering stack
(isolated EGL context on Flutter's display plus EGL-image handoff) kept
alive solely to host sessions that cannot bring up the Wayland plane -
X11/XWayland or a failed plane bootstrap - and it was the source of the
native lifecycle and EGL state-churn fixes of the 9f2e0507 era. Linux
video now requires a Wayland compositor; a session that cannot host the
plane fails initialization with VIDEO_PLANE_UNSUPPORTED instead of
silently rendering through the second stack.

Reverts the restore commit's machinery: mpv_texture.cc/.h and
mpv_gpu_bootstrap.cc/.h deleted, the plugin's texture-registrar,
bootstrap, and waitForVideoReady paths removed, MpvPlayer's texture-mode
render-context API dropped, and the Dart-side renderMode setting, its
settings tile, translation keys, and the Player textureId member
withdrawn. The #1874 HDR diagnostic work is unaffected.
2026-08-15 01:20:44 +02:00
edde746 fe3460ad12 fix(linux): drop the unreachable 8-bit render-context retry
The retry added while chasing the 2.13.0 hwdec regression never fires:
mpv probes hwdec interop lazily at the first decode attempt, and its
failure does not fail mpv_render_context_create, so the deep config never
gets rejected and the 8-bit tier is never reached. The actual regression
was the libmpv build losing the DRM providers, fixed in the previous
commit. Remove the dead retry and its prefer_deep plumbing, and correct
the pre-flight comment that claimed the interop probe runs at context
creation.
2026-08-14 21:49:46 +02:00
edde746 ae001d9ff3 fix(linux): restore VAAPI hardware decode and AV1 software fallback in the bundled libmpv
Hardware decoding stopped working for Linux users on 2.13.0 (Fedora 44
report): every source decodes in software, and AV1 plays black video with
audio. Two defects in the pinned libmpv build.

First, mpv's meson 'drm' feature silently disabled itself because the CI
builder lacks libdisplay-info, and every VAAPI path that does not depend
on a display server is derived from it: vaapi-copy's standalone render-node
device (the path 2.12.1 worked on) and the GL dmabuf interop for direct
vaapi. With only the Wayland VA provider compiled in, a machine whose
Wayland VA display fails to initialize has no fallback, and vaapi-copy
has an empty provider list - every source lands on software decoding.
Pin -Ddrm=enabled, -Dvaapi-drm=enabled, -Degl=enabled and
-Dvaapi-wayland=enabled, and add libdisplay-info-dev to the CI package
lists, so a missing piece fails the build instead of shipping silent
software decode.

Second, the bundled static FFmpeg has no AV1 software decoder: its native
av1 codec is hardware-accelerated only, so once hwdec fails there is no AV1
path at all - every packet errors, video hits EOF, the plane goes black
while audio keeps playing. Pin dav1d 1.5.4 (both VideoLAN remotes agree on
the tag object and root commit), build it static before ffmpeg, and pass
--enable-libdav1d.

The build-plan stub test now asserts the hwdec feature flags, the dav1d
static build, and ffmpeg's libdav1d. Verified in an ubuntu:24.04 container
with the production flag sets: meson reports drm, vaapi-drm, vaapi-wayland,
egl and dmabuf-interop-gl enabled, and ffmpeg configures CONFIG_LIBDAV1D=yes
with the AV1 VAAPI hwaccel.

close #1874
2026-08-14 21:49:41 +02:00
edde746 18c9f710bb style(linux): clang-format the native rendering changes 2026-08-14 19:50:28 +02:00
edde746 7937a7e30a style(dart): apply dart format to the Linux rendering changes 2026-08-14 19:30:32 +02:00
edde746 991184b451 fix(linux): repair native compile errors caught by the CI build
The first GitHub Actions build of the Linux path failed on six issues the
macOS host could not catch:

- HandleFrameDone is a static handler; CancelFrameAckWatchdog() needed the
  explicit self-> qualification.
- handle_texture_ready_result/handle_ready_timeout call
  release_video_resources before its definition; forward-declared.
- finish_leg captured self without using it (Werror).
- CanCommandOutputProperties was private; made public for the kUnknown
  live-core check.
- MPV_ERROR_UNKNOWN does not exist in libmpv; the timeout now reports
  MPV_ERROR_UNSUPPORTED (any non-success serves the latch; the log line
  names the reason).
- The hdr-tone-mapping error path referenced the handler's FlValue value
  inside an async lambda; an owned std::string copy is captured instead.
- The texture register-failure response used a heap string freed before the
  handler responded; use a literal.
2026-08-14 19:29:39 +02:00
edde746 c3dbbf6479 test(music): implement Player.textureId on the fake audio player 2026-08-14 18:20:12 +02:00
edde746 f56264dcce test(player): cover embedded-VO ownership, colour sanitization and styling refusals
Three new Linux startup cases, one isolate each (a second VideoPlayerScreen
in one isolate never reaches initialize):

- A custom mpv config naming vo/gpu-context/gpu-api cannot detach the
  embedded renderer: the writes are withheld by name while ordinary entries
  (sub-scale) still land.
- Unparseable stored subtitle colours (named, 3-digit hex) reach the wire
  canonicalized to the defaults, proving the OPT_COLOR sanitization.
- A refused sub-color write no longer aborts initialization: the write is
  attempted, contained, and playback continues past the styling block.
2026-08-14 18:18:09 +02:00
edde746 9cdfe7591e feat(linux): restore the Flutter-texture path as the SDR fallback
2.13.0 deleted the display-agnostic EGL/Flutter-texture renderer and made
the native Wayland plane the only path, hard-rejecting every session that
cannot host one - X11, XWayland (SteamOS Gaming Mode runs native apps
through Gamescope's XWayland), or a plane whose EGL bootstrap failed. This
restores the 2.11.0 texture path from git history as the fallback:

- Re-add mpv_texture.cc/.h and mpv_gpu_bootstrap.cc/.h (2.11.0 verbatim):
  an FlTextureGL whose populate renders mpv into an offscreen FBO sampled
  by Flutter via an EGL image.
- MpvPlayer gains the texture-mode render-context API (InitRenderContext,
  HasRenderContext, GetEglDisplay/Context, Render(w,h,fbo)) beside the
  plane's InitRenderContextForSurface/RenderToSurface. The isolated ES 2.0
  context on Flutter's display and the X11 display param are exactly the
  2.11.0 configuration hardware decode demonstrably worked in.
- initialize now tries the plane first and falls back to the texture path
  when it cannot be brought up, returning the texture id (Dart's 2.11.0
  'result is int' contract) with waitForVideoReady gating playback until
  the GPU bootstrap settles. Both paths share one mpv core, so the
  plane/texture decision precedes render-context creation.
- hdr-enabled/hdr-tone-mapping are intercepted in texture mode (no plane,
  no HDR); the HDR toggle hides itself via isHDRSupported.
- New Linux setting 'Video rendering mode' (Automatic / Texture) forces
  the fallback - the user-visible workaround for plane-only trouble and
  for the hwdec interop regression, plus translations in all locales.

SDR only on the fallback, matching 2.11.0; the plane path is unchanged.
2026-08-14 18:16:32 +02:00
edde746 48f365ab30 fix(linux): bound HDR transactions and restore hwdec interop fallback
Three failure modes in the 2.13.0 Wayland path, three fixes:

- The mpv leg of an HDR transaction had no timeout: the surface watchdog
  re-armed while it ran, but the HDR method call stayed unanswered when mpv
  never replied, leaving the transaction queue stuck behind a ghost forever.
  A 5 s timeout (sharing the surface's horizon) aborts the transition,
  withdraws any description, resumes presentation, and answers the request
  exactly once via a shared latch; a late mpv reply self-heals through the
  stale-token re-apply path.

- The kUnknown quarantine hid the plane for the whole session when mpv
  stopped answering - the AV1-transparent report was a plane hidden this
  way while sound kept playing. Hiding is now reserved for a core that is
  genuinely going away; a live one presents undescribed (sRGB by protocol),
  and a playback restart clears the quarantine so one poisoned source
  cannot hide every later one. Product decision: visible-wrong beats
  invisible.

- hwdec's dmabuf interop probe runs at mpv_render_context_create time
  against the plane's fresh EGL display/context, and drivers that fail it
  on a deep config silently land every source on software decoding (the
  Fedora 44 report; 2.12.1 created the context on Flutter's display). The
  prerequisites are now logged explicitly at creation, and a failed render
  context is retried once on the 8-bit config tier - the 2.12.1-equivalent
  configuration - with HDR off by the depth gate.
2026-08-14 18:11:14 +02:00
edde746 093109ae09 fix(linux): bound plane presentation against unacknowledged frames
Present() arms wl_surface.frame and frame_pending_ is cleared only by the
frame callback. Compositors are entitled to stop acknowledging frames for
occluded or minimized surfaces - wlroots-lineage compositors (Hyprland) do
exactly that - and nothing bounded the wait: one missed callback froze the
plane on its last buffer forever, because every later render bailed on
frame_pending(). That is the 2.13.0 black-video report on Hyprland: the
first commit is the pre-allocated 1x1 (or pre-video black) buffer, fully
occluded by the opaque Flutter surface, and the callback it armed never
arrives.

Two changes, one stall:

- A 500 ms frame-acknowledgement watchdog in Present(): on expiry the dead
  callback is withdrawn and a fresh present is asked for, so the plane
  re-commits instead of sitting on the latch. A miss budget (5) stops
  poking a surface the compositor is still ignoring; a real
  acknowledgement resets it.

- The very first present is refused until mpv actually has a frame: the
  first forced render happens at setVideoRect time, before anything is
  decoded, and committing that empty buffer is exactly the commit an
  occluded surface ignores. The sticky plane_needs_render flag keeps the
  owed resize refresh pending until content exists, so the first present
  still happens at the right size the moment the first frame lands.
2026-08-14 18:11:06 +02:00
edde746 14cae85931 fix(player): never fail playback on preference writes; keep vo authoritative
mpv 0.40's OPT_COLOR parser rejects anything but #RRGGBB/#AARRGGBB, so a
stored subtitle colour that does not parse made mpv refuse the write with
MPV_ERROR_PROPERTY_FORMAT — and the bare await in _runPlayerInitializationAttempt
turned that into the initialization error screen on every open. Subtitle
styling, volume-max, and the pre-open defaults (start/pause/sid) are now
sanitized (colours canonicalized to hex with fallback to the default) and
non-fatal, matching the existing hdr-enabled tolerance policy: a refused
preference write must never become "this session cannot play video".

The user mpv.conf editor is applied as runtime mpv_set_property writes, and
vo/gpu-context/gpu-api were not withheld, so a vo=gpu-next line re-created
mpv's output as a separate uncontrollable window and orphaned the embedded
render context. Add the VO family to the Linux-owned property set with a
key-aware skip log, a native reject for vo != libmpv on the video core (the
render API is OpenGL-only; gpu-next is windowed by construction), and a hint
in the mpv.conf editor explaining why, with translations across all locales.
2026-08-14 18:07:16 +02:00
edde746 9090ad6283 fix(linux): name refused property writes and surface the hwdec path
The SET_PROPERTY_FAILED error surfaced to Dart carries only mpv's own text
("unsupported format for accessing property"), which names the failure
mode, never the property. Every report of a refused write is therefore a
guessing game. Include name=value in the error description on all three
setProperty branches (generic, hdr-enabled, hdr-tone-mapping) and warn with
the same pair from MpvPlayer::SetPropertyAsync so the HDR transaction's
target-* writes — which never reach the channel — are attributable too.

mpv_request_log_messages is a single global level, and the vaapi probe and
"Using software decoding" fallback are MSGL_INFO, so at "warn" a silently
software-decoding session leaves no trace in the app log. Raise the request
level to "info" and observe hwdec-current natively, logging every decode
path transition from the player instead of relying on an overlay readout.
2026-08-14 18:07:09 +02:00
edde746 8f9ffc76eb fix(emby): show scrub previews from the /Videos/{id}/index.bif transport
Scrubbing an Emby video showed no preview thumbnails on the timeline
while Plex and Jellyfin both worked: Emby's scrubThumbnails capability
was off, so the player never attempted a load. It was off because the
4.9.5 test server answered the preview endpoints with empty payloads —
its extraction task had not run.

Emby's preview transport is a Roku-format BIF at
/Videos/{id}/index.bif?Width=320, the same wire format Plex serves, so
the existing BIF parser handles it unchanged. Enable the capability and
route Emby previews through the shared BifThumbnailService, whose load
now takes a bytes callback instead of a Plex-typed client. MediaBrowser
sources also carry videoAspectRatio from the video stream so the
tooltip sizes itself to the stream. A server without extracted frames
still answers a header-only BIF, which parses to zero frames and keeps
the tooltip suppressed.

close #1930
2026-08-14 13:36:29 +02:00
edde746 12d9865a41 fix(subtitles): render Korean glyphs on the MPV path via bundled Hangul font
GoNotoCurrent lacks Hangul syllables, and the Android libmpv build has no
fontconfig/system-font fallback, so libass could not resolve any Korean
glyph and subtitles rendered as boxes. Ship a Hangul subset of
GoNotoKurrent-Regular beside the default font in the extracted subtitle
fonts directory; libass picks it up as fallback by glyph coverage.

close #1932
2026-08-14 13:02:26 +02:00
edde746 688bfdacb4 fix(profiles): keep offline downloads visible when a profile's servers are unreachable
Switching to a profile whose only server is offline verified the PIN,
failed the bind with zero reachable servers, and rolled back to the
previous profile — whose scope owns none of the downloads. The Downloads
UI then showed nothing while the files and pinned metadata sat intact on
disk. Startup offline mode only covered the cold-start bind, so such a
profile could never be entered while its server was away.

The binder now classifies a settled bind failure as connectivity-only
when the profile expected servers, reached none, and none were
auth-rejected (snapshotting auth markers before the visibility sweep,
which clears them via removeServer). On such a failure,
switchProfileFromUi keeps the profile active when it owns downloads
instead of rolling back; OfflineModeProvider drives the offline UI from
the empty visible-server set. Auth failures, PIN cancels, and
downloads-free profiles keep the existing rollback and error snackbar.

close #1927
2026-08-14 12:20:20 +02:00
edde746 cd4432f27c fix(auth): keep polling Plex PIN after transient network errors 2026-08-14 12:20:20 +02:00
edde746 5f6d9dc610 fix(navigation): keep the sidebar Libraries section collapsed across restarts
Collapsing Libraries in the sidebar only lasted until the app closed. The
next launch always came back expanded, which buries the rest of the nav
menu for anyone running a lot of libraries.

The expansion flag was plain widget state on SideNavigationRailState. It
survived tab navigation only because MainScreen pins the rail with a
GlobalKey, and died on relaunch, profile switch or a layout change. It now
lives in the librariesSectionExpanded preference, so the rail reads and
writes the persisted value and follows a settings reset or import too.

close #1896
2026-08-13 21:44:25 +02:00
edde746 ce151a91e8 fix(player): show AAC instead of mp4a.40.2 in ExoPlayer track labels
ExoPlayer reports RFC 6381 codec IDs (mp4a.40.2, ec-3, dtsc) where mpv
reports ffmpeg names, so the audio track picker and performance overlay
showed raw identifiers instead of friendly names on the ExoPlayer path.

Normalize RFC 6381 audio IDs in CodecUtils.formatAudioCodec, route the
performance overlay's audio codec through it, and cover the video IDs
(hvc1/hev1, av01, vp09) that fell through the overlay's matcher.

close #1899
2026-08-13 21:23:08 +02:00
edde746 922789c014 fix(tv): stop the server session when a backgrounded TV releases the player
On Fire OS, standby never freezes a backgrounded app, so the paused
heartbeats that kept a suspended session "alive and resumable" pinned it
in the server dashboard indefinitely. The live session bought nothing:
the restore path already performs a fresh playback decision.

When the TV background grace expires, stop the heartbeat timer (its
paused tick also keeps a Plex transcode session alive) and report the
session stopped before releasing the native pipeline, since stop()
resets the player state the report reads. Standby entry can drop Wi-Fi
into power-save and mutations never fail over, so a failed terminal
report is redelivered on a bounded schedule, gated on actual backend
delivery and on the suspend still standing so a restored session is
never reported stopped.

close #1911
2026-08-13 20:34:12 +02:00
edde746 a49f506a0b fix(tv): keep quality labels visible when rating badges crowd the detail line
On TV, a movie carrying a full set of attributed scores pushed the resolution and audio labels past the right edge of the detail metadata line, which silently clipped them.

The line now sheds its least useful parts when it overflows -- surplus rating badges first, then the whole ratings slot, then quality labels -- instead of hard-clipping the tail. It also leads with the year instead of the redundant "Movie"/"TV Show" label, matching the desktop hero chip order, and the Discover spotlight line gets the same fitting. Branded badge icons are pinned to their SVG viewBox aspect so the fit is measured exactly.

close #1893
2026-08-13 02:29:01 +02:00
edde746 8cdf04122f fix(tv): pause on the first Select press after skipping the intro
After activating Skip Intro with the remote, the next Select only raised
the player controls and pausing took a second press. The skip button
autofocuses on TV, and every path that hides it dropped focus out of the
controls subtree - the screen node reclaimed the remote and its self-heal
consumed the next key to raise the chrome instead of toggling playback.

Hand the remote back to the player surface whenever the skip button
disappears while focused: after a skip seek, when the playhead leaves the
marker window, and when the auto-dismiss timer hides the button. The
credits-at-end path is exempt because the play-next flow requests its own
focus.

close #1890
2026-08-12 23:03:44 +02:00
edde746 3e1481c238 fix(windows): bundle the MSVC runtime so an outdated redist stops crashing launch
A user's plezy.exe 2.13.0 crashed on every launch with an access violation
in C:\WINDOWS\SYSTEM32\MSVCP140.dll 14.29.30139.0 (VS 2019-era redist). CI
builds with the current VS 2022 toolset, and since 17.10 std::mutex uses a
constexpr layout that an older msvcp140.dll misreads, so the app dies inside
the DLL before the first frame. Nothing shipped the runtime: the bundle,
installer, portable archive, and MSIX all relied on whatever redist the
machine happened to have.

Install the MSVC runtime DLLs next to plezy.exe via
InstallRequiredSystemLibraries; app-local copies precede System32 in the DLL
search order, so the bundled version always wins. One install rule covers the
installer, portable 7z, and MSIX, which all package the Release directory.
The Windows bundle verification in build.yml now requires msvcp140.dll and
vcruntime140.dll so a silently missing redist dir fails CI instead of
shipping.
2026-08-12 23:00:36 +02:00
YorickandGitHub 0d24240af4 fix(linux): defer buffer_scale until first frame is presented (#1876)
* fix(linux): defer buffer_scale until the first frame is presented

The video plane's wl_egl_window starts as a 1x1 placeholder. When playback starts, SetRect() sends wl_surface.set_buffer_scale(2) and resizes the window, but mesa commits the EGL surface's pre-allocated 1x1 back buffer on the first eglSwapBuffers. A 1x1 buffer is not an integer multiple of scale 2, so the compositor raises WL_SURFACE_ERROR_INVALID_SIZE, tears down the Wayland connection and plezy exits (issue #1872). At display scale 100% the same 1x1 buffer is legal, which is why that workaround worked.

Defer sending the new scale until after the first commit: the first buffer is 1x1 at scale 1 (always legal), then the scale change lands on the wire and applies to the next commit, whose buffer mesa allocates at the resized window size. The one-frame 1x1 flash is mpv's black first frame - imperceptible.

* fix(linux): gate buffer_scale on a first-frame latch, reset state on destroy

Address review: buffer_attached_ cannot represent "first frame
presented" - DetachBuffer() clears it while the committed scale stays
on the wire, leaving a crash path (present at scale 2, detach, move to
a scale-1 display, then a swap commits a scale-1 buffer while scale 2
is still active).

Replace it with first_frame_presented_, set once a frame has been
presented and cleared only when the wl_surface is torn down, so scale
changes queue even with no buffer attached. Reset scale_sent_ to 1 in
Destroy() too: a freshly created wl_surface starts at scale 1, and a
stale value would suppress the first scale request after recreation.
2026-08-12 11:18:36 +02:00
edde746 9769c4e092 chore: bump MPVKit to 1.0.20 2026-08-12 08:56:33 +02:00
edde746 f291425f20 fix(tvos): increase AVFoundation presentation lead 2026-08-12 02:22:44 +02:00
1c1bba7e27 Update installation instructions for Arch Linux (#1870)
* Update installation instructions for Arch Linux

Arch now maintains an official package for plezy.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-12 01:50:40 +02:00
edde746 26e98e898d fix(desktop): reserve root Escape for leaving fullscreen instead of quitting
Physical-keyboard Escape at root Home now exits window fullscreen on
Windows and Linux the way it already did on macOS, and never arms the
press-back-again quit — so Escape aimed at fullscreen can't close the
app. Remotes, gamepad B, and system back keep the double-press exit.

close #1748
2026-08-11 11:15:51 +02:00
edde746 6663353895 fix(player): retry episode advances that fail on a transient server blip
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
2026-08-11 09:08:07 +02:00
edde746 83c50d93a2 fix(subtitles): flatten atlas-overflow ASS frames into an RGBA composite
Signs built from hundreds of overlapping paint-stroke drawings (masked
smartphone screens and similar typesetting) sum to far more bitmap area
than the paged ALPHA_8 atlas can hold: the issue sample needs 5 pages of
16M px at 1080p and 19 at 4K against the 4-page cap, so the packer
dropped the painter-order tail - the sign's text and late mask strokes.

Move the packer out of the JNI file into AssPack.c (pure C, compilable
against a desktop libass for verification) and add a composite fallback:
when a frame can never fit MAX_ATLAS_PAGES pages or the vertex budget,
blend the image list CPU-side into one premultiplied RGBA rect over the
union bounding box - O(frame area) instead of O(sum of image areas) -
and draw it as a single quad through a new MODE_COMPOSITE path in the
GL renderer. Oversized composites reuse the existing grow-and-re-render
contract; the atlas fast path is byte-identical for every frame that fits.

Verified with a desktop harness compiling the shipped AssPack.c against
fork libass 0.18.3 and the issue sample: all atlas-mode frames byte-match
the previous packer, the sign's frames composite with zero truncation and
byte-match a reference full-frame blend at 1080p and 4K, and the
multi-page composite grow path round-trips.

close #1868
2026-08-11 08:41:56 +02:00
edde746 3a704a2b9b 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).
2026-08-10 23:04:35 +02:00
edde746 aff6b6576f fix(player): offer the TrueHD MAT carrier on API 29-32 routes
Carrier-or-decode gated the carrier on getDirectPlaybackSupport, which only
exists on API 33, so every older route force-decoded TrueHD - including
routes that bitstreamed it before the carrier existed. The #1863 Fire TV
Stick 4K Max is Fire OS 8 (API 30): its HDMI route advertises raw TrueHD
and IEC 61937 at 8 channels, 2.12.1 passed TrueHD through, and 2.13.0 hands
the same stream to the FFmpeg decoder. The Shield is API 30 as well.

API 29-32 now asks AudioTrack.isDirectPlaybackSupported about the exact
192kHz/7.1 IEC tuple before offering the carrier. It is coarser than the
API 33 probe - it cannot tell bitstream from offload - but an IEC 61937
track is PCM-shaped by definition, so direct support means the route
carries the frames. getMinBufferSize stays as the precondition on every
tier, and a route that still lies fails AudioTrack initialisation, which
the audio recovery path already answers by blocking direct output and
force-decoding in place. Below API 29 nothing can vouch for the tuple, so
the carrier is still not offered and TrueHD decodes as before.

The tier decision is split from the platform probes so it is unit-testable;
each probe is consulted only on the tiers where its API exists.
2026-08-10 22:54:17 +02:00
edde746 d19ec625cd fix(tv): remove the background Watch Next refresh
2.13.0's ShelfRefreshWorker boots a second headless FlutterEngine in
the app process to refresh the launcher row every six hours. Its
foreground guard is checked only once at worker start, so launching the
app during a run leaves two engines sharing a low-RAM TV for up to 90
seconds, and a failed run retries with backoff. Suspected of
destabilizing the compositor on the 32-bit TCL panel in #1862. The tvOS
Top Shelf live fetch is unaffected and stays.

The foreground sync pipeline keeps the row fresh while the app runs, as
before 2.13.0. Updated devices still carry the persisted periodic job,
which would wake the process once more only to fail instantiating the
deleted class; the package-replaced receiver now cancels it.
2026-08-10 22:30:51 +02:00
edde746 c2bd1d28fd fix(subtitles): load external subtitle files with the media whether or not selected
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
2026-08-10 21:23:55 +02:00
edde746 ea356a6112 fix(plex): use fMP4 HLS for video transcodes so HEVC presets stop corrupting
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
2026-08-10 20:38:45 +02:00
edde746 69fadc220d chore: clean up code comments 2026-08-10 20:28:41 +02:00
edde746 5611c6785a chore: bump version to 2.13.0 (129) 2026-08-10 19:03:03 +02:00
edde746 01279e5fbb fix(automotive): stop blocking parked playback when the car service has no verdict
CarRestrictionsMonitor.bind() treated a null getCurrentCarUxRestrictions() as
a restricted verdict with supported = true. Dart then latched the restricted
state, every play path refused to start, and on a car that stays parked no
restriction transition ever arrives to correct it — video never played for the
whole session. This is the failure mode behind the Play Automotive rejection of
version code 128 ("unable to play video content"): a review bench whose car
service tracks no restrictions for the resolved display gets exactly that null.

A missing verdict now stays pending instead: Dart keeps lifecycle gating
(parked, foregrounded video plays; while driving the platform blocks the
activity, so DD-2/DD-3 still hold), the registered listener adopts the first
real verdict, and every later getState retries the read. Listener registration
is identity-guarded because retries re-enter bind() with the same cached
manager instance.

Verified on an API 34 Automotive emulator: CarRestrictionsMonitorTest passes on
both connect routes, parked playback starts, driving pauses it behind the OS
blocking screen, and parking again leaves it paused until the user resumes.
2026-08-10 19:00:50 +02:00