Skip to content

docs(spf): HLS engine composition walkthrough + doc-driven cleanups#1512

Merged
cjpillsbury merged 36 commits intomainfrom
docs/spf-hls-engine-composition
May 5, 2026
Merged

docs(spf): HLS engine composition walkthrough + doc-driven cleanups#1512
cjpillsbury merged 36 commits intomainfrom
docs/spf-hls-engine-composition

Conversation

@cjpillsbury
Copy link
Copy Markdown
Collaborator

@cjpillsbury cjpillsbury commented May 5, 2026

Summary

  • Adds packages/spf/docs/hls-engine.md — a stage-by-stage walkthrough of how the HLS engine in @videojs/spf/hls composes behaviors, actors, and reactors. Reference implementation for any future SPF playback engine. Covers state/owners/config, stages 1–9, and a createComposition appendix.
  • Adds packages/spf/src/CLAUDE.md documenting the core//media//network//playback/ dependency boundaries enforced by per-subtree tsconfigs.
  • Bundles the doc-driven cleanups that fell out of writing it: each was a smell the doc surfaced or refused to gloss over.

Doc-driven cleanups

Structural reorg:

  • Sort features/ into behaviors/, actors/, primitives/ by role.
  • Enforce DOM-freedom on core/, media/, network/ via per-subtree tsconfig lib.
  • Move dom/network to top-level src/network.
  • Eliminate src/dom/, redistribute by role.
  • Consolidate behaviors, actors, and HLS engine under playback/.
  • Move HLS engine + SimpleHlsMediaElement to src/playback/engines/hls/.
  • Move select-tracks orchestrations to playback/behaviors/.

Behavior + primitive cleanups:

  • Make onMediaSourceReadyStateChange a callback-based primitive; bind to a state signal at the call site rather than reaching into core/ from media/dom.
  • Move mediaSourceReadyState onto state (drop initial-value casts).
  • Inject the VTT parser via provider config; drop the dom wrapper.
  • Split text-track loader and host provider; extract actor types to media/actors.
  • Rename parseVttSegmentresolveVttSegment, provideTextTrackActorssetupTextTrackActors.
  • Drop the no-op destroyVttResolver line from the HLS composition.

Engine + composition surface:

  • Drop *FromConfig suffix on engine wrappers; shadow imports in engine.ts.
  • Add an explicit-typed overload to createComposition for engines that aggregate many wrapper-style behaviors (works around distributive intersection inference dropping types).
  • Collapse presentationUrl back into a single presentation slot. A prior split into presentationUrl (input) + presentation (output) eliminated an any at the engine but introduced two slots for one concept and silently broke the sandbox harness. New MaybeResolvedPresentation type + isResolvedPresentation guard let every behavior agree on the same shape — Signal invariance is satisfied without any, and Presentation stays strict for parser/resolved consumers.

Other:

  • Move generateId to @videojs/utils/string.
  • Drop core/network references from media/ tsconfigs (boundary enforcement).
  • Delete stale committed .d.ts/.d.ts.map artifacts.

Breaking changes

Three refactor(spf)!: commits in the series rename or relocate exports under @videojs/spf/hls and the engine subtree. Consumers in this repo (packages/html, sandbox templates) are updated in the same series.

Test plan

  • pnpm -F @videojs/spf build clean
  • pnpm typecheck — no SPF errors (pre-existing react/html errors unrelated)
  • pnpm -F @videojs/spf test — 699 passing, 12 skipped, 0 failing, no type errors
  • pnpm check:workspace — 6/6 pass
  • Touched files lint-clean
  • Sandbox apps/sandbox/templates/spf-segment-loading runs end-to-end against the restored single-slot API
  • Verify e2e (chromium + webkit) before merge

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk due to public export-path changes (@videojs/spf/playback-engine@videojs/spf/hls), behavior/DOM API reshuffling, and a new createComposition overload that could affect type inference for downstream engine assemblies.

Overview
Adds new SPF documentation (docs/explainer.md, docs/hls-engine.md) plus contributor guidance (src/CLAUDE.md).

Refactors the public surface and module layout: replaces the ./playback-engine export with ./hls, introduces a new src/dom.ts aggregate, and moves/renames several DOM utilities (e.g. observeMediaSourceReadyStateonMediaSourceReadyStateChange, parseVttSegmentresolveVttSegment, and text-track actor setup split into setupTextTrackActors + host-agnostic loader).

Strengthens type/architecture boundaries by adding per-subtree tsconfigs (core/, media/, network/, etc.), making the network layer domain-agnostic (Resource), introducing MaybeResolvedPresentation + isResolvedPresentation, and adding an explicit-typed overload to createComposition; updates sandbox and React/Core wrappers to import the new SimpleHls engine/media props from @videojs/spf/hls.

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

cjpillsbury and others added 30 commits May 5, 2026 10:23
WIP scaffold for document-driven development of the HLS playback
engine composition. Bundles the full ancestry of that work onto
the fundamentals baseline so future rebases against main stay clean.
First step in the src/ reorganization ahead of the HLS engine doc.
Kind-separation only — no code changes, no abstraction changes, no
cross-concern moves. Purpose is to make the piles visible so the
later seam-extraction work (substrate, transport, engines) has
something obvious to pull from.

What moved:

src/dom/features/ → split into:
  - src/dom/behaviors/   — the {state, owners[, config]} → cleanup
                           composition integrations (end-of-stream,
                           load-segments, load-text-track-cues,
                           setup-mediasource, setup-sourcebuffer,
                           sync-text-tracks, track-current-time,
                           track-playback-initiated,
                           track-playback-rate, update-duration)
  - src/dom/actors/      — createXActor factories returning message/
                           transition actors (segment-loader,
                           text-track-segment-loader, text-tracks)

src/media/features/ → split into:
  - src/media/behaviors/  — DOM-free composition integrations
                            (calculate-presentation-duration,
                            quality-switching, resolve-presentation,
                            resolve-track, sync-preload-attribute)
  - src/media/primitives/ — pure selector functions (select-tracks)

The -actor suffix drops from filenames now that the actors/ directory
carries the kind. Export names keep the suffix
(createSegmentLoaderActor, TextTracksActor, etc.) so call sites
remain self-describing.

