Files
streamyfin/docs/superpowers/specs/2026-05-22-chromecast-player-split-design.md

5.6 KiB

Chromecast Player Split — Design

Date: 2026-05-22 Branch: refactor-chromecast (PR #1402) Sub-project: C of the Chromecast refactor Status: Approved design — pending implementation plan


1. Problem

app/(auth)/casting-player.tsx is a 1428-line god-component. One file owns: cast SDK hooks, item fetching, live-progress tracking, slider/scrubbing, trickplay, track selection, episode/season fetching, segment skipping, the dismiss gesture, and ~780 lines of JSX. It is hard to read, hard to change safely, and every sub-project so far has had to navigate it.

2. Scope

In scope: a purely structural decomposition of casting-player.tsx into 4 custom hooks, 6 presentational components, and a thin orchestrator.

Out of scope — and a hard constraint: zero behaviour change. This is a mechanical extraction. No bug fixes, no feature changes, no logic edits. Known issues in this code (the loadEpisode / currentItem race from sub-project A's review; the trickplay window truncation, the progress-bar touch overlap, the time-label position) are explicitly NOT touched here — they belong to a later UX sub-project, and are easier to fix once the code lives in small focused files.

3. Architecture

casting-player.tsx becomes an orchestrator (~150-200 lines): it calls the hooks, assembles the data, and renders the 6 components plus the 3 existing modal components.

State flows orchestrator → components by props (approved approach 1) — no React context. Presentational components take typed props; all logic lives in hooks.

4. Hooks (hooks/)

Hook Extracts Returns (shape)
useCastPlayerItem fetchedItem state, its fetch effect, the currentItem derivation { fetchedItem, currentItem }
useCastPlayerProgress slider shared values, isScrubbing, trickplayTime, scrubPercentage, liveProgress, the sync refs/effects, the useTrickplay call slider state, scrub handlers, progress, trickplay data
useCastEpisodes episodes / nextEpisode / seasonData state, their fetch effects, loadEpisode { episodes, nextEpisode, seasonData, loadEpisode }
useCastDismissGesture translateY, context, panGesture, dismissModal, animatedStyle { panGesture, animatedStyle, dismissModal }

Existing hooks are reused unchanged: useCasting, useCastSelection, useChromecastSegments, useTrickplay (the latter called inside useCastPlayerProgress).

useCastPlayerProgress is the most intricate hook (shared values, refs, the live-progress interpolation) — it must be extracted with care and reviewed closely.

5. Components (components/casting/player/)

Component Extracts (JSX section)
CastPlayerHeader dismiss chevron, connection indicator, settings button
CastPlayerTitle title + episode/season info
CastPlayerPoster poster image, buffering overlay, skip intro/credits bar
CastPlayerEpisodeControls the 4-button row (Episodes / Previous / Next / Stop)
CastPlayerProgressBar slider, trickplay preview, time display
CastPlayerTransportControls rewind / play-pause / forward

Each is a pure presentational component with typed props. The 3 modal components (ChromecastDeviceSheet, ChromecastEpisodeList, ChromecastSettingsMenu) already exist and stay as-is — the orchestrator keeps rendering them.

6. Constraints

  • Mechanical extraction only. Each component's / hook's props and returns mirror exactly what the inline code used. No new logic, no renamed behaviour, no fixes.
  • Component prop interfaces are derived from what the extracted JSX references in the original component scope.
  • Follow existing repo conventions: hooks flat in hooks/; the new components grouped under components/casting/player/.

7. Verification

The casting UI has no unit tests (consistent with the rest of the casting UI — it is React Native / SDK-heavy). Verification is:

  • bun run typecheck — green after every task (catches wiring/prop errors).
  • bun test utils/casting/ — stays green (the pure-logic suites are untouched).
  • A full manual re-test of the cast player after the split: cast a movie and an episode, switch audio / subtitle / quality / version, navigate episodes, scrub the progress bar, trigger trickplay, dismiss the player. Behaviour must be identical to before the split.

8. Files

Created:

  • hooks/useCastPlayerItem.ts, hooks/useCastPlayerProgress.ts, hooks/useCastEpisodes.ts, hooks/useCastDismissGesture.ts
  • components/casting/player/CastPlayerHeader.tsx, CastPlayerTitle.tsx, CastPlayerPoster.tsx, CastPlayerEpisodeControls.tsx, CastPlayerProgressBar.tsx, CastPlayerTransportControls.tsx

Modified:

  • app/(auth)/casting-player.tsx → thin orchestrator.

9. Success criteria

  • casting-player.tsx is ~150-200 lines and contains no inline logic clusters or large JSX sections — only hook calls and component composition.
  • Each new file has one clear responsibility and a typed interface.
  • bun run typecheck and bun test utils/casting/ pass.
  • The manual re-test shows behaviour identical to before the split.

10. Risks

  • The hooks share state (progress feeds the slider, the time display, and segment skipping). The extraction must preserve the exact data flow and effect ordering. Mitigation: one hook / component per task, bun run typecheck after each, and a full manual re-test at the end.
  • useCastPlayerProgress carries reanimated shared values and timing refs — the highest-risk extraction; its task gets the closest review.
  • No unit tests guard the UI — the manual re-test is the only behavioural safety net.