A player whose predecessor had not released the shared native channel
within three seconds skipped its own native dispose and chained its
release onto that predecessor. If the predecessor's teardown never
completed - one hung 4K session was enough - every later player waited
on the chain, failed to initialize, and playback stayed broken with
"Playback could not be started" until the app was killed.
Initialize and dispose now carry the creating instance's token. The
Android plugins remember which token created the current core and
acknowledge a dispose from any other token without touching the core,
so a dispose that lost the ownership race is provably stale and safe to
send. With that guard, a timed-out ownership wait force-disposes
instead of skipping, settles its release unchained, and frees the
channel for the next session; a dispose watchdog answers Dart even if
a native teardown hangs, leaking that one core instead of wedging all
future playback. Commands wait eight seconds (was three) so a slow but
healthy teardown delays the next session instead of failing it.
Verified on a Shield Pro: 38 back-to-back session races at 0.8-2.2s
gaps with zero failures and balanced teardowns, and a deep-link-over-
playback collision whose stale dispose is ignored, after which Back
returns to the still-playing session and Retry starts the new one.
Apple and desktop handlers ignore the token and keep the historical
skip semantics until they gain the guard.
Android mpv drew every frame through vo=gpu: an ImageReader/GLES copy
pinned to 8-bit RGB0 with a 100 ms timed wait per frame, which
truncated 10-bit and HDR output, raced acquireLatestImage, and
performed badly on low-end TVs.
Video now scans out on a SurfaceFlinger video plane through the fork's
vo=mediacodec: decoder buffers are queued to the surface at their
target PTS (av_mediacodec_release_buffer_at_time), so 10-bit and HDR
dataspaces reach the display untouched and frame pacing no longer
depends on GL vsync. Subtitles and OSD render on a sibling transparent
surface presented for the same PTS, frame-locked by construction.
The plane takes decoder buffers only, so software-decoded video stays
on a GL vo, with a chain-failure watchdog re-initializing the output
when the plane refuses a stream mid-session. The GL fallback is vo=gpu
- gpu-next breaks the Tegra GLES linker (#2010) - and GpuVoPolicy
selects gpu-next only when Dolby Vision RPU reshaping needs
libplacebo. AV1 film grain is applied in the decoder
(vd-lavc-film-grain=cpu) because the GL raster grain fallback is not
available on this path.
Pins libmpv-android v1.1.3, which carries the fork patches this path
rides on: the vo itself, BT2020-PQ EGL window surfaces, GLES direct
rendering treated as slow (H.264 Hi10P software decode on Tegra went
from 0.3x realtime with wrong colors to 1.0x with correct 10-bit
output), and multichannel IEC61937 for ao_audiotrack.
Verified on Shield Pro (Tegra/GLES), Pixel 7 (Mali), Box R 4K Plus
(Amlogic armv7), and Galaxy Z Fold3 (Adreno): 520 play/teardown cycles
and 3 hours of continuous 4K HDR playback; HDR engagement confirmed
via SurfaceFlinger dataspace BT2020_ITU_PQ.
Non-English users saw English text in a dozen places and blank labels in
sixteen more.
The English came from sites that produce their copy away from the widget
that renders it, which is what the structural hardcoded-string check
cannot see: picture-in-picture refused with a raw literal instead of the
pipErrors.notSupported key that already existed; the two Jellyfin/Emby
auth throws missing display: rendered their developer message on the
add-server form; ServerParsingException.toString() fed its English into
the localized "Failed to load servers" wrapper; Watch Together
interpolated the whole PeerError, so a failed create read "Failed to
create session: PeerError(PeerErrorType.timeout): Timed out creating
session" and join printed its prefix twice; the hub and playlist
continuation footers rendered exception.toString(); shader rows showed an
English title over an already translated subtitle; the player queue fell
back to the raw Dart enum name; a Plex home user with no title showed
"Unknown"; a failed player start showed "Exception: Failed to initialize
player"; and the tvOS top-shelf header was hardcoded in an extension that
has no Flutter engine.
The blanks came from three recent features that added English keys
without translations. clean_translations.py filled all 21 siblings with
empty strings, so the Android TV resolution switch, every Jellyfin/Emby
recording-rule field, the demuxer row, the Companion Remote address
caption and the Seerr blocklist pill rendered nothing at all.
Two fixes are structural rather than key swaps. ContinuationStatusSliver
now takes an errorContext and calls a new non-logging
localizedLoadErrorText, so no future throw can leak through it. lib/mpv
stays free of user-facing copy: it raises a PlayerInitializationException
sentinel and a PlayerError.playerInitFailed cause tag that the player
screen resolves to localized text. The tvOS section title travels in the
shelf payload, additively, so an older cache still renders.
Sub-4K content on a 4K TV was always upscaled by the playback device, so the TV's own (usually better) upscaler never received the native signal and could not apply resolution-specific processing.
Adds a Match Content Resolution setting on Android TV: playback picks the smallest display mode that still contains the video — never downscaling it — and rate-matches within that resolution when frame rate matching is also enabled. A resolution-only switch keeps the refresh rate as close to the current one as possible, and the settle watchdog now verifies the requested mode id so such switches still trigger the Android MPV decoder refresh. Transcodes get their target from decoded dimensions on the post-first-frame path. Mode selection is extracted into a pure, JVM-tested DisplayModeSelector.
close#2073
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
On Shield Experience 8.x (API 28) ExoPlayer force-decoded TrueHD to
multi-channel PCM, and enabling passthrough on the mpv backend made every
non-AAC file wedge on an infinite buffering spinner with no audio pipeline
at all.
Two defects behind one symptom pair:
- The MAT/DTS-HD carrier oracle stopped at API 29 because
AudioTrack.isDirectPlaybackSupported does not exist below it, so API 24-28
never offered the carrier even on routes that genuinely bitstream it. Below
API 29 the HDMI AudioDeviceInfo is the only vouching signal, so the carrier
is now offered when an HDMI output explicitly advertises IEC 61937 at
192kHz/8ch; unspecified (empty) capability arrays deliberately do not
count, because an unvouched IEC track that initialises without being
bitstreamed renders as full-scale noise. A route that advertises and still
refuses the track fails AudioTrack init into the existing force-decode
recovery.
- The mpv audio-spdif list was gated on the raw encodings the route
advertises, but mpv's audiotrack AO opens every spdif format as a stereo
ENCODING_IEC61937 track clamped to the 48kHz mixer rate. E-AC3, TrueHD and
DTS-HD MA structurally cannot survive that shape, and a route can advertise
raw encodings while its HAL takes no IEC track at all - naming any codec
there leaves mpv's audio chain stuck before AO init (reproduced on a Shield:
spdif_ac3 selected, no AudioTrack ever opened, playback never starts). The
list is now capped to ac3,dts and additionally gated on the route accepting
the stereo IEC shape, using the same tiered oracle as the carrier. The
primary mpv backend previously wrote a hardcoded five-codec list from Dart;
it now asks the plugin to derive the value from the audio route, like the
ExoPlayer fallback already did.
Verified on a Shield (API 30) with a full-capability EDID: TrueHD rides the
carrier and survives speed transitions (instrumentation), and an E-AC3 title
that previously wedged under mpv+passthrough now decodes and plays with
audio-spdif=ac3,dts.
close#1991
Every mpv open rebuilt the HTTP header list with one awaited channel
round trip per change-list command — with Plex's 9-13 identity headers
that is ~12 serialized round trips sitting between the playback resolve
landing and loadfile, on every mpv backend.
Dispatch the clr and append commands without awaiting between sends and
await them together: the method channel delivers messages in send order
and the native side executes them in arrival order, so the
clr-before-append contract holds while the latency collapses to one
round trip. Failures still propagate out of open() unswallowed.
Enabling audio passthrough on macOS 2.14.0 silenced every AC3/EAC3/DTS
item: the macOS player now pins ao=coreaudio, where audio-spdif redirects
to coreaudio_exclusive. That needs an IEC61937-capable device Mac setups
essentially never have, and with a restricted ao list mpv has no PCM
fallback, so the failed AO init stalls playback with no audio. The toggle
never delivered real bitstreaming on macOS anyway (2.13 decoded through
AVPlayer), so stop offering and applying it there.
Gapless playback still gapped between tracks on network streams: mpv
only opened the armed next track's stream at the moment the current one
ended, so any server whose connect+probe outlasts the audio output's
buffered tail (~0.5s) - remote Plex over TLS, a transcode session
starting up - produced an audible dropout at every transition.
Enable mpv's prefetch-playlist when arming a network track so the open
happens while the current track still plays. Measured on a Pixel 7 the
boundary goes from 60-233ms of inline network work to 9-16ms with no
network activity at all; a failed or superseded prefetch falls back to
the old boundary open. Local fdclose:// arms keep prefetch off: an
early open would consume the fd while playlist-pos still reads 0,
breaking _clearArmedNext's "provably never opened" close proof.
close#1869
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
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.
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.
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.
Nonzero mpv video-zoom flips vo_avfoundation into a per-frame Core
Image re-render that destroys HDR/DV passthrough - DV frames render
near-black on tvOS (verified on Apple TV 4K, DV P7->8.1 content:
panel luma mean 0.0 zoomed vs 87-103 unzoomed at locked exposure).
Zoom now scales the AVSampleBufferDisplayLayer itself (a
sublayerTransform on the container is ignored by the video plane)
via the existing Player.setVideoZoom seam, and VideoFilterManager
pins the mpv property to 0 on backends with native zoom. The layer
tree at 100% stays identical to before: clipping engages only while
zoomed, and updateFrame sizes the layer via bounds/position, which
frame= decomposes to anyway.
macOS keeps the property path (gpu-next zooms losslessly in-shader);
Android is untouched.
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.
A coalesced key-repeat skip pins its target so a slow backend cannot make
the next press rebase off a position the seek has not reached yet. Nothing
retired that pin when something else moved the playhead, so for the ten
seconds it survived, a skip taken after a timeline tap, a chapter jump, an
OS media control or a peer sync resumed from the superseded target and threw
the user back across their own jump.
Publish every playhead movement on the player and retire the pin whenever
the announced destination is not the accumulator's own commit. Overlapping
seeks and backend-chosen relocations arbitrate by which operation the
backend accepted, so a request that was merely asked for cannot speak for
where the playhead ended up.
close#1819
Groundwork for #1300. Establishes the session, buffering and route
handling Dolby's application guide prescribes, and adds the diagnostic
arm needed to find out whether Apple's sample-buffer renderer can carry
Atmos objects at all.
Audio session, per the guide's sequence:
- Adopt the long-form playback profile in one atomic call at app launch
and activate the session there. The SDK only accepts that policy with
category Playback, a Default/MoviePlayback/SpokenAudio mode and no
options, so it cannot be assembled from separate calls.
- Report the resolved rendering mode in the player, hidden unless the
system resolves it. Apple only resolves it for CarPlay and AirPlay, so
an unresolved value means unknown, never "not Dolby".
Diagnostics (Apple TV only, Settings > Video Playback > Atmos Output Test):
- Add a sample-buffer arm. It reads the asset with AVAssetReader at
outputSettings nil and hands the untouched compressed buffers and the
untouched format description straight to the renderer, with a variant
that rebuilds the description the way playback builds it. Every
existing mode went through AVPlayer, so nothing exercised the path
playback actually uses; this is what tells us whether the renderer or
our construction is at fault.
- Add an AirPlay route picker. AirPlay is the only route where the system
resolves the rendering mode and the supported channel layouts, so it is
what makes those observations reachable at all, and the AVPlayer arms
now allow external playback so every arm can be compared on the same
destination.
- Add a session-mode toggle for the one profile difference between the
guide and previous playback behaviour.
- Report the session profile, supported layouts, both format
descriptions, the magic cookie and the renderer status, and release the
session on stop so a failed run cannot contaminate the next one.
Also bumps MPVKit to 1.0.14, which carries the matching audio output
work: the channel layout AVFoundation itself uses for Dolby content, a
renderer-failure observer so the fallback to PCM can actually run, the
prescribed feed ordering and preroll, flush recovery that re-supplies the
discarded audio instead of shifting later audio into its place, and
capability-driven fallback on route and capability changes.
This does not yet fix#1300. Whether the sample-buffer renderer can carry
JOC is still unknown; it removes every difference from the documented
setup that could explain the failure, and gives us the arm to answer it
on real hardware.
setNext() appended raw content:// URIs that mpv cannot open, stalling
playback at the SD-card-download track boundary. Convert to fdclose://
like open(), track the armed fd, and reclaim it via a new closeContentFd
method when the entry is dropped unplayed (close only when provably
unconsumed — playlist-pos 0 before and after the remove; leak on doubt).
The playlist-pos pre-check also keeps the clear path from removing the
playing entry when mpv rolls into the armed track mid-clear.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mpv's http-header-fields string-list parser splits on commas inside
header values, so X-Plex-Device on Apple hardware (model ids like
Mac17,9) produced a colon-less garbage header line that Plex rejects
with 400 'Error parsing HTTP request' — first hit by music direct play
on macOS, latent in the mpv video path too. Deliver headers via
change-list append items (open) and per-entry -clr/-append loadfile
options (gapless arming), each item verbatim and %len%-quoted against
the outer option-list split.
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.
Re-enables Audio Passthrough on Apple TV against the MPVKit EAC3-JOC
AVPlayer sink (pin bump to v1.0.8 follows), adds an AVPlayer test
harness + diagnostics screen for #1300, and makes loudnorm yield while
passthrough is active (fixes a latent spdif conflict on desktop too).
- Extract track lifecycle logic from VideoPlayerScreen (3248→2928 lines) into TrackManager
- Parse isDefault/isForced for subtitle tracks in parseTrackList
- Emit forced flag from ExoPlayer's emitTrackList
- Fix detectSubtitleMimeType failing on URLs with query params
- Add missing external_ ID prefix in ExoPlayer addSubtitleTrack
- Replace polling loop with stream in selectAndApplyTracks
- Handle play() failure in resumeAfterSubtitleLoad
- Handle addExternalSubtitles failure in onBackendSwitched