Commit Graph
48 Commits
Author SHA1 Message Date
edde746 3f03262833 fix(player): recover playback after a lost native-channel handoff
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.
2026-08-26 18:59:28 +02:00
edde746 a1c98b9d27 feat(player): render Android mpv video on the MediaCodec plane
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.
2026-08-26 18:59:19 +02:00
edde746 70ffe8ff15 fix(player): mirror ExoPlayer rate into PlayerState and keep secondary subtitles out of the primary selection
On the default Android ExoPlayer backend the native core never emits a speed property, so PlayerState.rate stayed at 1.0 forever: the speed sheet checkmark, keyboard speed stepping, long-press 2x restore, and media-session rate all computed from a stale 1.0. setRate now mirrors its value into state like setVolume already did.

On mpv backends, a selected --secondary-sid subtitle later in track-list order overwrote the primary selection because parsing ignored main-selection; the track sheet then badged the secondary as primary and replaced the user's primary on tap. parseTrackList now treats main-selection 0/absent as primary and 1 as the secondary selection.
2026-08-21 19:23:39 +02:00
edde746 7a4cdc8b5e fix(player): pin loudnorm output to 48 kHz float to stop startup audio stutter
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.
2026-08-19 23:25:59 +02:00
edde746 05ef93c55b fix(player): keep gpu-next off the Android hardware-decode path
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
2026-08-18 23:21:19 +02:00
edde746 0f016e4e13 fix(player): start the playback fetch before player construction
The playback data resolve — the one network request that must land
before open() — was kicked off only after the SharedPreferences load,
the Windows display-mode sync, the music-session teardown in
claimVideo(), Player construction, and (Android/Exo) a native
setLogLevel, so none of that setup overlapped the round trip.

Move the kickoff to immediately after the settings reads; the resolve
depends only on settings and provider lookups, so display sync, player
construction, and the whole mpv property chain now hide behind the
network latency instead of preceding it.

Also remove redundant work elsewhere on the start path: memoize
PlayerAndroid.getHeapSize (asked again on every open for an immutable
device value), skip the ten subtitle-style settings reads on mpv
backends where setSubtitleStyle is a no-op, and stop navigateToVideoPlayer
awaiting SettingsService.getInstance twice per launch — the external-player
read now sits behind the supportsExternalPlayers guard.
2026-08-17 13:46:51 +02:00
edde746 953acedcab fix(player): pipeline mpv http-header commands at open
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.
2026-08-17 13:46:41 +02:00
edde746 49c117db83 fix(music): open the gapless next track ahead of the boundary
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
2026-08-16 22:40:52 +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 69fadc220d chore: clean up code comments 2026-08-10 20:28:41 +02:00
edde746 bcd6fe9906 feat(linux): HDR video on a native Wayland plane
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.
2026-08-10 08:48:13 +02:00
edde746 df0570b861 style: apply dart format to 21 drifted test files 2026-08-09 17:50:58 +02:00
edde746 e6be5f9fef fix(player): surface a persistent HTTP 503 at open instead of retrying forever
ffmpeg's reconnect loop deliberately retries 503 without bound (#1520), so a
server that keeps refusing the stream at open time left a silent black screen:
ExoPlayer fell back to MPV, MPV reconnected forever, and no error ever reached
the screen. A new open-phase watchdog arms on the first 503 seen before any
frame renders and, after 20s without one, synthesizes a server-http-503 error
that shows an actionable dialog. Mid-stream 503s and live TV keep their
existing ride-out paths.

close #1830
2026-08-08 12:06:32 +02:00
edde746 4816e3928f fix(player): skip relative to the position a jump landed on
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
2026-08-07 08:43:48 +02:00
edde746 660e375248 feat(exoplayer): let the read-ahead buffer depth be chosen instead of fixed at 50s
ExoPlayer's DefaultLoadControl was built with hard-coded durations picked from
one memory tier, so read-ahead stopped at 50s on any device reporting 2GB or
less free, with no way to raise it. On hardware where mpv cannot render at all
that ceiling is the whole buffer budget.