No intentional behavior changes. Build, tests, typecheck, lint,
workspace consistency — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds packages/spf/src/core/tsconfig.json as a separate TS project
with lib ["ES2022", "WebWorker"] — no DOM. Matches the pattern
already used in packages/core and packages/store.

The package-level tsconfig stays permissive and still covers src/core
(double coverage, same as how packages/spf/src/dom/tsconfig.json
already overlaps with the package config). The subtree config is
strict, so `pnpm typecheck` — which runs `tsgo --build` against the
root solution — now fails if src/core/ references DOM-only globals.
Verified: a deliberate `HTMLElement` reference in core produces
`error TS2304: Cannot find name 'HTMLElement'`.

Tests under src/core/**/tests/** are excluded from this subproject.
They use HTMLElement as example owner types (intentionally, to match
the fundamentals-doc illustrations), and rewriting them to be
DOM-free is a separate concern from enforcing purity on the source
code. They still typecheck through the package-level config.

WebWorker lib (not just ES2022) is required because core uses
AbortSignal, AbortController, setTimeout — universal primitives that
live in the DOM lib but are also in the worker lib; WebWorker gives
us them without pulling in the DOM.

Next step in the sequence: introduce a media subproject tsconfig and
fix the media → dom/network layering violation surfaced along the way
(resolve-presentation.ts and resolve-track.ts reach into dom/network
for fetch, which itself is universal JS and should not live in dom/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The composition type-tests previously used HTMLElement, HTMLVideoElement,
and HTMLCanvasElement as example owner types. Those are DOM-only, so
the prior commit had to exclude **/tests/** from the core subproject
to keep typecheck green.

Replace them with minimal DOM-free stand-ins that preserve the same
type relationships the tests exercise:

- Surface         — base "writable surface" with textContent
- VideoSurface    — extends Surface (covariance test)
- CanvasSurface   — sibling of VideoSurface (incompatibility test)

This drops the tests-exclude from packages/spf/src/core/tsconfig.json,
so src/core is now fully DOM-enforced — source code and tests alike.

No runtime behavior change; only the types used in example behaviors
and composition type-assertion tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fetch, Request, Response, and ReadableStream are universal JavaScript
APIs — available in browsers, Node 18+, Deno, and workers. They live
under src/dom/ only for historical reasons. Moving them out:

- Breaks a layering violation surfaced by the core tsconfig enforcement:
  src/media/behaviors/resolve-presentation.ts and resolve-track.ts
  reached into src/dom/network/fetch, which was a media → dom import
  that couldn't be modeled cleanly with project references (dom already
  depends on media, adding the reverse edge creates a cycle).

- Gives a natural home for future transport work. When DASH and MoQ
  introduce WebTransport or custom transports, they can land as
  siblings under src/network/ without reshaping dom/.

Changes:
- src/dom/network/{fetch,chunked-stream-iterable}.ts → src/network/
- tests move alongside
- packages/spf/vitest.config.ts: add a network project (Node env;
  these tests don't need a browser)
- import paths updated in all.ts, dom/behaviors/load-segments.ts,
  media/behaviors/resolve-{presentation,track}.ts, and the fetch.ts
  file's own media/types import

No runtime change. Build, tests (707), typecheck, lint, workspace
check all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds subtree tsconfigs for src/media and src/network, both with
lib ["ES2022", "WebWorker"] (no DOM). Matches the pattern core
already uses.

Breaking the media ↔ network cycle:

  Attempting to set up media as its own project surfaced a real
  cyclic project reference — network/fetch.ts imported
  AddressableObject from media/types, while media/behaviors reached
  the other way into network/fetch. Project references can't model
  cycles, and structurally this was also a layering mistake: network
  is the lower layer, so it shouldn't depend on media.

  Fix: network defines its own local Resource interface (url plus
  optional byteRange — a tiny universal shape). Media's
  AddressableObject is structurally compatible, so no media-side
  callers change. network/ is now standalone — no references out.

Fixing the one test-level DOM leak:
  src/media/behaviors/tests/sync-preload-attribute.test.ts typed its
  mock mediaElement as HTMLMediaElement. The behavior itself takes
  MediaElementLike (DOM-free media type); the test now matches.

Other files I had flagged as potential media DOM leaks earlier
turned out to be false positives from regex-based auditing — the
SourceBuffer / HTMLMediaElement / TextTrack references in
media/buffer/forward-buffer.ts, media/types/index.ts,
media/hls/parse-*.ts, etc. were all in comments or already in type
positions that get erased. Verified by deliberately injecting a
real DOM reference and watching it fail.

Resulting project graph (rooted at root tsconfig):

  utils → core → network
                 ↘
                  media
                   ↓
                  dom (plus package tsconfig as the permissive umbrella)

Each arrow is a project reference; no cycles. core, network, media
are all DOM-free enforced. dom is the only subtree with DOM lib.

All green: typecheck, build, 707 tests, workspace check, lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of decoupling the text-track actors from DOM. The actor types
had no semantic DOM dependency — the only DOM leak in the type graph
was AddCuesMessage.cues being typed as VTTCue[]. Rewrite as generic
over a host-agnostic Cue shape; the DOM factory instantiates it with
VTTCue.

Changes:

- media/types: add Cue interface (startTime, endTime, text). VTTCue
  structurally satisfies this; non-DOM hosts can too.

- media/actors/text-tracks.ts (new, type-only): TextTracksActor<C>,
  TextTracksActorMessage<C>, AddCuesMessage<C>, TextTracksActorContext,
  CueSegmentMeta. Generic over C extends Cue, default Cue.

- media/actors/text-track-segment-loader.ts (new, type-only):
  TextTrackSegmentLoaderActor, TextTrackSegmentLoaderMessage.

- dom/actors/text-tracks.ts: drops type declarations, re-exports from
  media/actors for existing consumers, factory now returns
  TextTracksActor<VTTCue>. isDuplicateCue's existing[] now typed Cue[].

- dom/actors/text-track-segment-loader.ts: drops type declarations,
  re-exports from media/actors, factory takes TextTracksActor<VTTCue>.

No behavior change. Build, tests (707), typecheck, lint, workspace
check — all green.

Next (Phase 2): parameterize createTextTrackSegmentLoaderActor over the
cue parser so the actor's runtime is also DOM-free, and move the
implementation out of dom/actors/. Planned to use the composition
config channel for injecting the DOM-bound parser at engine assembly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of decoupling text-track actors from DOM. The factory's runtime
had exactly one DOM dependency — the `parseVttSegment` import — and the
rest of its logic was already host-agnostic. Inject the parser instead
of importing it directly, and move the implementation to media/actors/.

Changes:

- media/actors/text-track-segment-loader.ts: now carries the factory
  implementation, generic over C extends Cue. Signature adds a
  `parseSegment: (url: string) => Promise<C[]>` parameter (typed as the
  new CueParser<C>). All core / media deps; no DOM imports.

- dom/actors/text-track-segment-loader.ts: becomes a thin 25-line
  wrapper that binds the browser's parseVttSegment into the media-level
  factory. Re-exports the types so existing call sites don't change.

- Error message: generalized from "Failed to load VTT segment" to
  "Failed to load text-track segment" since the injected parser need
  not be a VTT parser. Two test assertions updated to match.

Behavior is unchanged from a caller's perspective. Non-DOM engines
(future DASH, MoQ) can build the actor directly against the
media-level factory with their own cue parser — VTT isn't required
structurally.

Next (Phase 3): move load-text-track-cues behavior to media/behaviors/
with its DOM deps (parseSegment, actor factories, textTrack iteration)
declared via composition config. That's the last coupling between this
feature and the dom/ tree.

Build (161 files), tests (707), typecheck, workspace check, lint — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of decoupling text-track orchestration from DOM. The prior
single behavior in dom/behaviors/load-text-track-cues.ts did two jobs:
orchestrate state transitions (DOM-free), and manage actor lifecycle
tied to an HTMLMediaElement (DOM-bound). Splitting these concerns
lets the orchestration live host-agnostic in media/behaviors/.

Changes:

- media/types: add MediaElementWithTextTracks — MediaElementLike plus
  an iterable textTracks of {id: string}. HTMLMediaElement satisfies
  it structurally.

- media/behaviors/load-text-track-cues.ts (new): host-agnostic
  orchestrator. Reads actors from owners; never creates them. State
  machine drops 'setting-up' — actor presence is now just another
  precondition. DOM-free enforced by the media subproject tsconfig.

- dom/behaviors/provide-text-track-actors.ts (new): DOM-side provider
  that creates/destroys TextTracksActor and the segment-loader actor
  on mediaElement mount/unmount, writing them to owners. Replaces
  the actor lifecycle logic previously buried in the orchestrator.

- dom/behaviors/load-text-track-cues.ts: deleted. Owner management
  moves to the provider; orchestration moves to media.

- hls-engine: imports loadTextTrackCues from media/behaviors, and
  adds provideTextTrackActors to the composition list alongside it.

- dom/index.ts: replaces the loadTextTrackCues export with
  provideTextTrackActors (the loader is now re-exported by media).

- Tests updated: setup composes both provider and loader, types the
  shared owners signal as the intersection so it satisfies each
  behavior's shape (HTMLMediaElement narrows MediaElementWithTextTracks).

Benefits:
- The orchestration can now be reused by non-DOM hosts (worker,
  test fake, future MoQ engine) that provide their own actor factories.
- Actor lifecycle is explicit as its own behavior — easier to reason
  about and to substitute in alternate engines.
- media/behaviors/ now has a real occupant beyond the pure-function
  behaviors; validates the directory earning its name.

All green: build (163 files), tests (707), typecheck, workspace
check, lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates the dom/actors/text-track-segment-loader.ts wrapper that
was currying parseVttSegment into the media-level factory. The
provider behavior now declares `config.parseSegment` and receives
the parser through the composition config channel — the pattern
we'd agreed would be right for DOM-bound dependencies.

Changes:

- provideTextTrackActors: signature grows a `config: { parseSegment:
  CueParser<VTTCue> }` requirement. Imports createTextTrackSegmentLoaderActor
  from media/actors/ directly (no wrapper) and threads the parser through.

- hls-engine: adds a provideDomTextTrackActors wrapper that closes
  over parseVttSegment, matching the existing pattern for config-
  threading wrappers (selectVideoTrackFromConfig, etc.). Swaps the
  engine's TextTrackSegmentLoaderActor type import to the media/actors
  path (the dom wrapper no longer re-exports it).

- dom/actors/text-track-segment-loader.ts: deleted. Consumers import
  from media/actors directly and pass the parser explicitly.

- Test updates: the actor's direct test imports the factory from
  media/actors and passes parseVttSegment to each call site. The
  load-text-track-cues test supplies parseSegment in provider config.

Net: one fewer file, one less indirection, and the DOM/host seam is
now crisply at the composition-config layer. The parser doesn't leak
into HlsPlaybackEngineConfig — it's closed over by the engine's
internal wrapper, same as how other config-threading wrappers work.

All green: build (160 files), tests (707), typecheck, workspace
check, lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous implementation held three pieces of coupled state —
a lastMediaElement tracking variable, a teardown closure that read
actors back out of owners via untrack(), and a final disposer that
ran the effect cleanup and teardown separately. All three exist
because the teardown needed to reach actors it didn't own.

With `effect()`'s cleanup-return convention (the callback returns
its own teardown), actors live in the effect's closure and cleanup
destroys them directly. No tracking var, no untrack, no external
teardown.

The only preservation that required care: subscribing the effect
to a `computed(() => owners.get().mediaElement)` rather than to
`owners` directly. Without the projection, writing the actor slots
back to owners from inside the effect would re-trigger it —
destroying the actors it just created in a feedback loop. The
computed's equality short-circuits subsequent owner writes that
don't change mediaElement.

15 insertions, 22 deletions. No behavior change (tests pass without
modification). Semantics equivalent across all four transitions
(set, change, unset, final destroy).

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

The text-track segment fn does both fetch and parse — "resolver" captures
that more accurately than "parser". The config key especially was
misleading: `parseSegment` at the composition level doesn't convey that
it's for text-track segments (vs. video or audio segments), and doesn't
convey the fetch step either.

Renames, in order of surface:

- Concrete DOM impl:
  - parseVttSegment        → resolveVttSegment
  - destroyVttParser       → destroyVttResolver
  - parse-vtt-segment.ts   → resolve-vtt-segment.ts (file + test file)

- Generic type (media/actors/text-track-segment-loader.ts):
  - CueParser<C>           → TextTrackSegmentResolver<C>
    Domain-anchored rather than output-anchored; matches the config key.

- Factory parameter (internal to the actor):
  - parseSegment           → resolveSegment
    Local scope inside the text-track-segment-loader — "segment" is
    unambiguous here.

- Provider config key (engine-level surface):
  - parseSegment           → resolveTextTrackSegment
    Self-describing at the composition config layer where multiple
    segment kinds coexist.

Module-level dummy video cleanup is called destroyVttResolver now to
pair with the renamed function; JSDoc on the type explains the
"resolve" framing (fetch + parse into domain model).

Build, tests (707), typecheck, workspace, lint — all green.

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

The engine and its DOM adapter were buried under src/dom/playback-engine/,
which (a) conflated a cross-cutting composition (engine) with a DOM-bound
adapter (SpfMedia) under one directory, and (b) misnamed the subpath
(singular "playback-engine" won't age well once DASH/MoQ land, and "engine"
as a subpath category isn't where the adapter belongs).

New structure, anticipating multiple engines + possibly multiple HLS
compositions per engine:

  src/playback-engines/
    └── hls/
        ├── engine.ts      (was src/dom/playback-engine/hls-engine.ts)
        ├── adapter.ts     (was src/dom/playback-engine/adapter.ts)
        ├── index.ts
        ├── tests/
        └── tsconfig.json  — own subtree project, DOM lib

SpfMedia stays alongside the HLS engine for now because the current
adapter is coupled to this specific composition in practice (even though
structurally it just needs any composition with the right owners shape).
Revisit when a second engine lands and the engine-agnostic seam becomes
concrete.

BREAKING:
- @videojs/spf/playback-engine → @videojs/spf/hls (subpath renamed)
- @videojs/spf/dom no longer re-exports SpfMedia/SpfMediaMixin/
  spfMediaDefaultProps/SpfMediaAPI/SpfMediaProps — import them from
  @videojs/spf/hls instead.
- External consumers updated: packages/core/src/dom/media/simple-hls,
  packages/react/src/media/simple-hls-video, and the sandbox template
  for spf-segment-loading.

Scaffolding:
- Root tsconfig gains { path: "packages/spf/src/playback-engines/hls" }.
- packages/spf/package.json exports renamed.
- tsdown.config.ts entry "dom/playback-engine" → "hls".
- vitest.config.ts adds a "playback-engines" project (browser mode).

No behavior change. Build (160 files), 707 tests, typecheck, workspace
check, lint — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the current state: the HLS engine composition and its DOM adapter
are specifically for "simple HLS" (a minimal set of HLS behaviors over
MSE). Naming them as if they were THE HLS engine and THE media adapter
papered over that. This rename scopes the names to what they actually
are, leaving room for variants (low-latency, DRM-specific, etc.) as
siblings in the same hls/ directory.

Renames (in packages/spf/src/playback-engines/hls/):

Engine (engine.ts):
- createHlsPlaybackEngine    → createSimpleHlsEngine
- HlsPlaybackEngineState     → SimpleHlsEngineState
- HlsPlaybackEngineOwners    → SimpleHlsEngineOwners
- HlsPlaybackEngineConfig    → SimpleHlsEngineConfig

Adapter (adapter.ts):
- SpfMedia (class)           → SimpleHlsMediaElement
- SpfMediaMixin              → SimpleHlsMediaMixin
- SpfMediaAPI                → SimpleHlsMediaAPI
- SpfMediaProps              → SimpleHlsMediaProps
- spfMediaDefaultProps       → simpleHlsMediaDefaultProps

The class was renamed to SimpleHlsMediaElement to avoid colliding with
core's SimpleHlsMedia (custom element that extends this mixin).

External consumers updated:
- packages/core/src/dom/media/simple-hls/index.ts (uses mixin)
- packages/react/src/media/simple-hls-video/index.tsx (uses props + defaults)
- apps/sandbox/templates/spf-segment-loading/main.ts (uses engine)

Also updated one JSDoc comment in media/behaviors/sync-preload-attribute.ts
and one code example in the WIP spf explainer doc.

No behavior change. Build, tests (707), typecheck, workspace check, lint
— all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First half of the 4a/4b organizational pivot. The 4b "SPF-framework
integration" code was living under src/media/ (named as if it were
CML-like), conflating the two buckets. This move separates them.

Changes:

- src/media/behaviors/  → src/behaviors/
  (calculate-presentation-duration, load-text-track-cues,
   quality-switching, resolve-presentation, resolve-track,
   sync-preload-attribute — all DOM-free SPF behaviors)

- src/media/actors/     → src/behaviors/actors/
  (text-tracks and text-track-segment-loader — DOM-free actor
   types and the generic injected-parser factory; actors are
   nested under behaviors because they're building blocks that
   serve behaviors, not peers)

- All tests moved alongside their source files.

Scaffolding:
- New subtree tsconfigs at src/behaviors/ and src/behaviors/actors/,
  both DOM-free (lib ES2022 + WebWorker), referencing core / network
  / media / utils.
- Root tsconfig gains both references.
- playback-engines/hls/tsconfig.json adds references to behaviors/
  and behaviors/actors/.
- vitest.config.ts adds a 'behaviors' project (node env, excluding
  the not-yet-created behaviors/dom/ subtrees).

Import paths swept across src/. No behavior change.

Next (second half): eliminate src/dom/ entirely; redistribute
src/dom/media/ → src/media/dom/, src/dom/text/ → src/media/dom/text/,
src/dom/behaviors/ → src/behaviors/dom/, src/dom/actors/ →
src/behaviors/actors/dom/ (and pull source-buffer-actor from
dom/media/ to behaviors/actors/dom/ since it's a SPF actor, not a
CML-like MSE utility).

All green: build (160 files), tests (707), typecheck, workspace
check, lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second half of the 4a/4b pivot. The previous commit promoted SPF
behaviors/actors out of src/media/; this one splits src/dom/ along
the same axis: 4a CML-like DOM-bound code → src/media/dom/, 4b
SPF-integration DOM-bound code → src/behaviors/{dom,actors/dom}/.

"DOM vs. not" becomes a cross-cutting axis encoded per-subtree with a
`dom/` subfolder marker — not a top-level bucket. Enforcement via
sub-tsconfigs at each level; a file's closest `dom/` path segment
signals whether it can touch the DOM, absent means DOM-free.

Moves:

src/dom/behaviors/        → src/behaviors/dom/
  (end-of-stream, load-segments, provide-text-track-actors,
   setup-mediasource, setup-sourcebuffer, sync-text-tracks,
   track-current-time, track-playback-initiated,
   track-playback-rate, update-duration; plus tests)

src/dom/actors/           → src/behaviors/actors/dom/
  (segment-loader, text-tracks; plus tests)

src/dom/media/            → split:
  append-segment.ts           → src/media/dom/mse/ (4a, CML-like MSE)
  buffer-flusher.ts           → src/media/dom/mse/ (4a)
  mediasource-setup.ts        → src/media/dom/mse/ (4a)
  mediasource.d.ts            → src/media/dom/mse/ (4a ambient)
  source-buffer-actor.ts      → src/behaviors/actors/dom/source-buffer.ts
                               (this is a SPF actor, not a CML-like utility;
                                drop the '-actor' suffix since dir carries it)

src/dom/text/             → src/media/dom/text/ (4a VTT resolver)

src/dom/tests/            → src/behaviors/dom/tests/
  (end-of-stream.test.ts renamed to end-of-stream-partial-resolution.test.ts
   to avoid name collision with the behavior's own test)

src/dom/index.ts          → src/dom.ts (flat file, aggregation entry for
                            @videojs/spf/dom subpath)

Scaffolding:
- New subtree tsconfigs: src/media/dom/, src/behaviors/dom/,
  src/behaviors/actors/dom/ (all DOM-permissive, referencing siblings).
- src/media/tsconfig.json and src/behaviors/tsconfig.json add excludes
  to cleanly partition their dom/ subtrees.
- Old src/dom/tsconfig.json deleted.
- Root tsconfig references updated: remove packages/spf/src/dom; add
  packages/spf/src/{media/dom, behaviors/dom, behaviors/actors/dom}.
- playback-engines/hls/tsconfig.json references updated.
- tsdown.config.ts entry: 'dom/index' → 'dom.ts'.
- vitest.config.ts: 'dom' project now matches
  src/**/dom/**/*.test.ts across media, behaviors, and actors.

