mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 12:08:37 +01:00
docs(casting): add device profiles & capability detection design spec
Design for sub-project A of the Chromecast refactor: per-device capability detection, profile builder replacing the two static profiles, unified cast load, and load-path bug fixes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
249
docs/superpowers/specs/2026-05-21-chromecast-profiles-design.md
Normal file
249
docs/superpowers/specs/2026-05-21-chromecast-profiles-design.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Chromecast Device Profiles & Capability Detection — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-21
|
||||||
|
**Branch:** `refactor-chromecast` (PR #1402)
|
||||||
|
**Sub-project:** A (cornerstone) of the Chromecast refactor
|
||||||
|
**Status:** Approved design — pending implementation plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
Casting to Chromecast crashes for a large class of media:
|
||||||
|
|
||||||
|
- `Media control channel status code 2100` (LOAD_FAILED) on movies and high-bitrate media.
|
||||||
|
- 5.1 / multichannel audio crashes the receiver (issue #1085).
|
||||||
|
- Casting reportedly only works at video bitrate ≤ 2 Mb/s (issue #1423).
|
||||||
|
- Movies fail to load entirely; some media loads forever.
|
||||||
|
|
||||||
|
**Root cause:** the sender ships a device profile that does not match the actual
|
||||||
|
Chromecast. Two near-identical static profiles (`utils/profiles/chromecast.ts`,
|
||||||
|
`utils/profiles/chromecasth265.ts`) are selected by a single global boolean
|
||||||
|
`enableH265ForChromecast`. A 1st/2nd/3rd-gen Chromecast (H.264-only, 1080p, 2-channel
|
||||||
|
audio, no HEVC) can receive an HEVC / 10-bit / high-bitrate stream it cannot decode,
|
||||||
|
producing status 2100.
|
||||||
|
|
||||||
|
Secondary defects in the load path:
|
||||||
|
|
||||||
|
- `getStreamUrl` defaults `audioStreamIndex` to `0`, which is the video stream — wrong
|
||||||
|
audio track selection.
|
||||||
|
- `getStreamUrl` always takes `MediaSources[0]`, ignoring a requested `mediaSourceId`.
|
||||||
|
- Progress reporting uses `mediaInfo.contentId` (the item id) as `PlaySessionId` — the
|
||||||
|
Jellyfin dashboard cannot correlate the session.
|
||||||
|
- The load sequence (`getStreamUrl` → `buildCastMediaInfo` → `client.loadMedia`) is
|
||||||
|
duplicated in three places: `PlayButton.tsx`, and `casting-player.tsx`
|
||||||
|
(`reloadWithSettings` and `loadEpisode`).
|
||||||
|
- `loadMedia` failures are swallowed into `console.error` — the user sees nothing.
|
||||||
|
|
||||||
|
## 2. Scope
|
||||||
|
|
||||||
|
**In scope**
|
||||||
|
|
||||||
|
- Per-device Chromecast capability detection.
|
||||||
|
- A profile builder replacing the two static profile files.
|
||||||
|
- A single unified cast-load function.
|
||||||
|
- The load-path bug fixes listed above.
|
||||||
|
- Settings migration from `enableH265ForChromecast` to an explicit profile mode.
|
||||||
|
- Failure resilience (downgrade-on-failure retry, user-visible errors).
|
||||||
|
- A test matrix to empirically calibrate conservative defaults.
|
||||||
|
|
||||||
|
**Out of scope** (later sub-projects)
|
||||||
|
|
||||||
|
- Audio / subtitle / quality track switching — sub-project B.
|
||||||
|
- Splitting the 52 KB `casting-player.tsx` — sub-project C.
|
||||||
|
- Episode navigation, remote controller, UX features — sub-project D.
|
||||||
|
- Custom Cast receiver (PR #1521) — separate later sub-project.
|
||||||
|
|
||||||
|
## 3. Approach
|
||||||
|
|
||||||
|
Approach 3 of the brainstorm: a **profile builder** driven by a **capability
|
||||||
|
registry** with a **conservative default**, plus **advanced override settings**.
|
||||||
|
|
||||||
|
Detection produces smart defaults; power users can override. An unknown device always
|
||||||
|
falls back to a safe baseline (H.264 / 1080p / 2 channels) that cannot crash the
|
||||||
|
receiver. The user's test hardware (Chromecast 1/2/3 HD) maps exactly onto that
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## 4. Architecture
|
||||||
|
|
||||||
|
### New modules
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `utils/casting/capabilities.ts` | `ChromecastCapabilities` type, model registry, `detectCapabilities(device, settings)` |
|
||||||
|
| `utils/casting/buildProfile.ts` | `buildChromecastProfile(caps)` → Jellyfin `DeviceProfile` |
|
||||||
|
| `utils/casting/castLoad.ts` | `loadCastMedia(...)` — unified load, used by all call sites |
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `utils/profiles/chromecast.ts`
|
||||||
|
- `utils/profiles/chromecasth265.ts`
|
||||||
|
|
||||||
|
### Modified
|
||||||
|
|
||||||
|
- `components/PlayButton.tsx` — use `loadCastMedia`.
|
||||||
|
- `app/(auth)/casting-player.tsx` — `reloadWithSettings` and `loadEpisode` use `loadCastMedia`.
|
||||||
|
- `utils/atoms/settings.ts` — replace `enableH265ForChromecast`.
|
||||||
|
- Chromecast settings UI (`ChromecastSettings.tsx` / `ChromecastSettingsMenu.tsx`) — new controls.
|
||||||
|
|
||||||
|
## 5. Capability model
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ChromecastCapabilities {
|
||||||
|
/** HEVC 8-bit (Main profile) decode support. */
|
||||||
|
hevc: boolean;
|
||||||
|
/** HEVC 10-bit (Main10) decode support. */
|
||||||
|
hevc10bit: boolean;
|
||||||
|
/** Maximum video resolution height. */
|
||||||
|
maxResolution: 1080 | 2160;
|
||||||
|
/** Maximum video bitrate in bits/second. */
|
||||||
|
maxVideoBitrate: number;
|
||||||
|
/** Maximum audio channels the receiver can output. */
|
||||||
|
maxAudioChannels: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conservative default
|
||||||
|
|
||||||
|
Used for any unrecognised device. Equals a 1st/2nd/3rd-gen Chromecast:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const CONSERVATIVE: ChromecastCapabilities = {
|
||||||
|
hevc: false,
|
||||||
|
hevc10bit: false,
|
||||||
|
maxResolution: 1080,
|
||||||
|
maxVideoBitrate: 8_000_000, // initial guess — calibrated by the test matrix
|
||||||
|
maxAudioChannels: 2,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> `maxVideoBitrate` starts at 8 Mb/s. Issue #1423 (≤ 2 Mb/s) is suspicious — 2 Mb/s is
|
||||||
|
> below any real hardware limit and likely points to a transcode-readiness or HLS
|
||||||
|
> issue rather than a bitrate ceiling. The test matrix (§9) confirms the true value
|
||||||
|
> before this number is finalised.
|
||||||
|
|
||||||
|
### Registry
|
||||||
|
|
||||||
|
Keyed by `device.modelName`. Initial entries:
|
||||||
|
|
||||||
|
- `"Chromecast"` — gen 1/2/3 → conservative baseline.
|
||||||
|
- `"Chromecast Ultra"` — HEVC, 2160p, higher bitrate, multichannel passthrough.
|
||||||
|
- `"Chromecast with Google TV"` — HEVC, HEVC 10-bit.
|
||||||
|
- `"Google TV Streamer"` — HEVC, HEVC 10-bit, 2160p.
|
||||||
|
|
||||||
|
`modelName` is ambiguous (gen 1/2/3 all report `"Chromecast"`, and Google TV devices
|
||||||
|
may report a TV model string). This is acceptable: the registry only *upgrades* away
|
||||||
|
from the conservative baseline for confidently-identified devices; everything else
|
||||||
|
stays safe.
|
||||||
|
|
||||||
|
### `detectCapabilities(device, settings)`
|
||||||
|
|
||||||
|
1. Look up `device.modelName` in the registry; fall back to `CONSERVATIVE`.
|
||||||
|
2. Apply advanced settings overrides:
|
||||||
|
- `chromecastProfile: "force-hevc"` → `hevc: true`.
|
||||||
|
- `chromecastProfile: "force-h264"` → `hevc: false, hevc10bit: false`.
|
||||||
|
- `chromecastMaxBitrate` set → clamp `maxVideoBitrate`.
|
||||||
|
3. Return the merged capabilities.
|
||||||
|
|
||||||
|
## 6. Profile builder
|
||||||
|
|
||||||
|
`buildChromecastProfile(caps)` returns a Jellyfin `DeviceProfile`:
|
||||||
|
|
||||||
|
- **Video codecs:** `h264` always; `hevc` only when `caps.hevc`. When `hevc` is
|
||||||
|
enabled but `caps.hevc10bit` is false, add a codec condition rejecting 10-bit so the
|
||||||
|
server transcodes 10-bit sources down.
|
||||||
|
- **Audio:** both `DirectPlayProfiles` and `CodecProfiles` constrained to
|
||||||
|
`caps.maxAudioChannels`. This forces multichannel audio to transcode to stereo —
|
||||||
|
fixes the 5.1 crash (#1085). The current profiles only constrain `CodecProfiles`,
|
||||||
|
leaving a direct-play path open.
|
||||||
|
- **Bitrate:** `MaxStreamingBitrate` / `MaxStaticBitrate` = `caps.maxVideoBitrate`.
|
||||||
|
- **Resolution:** codec condition capping height at `caps.maxResolution`.
|
||||||
|
- Subtitle and container profiles preserved from the current working profile.
|
||||||
|
|
||||||
|
## 7. Unified cast load
|
||||||
|
|
||||||
|
```ts
|
||||||
|
loadCastMedia({
|
||||||
|
client, // RemoteMediaClient
|
||||||
|
device, // Cast Device — for capability detection
|
||||||
|
api, item, userId,
|
||||||
|
settings,
|
||||||
|
options: { audioStreamIndex?, subtitleStreamIndex?, maxBitrate?, mediaSourceId?, startPositionMs? },
|
||||||
|
}): Promise<{ ok: true } | { ok: false; error }>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sequence:
|
||||||
|
|
||||||
|
1. `detectCapabilities(device, settings)` → `buildChromecastProfile`.
|
||||||
|
2. `getStreamUrl` with the built profile.
|
||||||
|
3. `buildCastMediaInfo`.
|
||||||
|
4. `client.loadMedia`.
|
||||||
|
|
||||||
|
Replaces all three duplicated call sites. Bug fixes folded in:
|
||||||
|
|
||||||
|
- **Audio index:** when `options.audioStreamIndex` is absent, resolve the media
|
||||||
|
source's `DefaultAudioStreamIndex` (or the first `Audio`-type stream) instead of
|
||||||
|
defaulting to `0`.
|
||||||
|
- **Media source:** pass and honour `mediaSourceId`; select the matching
|
||||||
|
`MediaSource` rather than `[0]`.
|
||||||
|
- **PlaySessionId:** use the real `PlaySessionId` returned by `getStreamUrl`'s
|
||||||
|
`getPlaybackInfo` call, not `contentId`.
|
||||||
|
|
||||||
|
## 8. Settings
|
||||||
|
|
||||||
|
Replace `enableH265ForChromecast: boolean` with:
|
||||||
|
|
||||||
|
| Setting | Type | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `chromecastProfile` | `"auto" \| "force-hevc" \| "force-h264"` | `"auto"` |
|
||||||
|
| `chromecastMaxBitrate` | `number \| undefined` | `undefined` |
|
||||||
|
|
||||||
|
**Migration:** existing `enableH265ForChromecast === true` → `chromecastProfile:
|
||||||
|
"force-hevc"`; `false` or unset → `"auto"`.
|
||||||
|
|
||||||
|
UI: a profile-mode selector and an optional max-bitrate field in the Chromecast
|
||||||
|
settings screen. `"auto"` is presented as the recommended default; the overrides are
|
||||||
|
advanced controls.
|
||||||
|
|
||||||
|
## 9. Resilience
|
||||||
|
|
||||||
|
- **Downgrade-on-failure:** if `client.loadMedia` rejects with status 2100, retry
|
||||||
|
exactly once with a forced conservative profile (`force-h264`, 2 channels, low
|
||||||
|
bitrate). Handles unknown / mis-detected devices gracefully. A second failure
|
||||||
|
surfaces to the user.
|
||||||
|
- **User-visible errors:** a failed load shows an alert with an actionable message
|
||||||
|
instead of a silent `console.error`.
|
||||||
|
|
||||||
|
## 10. Verification — test matrix
|
||||||
|
|
||||||
|
A document, `docs/chromecast-test-matrix.md`, lists sample-file casting tests run
|
||||||
|
against the Chromecast HD test device:
|
||||||
|
|
||||||
|
| Dimension | Values |
|
||||||
|
|---|---|
|
||||||
|
| Video codec | H.264, HEVC 8-bit, HEVC 10-bit |
|
||||||
|
| Audio | AAC stereo, AAC 5.1, AC3 5.1, DTS, TrueHD |
|
||||||
|
| Container | MP4, MKV, TS |
|
||||||
|
| Bitrate | low, ~8 Mb/s, ~16 Mb/s, source-max |
|
||||||
|
|
||||||
|
Each row records: direct-play vs transcode, load result (OK / 2100 / infinite), notes.
|
||||||
|
Results calibrate `CONSERVATIVE.maxVideoBitrate` and confirm the true cause of #1423
|
||||||
|
and the 5.1 crash before the conservative defaults are finalised.
|
||||||
|
|
||||||
|
## 11. Success criteria
|
||||||
|
|
||||||
|
- A movie that previously failed with 2100 plays on the Chromecast HD.
|
||||||
|
- 5.1 / multichannel media plays (transcoded to stereo) without crashing.
|
||||||
|
- High-bitrate media plays or transcodes cleanly within the calibrated limit.
|
||||||
|
- Audio track defaults to the correct (non-video) stream.
|
||||||
|
- The Jellyfin dashboard shows a correctly correlated cast session.
|
||||||
|
- The load sequence exists in exactly one place.
|
||||||
|
- `bun run typecheck` and `bun run check` pass.
|
||||||
|
|
||||||
|
## 12. Risks
|
||||||
|
|
||||||
|
- The true cause of #1423 / the 5.1 crash is not yet confirmed; the test matrix
|
||||||
|
(§10) is the gate before defaults are locked. If the cause is HLS handling rather
|
||||||
|
than a profile mismatch, the design extends with a targeted fix at that point.
|
||||||
|
- `modelName` ambiguity limits registry precision — mitigated by the conservative
|
||||||
|
default never producing an unplayable stream.
|
||||||
Reference in New Issue
Block a user