Skip to content

feat(spf): text tracks switching#1687

Open
cjpillsbury wants to merge 10 commits into
mainfrom
feat/spf-text-tracks-switching
Open

feat(spf): text tracks switching#1687
cjpillsbury wants to merge 10 commits into
mainfrom
feat/spf-text-tracks-switching

Conversation

@cjpillsbury

@cjpillsbury cjpillsbury commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Migrates text-track (caption/subtitle) selection onto the track-switching rule chain introduced in #1658, so captions gain the same failed-CDN constraint (drop tracks on a CDN in failover cooldown) and active-CDN scope that video/audio already run. Because captions are opt-in and off-able, it adds an optional-selection seam (a variant may resolve to no pick). The text slot is split into a sticky userTextTrackSelection intent input (DOM + programmatic) and a single-writer selectedTextTrackId resolved output, retiring the old selectTextTrack behavior. Internal SPF only (alpha).

For reviewers — how to read this PR

Bucket 1 — Runtime (focus here)

Targeted careful read:

  • track-switching.ts (+204) — the resolveSelection seam (optional terminal; defaults to the rule-chain head, so video/audio are unchanged); switchTextTrack runs [excludeFailedCdns, preferActiveCdn] then pickResolvedTextTrack, resolving userTextTrackSelection (language partial / 'off' / undefined→auto) against the constrained candidates and may select nothing. Verify the optional-none path stays distinct from the empty-candidate clear path added for capability-probing (no-pick-by-intent vs. constraints-emptied)
  • sync-text-tracks.ts (+69) — DOM→intent bridge now writes userTextTrackSelection, not the resolved id (single-writer invariant: switchTextTrack is the sole writer of selectedTextTrackId); the echo guard (showingId === selectedTextTrackId) must also absorb resolver-driven mode corrections (e.g. a pick disabled because its CDN failed) without writing back a spurious 'off'
  • engine.ts (+31) — switchTextTrack composed in place of selectTextTrack; userTextTrackSelection exposed via shareSignals as the programmatic + DOM write path
  • media/primitives/select-tracks.ts (Δ) — pickTextTrackFromTracks extracted from pickTextTrack; shared auto-default policy (preferred language → DEFAULT+AUTOSELECT → none; forced excluded) across the presentation-picker and the candidate-list terminal

Skim structure only: behaviors/select-tracks.ts (selectTextTrack + SelectTextTrackConfig removed, pickTextTrack stays — no dangling refs), engine-audio-only.ts / text-track-slots.ts / all.ts (wiring + exports), engine.test.ts (<track>.default assertion flipped to false)

Skim file: Tests — track-switching.test.ts (+278), sync-text-tracks.test.ts (+65), select-tracks.test.ts (Δ) — assertion shape across intent-resolution, optional-none, and echo-guard mode corrections

Sandbox (runtime-adjacent), skim file: spf-segment-loading/main.ts (+124) — subtitle picker driven via native textTracks[id].mode so a click exercises the real DOM→intent bridge, not a direct slot write

Bucket 2 — Design docs (skim)

Skim file: text-track-architecture.md (+72) — the intent-input + single-writer-output model; one-way mode mirror + DOM→intent bridge + echo guard; enableDefaultTrack default corrected to false

Skim or skip: subtitles.md (+28) and the stale-selectTextTrack-reference sweep across the registry (architecture, conventions/behaviors, capability-probing — text has no capability constraint —, clusters, multi-language-audio, source-replacement, audio-playback, audio-only-mode-override, background-looping-video, hls-engine)

Bucket 3 — Working-reference plan, no runtime (skip)

Skim or skip: .claude/plans/spf-text-track-switching-refactor.md

Smoke test

Sandbox: /spf-segment-loading/?src=https://stream.mux.com/s41JYeqIpBMBzE4OzxDyGR2yrp2hD1CQ6gJN9SlVGDQ.m3u8?redundant_streams=true

  • Load locally (pnpm dev:sandbox → paste the path into the running Vite server) or via the PR's deploy preview. Press Play (the harness video is preload="none"). redundant_streams=true lists each rendition on multiple CDNs, so text selection exercises the active-CDN scope it now runs through.
  • Observe:
    • The Subtitles/Captions picker fills with one button per language + Off; on load it sits at the engine's opt-in default (off unless a DEFAULT+AUTOSELECT rendition), not auto-enabled.
    • Click a language → captions render for it; the status row shows it pinned.
    • Click Off → captions hide and stay off (no resolver echo flips it back on).
    • "Reset to auto" → returns to the opt-in default.

What changed — by surface

