fix(playback): advertise native Media3 HLS - #262
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe playback protocol adds a native HLS feature flag. Android capability detection advertises the flag on Media3 HLS delivery only. Robolectric tests verify the behavior for mobile and TV form factors. ChangesNative HLS playback capability
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This narrowly adds a native Media3 HLS capability for Android local HLS delivery while leaving other playback routes unchanged. No actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
Full details: Linked Issues checkExplanation The changes do not implement the primary requirements of linked issue Resolution Link this pull request to an issue that covers native Media3 HLS capability advertisement, or implement the requirements from
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
#257) * feat(android): server-driven card presentation presets on phone and TV Adds the "Cards & Posters" preference to both Android clients, porting the iOS/tvOS feature built on the canonical `ui.card_presentation` setting. The value is the contract's whole two-field object -- `poster_size` (compact/standard/large) and `caption` (title_metadata/title/artwork) -- surfaced as the same four presets Apple and the web UI offer (Balanced, Compact, Cinema, Artwork Only) plus per-axis pickers, with a synthetic "Custom" entry when the pair matches no preset. Per-device preferences: writes land at `profile_client` by default so a choice roams between like devices (Android TV joins the `tv` family, phone `mobile`, tablet `tablet` via a new X-Silo-Client-Family header), and an "Only this device" toggle writes at `profile_device` instead, which the server resolves ahead of the family and profile layers. Turning it off deletes that row so resolution falls back. Rendering follows Apple's semantics: rails and standalone cards scale 0.86/1.0/1.2, fixed grids shift a column either way, adaptive grids scale their min cell width, and the caption axis gates the title and metadata lines. The TV Skyline row band height is now derived from the scaled card height and caption rows rather than a fixed 0.50 fraction, so large cards have room without clipping the marquee -- reproducing today's band exactly at the standard preset. State lives in a new CardPresentationStore (android-shared), capability- gated on the settings contract, cached per server/profile/family/device for a jump-free cold start, with optimistic writes, latest-wins coalescing, and rollback on failure. It refreshes on foreground and reconnect alongside the existing overlay prefs, and clears on sign-out and on profile/server switch. Closes #163. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(diagnostics): expand Android client evidence (#261) * fix(playback): advertise native Media3 HLS (#262) * fix(tv): prefer native mounted transport for quick skips instead of re-anchoring + map seek input + fix second press for seek (#259) * fix(tv): prefer the mounted transport for quick skips instead of re-anchoring Server plans for growing-HLS transcodes and copy-remux transports publish an open-ended seek window with can_seek_anywhere=false, so every quick skip went through a protocol-V3 seek_reanchor: buffering UI, an API round trip, and a full restream, even when the target sat inside content the player already had mounted. decideSeek now accepts an optional mountedSeekableSourceRange: the source-time extent the currently mounted transport provably covers. The TV screen reports the Media3 window's seekability and extent alongside its existing position poll, and a target inside that range is a plain Player.seekTo. Targets the mounted transport cannot serve (before the stream origin, past the produced manifest head, or a growing progressive copy remux with no known length) still reanchor, and the existing same-route reanchor recovery remains the fallback for native seeks that fail with 404/410/416. * fix(tv): route media rewind and fast-forward keys through the player bridge The remote's rewind/fast-forward keys (plus SKIP_BACKWARD/SKIP_FORWARD) were unmapped, so the player bridge's null-action branch only revealed the transport controls and swallowed the first press - users had to press twice to seek - and the second press fell through to Media3's session-level seekBack/seekForward, bypassing the app's route-aware seek path and the Watch Together room transport gate. Map them to SkipBack/SkipForward like D-pad left/right. They ignore dpadHorizontalSeek because they are never focus navigation, and UP halves and auto-repeats are consumed so the system media-key fallback cannot seek a second time. * fix(tv): invalidate mounted-window seek facts when the transport is replaced The mounted-window facts feeding decideSeek's mountedSeekableSourceRange hint describe one Media3 item, but they were neither suppressed during a transport handoff nor cleared when a mount won. After a reanchor/replan/content switch, the previous transport's window end mapped through the new plan's timelineOffsetSeconds overstates the new transport's extent, and a quick skip inside that window could take a native path that clamps to the new item's short window - a silent, wrong-position seek with no error and no self-heal. The sharpest instance: onTransportMountApplied re-evaluates pendingNativeSeekAfterMount at exactly that moment of maximal staleness. Drop onPlayerWindowChanged reports while transportMountGate is suppressing (the same guard onPositionChanged applies), and clear both facts at the top of the applied(nonce) branch before the queued-seek re-evaluation. The next poll tick (<=500ms) repopulates from the mounted item; until then the hint is absent, which at worst costs an unnecessary reanchor. * docs(tv): document the seek-hint and media-key contracts KDoc for tvPlayerRemoteKeyAction (the full key-to-action mapping incl. why unmapped keys only reveal controls) and executeSeekTarget (the seek-commit routing ladder), plus one-line contracts on the new test functions. Addresses CodeRabbit's docstring-coverage pre-merge check on PR #259. --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> * fix(tv): keep the skyline rail gliding after a detail round trip (#260) returnRestoreRequest is bumped when a restore ladder starts and never reset for the feed's lifetime, while detailReturnPending is re-armed by every focus move. After the first detail round trip, every later focus move therefore re-fired TvMediaRow's restore effect and its instant scrollToItem, which cancels the rail pin's animated glide — the carousel snapped instead of scrolling until the feed was disposed again. Pass the restore request only while a ladder is actually in flight (restorationsInFlight > 0): the instant row scroll exists solely to compose a ladder's destination into the horizontal window. Also log restore scrolls through TvFocusLog so a regression is visible in logcat instead of only on screen. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: blurbery <271863866+blurbery@users.noreply.github.com> Co-authored-by: evulhotdog <365456+evulhotdog@users.noreply.github.com>
* feat: improve SiloCast remote controls * refactor(android): unify image loading on shared OkHttp and defer artwork presentation during scroll (#256) - Replace coil-network-ktor3 with coil-network-okhttp and register the fetcher explicitly: new SiloOkHttp foundation shares one ConnectionPool between the Ktor API clients and Coil, with per-client tuned dispatchers so image bursts and API fan-outs never queue behind each other. Loader config is unified in buildSiloImageLoader so phone and TV cannot drift. - Extend Home's existing artwork-presentation deferral to all artwork scroll surfaces via DeferImagePresentationWhileScrolling: freshly decoded images hold their thumbhash until the scroll settles (memory-cache hits still present instantly), now covering horizontal rows, grids, detail feeds, and vertical-scroll columns. - Fix stale-capture load-more triggers in CatalogGrid and SearchResults: keyless remember { derivedStateOf } froze first-composition hasMore/isLoading values, breaking pagination in some entry states. - Declare ktor-client-core explicitly in android-shared/androidApp (it was only reaching the compile classpath transitively via the old Coil artifact); regenerate dependency locks and verification metadata and drop stale TV lock pins. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(diagnostics): expand Android client evidence (#261) * fix(playback): advertise native Media3 HLS (#262) * fix(tv): prefer native mounted transport for quick skips instead of re-anchoring + map seek input + fix second press for seek (#259) * fix(tv): prefer the mounted transport for quick skips instead of re-anchoring Server plans for growing-HLS transcodes and copy-remux transports publish an open-ended seek window with can_seek_anywhere=false, so every quick skip went through a protocol-V3 seek_reanchor: buffering UI, an API round trip, and a full restream, even when the target sat inside content the player already had mounted. decideSeek now accepts an optional mountedSeekableSourceRange: the source-time extent the currently mounted transport provably covers. The TV screen reports the Media3 window's seekability and extent alongside its existing position poll, and a target inside that range is a plain Player.seekTo. Targets the mounted transport cannot serve (before the stream origin, past the produced manifest head, or a growing progressive copy remux with no known length) still reanchor, and the existing same-route reanchor recovery remains the fallback for native seeks that fail with 404/410/416. * fix(tv): route media rewind and fast-forward keys through the player bridge The remote's rewind/fast-forward keys (plus SKIP_BACKWARD/SKIP_FORWARD) were unmapped, so the player bridge's null-action branch only revealed the transport controls and swallowed the first press - users had to press twice to seek - and the second press fell through to Media3's session-level seekBack/seekForward, bypassing the app's route-aware seek path and the Watch Together room transport gate. Map them to SkipBack/SkipForward like D-pad left/right. They ignore dpadHorizontalSeek because they are never focus navigation, and UP halves and auto-repeats are consumed so the system media-key fallback cannot seek a second time. * fix(tv): invalidate mounted-window seek facts when the transport is replaced The mounted-window facts feeding decideSeek's mountedSeekableSourceRange hint describe one Media3 item, but they were neither suppressed during a transport handoff nor cleared when a mount won. After a reanchor/replan/content switch, the previous transport's window end mapped through the new plan's timelineOffsetSeconds overstates the new transport's extent, and a quick skip inside that window could take a native path that clamps to the new item's short window - a silent, wrong-position seek with no error and no self-heal. The sharpest instance: onTransportMountApplied re-evaluates pendingNativeSeekAfterMount at exactly that moment of maximal staleness. Drop onPlayerWindowChanged reports while transportMountGate is suppressing (the same guard onPositionChanged applies), and clear both facts at the top of the applied(nonce) branch before the queued-seek re-evaluation. The next poll tick (<=500ms) repopulates from the mounted item; until then the hint is absent, which at worst costs an unnecessary reanchor. * docs(tv): document the seek-hint and media-key contracts KDoc for tvPlayerRemoteKeyAction (the full key-to-action mapping incl. why unmapped keys only reveal controls) and executeSeekTarget (the seek-commit routing ladder), plus one-line contracts on the new test functions. Addresses CodeRabbit's docstring-coverage pre-merge check on PR #259. --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> * fix(tv): keep the skyline rail gliding after a detail round trip (#260) returnRestoreRequest is bumped when a restore ladder starts and never reset for the feed's lifetime, while detailReturnPending is re-armed by every focus move. After the first detail round trip, every later focus move therefore re-fired TvMediaRow's restore effect and its instant scrollToItem, which cancels the rail pin's animated glide — the carousel snapped instead of scrolling until the feed was disposed again. Pass the restore request only while a ladder is actually in flight (restorationsInFlight > 0): the instant row scroll exists solely to compose a ladder's destination into the horizontal window. Also log restore scrolls through TvFocusLog so a regression is visible in logcat instead of only on screen. * feat(android): server-driven card presentation presets on phone and TV (#257) * feat(android): server-driven card presentation presets on phone and TV Adds the "Cards & Posters" preference to both Android clients, porting the iOS/tvOS feature built on the canonical `ui.card_presentation` setting. The value is the contract's whole two-field object -- `poster_size` (compact/standard/large) and `caption` (title_metadata/title/artwork) -- surfaced as the same four presets Apple and the web UI offer (Balanced, Compact, Cinema, Artwork Only) plus per-axis pickers, with a synthetic "Custom" entry when the pair matches no preset. Per-device preferences: writes land at `profile_client` by default so a choice roams between like devices (Android TV joins the `tv` family, phone `mobile`, tablet `tablet` via a new X-Silo-Client-Family header), and an "Only this device" toggle writes at `profile_device` instead, which the server resolves ahead of the family and profile layers. Turning it off deletes that row so resolution falls back. Rendering follows Apple's semantics: rails and standalone cards scale 0.86/1.0/1.2, fixed grids shift a column either way, adaptive grids scale their min cell width, and the caption axis gates the title and metadata lines. The TV Skyline row band height is now derived from the scaled card height and caption rows rather than a fixed 0.50 fraction, so large cards have room without clipping the marquee -- reproducing today's band exactly at the standard preset. State lives in a new CardPresentationStore (android-shared), capability- gated on the settings contract, cached per server/profile/family/device for a jump-free cold start, with optimistic writes, latest-wins coalescing, and rollback on failure. It refreshes on foreground and reconnect alongside the existing overlay prefs, and clears on sign-out and on profile/server switch. Closes #163. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(diagnostics): expand Android client evidence (#261) * fix(playback): advertise native Media3 HLS (#262) * fix(tv): prefer native mounted transport for quick skips instead of re-anchoring + map seek input + fix second press for seek (#259) * fix(tv): prefer the mounted transport for quick skips instead of re-anchoring Server plans for growing-HLS transcodes and copy-remux transports publish an open-ended seek window with can_seek_anywhere=false, so every quick skip went through a protocol-V3 seek_reanchor: buffering UI, an API round trip, and a full restream, even when the target sat inside content the player already had mounted. decideSeek now accepts an optional mountedSeekableSourceRange: the source-time extent the currently mounted transport provably covers. The TV screen reports the Media3 window's seekability and extent alongside its existing position poll, and a target inside that range is a plain Player.seekTo. Targets the mounted transport cannot serve (before the stream origin, past the produced manifest head, or a growing progressive copy remux with no known length) still reanchor, and the existing same-route reanchor recovery remains the fallback for native seeks that fail with 404/410/416. * fix(tv): route media rewind and fast-forward keys through the player bridge The remote's rewind/fast-forward keys (plus SKIP_BACKWARD/SKIP_FORWARD) were unmapped, so the player bridge's null-action branch only revealed the transport controls and swallowed the first press - users had to press twice to seek - and the second press fell through to Media3's session-level seekBack/seekForward, bypassing the app's route-aware seek path and the Watch Together room transport gate. Map them to SkipBack/SkipForward like D-pad left/right. They ignore dpadHorizontalSeek because they are never focus navigation, and UP halves and auto-repeats are consumed so the system media-key fallback cannot seek a second time. * fix(tv): invalidate mounted-window seek facts when the transport is replaced The mounted-window facts feeding decideSeek's mountedSeekableSourceRange hint describe one Media3 item, but they were neither suppressed during a transport handoff nor cleared when a mount won. After a reanchor/replan/content switch, the previous transport's window end mapped through the new plan's timelineOffsetSeconds overstates the new transport's extent, and a quick skip inside that window could take a native path that clamps to the new item's short window - a silent, wrong-position seek with no error and no self-heal. The sharpest instance: onTransportMountApplied re-evaluates pendingNativeSeekAfterMount at exactly that moment of maximal staleness. Drop onPlayerWindowChanged reports while transportMountGate is suppressing (the same guard onPositionChanged applies), and clear both facts at the top of the applied(nonce) branch before the queued-seek re-evaluation. The next poll tick (<=500ms) repopulates from the mounted item; until then the hint is absent, which at worst costs an unnecessary reanchor. * docs(tv): document the seek-hint and media-key contracts KDoc for tvPlayerRemoteKeyAction (the full key-to-action mapping incl. why unmapped keys only reveal controls) and executeSeekTarget (the seek-commit routing ladder), plus one-line contracts on the new test functions. Addresses CodeRabbit's docstring-coverage pre-merge check on PR #259. --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> * fix(tv): keep the skyline rail gliding after a detail round trip (#260) returnRestoreRequest is bumped when a restore ladder starts and never reset for the feed's lifetime, while detailReturnPending is re-armed by every focus move. After the first detail round trip, every later focus move therefore re-fired TvMediaRow's restore effect and its instant scrollToItem, which cancels the rail pin's animated glide — the carousel snapped instead of scrolling until the feed was disposed again. Pass the restore request only while a ladder is actually in flight (restorationsInFlight > 0): the instant row scroll exists solely to compose a ladder's destination into the horizontal window. Also log restore scrolls through TvFocusLog so a regression is visible in logcat instead of only on screen. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: blurbery <271863866+blurbery@users.noreply.github.com> Co-authored-by: evulhotdog <365456+evulhotdog@users.noreply.github.com> * fix: address SiloCast review findings * fix: reconcile mute state for volume changes --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: blurbery <271863866+blurbery@users.noreply.github.com> Co-authored-by: evulhotdog <365456+evulhotdog@users.noreply.github.com>
What happened
Two users on the current Android build could not reliably start HEVC/Dolby Vision playback. The matching server logs showed the native Media3 HLS path being treated like a web MediaSource/hls.js client, so the server could not choose the correct HLS sample-entry recipe for Android.
The server-side compatibility fallback already makes unchanged build 15 clients work, but the durable fix is for Android to say explicitly that its local HLS delivery is handled by native Media3.
What I changed
native_hls_playback_v1capability name to the shared playback contract.This is intentionally delivery-scoped. It gives the server enough evidence to choose
hvc1for regular HEVC/DV7-to-HDR10 output anddvh1for preserved DV5/DV8 without changing unrelated playback routes.Live validation
I deployed the equivalent server behavior as build 122 and tested it with the two affected users on the unchanged Android build 15:
unsupported video sample-entry,transcode_start_failed, orspawn_failedsignatures. There were no genuine FFmpeg crashes.Both users confirmed playback now works, and User 2 continued watching multiple titles without another issue. This covers Auto and 2160p/4K, DV7 and DV8, audio-copy/AAC adaptation, seeking, HLS remux, and direct play. The server regression tests also cover Auto, Original, and 2160p planning.
Validation
Coordination and related work
Together with the companion server change, this closes out the remaining Android Media3 sample-entry/startup failure. It is a better boundary than globally allowing
hev1, which only moved the failure to the next Profile 8 validation and risked changing web playback behavior.Risk
Low and bounded. This adds one capability token to one delivery class. Existing build 15 clients remain supported by the server compatibility path, while upgraded clients provide explicit evidence. Original, progressive, Cast, Apple, and web behavior are unchanged.
AI Disclosure
gpt-5.6-sol), Ultra reasoningSummary by CodeRabbit
New Features
Tests