Extensive relative-path sweeps inside moved files (many went from
depth 3 → depth 4 or depth 4 → depth 5 in the tree). Biome auto-
organized imports across 22 files.

No public API change. No test behavior change.

Build (160 files), tests (707), typecheck, workspace check, lint
— all green.

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

Move src/{behaviors, behaviors/actors, playback-engines/hls} into a single
src/playback/{behaviors, actors, engines/hls} umbrella, making "playback" the
explicit domain instead of a generic top-level "behaviors/" name. Behaviors and
actors are 100% playback-specific in practice, so co-locating them with the
engines that consume them clarifies the conceptual model that the explainer
docs describe.

Public exports unchanged: @videojs/spf, @videojs/spf/dom, @videojs/spf/hls all
keep their current entry shapes. External monorepo consumers (packages/core,
packages/react, apps/sandbox) are unaffected.

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

Rewrites observeMediaSourceReadyState (signal-returning) as
onMediaSourceReadyStateChange (callback-based) so it stays a pure DOM
primitive in media/dom/mse/. Drops the only signal/effect dependency from
that file; the signal binding now happens at the call site in
playback/behaviors/dom/setup-mediasource.ts where signals are allowed.

This is the first of three media-→-core boundary fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits packages/spf/src/media/primitives/select-tracks.ts into pure logic
(stays in media/primitives/) and signal-based orchestrations
(selectVideoTrack, selectAudioTrack, selectTextTrack — moved to
playback/behaviors/select-tracks.ts). Drops core/signals dependency from
media/primitives/. Tests split correspondingly.