Text selection onto the track-switching chain. switchTextTrack replaces selectTextTrack, running the same applyConstraints pre-pass (failed-CDN constraint) + active-CDN scope as video/audio. A new optional resolveSelection(candidates, deps) hook on setupTrackSwitching lets a variant resolve to undefined (the head-of-chain default assumed a track is always picked); text supplies pickResolvedTextTrack, which resolves the standing intent against the constrained candidates.

Intent input vs. resolved output. The single selectedTextTrackId slot is split: userTextTrackSelection (a language partial, 'off', or undefined=auto) is the sticky intent written by the DOM bridge and programmatic callers; selectedTextTrackId becomes a single-writer resolved output of switchTextTrack. This removes the multi-writer contention where the DOM action and the resolver both wrote one slot.

Engine rewire + cleanup. engine.ts composes switchTextTrack, exposes userTextTrackSelection via shareSignals, and the dead selectTextTrack behavior + SelectTextTrackConfig are removed (the pickTextTrack primitive stays, refactored to share its auto-default policy).

DEFAULT=YES <track> fix. SPF no longer sets the default attribute on its <track> elements — that made the browser auto-activate the slot, firing a change the bridge recorded as user intent and enabling captions past SPF's opt-in policy.

Doc cascade. text-track-architecture.md + subtitles.md rewritten to the switchTextTrack model; stale selectTextTrack / multi-writer references swept across the registry.

Notable design decisions

  • DOM bridge writes intent (userTextTrackSelection), not the resolved id. Alternative considered: keep the bridge writing selectedTextTrackId directly (status quo). Rejected because the DOM action and the switchTextTrack resolver then contend for one slot; routing the DOM through the intent slot makes selectedTextTrackId single-writer and lets the resolver own corrections (e.g. disabling a pick whose CDN failed).
  • Optional selection via a resolveSelection seam, not a separate text path. Alternative considered: special-case text inside setupTrackSwitching or give text its own selection behavior. Rejected because a generic optional-terminal hook (defaulting to the chain head) keeps video/audio untouched while unifying text onto the same chain — that's what gives captions the failed-CDN constraint + active-CDN scope they previously lacked.
  • DEFAULT=YES is honored by engine policy, not the browser's default attribute. Alternative considered: keep propagating default so the UA hints the slot. Rejected because SPF owns selection (switchTextTrack), and the attribute auto-activated captions past the enableDefaultTrack=false opt-in policy.

Breaking changes

Internal SPF only (alpha; no external consumers). The selectTextTrack behavior and SelectTextTrackConfig are removed; text selection now runs the track-switching chain, selectedTextTrackId is a resolved output (write userTextTrackSelection instead), and SPF-owned <track> elements no longer carry default.

Test plan

  • Full SPF suite green (pnpm -F @videojs/spf test — 1042 passed / 14 skipped), including new switchTextTrack intent-resolution, optional-none, and echo-guard tests.
  • Playwright smoke of the DOM→intent bridge against a multi-language source: native mode-set → intent → resolved id; Off'off' (echo guard holds); reset → opt-in default.

Note

Medium Risk
Refactors core track-selection and DOM caption sync with a breaking alpha API (selectedTextTrackId read-only; write userTextTrackSelection), but behavior is heavily unit-tested and scoped to SPF internals.

Overview
Moves caption/subtitle selection onto the shared track-switching rule chain so text renditions get failed-CDN pruning and active-CDN scoping like video/audio. selectTextTrack is removed; the HLS engine composes switchTextTrack instead and exposes sticky userTextTrackSelection via shareSignals.

Intent vs resolved id: DOM and programmatic callers write userTextTrackSelection (language partial, 'off', or undefined = auto); switchTextTrack is the sole writer of selectedTextTrackId. syncTextTracks mirrors the resolved id into native TextTrack.mode one way and bridges change events to intent, with settling-window + echo guard so resolver-driven disables are not recorded as user “off.”

Framework seam: setupTrackSwitching gains optional resolveSelection (default chain head); text uses pickResolvedTextTrack so selection can legitimately clear. Text skips MSE excludeUnplayableTracks; user intent is handled in the terminal, not filterByUserSelection.

Opt-in policy fix: SPF-owned <track> elements no longer set the browser default attribute (avoids auto-enable bypassing enableDefaultTrack). Sandbox adds a subtitles picker driven through native modes to exercise the DOM→intent path.

Reviewed by Cursor Bugbot for commit 6b3d26b. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 17, 2026 8:43pm

Request Review

