Skip to content

feat(spf): keep track selection within the initial codec family - #2289

Merged
cjpillsbury merged 5 commits into
mainfrom
feat/sticky-codec-selection
Aug 24, 2026
Merged

feat(spf): keep track selection within the initial codec family#2289
cjpillsbury merged 5 commits into
mainfrom
feat/sticky-codec-selection

Conversation

@cjpillsbury

@cjpillsbury cjpillsbury commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Refs #1864

Summary

SPF implements no SourceBuffer.changeType(), so on a mixed-codec source — Apple's bipbop advanced example muxes HEVC + AVC renditions of the same content — a mid-stream re-pick across codec families appends undecodable data into a SourceBuffer created under the initial family's mimetype. This adds a hard codec-family sticky constraint (relational: the selected track's families), a configurable preference (default AVC/AAC) for which family the initial pick lands in, and the effect-scheduler fix that makes the constraint expressible.

Changes

  • stickToSelectedCodecs (new selection constraint, video + audio switch* pre-passes): prunes candidates whose codec-family set doesn't equal the currently selected track's (set-equality, so a muxed hvc1,mp4a rendition can't pass as a match for avc1,mp4a on its audio half). Purely relational — no state of its own. Constraint semantics are the point: pruned pre-pass, a cross-family user pick mid-stream never reaches the user filter's early-bail and falls through unhonored instead of killing playback, and a vanished selected family becomes a reported SVTA no-supported-track stop (recovering when the family returns) instead of a silent cross-family pick.
  • fix(core): effects that self-write through their own dependencies no longer go deaf. A constraint reading the selection slot — which the picking effect itself writes — exposed a real signals-engine bug: the self-write left an intermediate computed dirty with no notification, and the standing dirty flag then deduped away every later external change through it (failover and relabel re-picks went unseen). effect.ts now pulls an effect's sources after each run; the guarantee is liveness, not immediacy (an effect isn't re-run just for writing into its own graph — it catches up on the next genuine change, which is what used to be lost). Reproduced in ten lines of pure signals; pinned by new core/signals/tests/effect.test.ts cases for initial-run, flush-run, and convergence.
  • preferCodecFamilies (new selection scope): narrows the initial pick to the preferredCodecs families — the family the constraint then holds. Behind the user filter, so an explicit initial pick may land in any family. Soft: an HEVC-only ladder under the AVC default plays exactly as before.
  • preferredCodecs config on SwitchVideoTrackConfig, HlsVideoEngineConfig, and HlsAudioEngineConfig. Default ['avc1', 'avc3', 'mp4a']; [] disables the preference (ABR then picks the initial family and still can't leave it).
  • The background compositions are deliberately unwired: the pinned selectVideoTrack evaluates once and never re-picks, so it's immune by construction.
  • Sandbox: Apple's official mixed-codec fMP4 example joins the shared source registry as hls-mixed-codec, reachable from every preset picker and ?source= deep link. The spf-segment-loading harness's AVC-only toggle (plus its avcOnly query param and harness-local bipbop preset) is removed — it existed solely as the workaround this PR replaces with engine defaults.
Implementation details
  • Both selection pieces follow track-switching-model.md (hevc-variant-selection was already anticipated there as a codec scope); constraint placement is what guarantees a cross-family user pick can't early-bail past the policy.
  • A custom equals on the candidate-set computed does not avoid the scheduler bug — sameCandidateSet was in place the whole time. equals gates notification after a recomputation; the lost wakeup is the dirty flag set at write time, upstream of any comparison. Only a pull clears it, which is what revalidateSources does; it never writes or schedules, so it cannot loop.
  • Vanish-case semantics: the pre-pass empties → the type's no-supported-track code is reported and the selection clears; clearing releases the relational lock, so recovery (cooldown expiry, the family returning) re-picks. Whether the append layer survives a cross-family rebuild remains the changeType gap, unchanged by this PR.
  • The default's deliberate cost, documented on DEFAULT_PREFERRED_CODECS: a ladder whose top rungs are HEVC-only caps at the top AVC rung under the default.
  • Bundle: the HLS engine lands a few hundred bytes gzipped over the 20 KB advisory target in measure-size.js — main was already at 99.9% of it before this branch.

Testing

  • pnpm -F @videojs/spf test — full suite passes. Constraint coverage: mixed-ladder initial pick lands on AVC over a better-fitting HEVC rung, upgrades stay in-family, cross-family user selections are pruned while in-family ones are honored, a user's initial HEVC pick locks HEVC, muxed family sets compare whole, audio prefers AAC over a higher-bitrate E-AC-3 and holds it against a cross-family language request, and a failover that removes the selected family reports + clears, then recovers when the CDN returns. Scheduler coverage: effect.test.ts pins the self-write lost-wakeup for initial-run and flush-run plus convergence.
  • pnpm -F @videojs/spf build, pnpm typecheck, pnpm -F @videojs/spf size, lint — all clean (size note above).
  • Verified live against the sandbox (Chromium): the bipbop mixed ladder plays with the pick held in AVC/AAC, a programmatic cross-family selection is pruned while an in-family one is honored, and playback never interrupts.

Sandbox (Vercel preview, this branch)

Base: v10-sandbox. The mixed-codec source is in the shared registry, so it's also reachable from any preset's picker.

Scenario Link Expect
Mixed-codec (bipbop adv fMP4), SPF player HTML · React Plays. Initial pick lands in AVC/AAC and ABR stays there; renditions menu picks within AVC work, HEVC picks are ignored (playback keeps going)
Same source, Mux-flavored SPF HTML Identical behavior under the Mux media alias
Segment-loading harness (engine state readout) spf-segment-loading Selected rendition codecs stay avc1.*/mp4a.* (rendition buttons + window.state()). The AVC-only toggle is gone — no workaround needed
Control: background preset (pinned, out of scope) HTML Unchanged — the pinned variant never re-picks, so the constraint isn't composed there
Control: single-codec 4K CMAF HTML Unchanged ABR behavior — one family, constraint and preference both inert

🤖 Generated with Claude Code


Note

Medium Risk
Touches core effect scheduling and video/audio track selection, so mixed-codec and failover re-picks can change. Default AVC/AAC preference can cap quality on HEVC-top ladders unless preferredCodecs is overridden.

Overview
Keeps ABR inside the codec family of the first pick, because SPF still has no SourceBuffer.changeType(). Mixed HEVC+AVC (and AAC vs E-AC-3) sources no longer append undecodable data mid-stream.

Selection: stickToSelectedCodecs is a hard pre-pass on video/audio switch* — later picks must match the selected track’s full codec-family set. Cross-family user picks are ignored; if the family disappears (e.g. CDN cooldown), selection clears with a no-supported-track error and recovers when it returns. preferCodecFamilies (default avc1/avc3/mp4a) only shapes the initial pick; preferredCodecs: [] disables it. HEVC-only ladders are unchanged. Pinned selectVideoTrack is left unwired.

Signals: Effects that write a signal an intermediate computed in their own graph reads no longer go deaf. After each run, revalidateSources pulls sources so a standing dirty flag cannot swallow later wakeups. Guarantee is liveness, not an extra re-run.

Sandbox: Apple bipbop is a shared hls-mixed-codec source. The segment-loading AVC-only toggle/workaround is gone.

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

@netlify

netlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 856577c
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a88628a1ae7b60009f29f87
😎 Deploy Preview https://deploy-preview-2289--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.

@vercel

vercel Bot commented Aug 20, 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 Aug 21, 2026 2:38pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 20, 2026

Copy link
Copy Markdown
@videojs/core

pnpm add https://pkg.pr.new/@videojs/core@2289

@videojs/element

pnpm add https://pkg.pr.new/@videojs/element@2289

@videojs/html

pnpm add https://pkg.pr.new/@videojs/html@2289

@videojs/media

pnpm add https://pkg.pr.new/@videojs/media@2289

@videojs/react

pnpm add https://pkg.pr.new/@videojs/react@2289

@videojs/spf

pnpm add https://pkg.pr.new/@videojs/spf@2289

@videojs/store

pnpm add https://pkg.pr.new/@videojs/store@2289

@videojs/utils

pnpm add https://pkg.pr.new/@videojs/utils@2289

commit: 856577c

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — 4 small size changes
Path Base initial PR initial Initial diff Lazy diff
/media/hls-audio 21.81 kB 22.02 kB +214 B (+1.0%)
/media/hls-background-video 16.75 kB 16.77 kB +27 B (+0.2%)
/media/hls-video 27.79 kB 28.02 kB +229 B (+0.8%)
/media/mux-background-video 16.75 kB 16.78 kB +38 B (+0.2%)
Presets (7)
Entry Initial Lazy
/video (default) 58.69 kB 56.87 kB
/video (default + hls) 197.58 kB 56.87 kB
/video (minimal) 58.56 kB 56.87 kB
/video (minimal + hls) 197.52 kB 56.87 kB
/audio (default) 48.53 kB 56.87 kB
/audio (minimal) 44.72 kB 56.87 kB
/background 5.75 kB 56.87 kB
Media (20)
Entry Initial Lazy
/media/mux-video 142.29 kB
/media/mux-audio 142.00 kB
/media/background-video 1.14 kB
/media/cloudflare-video 6.25 kB
/media/container 3.12 kB 56.87 kB
/media/dash-video 211.89 kB
/media/google-cast 6.46 kB
/media/hls-audio 22.02 kB
/media/hls-background-video 16.77 kB
/media/hls-video 28.02 kB
/media/hlsjs-video 141.16 kB
/media/mux-background-video 16.78 kB
/media/mux-data 26.61 kB
/media/native-hls-video 7.51 kB
/media/shaka-video 199.34 kB
/media/spotify-audio 6.23 kB
/media/tiktok-video 6.32 kB
/media/twitch-video 6.20 kB
/media/vimeo-video 13.01 kB
/media/youtube-video 6.76 kB
Players (5)
Entry Initial Lazy
/video/player 10.36 kB 56.87 kB
/audio/player 7.08 kB 56.87 kB
/background/player 5.45 kB 56.87 kB
/live-video/player 9.75 kB 56.87 kB
/live-audio/player 7.10 kB 56.87 kB
Skins (29)
Entry Type Initial Lazy
/video/minimal-skin.css css 6.43 kB
/video/skin.css css 6.22 kB
/video/minimal-skin js 58.43 kB 56.87 kB
/video/minimal-skin.tailwind js 59.24 kB 56.87 kB
/video/skin js 58.67 kB 56.87 kB
/video/skin.tailwind js 59.47 kB 56.87 kB
/audio/minimal-skin.css css 4.71 kB
/audio/skin.css css 4.63 kB
/audio/minimal-skin js 44.71 kB 56.87 kB
/audio/minimal-skin.tailwind js 45.26 kB 56.87 kB
/audio/skin js 48.57 kB 56.87 kB
/audio/skin.tailwind js 49.28 kB 56.87 kB
/background/skin.css css 145 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 6.43 kB
/live-video/skin.css css 6.22 kB
/live-video/minimal-skin js 52.63 kB 56.87 kB
/live-video/minimal-skin.tailwind js 52.98 kB 56.87 kB
/live-video/skin js 55.03 kB 56.87 kB
/live-video/skin.tailwind js 55.68 kB 56.87 kB
/live-audio/minimal-skin.css css 4.71 kB
/live-audio/skin.css css 4.63 kB
/live-audio/minimal-skin js 38.65 kB 56.87 kB
/live-audio/minimal-skin.tailwind js 38.08 kB 56.87 kB
/live-audio/skin js 42.13 kB 56.87 kB
/live-audio/skin.tailwind js 41.79 kB 56.87 kB
/global.css css 183 B
/shared.css css 99 B
/skin-element js 1.38 kB
UI Components (43)
Entry Initial Lazy
/ui/airplay-button 11.05 kB 56.87 kB
/ui/alert-dialog 4.24 kB 56.87 kB
/ui/alert-dialog-close 1.94 kB 56.87 kB
/ui/alert-dialog-description 1.63 kB 56.87 kB
/ui/alert-dialog-title 1.65 kB 56.87 kB
/ui/audio-track-radio-group 11.21 kB 56.87 kB
/ui/buffering-indicator 8.50 kB 56.87 kB
/ui/captions-button 11.31 kB 56.87 kB
/ui/captions-radio-group 11.30 kB 56.87 kB
/ui/cast-button 11.04 kB 56.87 kB
/ui/compounds 35.08 kB 56.87 kB
/ui/controls 8.65 kB 56.87 kB
/ui/error-dialog 11.53 kB 56.87 kB
/ui/fullscreen-button 11.02 kB 56.87 kB
/ui/gesture 9.30 kB 56.87 kB
/ui/hotkey 9.74 kB 56.87 kB
/ui/menu 18.94 kB 56.87 kB
/ui/mute-button 11.06 kB 56.87 kB
/ui/pip-button 11.03 kB 56.87 kB
/ui/play-button 11.01 kB 56.87 kB
/ui/playback-rate-button 11.12 kB 56.87 kB
/ui/playback-rate-radio-group 11.20 kB 56.87 kB
/ui/popover 8.23 kB 56.87 kB
/ui/poster 8.56 kB 56.87 kB
/ui/quality-radio-group 11.79 kB 56.87 kB
/ui/seek-button 11.06 kB 56.87 kB
/ui/seek-indicator 12.45 kB 56.87 kB
/ui/seek-indicator-value 1.03 kB
/ui/slider 11.86 kB 56.87 kB
/ui/status-announcer 10.42 kB 56.87 kB
/ui/status-indicator 12.76 kB 56.87 kB
/ui/status-indicator-value 1.02 kB
/ui/thumbnail 9.68 kB 56.87 kB
/ui/time 10.76 kB 56.87 kB
/ui/time-slider 14.81 kB 56.87 kB
/ui/time-slider-chapter-title 8.66 kB 56.87 kB
/ui/time-slider-chapters 9.63 kB 56.87 kB
/ui/title 8.07 kB 56.87 kB
/ui/tooltip 9.58 kB 56.87 kB
/ui/volume-indicator 12.78 kB 56.87 kB
/ui/volume-indicator-fill 1.03 kB
/ui/volume-indicator-value 1.02 kB
/ui/volume-slider 12.51 kB 56.87 kB
⚛️ @videojs/react — 6 small size changes
Path Base initial PR initial Initial diff Lazy diff
/media/mux-video/spf 27.09 kB 27.27 kB +188 B (+0.7%)
/media/mux-audio/spf 21.07 kB 21.24 kB +172 B (+0.8%)
/media/hls-audio 20.09 kB 20.24 kB +155 B (+0.8%)
/media/hls-background-video 16.37 kB 16.42 kB +51 B (+0.3%)
/media/hls-video 26.06 kB 26.25 kB +188 B (+0.7%)
/media/mux-background-video 16.36 kB 16.42 kB +68 B (+0.4%)
Presets (7)
Entry Initial Lazy
/video (default) 46.03 kB 56.87 kB
/video (default + hls) 183.85 kB 56.87 kB
/video (minimal) 46.15 kB 56.87 kB
/video (minimal + hls) 183.97 kB 56.87 kB
/audio (default) 35.52 kB 56.87 kB
/audio (minimal) 35.63 kB 56.87 kB
/background 580 B 56.87 kB
Media (23)
Entry Initial
/media/mux-video/hls-js 140.58 kB
/media/mux-video/spf 27.27 kB
/media/mux-audio/hls-js 140.38 kB
/media/mux-audio/spf 21.24 kB
/media/background-video 394 B
/media/cloudflare-video 4.34 kB
/media/dash-video 210.24 kB
/media/google-cast 5.34 kB
/media/hls-audio 20.24 kB
/media/hls-background-video 16.42 kB
/media/hls-video 26.25 kB
/media/hlsjs-video 139.41 kB
/media/mux-audio 140.41 kB
/media/mux-background-video 16.42 kB
/media/mux-data 25.44 kB
/media/mux-video 140.47 kB
/media/native-hls-video 5.75 kB
/media/shaka-video 197.53 kB
/media/spotify-audio 4.21 kB
/media/tiktok-video 4.18 kB
/media/twitch-video 4.29 kB
/media/vimeo-video 11.21 kB
/media/youtube-video 4.87 kB
Players (5)
Entry Initial Lazy
/video/player 7.51 kB 56.87 kB
/audio/player 3.95 kB 56.87 kB
/background/player 2.26 kB 56.87 kB
/live-video/player 6.90 kB 56.87 kB
/live-audio/player 3.95 kB 56.87 kB
Skins (26)
Entry Type Initial Lazy
/video/minimal-skin.css css 6.34 kB
/video/skin.css css 6.11 kB
/video/minimal-skin js 46.01 kB 56.87 kB
/video/minimal-skin.tailwind js 52.87 kB 56.87 kB
/video/skin js 45.98 kB 56.87 kB
/video/skin.tailwind js 52.61 kB 56.87 kB
/audio/minimal-skin.css css 4.56 kB
/audio/skin.css css 4.48 kB
/audio/minimal-skin js 35.49 kB 56.87 kB
/audio/minimal-skin.tailwind js 40.50 kB 56.87 kB
/audio/skin js 35.47 kB 56.87 kB
/audio/skin.tailwind js 40.47 kB 56.87 kB
/background/skin.css css 90 B
/background/skin js 277 B
/live-video/minimal-skin.css css 6.34 kB
/live-video/skin.css css 6.11 kB
/live-video/minimal-skin js 38.11 kB 56.87 kB
/live-video/minimal-skin.tailwind js 44.70 kB 56.87 kB
/live-video/skin js 38.16 kB 56.87 kB
/live-video/skin.tailwind js 44.65 kB 56.87 kB
/live-audio/minimal-skin.css css 4.56 kB
/live-audio/skin.css css 4.48 kB
/live-audio/minimal-skin js 27.66 kB 56.87 kB
/live-audio/minimal-skin.tailwind js 31.48 kB 56.87 kB
/live-audio/skin js 27.71 kB 56.87 kB
/live-audio/skin.tailwind js 31.62 kB 56.87 kB
UI Components (37)
Entry Initial Lazy
/ui/airplay-button 10.27 kB 56.87 kB
/ui/alert-dialog 3.32 kB 56.87 kB
/ui/audio-track 7.88 kB 56.87 kB
/ui/audio-track-radio-group 9.04 kB 56.87 kB
/ui/buffering-indicator 7.72 kB 56.87 kB
/ui/captions-button 10.23 kB 56.87 kB
/ui/captions-radio-group 9.17 kB 56.87 kB
/ui/cast-button 10.25 kB 56.87 kB
/ui/controls 7.43 kB 56.87 kB
/ui/error-dialog 10.30 kB 56.87 kB
/ui/fullscreen-button 10.21 kB 56.87 kB
/ui/gesture 7.80 kB 56.87 kB
/ui/hotkey 8.22 kB 56.87 kB
/ui/live-button 8.68 kB 56.87 kB
/ui/menu 18.30 kB 56.87 kB
/ui/mute-button 10.25 kB 56.87 kB
/ui/pip-button 10.22 kB 56.87 kB
/ui/play-button 10.18 kB 56.87 kB
/ui/playback-rate 7.94 kB 56.87 kB
/ui/playback-rate-button 10.19 kB 56.87 kB
/ui/playback-rate-radio-group 9.08 kB 56.87 kB
/ui/popover 7.91 kB 56.87 kB
/ui/poster 7.49 kB 56.87 kB
/ui/quality 8.38 kB 56.87 kB
/ui/quality-radio-group 9.53 kB 56.87 kB
/ui/seek-button 10.22 kB 56.87 kB
/ui/seek-indicator 11.77 kB 56.87 kB
/ui/slider 12.53 kB 56.87 kB
/ui/status-announcer 9.95 kB 56.87 kB
/ui/status-indicator 12.05 kB 56.87 kB
/ui/thumbnail 8.63 kB 56.87 kB
/ui/time 9.73 kB 56.87 kB
/ui/time-slider 14.02 kB 56.87 kB
/ui/title 7.19 kB 56.87 kB
/ui/tooltip 8.65 kB 56.87 kB
/ui/volume-indicator 12.15 kB 56.87 kB
/ui/volume-slider 11.94 kB 56.87 kB
🧩 @videojs/core — no changes
Entries (76)
Entry Initial Lazy
. 12.88 kB
/dom 20.76 kB 56.87 kB
/vjsc 1.61 kB
/i18n 3.20 kB 56.87 kB
/i18n/locales/all 34.97 kB
/i18n/locales/ar 1.22 kB
/i18n/locales/az 1.09 kB
/i18n/locales/bg 1.29 kB
/i18n/locales/bn 1.23 kB
/i18n/locales/bs 1.00 kB
/i18n/locales/ca 1.07 kB
/i18n/locales/cs 1.07 kB
/i18n/locales/cy 1.02 kB
/i18n/locales/da 1006 B
/i18n/locales/de 1.09 kB
/i18n/locales/el 1.49 kB
/i18n/locales/en 785 B
/i18n/locales/es 1018 B
/i18n/locales/et 1.05 kB
/i18n/locales/eu 1.03 kB
/i18n/locales/fa 1.22 kB
/i18n/locales/fi 1.04 kB
/i18n/locales/fr 1.07 kB
/i18n/locales/gd 1.10 kB
/i18n/locales/gl 1022 B
/i18n/locales/he 1.14 kB
/i18n/locales/hi 1.30 kB
/i18n/locales/hr 1.02 kB
/i18n/locales/hu 1.10 kB
/i18n/locales/id 919 B
/i18n/locales/it 1.01 kB
/i18n/locales/ja 1.18 kB
/i18n/locales/ko 1.14 kB
/i18n/locales/lt 1.02 kB
/i18n/locales/lv 1.09 kB
/i18n/locales/mr 1.30 kB
/i18n/locales/nb 987 B
/i18n/locales/ne 1.29 kB
/i18n/locales/nl 1.00 kB
/i18n/locales/nn 987 B
/i18n/locales/oc 1.08 kB
/i18n/locales/pl 1.14 kB
/i18n/locales/pt 1.03 kB
/i18n/locales/pt-BR 1.03 kB
/i18n/locales/pt-PT 1.01 kB
/i18n/locales/ro 1.07 kB
/i18n/locales/ru 1.40 kB
/i18n/locales/sk 1.10 kB
/i18n/locales/sl 1.04 kB
/i18n/locales/sr 1.07 kB
/i18n/locales/sv 1.01 kB
/i18n/locales/te 1.32 kB
/i18n/locales/th 1.29 kB
/i18n/locales/tr 1.08 kB
/i18n/locales/uk 1.41 kB
/i18n/locales/vi 1.11 kB
/i18n/locales/zh 1.02 kB
/i18n/locales/zh-CN 1.02 kB
/i18n/locales/zh-TW 1.02 kB
/i18n/text/airplay 101 B
/i18n/text/buttons 137 B
/i18n/text/captions 93 B
/i18n/text/cast 114 B
/i18n/text/common 90 B
/i18n/text/container 75 B
/i18n/text/errors 285 B
/i18n/text/fullscreen 98 B
/i18n/text/live 126 B
/i18n/text/menu 249 B
/i18n/text/pip 101 B
/i18n/text/playback 80 B
/i18n/text/seek 105 B
/i18n/text/slider 65 B
/i18n/text/status 222 B
/i18n/text/time 280 B
/i18n/text/volume 133 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.72 kB
/html 703 B
/react 365 B
🔧 @videojs/utils — no changes
Entries (13)
Entry Initial
/array 271 B
/dom 4.73 kB
/events 320 B
/function 386 B
/i18n 194 B
/jwt 176 B
/object 535 B
/predicate 297 B
/percent 281 B
/string 239 B
/style 212 B
/time 1.01 kB
/number 199 B
📦 @videojs/media — no changes
Entries (20)
Entry Initial
. 1.07 kB
/dom 121 B
/dom/audio-host 1.10 kB
/dom/cloudflare 3.69 kB
/dom/custom-media-element 2.09 kB
/dom/dash 209.84 kB
/dom/google-cast 4.06 kB
/dom/hls-js 139.04 kB
/dom/media-host 1.20 kB
/dom/media-played-ranges 576 B
/dom/mux 162.74 kB
/dom/native-hls 5.25 kB
/dom/shaka 197.02 kB
/dom/spotify 3.58 kB
/dom/tiktok 3.56 kB
/dom/twitch 3.62 kB
/dom/video-host 1.39 kB
/dom/vimeo 10.62 kB
/dom/youtube 4.25 kB
/media-tracks 2.05 kB
📦 @videojs/spf — 9 small size changes
Path Base initial PR initial Initial diff Lazy diff
. 4.53 kB 4.55 kB +20 B (+0.4%)
/dom 6.58 kB 6.60 kB +20 B (+0.3%)
/hls 21.81 kB 22.01 kB +213 B (+1.0%)
/hls-background-video 15.80 kB 15.85 kB +48 B (+0.3%)
/hls-audio 19.61 kB 19.80 kB +193 B (+1.0%)
/hls-video 25.55 kB 25.79 kB +245 B (+0.9%)
/mux-audio 20.52 kB 20.71 kB +197 B (+0.9%)
/mux-background-video 15.83 kB 15.84 kB +10 B (+0.1%)
/mux-video 26.47 kB 26.65 kB +183 B (+0.7%)
Entries (10)
Entry Initial
. 4.55 kB
/dom 6.60 kB
/hls 22.01 kB
/media-tracks 504 B
/hls-background-video 15.85 kB
/hls-audio 19.80 kB
/hls-video 25.79 kB
/mux-audio 20.71 kB
/mux-background-video 15.84 kB
/mux-video 26.65 kB

ℹ️ How to interpret

Each entry is independently bundled, minified, and brotli-compressed. Initial size includes its static import graph; lazy dynamic chunks are reported separately.

Entries are not additive because their dependency graphs overlap. Preset rows represent realistic combined bundles.

Changes of 300 B or less across initial, lazy, and total size are collapsed, not discarded.

Run pnpm size locally to check current initial sizes.

@cjpillsbury
cjpillsbury marked this pull request as ready for review August 20, 2026 18:59
Comment thread apps/sandbox/templates/spf-segment-loading/main.ts Outdated
cjpillsbury and others added 2 commits August 20, 2026 12:40
SPF implements no SourceBuffer.changeType(), so on a mixed-codec source
(Apple's bipbop advanced example muxes HEVC + AVC renditions) a
mid-stream re-pick across codec families appends undecodable data into a
buffer created under the initial family's mimetype. Two new soft rules
in the switchVideoTrack/switchAudioTrack chains close that hole:

- stickToSelectedCodecs: re-picks narrow to the selected track's codec-
  family set. Head of the chain, ahead of user intent — physics, not
  preference — so a cross-family user pick falls through unhonored
  instead of killing playback.
- preferCodecFamilies: the initial pick prefers configurable codec
  families via `preferredCodecs` (default AVC + AAC, the broadest-decode
  pair), deciding which family the sticky rule then holds. Behind the
  user filter, so an explicit initial pick may land in any family.

The pinned selectVideoTrack (background compositions) never re-picks, so
it stays unwired by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vc-only workaround

Promote Apple's official bipbop advanced fMP4 example (HEVC + AVC
renditions of the same content) from a spf-segment-loading harness
preset to the shared source registry as `hls-mixed-codec`, so every
preset picker and `?source=` deep link reaches it.

The harness's AVC-only toggle existed solely to keep ABR inside one
codec family on that stream (no SourceBuffer.changeType()). The engine
now does that by default — `preferredCodecs` lands the initial pick on
AVC/AAC and the sticky codec-family rule holds it — so the toggle, its
`avcOnly` query param, and the harness-local preset are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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 1 potential issue.

Fix All in Cursor

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bb09627. Configure here.

Comment thread packages/spf/src/playback/behaviors/track-switching.ts Outdated
cjpillsbury and others added 2 commits August 21, 2026 06:47
…cies

An effect that writes a signal one of its own dependency computeds reads
ended its run clean-but-stale: the write marked the intermediate dirty
while the effect was the in-flight consumer, so no watcher notification
fired — and every later external change routed through that intermediate
was deduped against its standing dirty flag, leaving the effect
permanently deaf on that path.

Pull each source once after an effect run (`revalidateSources`): a
dirtied intermediate recomputes and its flag clears, so the next change
through it propagates normally. The guarantee is liveness, not immediacy
— an effect is deliberately not re-run just for writing into its own
graph; it catches up on the next genuine change, which is exactly what
used to be lost. Clean or equals-gated sources cost a cached read.

Surfaced by moving `stickToSelectedCodecs` into the constraints pre-pass
(the candidate-set computed then reads the selection slot the picker
writes); reproduced in ten lines of pure signals and pinned by the new
effect.test.ts for initial-run, flush-run, and convergence cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stickToSelectedCodecs keeps its relational definition — the codec
families of the currently selected track, looked up in the presentation
— and moves from the rule chain into the constraints pre-pass, where its
semantics are truthful: it's playability physics (no
SourceBuffer.changeType()), not preference. Survivors stay playable by
construction, a cross-family user pick is pruned before the user
filter's early-bail can honor it, and a vanished selected family becomes
a reported no-supported-track stop (recovering when the family returns)
instead of a silent cross-family pick.

Reading the selection from the pre-pass — a slot the picking effect
itself writes — is safe only atop the effect-scheduler fix in the
previous commit; the constraint's JSDoc points at it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Several comments on this branch narrated the change's history ("used
to", "before the fix", "the old soft rule", "anymore") instead of the
current behavior. Restate them as present-tense contracts; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cjpillsbury
cjpillsbury merged commit a55c539 into main Aug 24, 2026
33 checks passed
@cjpillsbury
cjpillsbury deleted the feat/sticky-codec-selection branch August 24, 2026 16:19
@luwes luwes mentioned this pull request Aug 24, 2026
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.

2 participants