This is the second of three media-→-core boundary fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes the generateId helper out of @videojs/spf/core/utils into
@videojs/utils/string where it belongs as a generic, framework-agnostic
helper. Updates both consumers (core/tasks/task.ts and
media/hls/parse-multivariant.ts) to import from the new home.

This is the third of three media-→-core boundary fixes; @videojs/spf/media
no longer imports from @videojs/spf/core anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the three media-→-core fixes landed, neither media/ nor media/dom/
imports anything from core/ or network/. Removing the unused project
references from their tsconfigs structurally enforces the boundary:
future violations will surface at typecheck time rather than rely on
review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the four-bucket layout (core/media/network/playback) and the
dependency rules that govern them, noting which rules are structurally
enforced via tsconfig references and lib settings vs. which rely on
review. Aimed at AI agents and contributors deciding where to put new
code.
Renames the behavior so it slots into the existing setup* family
(setupMediaSource, setupSourceBuffers) — all three create stateful
resources and publish them onto owners. The provide* prefix was the
only verb of its kind in playback/behaviors/dom/ and stood out as
inconsistent.

Also drops the orphaned .d.ts/.d.ts.map for the renamed module
(stale build artifacts that had been committed in error).

Touches:
- packages/spf/src/playback/behaviors/dom/{provide,setup}-text-track-actors.ts
- packages/spf/src/dom.ts (re-export)
- packages/spf/src/playback/engines/hls/engine.ts (import + local wrapper)
- packages/spf/src/playback/behaviors/dom/tests/load-text-track-cues.test.ts
- packages/spf/src/playback/behaviors/load-text-track-cues.ts (comment)