@cjpillsbury cjpillsbury force-pushed the feat/spf-capability-probing branch 2 times, most recently from 09b3404 to b2f53d8 Compare June 17, 2026 15:12
@cjpillsbury cjpillsbury force-pushed the feat/spf-text-tracks-switching branch from 052dee0 to b3605de Compare June 17, 2026 15:18
@cjpillsbury cjpillsbury force-pushed the feat/spf-capability-probing branch from b2f53d8 to 6134d8e Compare June 17, 2026 15:24
Base automatically changed from feat/spf-capability-probing to main June 17, 2026 18:22
@cjpillsbury cjpillsbury force-pushed the feat/spf-text-tracks-switching branch from b3605de to 7e53ffd Compare June 17, 2026 19:53
@netlify

netlify Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 6b3d26b
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a3306f04fe8f40008dcf87e
😎 Deploy Preview https://deploy-preview-1687--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@cjpillsbury cjpillsbury marked this pull request as ready for review June 17, 2026 20:35
cjpillsbury and others added 10 commits June 17, 2026 13:42
…selection

The final pick was hardcoded to the rule-chain head, which assumes a track is
always selected. Add an optional resolveSelection(candidates, deps) config hook
(defaulting to the chain head) so a variant whose selection is legitimately
optional — text: opt-in captions, explicit off — can resolve to undefined and
clear the slot. Video and audio don't supply it and are unchanged.

Also lands a working-reference plan for the text-track-switching refactor under
.claude/plans/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…selection

Migrate text selection onto the track-switching rule chain so it gains the
failed-CDN constraint and active-CDN scope it previously lacked. Unlike video
and audio, text selection is optional (captions are opt-in and off-able), so
the variant runs [excludeFailedCdns] + [preferActiveCdn] and supplies a
text-specific terminal (pickResolvedTextTrack) via the resolveSelection seam:
it resolves the standing userTextTrackSelection intent (a language partial,
'off', or undefined=auto) against the constrained, CDN-scoped candidates and
may yield no selection.

Extract pickTextTrackFromTracks from pickTextTrack so the auto-default policy
(preferred language -> DEFAULT+AUTOSELECT -> none, forced excluded) is shared
between the presentation-shaped picker and the candidate-list terminal.

Additive only: switchTextTrack is not yet wired into an engine — selectTextTrack
still owns text selection until the engine rewire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the resolved id

syncTextTracks no longer writes selectedTextTrackId. Its change-bridge now writes
userTextTrackSelection — a language-based partial (sticky across sources) or 'off'
— which switchTextTrack resolves into selectedTextTrackId. The behavior reads the
resolved id only to mirror DOM modes one-way and as the echo-guard reference, so
DOM action and the resolver no longer contend for one slot.

The echo guard (showingId === selectedTextTrackId) now also absorbs resolver-driven
mode corrections: a correction that disables the user's pick (e.g. its CDN failed)
is recognized as an echo and not written back as a spurious 'off'.

Not yet wired into an engine — selectTextTrack still owns text selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tTrack

Compose switchTextTrack in place of selectTextTrack so text selection runs the
track-switching chain (failed-CDN constraint + active-CDN scope) and resolves
the standing userTextTrackSelection intent. Expose userTextTrackSelection via
shareSignals as the programmatic + DOM write path; selectedTextTrackId is now a
single-writer resolved output. Remove the now-dead selectTextTrack behavior and
its SelectTextTrackConfig (pickTextTrack primitive stays).

Update the sandbox to drive text selection through userTextTrackSelection
instead of writing the resolved id directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtTrack model

Reflect the shipped refactor: selectedTextTrackId is a single-writer output of
switchTextTrack; userTextTrackSelection is the sticky intent input (DOM +
consumer); selection runs the track-switching chain (failed-CDN constraint +
active-CDN scope). Rewrite the bidirectional-sync section as a one-way mode
mirror + DOM→intent bridge with the echo guard, and correct the enableDefaultTrack
default (false, not true).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… demo

A Subtitles/Captions picker (Off + one button per language, with an
auto/pinned/off status row and reset) wired to userTextTrackSelection, so text
selection is driven through the intent path like the audio picker. Replaces the
stage-d auto-select-first-text hack, so the engine's real opt-in default policy
runs on load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setting the `default` attribute on an SPF-owned <track> made the browser
auto-activate that slot on insertion, firing a `change` that syncTextTracks
recorded as user intent — auto-enabling captions for a DEFAULT=YES rendition
past SPF's opt-in policy (enableDefaultTrack defaults to false). SPF owns
selection via switchTextTrack + selectedTextTrackId, so the slots carry no
selection hint; DEFAULT=YES is honored (or not) by the engine's default policy,
not the browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ack model