Playback Buffer offers Auto, Large and Extra Large. The durations are taken
from jellyfin-androidtv and jellyfin-android so the same words mean the same
thing across Jellyfin clients; Auto keeps the memory-tiered values that
shipped. Named tiers rather than a duration because a duration would be a
promise the load control cannot keep: prioritizeTimeOverSizeThresholds is
disabled, so targetBufferBytes stops the loader even below minBufferMs and the
byte cap binds first above roughly 23 Mbit/s.

The tier crosses the method channel as a string and resolves in the core,
where an unrecognised name falls back to Auto. LoadControlPolicy clamps the
resulting pair: media3 validates the ordering with Guava Preconditions, an
unconditional throw R8 does not elide, so a bad pair would be an
IllegalArgumentException out of player construction rather than a bad buffer.

The two play-start thresholds stay fixed even though the Jellyfin tiers move
them. BufferingStallPolicy.MIN_BUFFER_AHEAD_MS is a const derived from
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, so a runtime value there would make the
stall watchdog indict a player that is obeying its own load control.

That leaves the byte target as a second, often smaller ceiling, and nothing
surfaced either. The resolved values now reach getStats, and the overlay's
Buffer section gains a Cache Limit row reading "120s / 128MB" next to the
buffered-ahead duration, so a tier that appears to do nothing on a
high-bitrate file explains itself.

close #1816
2026-08-07 08:43:47 +02:00
edde746 db4f7a643b test: prune low-value coverage 2026-08-06 05:33:18 +02:00
edde746 f93952ba6f fix(android): stop tunneling 24p video on the Fire TV Stick 4K
Tunneled playback on an AFTMM judders continuously through 23.976p direct play.
The #1802 reporter isolated it: turning off Tunneled Playback with every other
setting unchanged makes it smooth, and their log shows tunneling active for the
whole session with E-AC3 bitstreamed and the decoded-PCM guard never firing.

Audio Passthrough looked like the trigger only because it is the one user-facing
switch that decides it. Passthrough off, or Downmix to Stereo on, both force the
Dolby track to decode to PCM, which trips the #1458 guard and takes tunneling
down with it. Passthrough on with downmix off is the only combination that keeps
a bitstreamed track, so it is the only one that stays tunneled.

Withdraw tunneling on that model for content at or below 30fps. The cut-off
keeps 4K50/60 tunneled, which is the workload Amazon documents the feature for.
The mechanism stays unconfirmed: tunneling fires no VideoFrameMetadataListener
and stops media3 counting frames in the codec, so nothing app-side can measure
the cadence. Only the trigger is established, and the quirk is scoped to it.

That needs a frame rate the app did not have. Neither MatroskaExtractor nor
Mp4Extractor populates Format.frameRate, and a tunneled session renders no
frames back for the native detector, so the server's rate now rides on the open
call. It is sent only for direct play, matching _primeDisplayCriteria: a
transcode's metadata describes the source, not what the server is about to send.

Also move Audio Passthrough out of the in-player settings sheet. It configures
the audio output route rather than the current playback, and applying it
mid-stream bounces the audio renderer and re-decides tunneling. Settings > Video
Playback already owns it, next to Tunneled Playback, which is applied the same
way. That description now mentions stutter, not only black HDR video, so the
workaround is findable on hardware this quirk does not cover.

The mpv backend failing to start the same 4K file is a separate defect and is
not addressed here; its uploaded log is no longer retrievable.
2026-08-06 03:45:09 +02:00
edde746 2b4875d389 fix(player): keep hidden and cycled subtitles off in the next episode
Episode navigation carries the subtitle choice this screen has committed, so
a way of turning subtitles off that the screen never sees is undone by the
next episode.

ExoPlayer has no renderer-level visibility switch, so the player's hide
toggle is emulated by deselecting the track. That emulation lasted until the
next selection: the automatic pass after an episode change put subtitles
straight back on screen while the toggle still read "hidden", and un-hiding
then restored a track id belonging to the episode that had already ended.
Hiding is now sticky across media opens the way mpv's global sub-visibility
is, selections made while hidden become what un-hiding restores, and the
toggle no longer refuses to restore because the hidden track reads as Off.