Public surface: @videojs/spf/dom now exports setupTextTrackActors
instead of provideTextTrackActors.
22 .d.ts/.d.ts.map files were checked into src/ in error — they're
compiler-generated declarations (sourceMappingURL footer, export declare
syntax) that should land in dist/types/, not alongside the source.
Deleting them removes the "two files for one module" confusion when
walking the source tree.

Preserved: src/globals.d.ts and src/media/dom/mse/mediasource.d.ts —
both hand-written ambient declarations (declare const, declare global).
Initial draft of the HLS engine composition doc. Covers the engine at a
glance (full createSimpleHlsEngine composition list), the state/owners/
config split, and stage 1 (syncPreloadAttribute, trackPlaybackInitiated,
resolvePresentation).

Aligned with fundamentals.md terminology: "resources" for things in
owners, channels for state/owners, config as static. Substitutes
"imperative interfaces" for the "behavior" overload to avoid clashing
with the technical SPF term.

Includes a "Friction surfaced by writing this doc" section that captures
awkward bits encountered during drafting (presentation?: any, the
trailing destroyVttResolver call, casts on initial values, signal slot
on owners). These are candidates for doc-driven cleanup as the draft
expands.
…lue casts

Two doc-driven cleanups surfaced while writing hls-engine.md.

mediaSourceReadyState was published as a ReadonlySignal slot on owners
so behaviors could observe it reactively. But owners is for resources —
values with identity and imperative interfaces — and a signal is data.
Move it to state as a plain MediaSource['readyState'] string; setupMediaSource
writes via update(state, {...}) on each event. Consumers (update-duration,
end-of-stream) read state.mediaSourceReadyState directly.