Update the design/package docs that still named the removed selectTextTrack
behavior or the old multi-writer selectedTextTrackId pattern: architecture,
capability-probing (text has no capability constraint), audio-playback,
source-replacement, background-looping-video, audio-only-mode-override,
multi-language-audio, clusters, conventions/behaviors, and the hls-engine
walkthrough. The multi-writer convention + cluster note are reframed around the
intent→resolved resolution (route differing inputs to a userTextTrackSelection
intent slot; switchTextTrack is the single writer of selectedTextTrackId).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ult attribute

Completes a8de404: the full-engine "creates track elements for all text tracks"
test still asserted the DEFAULT=YES rendition's <track>.default === true. Assert
false — the attribute is intentionally not propagated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Off + language buttons now set mediaElement.textTracks[id].mode (what a
captions button / browser UI touches) instead of writing userTextTrackSelection
directly, so a click exercises the real syncTextTracks DOM→intent bridge
(change event → language partial / 'off' → switchTextTrack → resolved id).
"Reset to auto" stays a direct userTextTrackSelection=undefined write — it has
no native-mode analog (it means "forget my preference").

Smoke-tested via Playwright against a multi-language source: native selection
bridges to intent, Off bridges to 'off' (echo guard holds), reset returns to the
opt-in default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 41.80 kB
/video (default + hls) 181.22 kB
/video (minimal) 41.46 kB
/video (minimal + hls) 181.08 kB
/audio (default) 35.62 kB
/audio (minimal) 32.88 kB
/background 4.22 kB
Media (9)
Entry Size
/media/background-video 1.07 kB
/media/container 1.72 kB
/media/dash-video 242.65 kB
/media/hls-video 141.12 kB
/media/mux-audio 163.61 kB
/media/mux-video 163.61 kB
/media/native-hls-video 8.94 kB
/media/simple-hls-audio-only 17.04 kB
/media/simple-hls-video 18.55 kB
Players (5)
Entry Size
/video/player 7.61 kB
/audio/player 5.39 kB
/background/player 3.92 kB
/live-video/player 7.63 kB
/live-audio/player 5.40 kB
Skins (30)
Entry Type Size
/video/minimal-skin.css css 5.23 kB
/video/skin.css css 5.19 kB
/video/minimal-skin js 41.46 kB
/video/minimal-skin.tailwind js 41.95 kB
/video/skin js 41.78 kB
/video/skin.tailwind js 42.21 kB
/audio/minimal-skin.css css 3.45 kB
/audio/skin.css css 3.36 kB
/audio/minimal-skin js 32.92 kB
/audio/minimal-skin.tailwind js 33.22 kB
/audio/skin js 35.70 kB
/audio/skin.tailwind js 36.02 kB
/background/skin.css css 133 B
/background/skin js 1.16 kB
/live-video/minimal-skin.css css 5.23 kB
/live-video/skin.css css 5.19 kB
/live-video/minimal-skin js 40.88 kB
/live-video/minimal-skin.tailwind js 41.25 kB
/live-video/skin js 40.86 kB
/live-video/skin.tailwind js 41.13 kB
/live-audio/minimal-skin.css css 3.45 kB
/live-audio/skin.css css 3.36 kB
/live-audio/minimal-skin js 27.17 kB
/live-audio/minimal-skin.tailwind js 26.64 kB
/live-audio/skin js 29.61 kB
/live-audio/skin.tailwind js 29.19 kB
/global.css css 176 B
/shared.css css 88 B
/tailwind.css css 228 B
/skin-element js 1.37 kB
UI Components (37)
Entry Size
/ui/airplay-button 2.92 kB
/ui/alert-dialog 1.27 kB
/ui/alert-dialog-close 558 B
/ui/alert-dialog-description 448 B
/ui/alert-dialog-title 450 B
/ui/buffering-indicator 2.79 kB
/ui/captions-button 2.96 kB
/ui/captions-radio-group 2.21 kB
/ui/cast-button 2.87 kB
/ui/compounds 8.22 kB
/ui/controls 2.19 kB
/ui/error-dialog 3.25 kB
/ui/fullscreen-button 2.91 kB
/ui/hotkey 2.04 kB
/ui/menu 5.38 kB
/ui/mute-button 2.96 kB
/ui/pip-button 2.88 kB
/ui/play-button 2.88 kB
/ui/playback-rate-button 2.93 kB
/ui/playback-rate-radio-group 2.18 kB
/ui/popover 2.10 kB
/ui/poster 2.59 kB
/ui/seek-button 2.87 kB
/ui/seek-indicator 3.76 kB
/ui/seek-indicator-value 241 B
/ui/slider 1.50 kB
/ui/status-announcer 3.39 kB
/ui/status-indicator 3.48 kB
/ui/status-indicator-value 272 B
/ui/thumbnail 3.16 kB
/ui/time 1.99 kB
/ui/time-slider 2.90 kB
/ui/tooltip 2.23 kB
/ui/volume-indicator 3.67 kB
/ui/volume-indicator-fill 222 B
/ui/volume-indicator-value 222 B
/ui/volume-slider 3.67 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 34.72 kB
/video (default + hls) 172.88 kB
/video (minimal) 34.77 kB
/video (minimal + hls) 173.08 kB
/audio (default) 28.43 kB
/audio (minimal) 28.51 kB
/background 754 B
Media (8)
Entry Size
/media/background-video 575 B
/media/dash-video 241.20 kB
/media/hls-video 139.64 kB
/media/mux-audio 162.30 kB
/media/mux-video 162.33 kB
/media/native-hls-video 7.41 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.11 kB
Skins (27)
Entry Type Size
/tailwind.css css 228 B
/video/minimal-skin.css css 5.14 kB
/video/skin.css css 5.10 kB
/video/minimal-skin js 34.69 kB
/video/minimal-skin.tailwind js 40.13 kB
/video/skin js 34.63 kB
/video/skin.tailwind js 39.99 kB
/audio/minimal-skin.css css 3.32 kB
/audio/skin.css css 3.23 kB
/audio/minimal-skin js 28.45 kB
/audio/minimal-skin.tailwind js 28.91 kB
/audio/skin js 28.36 kB
/audio/skin.tailwind js 31.97 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.14 kB
/live-video/skin.css css 5.10 kB
/live-video/minimal-skin js 30.93 kB
/live-video/minimal-skin.tailwind js 36.15 kB
/live-video/skin js 30.88 kB
/live-video/skin.tailwind js 36.14 kB
/live-audio/minimal-skin.css css 3.32 kB
/live-audio/skin.css css 3.23 kB
/live-audio/minimal-skin js 20.94 kB
/live-audio/minimal-skin.tailwind js 23.71 kB
/live-audio/skin js 20.96 kB
/live-audio/skin.tailwind js 23.77 kB
UI Components (31)
Entry Size
/ui/airplay-button 2.87 kB
/ui/alert-dialog 1.21 kB
/ui/buffering-indicator 2.60 kB
/ui/captions-button 2.86 kB
/ui/captions-radio-group 2.58 kB
/ui/cast-button 2.88 kB
/ui/controls 2.52 kB
/ui/error-dialog 2.49 kB
/ui/fullscreen-button 2.83 kB
/ui/gesture 2.05 kB
/ui/hotkey 2.65 kB
/ui/live-button 2.78 kB
/ui/menu 5.53 kB
/ui/mute-button 2.94 kB
/ui/pip-button 2.88 kB
/ui/play-button 2.89 kB
/ui/playback-rate 2.54 kB
/ui/playback-rate-button 2.83 kB
/ui/popover 2.42 kB
/ui/poster 2.43 kB
/ui/seek-button 2.91 kB
/ui/seek-indicator 2.06 kB
/ui/slider 4.38 kB
/ui/status-announcer 1.85 kB
/ui/status-indicator 1.96 kB
/ui/thumbnail 2.76 kB
/ui/time 2.66 kB
/ui/time-slider 3.98 kB
/ui/tooltip 2.63 kB
/ui/volume-indicator 2.05 kB
/ui/volume-slider 3.40 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (11)
Entry Size
. 7.94 kB
/dom 16.40 kB
/dom/media/custom-media-element 2.00 kB
/dom/media/dash 236.79 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls 135.58 kB
/dom/media/media-host 1.31 kB
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.02 kB
/dom/media/simple-hls 16.52 kB
/dom/media/simple-hls-audio-only 14.91 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 2.22 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 192 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Size
. 4.45 kB
/dom 6.32 kB
/hls 15.37 kB
/background-looping-video 12.90 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6b3d26b. Configure here.

Comment thread packages/spf/src/playback/behaviors/track-switching.ts
if (showingId === state.selectedTextTrackId.get()) return;
// Genuine user action → write intent (resolved into selectedTextTrackId
// by switchTextTrack), not the resolved id.
state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale model tracks in bridge

Low Severity

The DOM change handler captures modelTextTracks once on sync-active entry and passes that snapshot into deriveTextTrackIntent on every user event. If the resolved presentation’s text renditions change while the reactor stays in sync-active, intent can be derived from outdated track metadata even though switchTextTrack reads the live presentation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6b3d26b. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely fine, but will confirm edge case concerns/considerations.

@luwes luwes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants