Skip to content

Add trickplay scrub-preview thumbnails to the Player scrubber - #87

Merged
imbenjamin merged 3 commits into
developfrom
feature/trickplay-scrub-thumbnails
Aug 17, 2026
Merged

Add trickplay scrub-preview thumbnails to the Player scrubber#87
imbenjamin merged 3 commits into
developfrom
feature/trickplay-scrub-thumbnails

Conversation

@imbenjamin

Copy link
Copy Markdown
Owner

Summary

Dragging the scrubber now shows a small preview thumbnail above the thumb, sourced from Jellyfin's own server-generated Trickplay tile sheets rather than AetherEngine.

An AetherEngine-backed version was tried first (its own cache-backed scrub-still API) but confirmed dead on a real device: it only serves stills from segments already decoded into a narrow window near the current playhead, so every request during an actual scrub-bar drag came back nil. That plumbing has been fully reverted in favor of Trickplay, which is server-pre-rendered and covers the entire timeline.

Changes

  • New TrickplayInfo DTO on BaseItemDto.trickplay (Fields=Trickplay), ImageURLBuilder.trickplayTileURL(...), and TrickplayMath/TrickplayThumbnailProvider (pure sheet/tile lookup + fetch-and-crop, reusing RemoteImageLoader's existing URL-keyed caching).
  • PlayerViewModel resolves a TrickplayThumbnailProvider in start() once the active media source is known; PlayerControlsOverlay debounces (120ms) scrub-thumbnail fetches per drag tick.
  • Two scrubber-bar polish fixes found during manual testing:
    • Fixed-width leading/trailing timestamp labels so the scrubber track no longer resizes when the countdown crosses an hour/minute digit boundary.
    • The thumbnail bubble is now attached via .overlay instead of as a ZStack sibling, so it can no longer inflate the scrubber track's own layout height.
  • Blank-space taps on the controls overlay now dismiss it immediately instead of waiting for the auto-hide timer, but only in the middle band — the top button row and scrubber row stay "protected" from near-miss taps.

Testing

  • New TrickplayMathTests/TrickplayThumbnailProviderTests, ImageURLBuilderTests coverage for the new tile URL, and PlayerViewModelTests coverage for trickplayProvider resolution.
  • 469 tests passing (xcodebuild test).
  • Manually verified in the Simulator: thumbnails load and track drags smoothly (including across sheet boundaries), scrubber bar stays fixed-width, blank-tap dismiss respects the protected top/scrubber bands, and the thumbnail bubble no longer pushes the scrubber row down.

🤖 Generated with Claude Code

imbenjamin and others added 2 commits August 17, 2026 15:05
Dragging the scrubber now shows a small preview thumbnail above the
thumb, sourced from Jellyfin's own server-generated Trickplay tile
sheets (a still every few seconds across the whole runtime, packed
into tile-sheet JPEGs) rather than AetherEngine's playback engine.

An AetherEngine-backed version was tried first (its own cache-backed
scrub-still API) but confirmed dead on a real device: it only serves
stills from segments already decoded into a narrow window near the
current playhead, so every request during an actual scrub-bar drag
(aimed at wherever the user hasn't watched yet) came back nil. That
plumbing has been fully reverted in favor of Trickplay, which is
server-pre-rendered and covers the entire timeline.

- New `TrickplayInfo` DTO on `BaseItemDto.trickplay` (`Fields=Trickplay`),
  `ImageURLBuilder.trickplayTileURL(...)`, and `TrickplayMath`/
  `TrickplayThumbnailProvider` (pure sheet/tile lookup + fetch-and-crop,
  reusing `RemoteImageLoader`'s existing URL-keyed caching).
- `PlayerViewModel` resolves a `TrickplayThumbnailProvider` in `start()`
  once the active media source is known; `PlayerControlsOverlay` debounces
  (120ms) scrub-thumbnail fetches per drag tick, same cancel-and-replace
  shape `SearchViewModel`'s search debounce already uses.
- Two scrubber-bar polish fixes found during manual testing:
  - Fixed-width leading/trailing timestamp labels (reserved via an
    invisible worst-case reference string) so the scrubber track no
    longer resizes when the countdown crosses an hour/minute digit
    boundary (e.g. "-1:00:00" -> "-59:59") — this was also destabilizing
    the thumbnail bubble's own drag-to-x-offset math.
  - The thumbnail bubble is now attached via `.overlay` instead of as a
    `ZStack` sibling, so its own taller natural size can no longer
    inflate the scrubber track's reported layout height (was visibly
    dropping the whole scrubber row on every drag).
- Blank-space taps on the controls overlay now dismiss it immediately
  (fading out) instead of waiting for the auto-hide timer, but only in
  the middle band between the top button row and the scrubber row —
  those two rows stay "protected" so a near-miss tap trying to hit an
  actual button/the scrubber can't accidentally close the UI.

Tests: new `TrickplayMathTests`/`TrickplayThumbnailProviderTests`,
`ImageURLBuilderTests` coverage for the new tile URL, and
`PlayerViewModelTests` coverage for `trickplayProvider` resolution
replacing the reverted AetherEngine-based cases. 469 tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bing

Full review of the trickplay branch surfaced four issues, all fixed here:

- TrickplayThumbnailProvider now uses its own dedicated RemoteImageLoader
  (80MB budget) instead of RemoteImageLoader.shared. A decoded trickplay
  sheet is ~23MB (RemoteImageLoader.estimatedByteCost costs by decoded
  size, not file size); a scrub session touching a handful of sheets could
  fill the entire shared 150MB cache on its own and evict posters/
  backdrops the rest of the app depends on for instant redisplay. The
  dedicated instance is also scoped to and released with the player
  session instead of lingering under the shared cache's LRU policy.

- item(userID:itemID:) now takes an overridable `fields:` param, mirroring
  items(...)'s existing convention. detailFields is back to its
  pre-trickplay value; only PlayerViewModel.start() requests `Trickplay`
  explicitly via the new detailFieldsWithTrickplay constant. Previously
  every caller of item(userID:itemID:) - including several
  AssetDetailViewModel polling loops that never touch trickplay data -
  was requesting it too.

- PlayerControlsOverlay now cancels scrubThumbnailTask on .onDisappear.
  SwiftUI doesn't auto-cancel a plain Task stored in @State the way it
  does a .task{} modifier's Task, so closing the player mid-drag left a
  scheduled fetch/crop running to completion against an orphaned view.

- requestScrubThumbnail(at:) is now a throttle instead of a debounce.
  The old cancel-and-restart-on-every-tick debounce could in principle
  never fire during a sufficiently fast, sustained, continuous drag (a
  debounce only fires once input goes quiet). It now guarantees a fetch
  roughly every 120ms during continuous movement - firing immediately if
  that long has passed since the last fetch, otherwise scheduling exactly
  one trailing fetch that reads whatever position is latest when it runs.

469 tests passing. Manually verified: posters/backdrops on Home stay
cached (no reload flicker) after a scrub session touching multiple
trickplay sheets, closing the player mid-drag doesn't crash or warn, and
a sustained continuous drag updates the thumbnail steadily rather than
only once the finger pauses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@imbenjamin imbenjamin linked an issue Aug 17, 2026 that may be closed by this pull request
@imbenjamin
imbenjamin merged commit 02e0550 into develop Aug 17, 2026
2 checks passed
@imbenjamin
imbenjamin deleted the feature/trickplay-scrub-thumbnails branch August 17, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Thumbnail scrubbing / trickplay

1 participant