Also drops the unnecessary `as SimpleHlsEngineState` / `as SimpleHlsEngineOwners`
casts on createSimpleHlsEngine's initial values — the interfaces are
all-optional, the casts were noise.

Tests in setup-mediasource, end-of-stream, and update-duration updated to
match the new shape; helpers now default mediaSourceReadyState to 'open'
to mirror what setupMediaSource produces in production.

hls-engine.md updated to reflect the new shapes; the friction list moves
both items to a "Resolved during drafting" subsection so the trail of
doc-driven changes stays visible.
…ition

The trailing `() => destroyVttResolver()` was running at engine SETUP
(not destroy), and the dummy-<video> singleton it tries to free is lazy
— so the call did nothing. Removing it leaves the composition list
uniform (every entry is a behavior with deps) and surfaces the real
missing concept: per-engine VTT resolver lifecycle. Singleton callers
who really need cleanup can still invoke destroyVttResolver from
@videojs/spf/dom.

A side effect: the engine's `initialState: { bandwidthState: ... }`
needs `as SimpleHlsEngineState` again. With every behavior now taking
typed deps, TypeScript's inference for the composition list collapses
the engine's state shape and drops fields. The cast is annotated with a
comment pointing at this and added back to the friction list as a
known issue to investigate in createComposition.

hls-engine.md updated: destroyVttResolver moves to "Resolved during
drafting", the cast moves back to active friction.
…tation

Presentation lifecycle was modelled with a single `presentation` slot that
transitioned from `{ url }` (caller's input) to `Presentation` (resolver's
output). Different behaviors needed different shapes for it, so the engine
declared `presentation?: any` to thread the needle through Signal
invariance. The any was a doc-driven smell — the doc had to wave its hands
at "shape that becomes another shape."

Split the slot into two:
- `presentationUrl?: string` — input, written by the caller (e.g. adapter
  src setter). Watched by resolvePresentation.
- `presentation?: Presentation` — output, written by resolvePresentation
  on parse success.

Every downstream behavior now declares `presentation?: Presentation` —
no union, no `any`. `UnresolvedPresentation` and `isUnresolved` go away.
`resolvePresentation`'s `deriveState` re-resolves when `presentationUrl`
changes after a previous resolution (so adapter-style URL swaps still
work in-engine, even though SimpleHlsMediaElement destroys/recreates).

Tests across `resolve-presentation`, `track-playback-initiated`,
`setup-mediasource`, `engine`, and `adapter` updated to use
`presentationUrl` as the input slot. The `as SimpleHlsEngineState` cast
on initialState stays — that's a separate TypeScript inference issue
in the composition-list inference, surfaced in the friction list.

hls-engine.md updated: state shape, the resolvePresentation walkthrough,
and the friction "Resolved during drafting" section.
When an engine aggregates many wrapper-style behaviors all sharing the
same Behavior<S, O, C> type, TypeScript's distributive intersection
inference over Behaviors[number] collapses partway through and drops
fields from the inferred state shape. For the HLS engine that meant
losing `bandwidthState` from the inferred type, requiring an
`as SimpleHlsEngineState` cast on initialState.

Add a second overload that takes explicit <S, O, C> type arguments:

    createComposition<MyState, MyOwners, MyConfig>(behaviors, options);

This sidesteps the inference and uses the caller's declared shapes
directly. The inference-style overload remains the right call for
small or single-behavior compositions; engines with many behaviors
should use the explicit form.

Engine.ts now uses the explicit form, drops the cast, and goes back
to plain (deps: Deps) typing on the wrappers — no more local
EngineBehavior alias needed.

