Video on Linux went through a Flutter texture: 8-bit sRGB, which cannot carry
HDR at all, and which forced a whole-window Flutter recomposite for every video
frame. This moves it onto a wl_subsurface stacked below the Flutter surface, with
mpv rendering into an EGL window surface on it through the libmpv render API. The
subsurface is desynchronized, so video and UI now present independently.
With the plane in place HDR follows: the surface is described to the compositor
through wp_color_manager_v1 as the source's own curve and gamut - PQ or HLG,
BT.2020 - carrying whatever HDR10 static metadata the stream actually declares.
The description and the buffer it describes land on the same commit, staged and
validated before mpv is switched, so a PQ frame is never presented labelled sRGB.
A five-second watchdog bounds the one wait a compositor could otherwise leave
hanging. A session that cannot host the plane - X11, or a compositor without
wl_subcompositor - fails initialize with VIDEO_PLANE_UNSUPPORTED naming the
reason: the texture path is gone, and refusing by name beats degrading to
something the user cannot see. An SDR output, a missing capability or an 8-bit
config keep the plane and simply leave it undescribed.
The output's colour state is trusted only when it has been earned. Every landed
property step records itself as it lands; a reset or sequence that cannot
finish downgrades its result to unknown and marks the applied-output cache
untrusted until a clean apply earns it back. A plane whose output state cannot
be named is quarantined - hidden, its description withdrawn - and the
quarantine is recorded state: an unrelated visibility change cannot put a
mislabelled plane back on screen, and only a commit that resolves to a nameable
outcome lifts it. A rect collapsing to zero detaches the buffer exactly as
hiding does, a refused setVideoRect drops the Dart-side sent-rect cache so the
next layout pass retries for free, and a refused tone-mapping pick tells the
user instead of dying in a log.
NVIDIA's Wayland EGL (through at least 610.xx) offers no 10-bit unorm window
configs, so the plane takes half-float as the tier between 10-bit unorm and
8-bit, declares the whole surface opaque so the compositor never reads the
alpha those configs carry, and states GL_RGBA16F rather than a 10-bit lie.
Whether the output is in HDR is read from luminance headroom above its own
reference white rather than from the preferred transfer function, which current
KWin no longer answers PQ for; the margin is half a stop, because KWin reports
an undimmed maximum over a software-dimmed SDR white. Validated on an RTX 4090
(driver 610.57.04) under KWin 6.7.4 with locked-exposure photographs.
Who tone-maps is a user choice. The default is the compositor: photographed on a
400-nit HDR output against a PQ chart it keeps 400 -> 1000 nits monotonic and
separated where the player leg flattens them, because the player path drives
mpv's legacy vo_gpu, whose own standalone output scores the same. The gap is the
renderer, not the wiring.
The decision itself - what the source carries, what the output supports, what to
tell mpv and what to tell the compositor - lives in hdr_metadata.h, free of
Wayland and GTK so its luminance validation can be tested without a display
server. Sending an incoherent luminance set is a protocol error that disconnects
the client, so the rules are worth a unit test.
The deb, rpm and pacman packages now declare wayland-client, wayland-egl and EGL:
the plane links them directly and bundle-libs.sh deliberately never bundles them,
since they are coupled to the running compositor and GPU driver.
lib/dev/harness_main.dart is a second entrypoint for measuring this on hardware -
it drives one clip with scripted mpv properties and reports the colour state mpv
actually settled on. Nothing imports it, so it is tree-shaken out of the app.
Verified on a Steam Deck against an external 400-nit HDR display: the compositor
reports PQ / BT.2020, the connector carries HDR_OUTPUT_METADATA, and against mpv
vo=gpu-next on the same frame the shipped build sits 4.90 counts away overall -
closer to the reference HDR player than to its own SDR fallback.
wayland-scanner output for the staging colour-management protocol, which the
native video plane uses to describe itself to the compositor as PQ / BT.2020.
Committed rather than generated at build time: the protocol only appeared in
wayland-protocols 1.41, newer than the version the distributions this app is
built for ship. Vendoring keeps the build working regardless of the host and
adds no build dependency on wayland-scanner. Generated from wayland-protocols
1.49 with wayland-scanner 1.25.0.
Nothing links these yet; the CMake wiring and the plane that uses them follow.
The local .clang-format exempts the generated sources from the runner's style.
This was resulting in two audio stutters per playback resume.
Regression originates in 9f2e05079 (release 2.10.0).
Co-authored-by: Torin Cooper-Bennun <torin.cbennun@googlemail.com>
Every URL the player opens is a media-server stream or a local file, so mpv's
bundled ytdl_hook has nothing to resolve. It still ran an on_load hook per
open and, whenever an open failed, spawned yt-dlp with the full stream URL in
its argv — access token included, readable through /proc on Linux. It also
added ~700ms to every failed open and buried the real "[stream] Failed to
open" line under three ytdl_hook errors.
mpv decides whether to load the builtin script inside mpv_initialize, so this
has to be an option set beforehand rather than a property set from Dart.
Verified against mpv 0.41: --ytdl=yes logs "Loading lua script
@ytdl_hook.lua", --ytdl=no never loads it.
Apple is deliberately excluded: the bundled libmpv is built without Lua, so
the option does not exist there and setting it would only print an mpv error
on every player init.
The Linux native reliability job stopped compiling mpv_player.cc: the
node-conversion builder exposed a leaf named `Bool`, and X11's Xlib.h —
reached through epoxy/egl.h -> EGL/eglplatform.h — defines `Bool` as a
macro for `int`, so the declaration was rewritten into nonsense.
Renames that leaf to `Boolean` across the shared walk and all three
builders. The name is the only thing that changes; no conversion
behaviour differs.
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.
Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.
Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
Audio-only mpv core on every platform (dedicated
com.plezy/mpv_audio_player channels): parameterized android/windows/
linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping
video/window paths (vid=no, audio-display=no, gapless-audio=weak).
MusicPlaybackService drives an in-memory queue with shuffle/repeat,
file-loaded-event gapless arming (property edges coalesce and the
android bridge drops them), per-track progress reporting, OS media
controls, audio focus, sleep timer, and error auto-skip.
PlaybackCoordinator enforces one live native player: starting video
disposes the audio core first.
- Default MPV native log level to "warn" instead of "v" (Linux/Windows),
add setLogLevel method channel to raise it when debug logging is enabled
- Coalesce MPV render requests via atomic compare_exchange on needs_redraw_
- Clear redraw flag before render so new redraws queue during current frame