Cycling subtitles over the native track list — downloads, and items whose
server exposes no subtitle rows — went straight to the track manager, which
owns the player selection and the server write-back but not the committed
choice. The screen records the cycled track now.
2026-08-03 17:07:14 +02:00
edde746 395798f28e fix(player): stop handing ExoPlayer the demuxer's buffer budget on Auto
On Auto, Dart derives a buffer size for mpv's demuxer from the device heap and
sets it as `demuxer-max-bytes`. The Android player forwarded that same number to
`DefaultLoadControl.setTargetBufferBytes`, so ExoPlayer's sample allocator was
sized by a tier table written for a different consumer: 64MB on any device whose
large heap is 512MB or less, which every Shield is.

`targetBufferBytes` is a byte cap, so the media it represents collapses as
bitrate rises — 64MB is 53s of a 10 Mbit/s stream but 5.2s of a 103 Mbit/s UHD
remux. With `prioritizeTimeOverSizeThresholds` false the cap is hard:
`shouldContinueLoading` returns false the moment the allocator reaches it no
matter how little media that is, and `shouldStartPlayback` reports READY off the
same byte term. Read-ahead that short starves the audio sink in bursts, and on a
passthrough route that is enough to keep the AudioTrack from ever starting — the
track initializes, accepts one access unit and never renders a frame. Because an
enabled audio renderer owns the MediaClock, the whole player freezes and the
black-screen watchdog then blames the video decoder and drops the session to
mpv.

Size the LoadControl target natively instead, from what actually bounds
`DefaultAllocator`: the Java heap. `min(media3's own default for a video+audio
selection, largeMemoryClass/4, availMem/4)` with a 32MB floor, the lowest tier
that has already shipped. The quarter matches the threshold the Buffer Size
setting already warns at, and the media3 default is a ceiling — this is not
"buffer more than upstream", it is "stop buffering less". Deliberately not
bitrate-aware, because the LoadControl is built during initialize, before any
media is opened. `bufferSizeAuto` carries the distinction over the channel;
`bufferSizeBytes` still travels with it because the plugin's mpv fallback
replays it as a real demuxer property, and an explicit Buffer Size choice is
still honoured verbatim.

Confirmed against the hardware in the 2.9.1 passthrough report. That reporter's
own log is a natural A/B: three runs at 64MB fail with `0 frames rendered after
8002ms`, spanning both DV conversion modes and both tunneling states, while the
single run after he manually selected 128MB logs `Position advancing` and
renders. Reproduced on the same Shield model with codec and bitrate held fixed
and only the cap varied — 6s of audio demand stalls at 64MiB and plays at
128MiB, 4 of 4 predictions, with read-ahead measured off an injected
DefaultAllocator at 65 664 and 131 776 KiB. That device reports
`dalvik.vm.heapsize` 512m, so the heap term binds first at every free-memory
level in his log and Auto now derives exactly the 128MB he had to pick by hand;
the shipped path logs `Buffer: 128MB limit (auto, heap=512MB, available=568MB)`
where it previously logged 64MB.
2026-08-01 06:59:21 +02:00
edde746 86c8011b72 fix(player): tell the user when the server cannot read the media file
A 404 on the media stream means the server resolved the item but could not
open the file behind it — moved, deleted, or on storage that went away.
Jellyfin maps the resulting FileNotFoundException to 404, and PlaybackInfo
never stats the file, so negotiation succeeds and only the stream request
fails. Playback then died with a snackbar reading "Failed to open
[REDACTED_URL]" before popping the route, which tells the user nothing and
leaves nothing useful in a bug report.

Generalize the HTTP-500 log probe into PlayerError.httpStatusFromLog and
latch every status in fatalPlaybackHttpStatuses. Each latches on its own so
the 503 that stream-lavf-o deliberately retries cannot mask the fatal status
behind it. A 404 now raises a dedicated modal naming the cause and the fix.