hls-engine.md gets a new "Two ways to call createComposition" section
explaining when each form fits, replacing the friction-list entry.
Track selection (stage 2) introduces the engine's wrapper pattern with
both flavours visible in the composition list — media-type wrappers
(close over a fixed `type`) and config-aware wrappers (close over the
engine's config). Calls out why select-tracks is split between
media/primitives (pure logic) and playback/behaviors (orchestration):
the pure helpers stay reusable outside SPF.

Track resolution (stage 3) reuses the same wrapper pattern across
video/audio/text and explains how a selected id becomes a fully
populated track via media-playlist parsing.

Also moved the `createComposition` overload discussion away from
between stages — it interrupted the walkthrough. Will land it as an
appendix at the end of the doc once more stages are in.
cjpillsbury and others added 5 commits May 5, 2026 10:24
Presentation duration (stage 4) is a tiny derivation behavior — small
on its own but a useful pattern call-out for "derived state lives in a
behavior so re-derivation is automatic when inputs change."

MSE setup (stage 5) is the bigger stretch — three behaviors that bridge
SPF state into the imperative MSE pipeline. Calls out:
- The owners/state split for MediaSource (resource) vs readyState (data)
- The "preconditions as signal reads, framework decides when" pattern
  illustrated by updateDuration's safety gates
- The actor-wrapping of SourceBuffer, with raw-buffer and actor co-existing
  on owners for different consumers
Playback tracking and ABR (stage 6) — call out two patterns: state-as-
bus (behaviors read/write a shared signal slot, never push or pull from
each other) and the cascade of selection changes (writing selectedVideoTrackId
re-runs everything downstream because effects tracked the reads).

Segment loading (stage 7) is the engine's busiest behavior — fetches,
plans the forward buffer, samples bandwidth, dispatches append messages
to the SourceBufferActor. Calls out the same logic-vs-orchestration
split we saw in track selection: the forward-buffer planner is a pure
function in media/buffer/, the loader is the SPF-integrated wrapper.
Also notes how serial appendBuffer ordering is the actor's job, not the
loader's.
End of stream (stage 8) is the one behavior that has to coordinate
across the whole pipeline. Calls out two SPF properties this makes
visible: coordination is just reading (no callbacks, no event bus —
N inputs not N² connections), and actor snapshots are signals too
(transitions trigger re-evaluation just like state writes).

Text tracks (stage 9) — different shape from MSE (cues land directly on
<track> elements, no source buffers) but the same SPF patterns: actor-
as-resource, orchestrator-as-behavior. The DOM-specific VTT resolver is
wired in by an engine-level wrapper, parallel to how the engine wires
config to its select-track behaviors.

The "Two ways to call createComposition" section lands as an appendix
at the end of the doc — it's an API note, not a stage in the
walkthrough, so it shouldn't interrupt the narrative.

Doc now covers all 9 stages plus the API note. ~380 lines.
…orts

The *FromConfig suffix read as if config was the source of selection
(e.g. "select a track from config") rather than what it actually means
(an engine-local wrapper that threads engine config to the underlying
behavior). The wrappers are really just the engine's adapted version
of each behavior.

Drop the suffix and shadow the imports — engine.ts now defines local
selectVideoTrack, selectAudioTrack, selectTextTrack, switchQuality, and
setupTextTrackActors that wrap underscore-aliased imports of the same
names. The composition list reads at the right level of abstraction
("the engine selects video tracks") and the wrappers vanish into glue.

hls-engine.md updated to reflect the rename in the composition list,
the Stage 2 wrapper-pattern explanation, and Stage 9's text-track setup.
… slot

c8bab06 split state into `presentationUrl` (string input) and
`presentation` (Presentation output) to avoid an `any` at the engine
level. The `any` was real, but the fix introduced two slots for one
concept: when resolved, `presentationUrl` and `presentation.url`
carry the same value, the adapter's `src` getter has to read the
non-semantic slot, and resolvePresentation grew explicit
URL-change-after-resolved logic.

Restore the single-slot model. Address the `any` smell at the
behavior level instead: every behavior interface and the engine
state now declares `presentation?: MaybeResolvedPresentation` (an
intersection: `AddressableObject & Partial<Omit<Presentation,
keyof AddressableObject>>`). All sides agree on the type, so
Signal invariance no longer forces an `any` bridge. `Presentation`
itself stays strict — parser output and resolved consumers are
unaffected.

A new `isResolvedPresentation` guard in `media/types` narrows
`MaybeResolvedPresentation` to `Presentation` for behaviors that
need resolved fields. Most existing access sites already used `?.`
chains and required no change.

URL changes after resolution work naturally: writing
`presentation: { url: 'new' }` overwrites the slot with an
unresolved value; deriveState sees no `id` and transitions through
'resolving'. The dedicated re-resolution branch from c8bab06 is
gone.

Net diff is similar in size to c8bab06; the win is conceptual:
one slot, one type, presentation lifecycle in a single place.

This also unbreaks the `apps/sandbox/templates/spf-segment-loading`
harness, which was silently broken by c8bab06 — it kept writing
`{ presentation: { url: src } }` against the strict-typed slot
while resolvePresentation watched the new `presentationUrl` slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment May 5, 2026 6:53pm

Request Review

@netlify
Copy link
Copy Markdown

netlify Bot commented May 5, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 174052e
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69fa3cab8bf7a600078647bc
😎 Deploy Preview https://deploy-preview-1512--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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 32.84 kB
/video (default + hls) 166.49 kB
/video (minimal) 32.42 kB
/video (minimal + hls) 166.03 kB
/audio (default) 27.34 kB
/audio (minimal) 24.45 kB
/background 4.15 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.72 kB
/media/dash-video 236.58 kB
/media/hls-video 134.87 kB
/media/mux-audio 160.89 kB
/media/mux-video 161.01 kB
/media/native-hls-video 4.62 kB
/media/simple-hls-video 15.99 kB
Players (5)
Entry Size
/video/player 7.00 kB
/audio/player 5.12 kB
/background/player 3.85 kB
/live-video/player 7.01 kB
/live-audio/player 5.13 kB
Skins (30)
Entry Type Size
/video/minimal-skin.css css 4.46 kB
/video/skin.css css 4.46 kB
/video/minimal-skin js 32.36 kB
/video/minimal-skin.tailwind js 32.83 kB
/video/skin js 32.79 kB
/video/skin.tailwind js 33.24 kB
/audio/minimal-skin.css css 2.69 kB
/audio/skin.css css 2.65 kB
/audio/minimal-skin js 24.44 kB
/audio/minimal-skin.tailwind js 24.61 kB
/audio/skin js 27.36 kB
/audio/skin.tailwind js 27.50 kB
/background/skin.css css 115 B
/background/skin js 1.15 kB
/live-video/minimal-skin.css css 4.46 kB
/live-video/skin.css css 4.46 kB
/live-video/minimal-skin js 32.17 kB
/live-video/minimal-skin.tailwind js 32.49 kB
/live-video/skin js 32.22 kB
/live-video/skin.tailwind js 32.48 kB
/live-audio/minimal-skin.css css 2.69 kB
/live-audio/skin.css css 2.65 kB
/live-audio/minimal-skin js 24.32 kB
/live-audio/minimal-skin.tailwind js 24.25 kB
/live-audio/skin js 26.84 kB
/live-audio/skin.tailwind js 26.87 kB
/global.css css 176 B
/shared.css css 88 B
/tailwind.css css 228 B
/skin-element js 1.37 kB
UI Components (33)
Entry Size
/ui/alert-dialog 632 B
/ui/alert-dialog-close 272 B
/ui/alert-dialog-description 237 B
/ui/alert-dialog-title 230 B
/ui/buffering-indicator 2.00 kB
/ui/captions-button 2.03 kB
/ui/cast-button 2.00 kB
/ui/compounds 3.63 kB
/ui/controls 1.91 kB
/ui/error-dialog 2.44 kB
/ui/fullscreen-button 1.97 kB
/ui/hotkey 2.66 kB
/ui/mute-button 2.00 kB
/ui/pip-button 2.01 kB
/ui/play-button 1.98 kB
/ui/playback-rate-button 2.10 kB
/ui/popover 1.49 kB
/ui/poster 1.79 kB
/ui/seek-button 1.98 kB
/ui/seek-indicator 2.62 kB
/ui/seek-indicator-value 259 B
/ui/slider 1.17 kB
/ui/status-announcer 2.42 kB
/ui/status-indicator 2.37 kB
/ui/status-indicator-value 147 B
/ui/thumbnail 2.54 kB
/ui/time 1.93 kB
/ui/time-slider 2.94 kB
/ui/tooltip 1.59 kB
/ui/volume-indicator 2.65 kB
/ui/volume-indicator-fill 247 B
/ui/volume-indicator-value 250 B
/ui/volume-slider 3.53 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 26.36 kB
/video (default + hls) 158.72 kB
/video (minimal) 26.35 kB
/video (minimal + hls) 158.74 kB
/audio (default) 19.19 kB
/audio (minimal) 17.65 kB
/background 756 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.21 kB
/media/hls-video 133.39 kB
/media/mux-audio 159.70 kB
/media/mux-video 159.68 kB
/media/native-hls-video 3.13 kB
/media/simple-hls-video 14.56 kB
Skins (27)
Entry Type Size
/tailwind.css css 228 B
/video/minimal-skin.css css 4.38 kB
/video/skin.css css 4.37 kB
/video/minimal-skin js 26.24 kB
/video/minimal-skin.tailwind js 30.88 kB
/video/skin js 26.28 kB
/video/skin.tailwind js 30.80 kB
/audio/minimal-skin.css css 2.56 kB
/audio/skin.css css 2.51 kB
/audio/minimal-skin js 17.58 kB
/audio/minimal-skin.tailwind js 20.20 kB
/audio/skin js 19.11 kB
/audio/skin.tailwind js 20.24 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 4.38 kB
/live-video/skin.css css 4.37 kB
/live-video/minimal-skin js 22.91 kB
/live-video/minimal-skin.tailwind js 27.39 kB
/live-video/skin js 22.98 kB
/live-video/skin.tailwind js 27.43 kB
/live-audio/minimal-skin.css css 2.56 kB
/live-audio/skin.css css 2.51 kB
/live-audio/minimal-skin js 16.23 kB
/live-audio/minimal-skin.tailwind js 18.71 kB
/live-audio/skin js 17.83 kB
/live-audio/skin.tailwind js 18.81 kB
UI Components (26)
Entry Size
/ui/alert-dialog 1.16 kB
/ui/buffering-indicator 1.83 kB
/ui/captions-button 2.08 kB
/ui/cast-button 2.02 kB
/ui/controls 1.87 kB
/ui/error-dialog 2.38 kB
/ui/fullscreen-button 2.00 kB
/ui/gesture 1.26 kB
/ui/hotkey 1.93 kB
/ui/live-button 2.06 kB
/ui/mute-button 2.06 kB
/ui/pip-button 1.97 kB
/ui/play-button 2.02 kB
/ui/playback-rate-button 2.02 kB
/ui/popover 1.88 kB
/ui/poster 1.69 kB
/ui/seek-button 2.04 kB
/ui/seek-indicator 1.98 kB
/ui/slider 3.34 kB
/ui/status-indicator 1.98 kB
/ui/thumbnail 2.06 kB
/ui/time 2.56 kB
/ui/time-slider 3.06 kB
/ui/tooltip 2.38 kB
/ui/volume-indicator 1.99 kB
/ui/volume-slider 2.39 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (9)
Entry Size
. 7.02 kB
/dom 12.31 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.36 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 132.98 kB
/dom/media/mux 159.10 kB
/dom/media/native-hls 2.52 kB
/dom/media/simple-hls 13.93 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 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 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

Path Base PR Diff %
/dom 13.40 kB 7.48 kB -5.92 kB -44.2% 🔽
/hls 13.32 kB 🆕
/playback-engine 13.26 kB −13.26 kB −100% 🗑️
Entries (3)
Entry Size
. 4.29 kB
/dom 7.48 kB
/hls 13.32 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.

Comment thread packages/spf/src/media/types/index.ts Outdated
The guard previously narrowed to `Presentation` based solely on
`'id' in presentation`, but `Presentation` also requires
`selectionSets`. A `{ url, id }` value with no `selectionSets`
would slip through the guard and crash downstream behaviors
(`resolveTrack`, `selectTextTrack`, the resolvePresentation FSM)
when they accessed `selectionSets`.

Tighten the guard to check both fields by value (not just `in`,
which can't distinguish a present-but-undefined property). Add
guard tests covering the partial-value cases plus the empty
`selectionSets: []` case (a valid resolved manifest with no
playable tracks, which must still narrow to `Presentation`).

Surfaced by Cursor Bugbot on #1512.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 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.

Reviewed by Cursor Bugbot for commit 174052e. Configure here.

if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== 'open') return;
owners.set(Object.assign({}, owners.get(), { mediaSource, mediaSourceReadyState }) as O);
if (!!mediaSourceSignal.get() || state.get().mediaSourceReadyState !== 'open') return;
owners.set(Object.assign({}, owners.get(), { mediaSource }) as O);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ready state can cross streams

Medium Severity

mediaSourceReadyState is now a shared state slot, so an old MediaSource listener can mark the current setup as open. A detach/reattach before sourceopen can publish a new mediaSource before it is actually open, causing downstream addSourceBuffer() setup to fail.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174052e. Configure here.

@cjpillsbury
Copy link
Copy Markdown
Collaborator Author

Local sandbox smoke testing and local run of e2e (all) look good. This is an intermediary step for SPF architecture hardening (pre-onsite discussions/followups).

@cjpillsbury cjpillsbury enabled auto-merge (squash) May 5, 2026 19:04
@cjpillsbury cjpillsbury merged commit 0cfd3bb into main May 5, 2026
26 checks passed
@cjpillsbury cjpillsbury deleted the docs/spf-hls-engine-composition branch May 5, 2026 19:07
@luwes luwes mentioned this pull request May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants