INetworkListManagerEvents Advise fails with
RPC_E_CANTCALLOUT_ININPUTSYNCCALL when the platform thread is inside an
input-synchronous call at subscribe time, and an OnListen error can
only surface in Dart as an uncatchable fatal FlutterError. The fork now
retries the registration via a message-loop timer and reports only
persistent failures through the event sink, where stream onError
handlers observe them.
saf_util 2.0.0 leaves an already-answered MethodChannel.Result behind
in pendingResult: unrelated activity request codes consume (and answer)
the pending picker, and a failed picker launch replies without clearing
the pending state. The next delivery for request 1001 then replies a
second time — IllegalStateException "Reply already submitted" in
MainActivity.onActivityResult, crashing the process.
Vendored at packages/saf_util (BSD-3) with take-and-clear reply
ownership, request codes scoped to the plugin's own pickers, and a
guarded reply on the teardown race.
_ensureFallbackOnDeckEpisode is reached through unawaited fetch
continuations and reads providers through State.context — leaving the
screen before the season fetch settled crashed on the null-check in
the context getter.
Player backends share static per-backend EventChannel names, so two
overlapping instances (episode handoff, quick exit/reopen) collide:
the newer instance's listen displaces the older sink, the older
instance's late cancel then tears down the newer stream — silently
freezing its events — and the final cancel earns the engine's
"No active stream to cancel" reply, which the framework can only
report as an uncatchable fatal FlutterError (363 events).
Track the owning instance per channel: only the current owner sends
the native cancel; a displaced instance drops its dead subscription.
Residual benign teardown races are dropped in beforeSend.
A failed MAC check (key/ciphertext divergence: restored backup,
clobbered prefs, racing key generation across isolates) threw from
CredentialVault.reveal on the startup profile-settings path and
crash-looped the app until data was wiped — one device logged 31
fatals in 16 minutes on 2.8.0.
Decrypt failure now means the credential is lost, never a crash:
reveal() returns null, ProfileConnectionRegistry maps it onto the
existing empty-token lazy-fetch sentinel and heals the row so later
boots re-acquire the token instead of re-failing, and
revealConnectionConfig degrades tokens to empty strings without
marking them migrated. Key init also reloads prefs before deciding to
generate and re-reads after writing, adopting whatever landed so all
isolates converge on a single key instead of orphaning ciphertext.
Race candidates created with IOWebSocketChannel.connect fail their
ready future on connect timeout or an unreachable address, and nothing
observed it — web_socket_channel reports connect errors there, so every
losing candidate surfaced as an unhandled fatal (the tracker's single
largest crash source, ~1300 events: bare 5s TimeoutException plus the
recurring no-route-to-host WebSocketChannelExceptions). The stream
listener's onError only covers post-connect errors. Swallow and log the
ready failure per candidate; the race outcome is unaffected.
Crashpad uploads minidumps straight to the DSN's /minidump endpoint,
which our tracker does not implement — every native Windows crash
(e.g. faults inside libmpv threads, #1490) was silently dropped.
The inproc handler stackwalks at crash time and reports through the
regular envelope endpoint, and crashpad_handler.exe/crashpad_wer.dll
leave the bundle.
Visited seasons' 200-item pages were retained for the screen's
lifetime — irrelevant for a 3-season show, tens of MB of Dart heap for
a 30-season one. Low-end TV now keeps the prefetch window (selected
season plus/minus one); evicted seasons transparently refetch through the
existing unloaded-hub path.
Ref #1349
Android trim callbacks are best-effort — LMK can kill without ever
delivering one — yet the RSS watchdog only ran on desktop with a fixed
1.5GB bar no 2GB TV box ever reaches before dying. The watchdog now
runs on Android with a threshold scaled to device RAM, an eviction
floor and cooldown against refetch churn, and a lower backgrounded bar
since a paused app is LMK's first candidate.
The logs-screen header and startup line now record the renderer, the
effects tier with its raw hardware signals, and the boot RSS, so
uploaded reports answer whether the reduced tier engaged.
Ref #1349
Flutter's imageCache budget only bounds decoded CPU bitmaps: Skia's GPU
resource cache is sized from the surface area (hundreds of MB on a
4K-composited TV) and the Dart old gen defaults to a large fraction of
physical RAM. Both drive LMK kills on 2GB boxes. Cap them via engine
shell args on hardware matching the reduced-tier triple.
Ref #1349
The decode clamp was height-only, so ultra-wide originals could decode
far past the display budget, and two small-slot fallbacks skipped
server-side resizing entirely — handing multi-megapixel originals to
the decoder behind tiny slots. The TV spotlight background's offline
branch had no decode bound at all. All artwork now flows through a
shared both-axes bound (ResizeImagePolicy.fit: aspect-preserving,
scale-down only), and the reduced tier gets matching thumb/poster
decode caps.
Ref #1349
Extract the manage/reorder-libraries sheet from the libraries screen into
a shared widget with a public launcher, and add a settings tile (top
group, gated on having libraries) that opens it. The libraries screen
keeps its side-nav refresh and re-select-on-hide behavior via launcher
hooks; the TV dialog path lives in the launcher so the entry works there
too.
Desktop overlay sheets defaulted to a fixed 400px max height, which left
the reorder sheet ~280px tall. Both default-constraint sites now use 75%
of window height, matching mobile.
close#1468
Some TV SoCs (Amlogic Mi Box class) stall the MediaCodec output path
after pause/resume: audio and the clock keep advancing but the picture
freezes until a seek flushes the codec. Arm a watchdog on every
transition to playing with a warm decoder; if rendered frames stop
advancing while the position moves, recover with a 250ms seek-back
(same-position seeks are short-circuited without a codec flush),
capped per session and logged for field diagnosis.
close#1454
Initialize TvDetectionService on every platform so the existing
force_tv_mode setting drives the 10-foot TV interface on Windows,
macOS, and Linux, and surface the toggle in appearance settings there.
Desktop keeps real-mouse behavior in TV mode: InputModeTracker still
flips between pointer and keyboard modes (cursor hidden while
keyboard-driven), segmented controls keep their hover affordance, and
the settings backup section stays available (only Android TV lacks a
document picker).
Adds PlatformDetector.debugSetIsDesktopOSOverride so TV-device
simulations in widget tests don't inherit the desktop test host's
platform.
close#1409
The two prior attempts at #783 failed for lack of a reliable trigger:
re-setting audio-device to its own value is a no-op, and gating
recovery on audio-device-list changes while current-ao is null never
fires in the real flows (a paused player only discovers the dead
WASAPI session at unpause, when the device list is already stable;
a zombie session that consumes samples silently never goes null).
Replace the single gate with a recovery scheduler on the mpv event
thread, ticked every wait-event timeout:
- WM_POWERBROADCAST resume (both PBT variants, via the plugin's
existing window-proc delegate) requests unconditional ao-reloads at
+1.5s and +6s after wake — the only trigger that reaches zombie
sessions; the second shot covers a first reload landing while the
audio stack is still restoring. The handoff is a single atomic
flag: no mpv calls or timers from the platform thread, so there is
nothing to clean up on dispose.
- current-ao falling to null arms clock-driven retries (budget 5,
x2 backoff capped at 8s); retries must be clock-driven because a
failed ao-reload falls back to null without a property change
event. A device-list change while null refreshes the budget, and
audio-device-list is now observed natively so recovery does not
depend on the Dart side.
Every trigger, attempt, and outcome logs as [mpv:audio-recovery] at
warn level so exported logs from the reporter are actionable if this
still misses. Verified with synthetic PBT_APMRESUMEAUTOMATIC while
playing and while paused: full WASAPI reinit both times, audio
restarts on unpause.
Ref #783
Same clients.plex.tv surface that broke in #1488: /home/users powers
first sign-in and profile setup, so a drifted scalar shape would kill
the whole fetch. Coerce strings, bools, and ints tolerantly instead of
hard-casting.
Around July 3 plex.tv started returning the profile language-list fields
(defaultAudioLanguages, defaultSubtitleLanguages, mediaReviewsLanguages)
as comma-separated strings instead of arrays. The generated cast threw on
the successful 201 /switch response, dropping the freshly minted Home
user token: every rebind failed, the binder retried a /switch mint every
1-2s, and the app sat permanently in offline mode even after re-signing
in. Accounts without language prefs set were unaffected, which is why
the breakage looked sporadic.
Parse the language lists with a CSV-aware coercion, and make
UserSwitchResponse.fromJson strict only about authToken: decorative
fields now coerce tolerantly and a broken profile blob falls back to
defaults, so account-API drift can never brick token minting again.
close#1488
The library Recommended tab never subscribed to deletion events, so
"Delete from server" left the episode sitting in Continue Watching
until a full reload. Make the tab DeletionAware (remove in place across
all hubs, then resync) and give DiscoverProvider the same subscription
so the home row and hubs drop deleted items too.
close#1486
Impeller was blanket-disabled on TV in e759dafa as a precaution when the
Tensor/NVIDIA fallbacks landed, and #749 (API 30 Fire TV Cube freezing
until the Skia build) validated it. Flutter has since deprecated the
Impeller opt-out, so start walking the disable back where the hardware
can take it: TV devices on Android 12+ with a Vulkan 1.1 driver use
Impeller, while Fire OS (modern API levels on GPUs whose drivers can't
back it up), pre-12 boxes, and the existing NVIDIA/Huawei/e-waste quirks
stay on Skia. Tag Sentry events with the active renderer so TV
regressions are attributable while this rolls out.
TV boxes share limited RAM with 4K video decode buffers, so the 100MB
phone budget crowds out the decoder. Low-RAM TVs already on the reduced
tier keep the tighter 48MB cap.
From #1479.
Profile traces showed 100-370ms UI-thread frames while scrolling the
library screen, dominated by rebuilding and inflating media cards.
- Gate per-card focus/pointer chrome on input mode: FocusableWrapper
skips the scale/border wrappers and creates its AnimationController
lazily outside keyboard mode, and ClickableCursor plus the card tap
region only build MouseRegion/InkWell machinery on desktop - TV and
touch use a bare GestureDetector. Hub cards also drop their outer
gesture wrapper outside keyboard mode; the card's own tap region
always won the gesture arena anyway.
- Memoize sliver children (SliverChildMemo): browse/collections grids
and hub rows return identical widget instances for unchanged items,
so delegate swaps from pagination, watch-state, and deletion
setStates no longer rebuild every realized card inside layout. The
browse tab prunes the memo in lockstep with focus-node eviction so a
cached card can never resurrect a disposed FocusNode.
- Budget fresh inflation (CardInflationBudget): while a scrollable is
moving in pointer/touch mode at most one new card inflates per frame,
the rest render as SkeletonMediaCard and upgrade on following frames.
Hub rows also stop pre-inflating 250px of off-screen cards on entry.
Device traces: worst frame 373ms -> 103ms, per-card build 3.6ms ->
2.4ms median; remaining row-entry work is spread across frames.
Resolve platform, hardware model, and friendly device name once via a
shared DeviceIdentityService and send them to both backends: Plex gets
a real X-Plex-Platform plus X-Plex-Device/X-Plex-Device-Name (shown as
Player in dashboards/Tautulli), Jellyfin gets the device name in the
MediaBrowser auth header. Transcode and live-TV decision requests keep
their pinned platform names, which Plex validates server-side.
close#1270
Anchor the overlay to the screen and seekbar clearance, widen the card
on height-limited screens so sections pack side by side, and scale down
as a last resort instead of clipping the bottom sections.
close#1469
Signing in triggered two back-to-back profile rebinds; the second re-added
the same Jellyfin connection, which tore down the live client and aborted
the home screen's in-flight fetches. The aborted pass was committed as
loaded-empty, flashing 'no content available' until the follow-up load
landed. Fix at the root instead of patching the sign-in window:
- addJellyfinConnection now reuses the live client when the connection is
unchanged (token, deviceId, URL set), matching the existing Plex
refreshTokensForProfile behavior; material changes still recreate it.
- Cancelled requests are classified end-to-end: the client's
treat-as-empty helpers rethrow cancellations, and the aggregation
fan-outs report cancelledServerIds alongside succeededServerIds.
- A fetch pass in which zero servers succeeded is never authoritative:
it keeps existing content instead of wiping it (also fixes the
pre-existing blanking of home/sidebar on a totally failed refresh),
stays in loading while disrupted (cancellation or binding in flight),
and only commits loaded-empty on a settled failure.
The hero dots/pause row was gated on live input mode, so any navigation
key event (Android back key, BT keyboards, gamepad-source noise) hid it
until the next pointer event - on phones it ended up permanently hidden.
Gate on the TV platform instead (issue #600's actual scope); the TV
layout never renders the carousel, so nothing changes there.
The bottom fade lost its guaranteed opaque band when the section-level
gradient was removed (686a61ac): the per-item overlay only reached full
background at the literal last pixel, letting artwork ghost through the
final 15% and read as a hard cut against the content below - worst on
phones, where square hero art is bright at the bottom. Finish the fade
at solid background from the 0.94 stop (~32px band on a phone hero).
The preview cleared unconditionally 2s after a committed seek; on a device
still buffering, the live position hasn't caught up yet, so the next key
press re-based off the stale position and re-targeted the same spot -
discarding the seek exactly on the slow hardware the coalescing targets.
Re-arm the clear until the live position is near the target, with a 10s
ceiling backstop (same settle pattern as LiveSeekAccumulator).
350ms sits below the typical KeyDown-to-first-repeat gap (~400-500ms on
Android/TV), so every hold committed an intermediate seek before repeats
even began. 800ms matches Jellyfin ATV (media3's DefaultTimeBar uses 1s);
key release still flushes synchronously, so taps are unaffected.
_lastDispatchedTimelineSeek was set by the drag throttle and never reset, so
with key-seeks now routed through onSeekEnd a coalesced flush targeting the
same position as a stale drag (realistic at the 0/duration clamp boundaries)
skipped the real seek while still reporting completion.
A held fast-forward fired a real seek on every key-repeat for direct/
progressive streams, flooding the source with overlapping range requests and
wedging low-power devices (e.g. Fire TV Stick) in BUFFERING until the player
was restarted. Route direct play through the same preview/debounce path the
transcode case already uses: move only the timeline preview while the key is
held and commit a single seek on release or pause.
close#1424
Dual-stack hostnames with unreachable IPv6 stalled past the endpoint
probe budget because WinHTTP tries addresses sequentially. Enable
WINHTTP_OPTION_IPV6_FAST_FALLBACK (Happy Eyeballs) via win_http 0.2.2
so IPv4 is raced after the IPv6 attempt, matching curl/Cronet/NSURLSession.
close#1128
When focus drifts to the player's screen-level Focus node (after an
overlay sheet closes with the controls hidden), a hardware play/pause
key only revealed the chrome and leaked to Android's MediaSession,
which pauses unreliably when its state is throttled or suspended.
Intercept media play/pause at the screen node on TV-style navigation:
toggle via the same playback-intent path as the Apple TV bridge and
consume the event.
close#1375
Backing out to home selected the home tab but let focusActiveTabIfReady
steal real focus into the content while _isSidebarFocused stayed true,
leaving the rail stuck expanded over a shifted content box that clipped
the end of every hub row. Skip the content-focus grab while the sidebar
owns focus, move the sidebar focus ring to the Home item on back-to-home,
and self-heal the flag whenever the content scope actually gains focus.
close#1411
Badge the mobile Library options icon with a dot while filters are
active, and replace the "This library is empty" state with a
"No items match the active filters" message plus a reset button.
close#1470
FFmpeg's protocol lookup is case-sensitive, so a stored "Https://" base
URL reaches mpv verbatim through the direct-play string concat and fails
with "Protocol not found" (API calls survive because Dart's Uri
lowercases the scheme). Canonicalize at Jellyfin URL intake and in the
connection constructor so persisted configs self-heal on load, and
register mpv-escaped (https\://) redaction variants so option-value
logs stop leaking the server host.
close#1465