On Android a 404 previously failed the "Response code: 500" string test and
fell through to the ExoPlayer→MPV fallback, showing "switching to compatible
player" before failing again on the same request. Read the real status off
HttpDataSource.InvalidResponseCodeException instead and skip the fallback:
an HTTP status is not a codec problem.
2026-07-31 21:45:33 +02:00
edde746 a183c17c3b fix(android): derive the mpv fallback passthrough list from the audio route
Audio passthrough defaults on for Android TV, scoped to ExoPlayer because
mpv force-passes through every codec named in audio-spdif and has no
decode fallback. That scoping did not survive the ExoPlayer to mpv
handoff: PlayerAndroid queued the raw ac3,eac3,dts,dts-hd,truehd list as
a pending mpv property and prepareMpvFallback replayed it verbatim, so a
sink that bitstreams only Dolby formats was told to force TrueHD and
DTS-HD anyway. mpv selected spdif_truehd, the audio output never
initialised, and playback froze at its start position while still showing
a first frame — the stop timeline reported the position it opened with.

Treat passthrough as a request and resolve the codec list against the
route when mpv actually starts, so an HDMI or AVR change between
ExoPlayer startup and the handoff cannot replay codecs from the old sink.
Gate each codec on the exact advertised encoding rather than media3's
passthrough probe: that probe answers DTS-HD by downgrading to the DTS
core, and mpv reads "dts,dts-hd" as "dts-hd" alone, so accepting the
downgrade would name DTS-HD MA to a core-only receiver and lose DTS too.
2026-07-28 19:44:16 +02:00
edde746 2b3853a882 fix(player): preserve transcoded subtitles at high speed
close #1622
2026-07-25 04:21:37 +02:00
edde746 0643787fbe fix(android): prevent ghost playback after autoplay failures
close #1673
2026-07-25 04:12:54 +02:00
edde746 9f2e050797 fix(native): bound cross-platform lifecycle ownership 2026-07-24 03:56:40 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
edde746 0707d4d9b1 fix(live-tv): stabilize HLS playback 2026-07-18 20:04:09 +02:00
edde746 a1b6a89714 fix(player): load subtitle sidecars with media
close #1583
2026-07-17 23:09:07 +02:00
edde746 e6e7d8cdfd test: remove redundant coverage and shorten timers 2026-07-13 02:15:03 +02:00
edde746 e03edad824 fix: prevent async state and platform regressions 2026-07-13 00:03:52 +02:00
edde746 cd88f73f05 fix: serialize async state transitions 2026-07-12 18:59:14 +02:00
edde746 b7078448ea refactor(mpv): unify property decoding 2026-07-12 08:42:26 +02:00
edde746 f7f4e3a992 fix(windows): support structured mpv observations 2026-07-12 08:42:24 +02:00
edde746 3d36ecf094 fix(mpv): harden native initialization and logging 2026-07-12 08:42:23 +02:00
edde746 0135318ded fix(android): include mpv end-file diagnostics 2026-07-12 08:42:22 +02:00
edde746 5876a602c3 fix(android): surface mpv command failures 2026-07-12 08:42:22 +02:00
edde746andClaude Fable 5 73be8ab1c8 fix(music): convert SAF content:// downloads for gapless arming
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>
2026-07-06 15:26:59 +02:00
edde746 e90835c234 fix(ci): restore sanity checks 2026-06-25 07:21:16 +02:00
edde746 618251b8eb fix(player): attach external subtitles during open 2026-06-15 17:21:08 +02:00
edde746 7195702242 fix(player): repair Plex transcode seeking
close #1341
2026-06-14 23:39:05 +02:00
edde746 f3cb508649 fix(player): wait for mpv subtitles after load
close #1331
2026-06-13 23:52:19 +02:00
edde746 75ba6b0627 fix(player): isolate dispose teardown races 2026-06-13 13:23:29 +02:00
edde746 cd9498abb8 refactor(player): centralize playback opening
close #1280
2026-06-10 02:26:26 +02:00
edde746 985c279b1f fix(exoplayer): correct transcode seek position double-count 2026-06-02 06:23:08 +02:00
edde746 1685ecc991 fix(playback): prevent native subtitle autoselect 2026-05-30 00:05:29 +02:00
edde746 2b45557743 fix(android): preserve mpv frame-rate startup position 2026-05-21 09:58:13 +02:00
edde746 c93a266722 fix(plex): support no-burn transcode subtitles 2026-05-20 17:21:31 +02:00
edde746 f4f2571168 fix(android): apply dv conversion mode during init 2026-05-18 20:32:34 +02:00
edde746 12073ea134 fix(player): clear stale tracks before opening media
close #1028
2026-05-13 09:22:17 +02:00