ktlint's spacing-between-declarations-with-annotations and
-with-comments rules failed on android/libmpv/build.gradle.kts, so
scripts/format_native.sh --check - and with it scripts/ci_checks.sh -
reported a failure on an untouched file.
Add the blank line before the @Suppress'd mpvAssets declaration and
before the comment above stagedArchiveName.
PlexConfig.acceptJson defaulted to true in both constructors and
travelled through copyWith, but no production or test caller ever passed
false, and every Plex path decodes JSON, so the header was conditional on
a flag that could not be off.
Emit `Accept: application/json` directly and drop the field. Headers on
the wire are unchanged.
The video controls' key handling redefined the directional, horizontal
and select key sets privately, duplicating DpadKeyExtension with
identical members; the private select helper then handed the event to
handleOneShotSelect, which classified it again with the canonical
isSelectKey.
Use isDpadDirection, isLeftKey/isRightKey and isSelectKey and delete the
copies.
The two setup acknowledgements ran the same sequence twice: accept and
validate the response, handle the same two exceptions, install the peer
roster, publish connected state, and complete the setup completer. Only
the log line differed, plus a roster loop that skipped the
already-present check.
One case handles both, using the guarded add so the roster publishes
idempotently for either acknowledgement.
LiveTvDvr parsed 21 DVR fields plus nested Setting and raw Device lists
on every Live TV availability refresh, while the app reads only the key,
the three lineup labels, and the enabled channel mappings. The rest
describe tuner hardware and DVR setup, which Plezy does not implement.
Keep key, lineup, lineupTitle, lineupURL and channelMappings, and
channelKey/enabled on ChannelMapping. The tests keep the root
channel-mapping fallback, malformed-sibling tolerance and flexible
`enabled` parsing.
AccountPreferencesController subscribed to watchConnections() and
watchForProfile() and threw both payloads away, then re-read the same two
tables through list() and listForProfile() on every resolve - four
one-shot queries during attach alone, each re-decoding rows and revealing
credentials the watcher had already decoded. The generation counter
discarded stale results but never the queries.
Retain the rows the subscriptions deliver and resolve from them, clearing
the profile rows whenever the watched profile changes so one profile's
rows can never pair with another's. Until both watchers have delivered,
the resolve task parks on that first delivery, so ensureActiveLoaded
still has something to wait for.
The audio-only core reused the video core's property registrations
wholesale, so every music session observed track-list,
demuxer-cache-state, audio-device-list, audio-device, secondary-sid,
seekable, paused-for-cache, volume, speed, aid and sid. libmpv sends an
initial notification plus an event per edge for each one, and three of
them decode structured nodes into track and device models nothing on the
music path reads.
Register time-pos, duration, pause, eof-reached and playlist-pos there;
the video path keeps the full set. Also drop the post-initialization
`gapless-audio` write: every native audio core already sets it before
mpv_initialize, which is where libmpv wants initial configuration.
The audio ladder still carried a per-item language tier between the
server's selected stream and the account preference. For Jellyfin and
Emby the value mapped to it was `PreferredMetadataLanguage`, which is the
library's metadata-scraping language - it inherits from the parent, the
library options, or the server config and says nothing about playback -
so on any source whose selected/default stream did not map to a native
track it silently picked audio in the scraping language instead of the
user's preferred one. Plex's item-level `audioLanguage` reached the same
tier, after 2dfebf32b and 7ae293631 had already settled that the server
folds every preference level into the stream `selected` flags.
Drop the tier and the plumbing that only fed it:
TrackSelectionPriority.perMedia, MediaItem.audioLanguage and
subtitleLanguage, PlexMetadataDto.audioLanguage/subtitleLanguage/
subtitleMode, and the Jellyfin PreferredMetadataLanguage mapping. The
order is now carried selection, server-selected or default source
stream, account preference, native default. Persisted cache entries
holding the old keys decode unchanged.
The action row's track status was a button that opened a pre-play
chooser, but on TV the hero describes whichever episode the rail has
focused, and focusing the row hands the hero back to the show, so there
was never a per-episode target for the control to act on. It also sat
inline with the buttons in muted grey, reading as a sixth, low-contrast
action.
The status is now plain text at the row's far end, right-aligned to the
hero's text column, in the hero's own ink, off the focus path. The
chooser sheet, the per-screen choice, and the Play pass-through go with
it, along with the overlay-host and track-tile changes that only served
the chooser.
The detail screen gave no hint of what pressing Play would do with the
file: which audio track and which subtitles the player would start with,
or the picture format. Changing tracks meant starting playback first.
The action row now ends with a status line — picture labels, then the
audio and subtitle rows the player will select — computed by the player's
own selection ladder (TrackSelectionService) over the server's stream
rows, so it agrees with what the player then does. Select opens a
pre-play chooser with the track sheet's two columns; Play and rail
activation pass the pick to the player as its navigation-tier preference,
and a pick carries across episodes by semantics, forced-ness included.
Plex listings carry no stream rows, so a focused episode is probed once,
debounced, and cached for the screen.
Along the way: MediaStream keeps the server's `selected` apart from the
container `default` flag (they were OR'd, so the account's pick was
indistinguishable from the default track); the overlay sheet host no
longer overrides a row that claimed focus itself.
The TV show detail page spent its hero rows on information that never
changes while browsing episodes (the show's genres, the file's stream
quality) while the rail cards repeated the show name on every card and
pushed the episode title into a truncated subtitle. The Play button also
kept naming the on-deck episode while the hero described the focused one.
Rail cards inside the show now headline the episode title with
`S1 E3 · 23min` as subtitle; genres and quality labels move from the hero
to the details sheet (which now also names the episode and takes the show's
genres explicitly); Play follows the hero's episode and falls back to
on-deck when nothing is focused. The freed rows go back to the logo and the
third description line.
close#2217
On the TV detail screen the hero follows the focused episode: its metadata
line and description update, but the episode's own title is nowhere in the
hero. The only copy is the rail card's subtitle, which truncates for most
titles, so the viewer cannot read what they are about to play.
Add a title line between the show logo and the episode metadata line,
reserved in the hero's height budget like the other rows, announced in the
hero's accessibility label, and inside the focusable info block that opens
the details sheet. The hide-spoilers path no longer substitutes the episode
title for a missing summary, since the title line already names it.
close#2217
The remove button's default 48px tap target set each row's height, adding
about 12px of dead space around every 36px field on top of the row gap.
Constrain the button to 36px so the field decides the row height, and trim
the field padding and row gap.
On Android TV the mpv.conf editor used Plezy's own on-screen keyboard
instead of Gboard, and the only way to get a long conf onto a TV was to
retype it key by key (#2232). The system keyboards cannot host the
multiline editor: FireTVIME has no newline key, and both FireTVIME and
Gboard could wipe a pre-filled multiline field on the first keystroke
after the first-show restartInput.
On TV, render one single-line native field per line, with a line-number
gutter, a remove button per row and an Add line button. Rows open their
keyboard on Select only, so D-pad traversal does not raise it. A value
arriving with newlines (a paste from a phone remote) is split into rows
and typing continues on the last one; the rows are keyed so the split
does not rebind another row's input host. The keyboard's action key does
not insert a row: the app never learns which action the IME sent, and
FireTVIME reports Back as `previous`, so an insert-on-action editor kept
opening rows on every Back. Completion hands focus down instead — the
next row, or Add line after the last one.
TvTextInputController gains focusAndOpenTextInput() so a row the app
creates (Add line, paste split) can be typed into without a second
Select; it activates after the focus request lands, since the host's
focus sync deactivates an unfocused field.
Verified on a Google TV box (Gboard, Android 14) and a Fire TV Stick
(FireTVIME): rows keep their text on open, Back closes the keyboard
without side effects, edits persist across leaving the screen. Phone,
desktop and pointer platforms keep the text-area editor.
close#2232
Plex Web reads only the `selected` flag PMS stamps on each stream; the
account's autoSelectSubtitle is applied server-side when that flag is
computed. Plezy's Priority 2 already trusts the same flag, but once
playback read AccountPreferences the Plex mode became non-null and could
reach the client-side profile pass whenever there was no media info or no
embedded subtitle streams - forcing subtitles off for Plex's 0 ("manually
selected") or on for 2, over a decision the server had already made.
TrackSelectionService now skips the profile subtitle mode for Plex items;
MediaBrowser keeps it because those servers do not pre-select.
UserProfileProvider kept a second cache of the active user's audio/subtitle
defaults beside AccountPreferencesRepository, re-implemented the active
profile/connection listener wiring and Plex Home token resolution, and for
Plex Home profiles bypassed the shared cache entirely (fetching /user with
its own client), so a write in the Account preferences screen never reached
Home-profile playback.
The controller now resolves the active account (the head of its sorted
account list), loads it through the one repository on attach, on profile
switch, and when the account's token changes, and exposes it as
activePreferences; ensureActiveLoaded replaces initialize() for the startup
gate. Playback, offline waiting, and logout storage clearing read the
controller directly; UserProfileProvider, the per-backend user-profile DTOs,
PlexAuthService.getUserProfile, and JellyfinClient.fetchUserProfile are
deleted.
The ranked defaultAudioLanguages/defaultSubtitleLanguages lists go with
them: Plex Web never reads them, PMS applies them when it stamps `selected`
on streams, and Plezy already trusts that flag ahead of the profile. They
only refined a best-effort audio fallback for Plex items with no media
info, which the primary language covers; on that path a second preferred
language is no longer tried before the file's default track.
Plex Home profiles now read /user/profile with the switched token through
the same cache as local profiles.
The memo-lookup, scroll-budget, skeleton-scheduling sequence was copied
in the paginated grid, the browse tab, and hub rows; SkeletonUpgradeScheduler
now owns it as realizeBudgeted with per-site skeleton and card builders,
keeping the hub's focus salt and padded skeleton.
The video player screen held fourteen nullable subscription fields and
listed them twice when cancelling; they are two lists (player re-wire,
media controls) snapshotted and cleared before cancellation, with the
screen-lifetime Apple TV and sleep-timer subscriptions left as named
fields. Live seek and subtitle switches open through one helper, and the
initial live open applies the shared stream options instead of an
inline property write.
The 32-byte reconnect token and its 43-character base64url shape were
pinned by hand in both server/main.go and the Dart peer service, and the
supported-version check was spelled out three times in Go. The spec now
carries reconnectTokenBytes and the generator emits the token size,
encoded length, and validator for Dart plus the size and
supportedRelayProtocolVersion for Go, failing before writing either
target when the key is missing. The last handwritten error code in lib/
('not_in_room') uses the generated constant, and releaseSession's
in-flight join is a FutureCoalescer.
MDBList and Trakt repeated the rating type, entry matching, and request
body construction; tracker_rating_match.dart now owns them with the
service label passed in. The collection detail screen re-implemented
fetchAndQueueListDownload minus its cellular-blocked branch and now uses
it, and the three showDownloadOptionsAndQueue call sites share
queueDownloadWithFeedback, so the detail action buttons surface a queue
failure as an error snackbar instead of an unhandled async error.
Seven sheets hand-rolled the Column(min) + BottomSheetHeader + Flexible
tree that BottomSheetPageScaffold already produces; each now passes its
header arguments to the scaffold. No onBack is supplied, so the widget
tree, height, and focus behaviour are unchanged.
Three MediaBrowser pagination loops re-implemented drainPages; it gains an
onPage hook (fires after intermediate pages only) so the two
progressive-render loops fit as well as the episode queue. Five
handwritten FIFO future tails (companion lifecycle, Seerr persistence,
tracker write lock, companion host-auth commit, Watch Together message
routing) become SerialFutureQueue, and the companion peer service's
disconnect/dispose in-flight joins become FutureCoalescer.
Loading the downloads list re-resolved the backend and profile scope for
every row, and the provider's bulk lookup only tried the public key, so
every MediaBrowser leaf fell back to a per-item lookup that resolved the
scope again (twice more for episodes and tracks). Startup was therefore
O(downloads x profile bindings) in database selects.
getAllPinnedMetadata now resolves each distinct server once, covers
queued and in-progress rows too, and returns the scope snapshot beside
the items; hydration tries the exact compound key, then the public key,
then the existing per-item fallback, and reuses the snapshot for parent
rows. findProfileScopeId prefetches bound connections in one select while
keeping binding precedence. A provider test pins the select count as
constant across download count.
Deleting a container also read the row, resolved metadata, and queried
its children twice; deleteDownload now loads them once and threads the
snapshot through the file deletion, keeping the cancel/progress/file/row
order intact.
Linux and Windows each wrote the same eleven pre-initialize mpv options
(keep-open, idle, input disabling, OSC, ytdl, audio fallback, and the
audio-only set); ApplyCommonStartupOptions in mpv_player_common.h owns
them once, with the ytdl security rationale kept in one place. Rendering,
windowing, HDR, and log-level options stay per platform.
The Android JNI event thread forwarded every mpv event to Kotlin, where
MpvEvent modelled ten variants but MpvPlayerCore consumes four. The
native switch now forwards only START_FILE, FILE_LOADED, and
PLAYBACK_RESTART (END_FILE keeps its own path), and the unused variants
are gone.
windows/CMakeLists.txt repeated the mpv-build key, asset base, and both
SHA-256 values that mpv-build.lock.json already owns, and the pin mover
never touched it, so a lock bump left Windows silently stale. CMake now
reads artifacts.windows with string(JSON) (floor raised to 3.19) and the
Windows native cache key hashes the lock; a workflow guard rejects
hand-pinned checksums.
The byte-identical Linux libmpv fetch heredoc in build.yml and ci.yml
moves to scripts/fetch_linux_libmpv.py, which verifies the checksum
before extracting and is what the packaging script now points local
builds at. The package-windows and release jobs and
linux/packaging/build-packages.py read the version through
scripts/pubspec_version.py instead of three looser regexes.
Home, privacy, and scan each repeated the same 13 title/canonical/Open
Graph/Twitter tags; PageMetadata now emits them once with route-specific
title, description, url, and an optional noindex. Screenshots keeps one
record per device instead of four parallel arrays plus a lookup table.
FAQ hash jumps and screenshot arrows forced behavior: 'smooth' from JS,
which bypasses the prefers-reduced-motion override in layout.css; both
now defer to CSS scroll-behavior. Removes the unused adapter-auto
dependency.
ConnectionBootstrap kept a private cache read/fetch/write path for Plex
Home users beside PlexHomeService, which already owns refresh
coalescing, generation checks, durable writes, and publication. The
bootstrap now keeps only the one-time legacy cache migration, then
reloads the service from storage or asks it to refresh, and reads the
hydrated users from the service snapshot. A failed hydration clears the
service's empty cache slot before the migrated account is removed.
The timeline bar rendered whole seconds but rebuilt on every ~250ms
position emission, so three of four rebuilds produced identical output.
It now derives a distinct position-seconds stream, the same pattern
ContentStrip uses for its chapter index, and rebinds only when the player
changes; streamStartEpoch changes still apply on the next build.
The movie/show/season/episode/artist/album/track to 1/2/3/4/8/9/10 table
was repeated in the query translator (forward and inverse), collection
creation, and the metadata-edit adapter; PlexMetadataType now owns
forKind/kindFor and each caller keeps its own unsupported sentinel.
getMetadataWithImagesAndOnDeck and _getMetadataWithImages shared the
endpoint, cache key, query flags, first-row parsing, and library-section
stamping and differed only by includeOnDeck; they are one private typed
fetcher returning (metadata, onDeckEpisode), so fetchItemWithOnDeck no
longer reads a string-keyed dynamic map.
Emoji and other supplementary-plane characters in titles, file names, and
subtitle paths were corrupted on the Android MPV path, and malformed bytes
from mpv logs could abort under CheckJNI. NewStringUTF/GetStringUTFChars
speak JNI's modified UTF-8, not the standard UTF-8 mpv produces and
consumes; the native lead-byte filter and the Kotlin surrogate scrubber
ran on the wrong side of that conversion and could not recover it.
Both directions now go through UTF-16 (NewString/GetStringChars) with a
small JNI-free transcoder that replaces malformed input with U+FFFD, the
same policy as shared/cpp/sanitize_utf8.h on desktop. The header is
covered by the Android host native test harness.
Capping quality against an Emby server on macOS (or any device with an
AV1 decoder) failed with HTTP 500: Emby takes the first entry of the
TranscodingProfile codec list verbatim and has no AV1 encoder. Jellyfin
rotates admin-disabled codecs to the back, which is what the AV1-first
list relied on. Gate AV1 on that dialect behavior.
close#2230
High-bitrate HEVC episodes showed smeared frames and green flashes for the
first 10-15 seconds of playback on the Fire TV Stick 4K Max 2nd gen with the
mpv backend, while ExoPlayer played the same file cleanly (#2227). The
reporter's clip failed at exactly two timestamps on every run: the two IDR
frames larger than 1 MiB.
The FFmpeg mediacodec decoder never set "max-input-size" on the codec
format, so OMX.MTK.VIDEO.DECODER.HEVC allocated 1,052,672-byte input buffers
and FFmpeg split any larger packet across several queueInputBuffer calls.
The MediaTek VPU treats each buffer as a complete access unit, rejects the
remainder ("invalid u4BufferAccumulated"), fails the slice and conceals with
lost-picture references until the next IDR. ExoPlayer is unaffected because
media3 sizes HEVC input buffers at max(2 MiB, w*h*3/4) and never splits a
sample.
Move the native pin to edde746/mpv-build@83879375c9, which sizes the
buffers the same way ExoPlayer does for H.264, HEVC, MPEG-4, VP8, VP9 and
AV1 and logs a warning if a packet still does not fit. Verified on the
AFTKRT with the reporter's clip: zero VPU errors and no lost pictures across
repeated runs, and an H.264 file with ~1 MB keyframes still plays on the
MediaTek AVC decoder; both files also play on a Pixel 7 (Exynos C2). Apple,
Linux and Windows assets are unchanged (their content keys did not move).
close#2227
On a dual-stack network Plezy pinned API traffic and the notification
websocket to IPv4 while the native Plex apps and the media stream used
IPv6. dart:io looks up A before AAAA and connects in arrival order, so
the AAAA address was never tried when the IPv4 path answered within
250 ms.
Install an HttpClient.connectionFactory that does one lookup (keeping
the resolver's RFC 6724 order), interleaves families per RFC 8305 and
races candidates 250 ms apart. The task is returned before the lookup
starts so connectionTimeout and cancel cover resolution. TLS is done in
the factory since the SDK skips it once one is installed; proxied
requests stay plain. The library-event and companion-remote websockets
get the same client because WebSocket.connect otherwise builds its own.
Supersedes #2233.
Playing lossless passthrough audio (TrueHD, DTS-HD MA) on an Nvidia Shield
running Android 9 dropped a video frame every ten to twenty seconds with the
mpv backend, while lossy passthrough and PCM were unaffected and A/V sync
never moved (#2202).
IEC 61937 and raw passthrough tracks in ao_audiotrack never use
AudioTimestamp; their clock is AudioTrack.getPlaybackHeadPosition() taken at
face value, which on a direct track is the HAL's render position and
advances in HAL-period steps - 8192 frames at 192 kHz (42.7 ms) for the HBR
output on the Shield, longer than a 24p frame. The AO thread samples it once
per chunk and feeds it straight into the end time mpv schedules video
against, so a forward snap larger than a frame's remaining lead makes mpv
drop that frame; lossy passthrough runs at 48 kHz where the period stays
below a frame.
Move the native pin to edde746/mpv-build@af74e1906e, which estimates the
passthrough position the way media3 does: a ten-sample average of
(head - clock) offsets taken at least 30 ms apart, so a period-sized step is
spread over the window instead of landing on one frame. PCM tracks keep
their AudioTimestamp path. Apple, Linux and Windows assets are unchanged
(their content keys did not move).
Grid Spacing (Settings > Appearance) only changed the library grid. The
home, explore, detail and library-recommended rows were byte-identical
across Tight/Normal/Spacious because #2083 deliberately opted hub-row
cell packing out of the setting and hard-coded a 2px pad per card, and
the TV home rail never read the setting at all.
Treat a hub row as one row of the grid. MediaGridDelegate.cellWidth
packs row cells with the same gutter MediaGridGeometry uses, so a row
and the "see all" grid behind it render the same card at every setting.
HubSection renders max(4, gap) between cards - Tight keeps its historical
4px so nothing moves on update, Normal/Spacious render the grid's 6/12px
gutter - and feeds the same gap into its D-pad item extent and card memo
epoch. TvBrowseRail threads GridSpacing through metricsForHub,
maxActiveRailHeight and estimateHeight; non-full-card rails use the
setting scaled with the rest of the rail metrics, full-card rails keep
their own scale-derived gutter like full-bleed grids. The spotlight and
detail rail height reservations pass the setting too so reserved
heights track the narrower cards. Both surfaces watch the pref, so rows
re-lay out live.
close#2226
Every D-pad step on the TV home ends with a spotlight swap, and on a
low-end box that frame ran over budget: 12 % of UI-thread CPU during
navigation was paragraph layout, most of it shaping strings a second time
to measure them before the render tree shaped them for real.
- FittingTitleText no longer lays the title out to learn whether it fits:
Text wraps and ellipsizes at maxLines, so the box can only overflow
vertically, and one cached one-glyph line height settles that. Only a
box shorter than maxLines lines still searches, now bracketed by the
proportional shrink of the base layout and stopped at a 0.25 px
tolerance instead of a fixed 12 bisections.
- FittedMetadataLine and inlineRatingBadgeWidth measure through a small
bounded cache keyed by text, style, scaler and direction, so separators,
type labels, certifications, runtimes and years measure once per
session.
- MediaHub.isContinueWatchingHub/usesContinueWatchingAction memoize their
regex tokenisation per key; every card build and step re-asked them.
Cold navigation on the Android 14 box (30 steps over three hubs): build
312-316 ms -> 284-285 ms, layout 530-540 ms -> 515 ms, UI frame p90
7.5-7.9 ms -> 6.8-7.2 ms.
Android enables Flutter semantics for any bound accessibility service, so a
TV running the Projectivy Launcher (whose service only wants foreground-app
events) paid for a full semantics tree on every frame that touched a node:
on an Android 14 box, 2 ms per frame, ~20 ms on each spotlight swap, and a
third of the navigation-time GC churn.
The Android side now reports whether any enabled service can consume the
tree (touch exploration, an accessibility tool, or spoken/braille/audible/
visual feedback; an empty list means UiAutomation, which reads it too), and
AssistiveTechnologyService closes a SemanticsTreeGate on the app binding
when none can. The gate sits under SemanticsBinding.semanticsEnabled, so the
pipeline owner drops the semantics owner exactly as if the platform had
turned accessibility off; explicit ensureSemantics clients (the debug handle
for Maestro) always win. Re-evaluated on platform toggles, service-list
changes (API 33+) and resume.
Cold navigation on the box with Projectivy's service bound, 30 D-pad steps
over three hubs: SEMANTICS phase 700-770 ms -> 0, UI frame max 31-37 ms ->
25 ms, GC 3.0 s -> 2.2-2.6 s.
D-pad navigation on Android TV felt sluggish next to Projectivy and the
Fire TV launcher: every step of the home rail and hub rows trailed the
focus border in a 500ms ease-out, and hub moves deferred the vertical
glide by a frame. The 500ms was measured from the tvOS focus engine
(f357be407) and applied to every platform; Leanback prices a one-card
step at roughly 100-150ms, so the same glide reads as input lag on a
D-pad.
FocusTheme.navigationScrollDuration now vends the per-platform value:
Apple TV keeps the measured 500ms, everything else glides in 150ms.
TvBrowseRail and HubSection use it for row and hub-list scrolls, and a
hub move starts the vertical animation in the same frame when the
build-time section offsets are already known.
Measured with screen recordings on a Shield TV (2019) and a Fire TV
Stick 4K Max: row motion per press 470-510ms before, 135-150ms after;
hub move 480ms before, 120-150ms after.
Dolby Vision profile 7 titles (UHD Blu-ray remuxes, typically TrueHD or
DTS-HD MA) never showed a frame on the Fire TV Stick 4K Max 2nd gen with the
mpv backend: the spinner stayed forever while audio came up. Profile 8.1
titles carrying HDR10+ on the same device went fully black, controls
included, with audio playing.
Both are the FFmpeg mediacodec DV packet filter. It emitted every rewritten
access unit with a five-byte "00 00 00 00 01" start code - the leading zero
was copied once as head bytes and again as part of the first NAL region -
and MediaTek's VPU rejects such units ("H265_decode invalid NALU"), so the
profile 7 conversion and strip paths both produced nothing. And the filter
never dropped in-band HDR10+ SEI, which these chipsets cannot take next to
the RPU on the native DV decoder (#1296 on the ExoPlayer path).
Move the native pin to edde746/mpv-build@0ad6388c, which counts that zero
once and strips the ST 2094-40 SEI whenever a video/dolby-vision decoder is
selected. Verified on an AFTKRT with the published armeabi-v7a asset: a
P7.6 remux sample decodes on both the HEVC (strip) and DVHE.STH (convert)
paths with no VPU errors, and a P8.1 + HDR10+ remux plays on the DV decoder
with a single output-format change. Apple, Linux and Windows assets are
unchanged (their content keys did not move).
close#2176
Enabling Deinterlacing had no effect on Android and Android TV for
interlaced MPEG-2 (and any other software-decoded stream): mpv asks
libavfilter for bwdif, and the pinned Android and Linux FFmpeg builds were
configured with --disable-filters, so the request failed with
"filter 'bwdif' not found" and the frame passed through combed. Windows
and Apple ship FFmpeg with the filter, which is why the same file
deinterlaced on Windows.
Move the native pin to edde746/mpv-build@9bfbcd69, which builds bwdif into
the Android and Linux FFmpeg. Apple and Windows assets are unchanged
(content-addressed keys did not move). Verified on a Shield TV Pro with the
published arm64 asset: 480i MPEG-2 now deinterlaces cleanly.
close#2224
With an external player on Plex, starting a never-watched episode could
open a "resume playback?" prompt at the position the previous, unrelated
file was exited. The intent carried no start hint when the item had no
resume point, so players with their own bookmark store (Zidoo native
player, VLC) consulted it - and every Plex part URL ends in the same
`file.<ext>`, so those bookmarks collide across items.
Send `from_start=true` on fresh launches and `from_start=false` beside
`position` when resuming, matching the Zidoo integrations in
jellyfin-androidtv-zidoo and PlexToZidoo. MX, Just Player and mpv ignore
the extra.
close#2223
AAC/AC3/EAC3 5.1 tracks at 44.1 kHz played as stereo through mpv on Android:
ao_audiotrack sized its buffer as 75 ms aligned to the sample size, which at
44.1 kHz is not a whole number of 6-channel frames, so AudioTrack refused it
("Invalid audio buffer size") and mpv fell back to the stereo-only OpenSL ES
output. 48 kHz sources (DTS, or audio-samplerate=48000) happened to align.
Pin mpv-build 1779135, which frame-aligns the audiotrack PCM buffer
(edde746/mpv-build@d89cfbb). Only the Android libmpv key moves.
close#1445
Rooms drifted in speed and kept pausing on poor links. A 2x long-press or a
speed pick on any peer propagated with no attribution, so it read as the app
changing speed on its own; a host whose cache starved let mpv resume its player
on its own timetable while the room waited, so every recovery re-anchored ahead
of the guests and they hard-seeked in a loop; and a wall-clock step on a guest
made every host anchor read stale and seeked the file by the size of the step.
Rate intent is now declared by the screen (speed sheet, keyboard, long-press,
media controls) through WatchTogetherProvider.onLocalRate instead of being
inferred from the player's rate stream, matching how seeks already work. The
expectation ledger no longer tracks rate at all, so a nudge, a default-speed
apply or a late ack can never become a room rate change, and a promoted host
adopts the room rate rather than its player's momentary one. Remote rate
changes surface as "<name> set the speed to <rate>".
The host pauses its own player when it stalls, anchors the room there, raises
mpv's cache-pause-wait to 4s, and holds the resume until it has buffered three
times the stall it just had. Heartbeats during a sub-grace blip keep
extrapolating the previous anchor instead of re-anchoring on a frozen position.
The sync layer's clock is monotonic. ClockSync discards its window when a
sample's offset moves further than the round trips involved can explain, and a
guest that receives an anchor reading implausibly old holds corrections and
re-converges instead of seeking.
Searching for items in a Plex "Other Videos"/home-video library returned
nothing, while the same query worked in Plex Web. `/library/search` with
`searchTypes=movies,tv,music` skips libraries on the Personal Media agent;
they only answer to the `otherVideos` category.
Add `otherVideos` to the search request and treat personal-media rows
(`*.agents.none://` guids) as their own category when a saturated response
is supplemented, so a large movie library cannot starve home videos and
vice versa.
close#2216
A local Seerr account with request rights could sign in to Plezy but never
saw the Request action. POST /auth/local loads only the columns it needs to
check the password, so the entity it returns carries the class default
permissions of 0 (and the email as display name); the session stored that
snapshot and the detail screen gated Request on it.
Sign-in now ignores the login body and reads the user back through
GET /auth/me with the fresh cookie, for every method. SeerrUser.permissions
is required, so a user without a mask fails sign-in instead of persisting 0.
Two related gaps kept an affected session broken until a manual reconnect:
- Seerr answers an expired session with 403, never 401, so the silent
re-auth never ran. A 403 now re-auths once GET /auth/me confirms the
cookie is dead; a 403 from a live session stays a permission denial so a
Quick Connect session is not unlinked over one.
- Nothing refreshed a stored session's permissions. The account provider
now re-reads the user on bind, so sessions persisted with 0 by earlier
builds, and admin-side permission changes, reach the Request action on
the next launch.
close#2213
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plezy consumed mpv through four unrelated supply chains: an MPVKit fork
via SwiftPM for the Apple platforms, a libmpv-android fork's AAR for
Android, an in-CI from-source build for Linux, and an unpinned
sourceforge mpv-dev 7z for Windows. All four now consume the same
per-commit, content-addressed binaries from
https://github.com/edde746/mpv-build, pinned to one commit and built
from one set of pinned sources (mpv v0.41.0 on Apple/Android/Windows,
ffmpeg n8.0.1, our libass fork).
- Apple: the SwiftPM package moves from edde746/MPVKit to
edde746/mpv-build across the ios/macos/tvos projects;
scripts/set_mpvkit_revision.sh becomes set_native_revision.sh, writes
every pin site plus the new repo-root mpv-build.lock.json, and
tvos/scripts/wire_mpv.rb derives the package repo from the locks.
- Android: the mpv Kotlin API and JNI glue move in-app under
android/libmpv (repackaged com.edde746.plezy.libmpv, exports renamed,
shrinker rules covered), and the module downloads per-ABI native
tarballs (lib/*.so incl. libc++_shared.so + include/) driven entirely
by mpv-build.lock.json, with a PLEZY_LOCAL_MPV_DIR escape hatch for
locally built artifacts. The fork AAR and its Maven coordinates are
gone.
- Linux: CI downloads the prebuilt self-relocating libmpv prefix
(lock-driven, sha256-verified) instead of compiling mpv/ffmpeg/dav1d/
libplacebo/shaderc from source; linux/packaging/build-libmpv.sh and
its test are deleted and native-inputs.json shrinks to the simdutf
entry the CMake builds still fetch.
- Windows: both arches FetchContent the mpv-build dev zips with
URL_HASH enforcement, replacing the checksum-less sourceforge
download and its ARM64 7-Zip special case; guard scripts updated.
The lock plus the Apple pin sites all point at mpv-build commit
d7c3d559, whose manifest was verified asset-by-asset against the
published release digests (17/17 match). Verified locally: wire and
pin-script suites green, runtime-input checks green, all four Android
ABI tarballs downloaded/verified/extracted through the real Gradle
tasks, the Windows FetchContent block exercised end to end through
cmake, the Linux asset hash and layout checked against the workflow
contract, and xcodebuild resolved the flipped SwiftPM graph with
SwiftPM validating every binary checksum.
Opening View Logs with a full 5 MiB buffer laid out the entire log as one
paragraph, freezing the frame for tens of seconds and using ~1 GB of glyph
data — the OS killed the app on low-memory phones and TVs (watchdog/OOM).
Render one Text.rich per entry in a SliverList.builder so only the visible
slice is laid out, keeping selection through a SelectionArea. Also cap the
copy-to-clipboard payload at 256 KiB (newest lines kept) to stay under
Android's ~1 MiB binder transaction limit.