Skip to content

feat(tv): stagger + virtualize thumbnail loading#143

Merged
robbiebyrd merged 7 commits into
mainfrom
feat/tv-channel-stagger
Jun 25, 2026
Merged

feat(tv): stagger + virtualize thumbnail loading#143
robbiebyrd merged 7 commits into
mainfrom
feat/tv-channel-stagger

Conversation

@robbiebyrd

Copy link
Copy Markdown
Collaborator

Summary

  • Concurrency-bounded queue: K players load concurrently (K from deviceMemoryhardwareConcurrency/2 → 4, clamped to [2,8]). Below 4 channels, mounts everything with no queue.
  • IntersectionObserver virtualization: thumbnails outside the strip viewport (+ 200px margin) are unmounted; priority channels (the focused player) are pinned visible even when scrolled off-screen.
  • Per-tier buffer caps: focused player gets a 30s/60MB buffer; grid players 10s/20MB; thumbnails 6s/10MB — runtime-mutated via capHlsLevel alongside the existing ABR ceiling.

Files

File Role
staggerLoad.ts Pure state machine: computeConcurrency, reconcile, markLoaded, shouldMount, bufferCapsForLevel
useStaggeredLoad.ts React hook: IntersectionObserver + queue, stable refs, no render-loop
TV.tsx Wiring: stripRef, priorityIds, channelIds, gate shouldMount, markLoaded in onReady, buffer caps via capHlsLevel

Bugs caught and fixed during review

  • Buffer caps frozen at level 0: hlsConfigFor caches config on first render (all players at level 0), so buffer tiers were never reachable. Fixed: capHlsLevel now mutates el.api.config.{maxBufferLength,backBufferLength,maxBufferSize} at runtime alongside autoLevelCapping.
  • Grid-mode queue jam: priorityIds = selectedPlayers in grid mode caused selected channels (which render title-only, never mounting a <ReactPlayer>) to permanently occupy concurrency slots. Fixed: priorityIds = [] in multiSelectMode.

Test plan

  • Open TV with ≥5 channels — confirm only K players load initially (watch Network tab: only K HLS fetches start)
  • Scroll the thumbnail strip — off-screen thumbnails unmount, on-screen ones load
  • Click a channel — it loads immediately (priority promotion); other slots fill behind it
  • Enter grid/multi-select mode — all visible strip thumbnails load normally (no queue jam)
  • npx vitest run in packages/frontend → 179/179 passing

Stacked on

PR #142 (feat/tv-graceful-quality) — base is feat/tv-graceful-quality, not main. Merge #142 first, then this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EnhE6P81JhB2ePneazHwMi

@robbiebyrd robbiebyrd changed the base branch from feat/tv-graceful-quality to main June 25, 2026 15:47
Robbie Byrd and others added 7 commits June 25, 2026 15:52
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnhE6P81JhB2ePneazHwMi
- capHlsLevel: also mutate el.api.config buffer fields alongside
  autoLevelCapping; hlsConfigFor caches at first render (always level 0),
  so buffer caps never reached higher tiers at runtime without this
- priorityIds: pass [] in multiSelectMode — selected players render title-
  only (no <ReactPlayer>) so they would never call markLoaded and would
  permanently occupy concurrency slots, starving strip thumbnails
- recompute: union priorityIds into effective visible set so the focused
  channel stays mounted even if its thumbnail scrolls out of the strip
- TV.module.scss: update stale comment (players are now unmounted off-screen)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnhE6P81JhB2ePneazHwMi
@robbiebyrd robbiebyrd force-pushed the feat/tv-channel-stagger branch from 8a7334e to b0be7f6 Compare June 25, 2026 15:52
@robbiebyrd robbiebyrd merged commit c4e4f86 into main Jun 25, 2026
4 checks passed
robbiebyrd pushed a commit that referenced this pull request Jun 25, 2026
Removes stagger/IntersectionObserver loading (PR #143 — Safari unstable).
Each thumbnail is now a static <img> refreshed every 4 s via thumbTick
and served from thumbnails/{slug}.jpg on Wasabi. Falls back to
thumbnails/offline.jpg on error (onError handler). Also reverts the
capHlsLevel buffer-cap mutation and bufferCapsForLevel spread in
hlsConfigFor that were added as part of the stagger work.
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.

1 participant