From 0cfd3bb395332b19cf85e9dc7eb08f656bec3e2b Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Tue, 5 May 2026 12:07:26 -0700 Subject: [PATCH] feat(spf): HLS engine composition walkthrough + doc-driven cleanups (#1512) Co-authored-by: Claude Opus 4.7 (1M context) --- .../templates/spf-segment-loading/main.ts | 10 +- .../core/src/dom/media/simple-hls/index.ts | 4 +- .../src/media/simple-hls-video/index.tsx | 10 +- packages/spf/docs/explainer.md | 303 ++++++++++++++ packages/spf/docs/hls-engine.md | 386 ++++++++++++++++++ packages/spf/package.json | 8 +- packages/spf/src/CLAUDE.md | 77 ++++ packages/spf/src/all.ts | 31 +- .../core/composition/create-composition.ts | 56 +-- .../tests/create-composition-types.test-d.ts | 42 +- .../tests/create-composition-types.test.ts | 28 +- packages/spf/src/core/tasks/task.ts | 2 +- packages/spf/src/core/tsconfig.json | 11 + packages/spf/src/core/utils/generate-id.ts | 16 - packages/spf/src/dom.ts | 22 + .../src/dom/features/load-text-track-cues.ts | 186 --------- packages/spf/src/dom/index.ts | 17 - packages/spf/src/dom/playback-engine/index.ts | 8 - packages/spf/src/dom/tsconfig.json | 10 - packages/spf/src/index.ts | 2 +- .../media => media/dom/mse}/append-segment.ts | 0 .../media => media/dom/mse}/buffer-flusher.ts | 0 .../dom/mse}/mediasource-setup.ts | 33 +- .../media => media/dom/mse}/mediasource.d.ts | 0 .../dom/mse}/tests/append-segment.test.ts | 0 .../dom/mse}/tests/buffer-flusher.test.ts | 0 .../dom/mse}/tests/mediasource-setup.test.ts | 0 .../dom/text/resolve-vtt-segment.ts} | 4 +- .../cue-persistence-investigation.test.ts | 0 .../text/tests/resolve-vtt-segment.test.ts} | 44 +- packages/spf/src/media/dom/tsconfig.json | 11 + .../spf/src/media/hls/parse-multivariant.ts | 2 +- .../{features => primitives}/select-tracks.ts | 115 +----- .../tests/select-tracks.test.ts | 209 +--------- packages/spf/src/media/tsconfig.json | 12 + packages/spf/src/media/types/index.ts | 56 ++- .../spf/src/media/types/tests/guards.test.ts | 72 +++- .../spf/src/media/utils/track-selection.ts | 8 +- .../network/chunked-stream-iterable.ts | 0 packages/spf/src/{dom => }/network/fetch.ts | 28 +- .../tests/chunked-stream-iterable.test.ts | 0 .../src/{dom => }/network/tests/fetch.test.ts | 17 +- packages/spf/src/network/tsconfig.json | 11 + .../actors/dom/segment-loader.ts} | 19 +- .../actors/dom/source-buffer.ts} | 10 +- .../actors/dom/tests/source-buffer.test.ts} | 4 +- .../text-track-mode-cue-preservation.test.ts | 0 .../tests/text-track-segment-loader.test.ts} | 53 +-- .../actors/dom/tests/text-tracks.test.ts} | 4 +- .../actors/dom/text-tracks.ts} | 45 +- .../spf/src/playback/actors/dom/tsconfig.json | 18 + .../actors/text-track-segment-loader.ts} | 40 +- .../spf/src/playback/actors/text-tracks.ts | 33 ++ .../spf/src/playback/actors/tsconfig.json | 12 + .../calculate-presentation-duration.ts | 8 +- .../behaviors/dom}/end-of-stream.ts | 28 +- .../behaviors/dom}/load-segments.ts | 24 +- .../behaviors/dom}/setup-mediasource.ts | 29 +- .../behaviors/dom}/setup-sourcebuffer.ts | 18 +- .../behaviors/dom/setup-text-track-actors.ts | 80 ++++ .../behaviors/dom}/sync-text-tracks.ts | 12 +- .../end-of-stream-partial-resolution.test.ts} | 8 +- .../dom}/tests/end-of-stream.test.ts | 43 +- .../tests/load-segments-track-switch.test.ts | 18 +- .../dom}/tests/load-segments.test.ts | 6 +- .../dom}/tests/load-text-track-cues.test.ts | 132 +++--- .../dom}/tests/setup-mediasource.test.ts | 47 +-- .../dom}/tests/setup-sourcebuffer.test.ts | 20 +- .../dom}/tests/sync-text-tracks.test.ts | 2 +- .../dom}/tests/track-current-time.test.ts | 2 +- .../tests/track-playback-initiated.test.ts | 4 +- .../dom}/tests/track-playback-rate.test.ts | 2 +- .../dom}/tests/update-duration.test.ts | 11 +- .../behaviors/dom}/track-current-time.ts | 4 +- .../dom}/track-playback-initiated.ts | 9 +- .../behaviors/dom}/track-playback-rate.ts | 4 +- .../src/playback/behaviors/dom/tsconfig.json | 20 + .../behaviors/dom}/update-duration.ts | 21 +- .../behaviors/load-text-track-cues.ts | 141 +++++++ .../behaviors}/quality-switching.ts | 12 +- .../behaviors}/resolve-presentation.ts | 61 ++- .../behaviors}/resolve-track.ts | 15 +- .../src/playback/behaviors/select-tracks.ts | 114 ++++++ .../behaviors}/sync-preload-attribute.ts | 4 +- .../calculate-presentation-duration.test.ts | 2 +- .../tests/quality-switching.test.ts | 4 +- .../tests/resolve-presentation.test.ts | 83 ++-- .../behaviors}/tests/resolve-track.test.ts | 12 +- .../behaviors/tests/select-tracks.test.ts | 268 ++++++++++++ .../tests/sync-preload-attribute.test.ts | 26 +- .../spf/src/playback/behaviors/tsconfig.json | 18 + .../engines/hls}/adapter.ts | 46 +-- .../engines/hls/engine.ts} | 120 +++--- .../spf/src/playback/engines/hls/index.ts | 8 + .../engines/hls}/tests/adapter.test.ts | 74 ++-- .../engines/hls/tests/engine.test.ts} | 52 +-- .../src/playback/engines/hls/tsconfig.json | 21 + packages/spf/tsdown.config.ts | 4 +- packages/spf/vitest.config.ts | 38 +- packages/utils/src/string/generate-id.ts | 11 + packages/utils/src/string/index.ts | 1 + .../src/string}/tests/generate-id.test.ts | 0 tsconfig.json | 10 +- 103 files changed, 2509 insertions(+), 1277 deletions(-) create mode 100644 packages/spf/docs/explainer.md create mode 100644 packages/spf/docs/hls-engine.md create mode 100644 packages/spf/src/CLAUDE.md create mode 100644 packages/spf/src/core/tsconfig.json delete mode 100644 packages/spf/src/core/utils/generate-id.ts create mode 100644 packages/spf/src/dom.ts delete mode 100644 packages/spf/src/dom/features/load-text-track-cues.ts delete mode 100644 packages/spf/src/dom/index.ts delete mode 100644 packages/spf/src/dom/playback-engine/index.ts delete mode 100644 packages/spf/src/dom/tsconfig.json rename packages/spf/src/{dom/media => media/dom/mse}/append-segment.ts (100%) rename packages/spf/src/{dom/media => media/dom/mse}/buffer-flusher.ts (100%) rename packages/spf/src/{dom/media => media/dom/mse}/mediasource-setup.ts (84%) rename packages/spf/src/{dom/media => media/dom/mse}/mediasource.d.ts (100%) rename packages/spf/src/{dom/media => media/dom/mse}/tests/append-segment.test.ts (100%) rename packages/spf/src/{dom/media => media/dom/mse}/tests/buffer-flusher.test.ts (100%) rename packages/spf/src/{dom/media => media/dom/mse}/tests/mediasource-setup.test.ts (100%) rename packages/spf/src/{dom/text/parse-vtt-segment.ts => media/dom/text/resolve-vtt-segment.ts} (93%) rename packages/spf/src/{ => media}/dom/text/tests/cue-persistence-investigation.test.ts (100%) rename packages/spf/src/{dom/text/tests/parse-vtt-segment.test.ts => media/dom/text/tests/resolve-vtt-segment.test.ts} (77%) create mode 100644 packages/spf/src/media/dom/tsconfig.json rename packages/spf/src/media/{features => primitives}/select-tracks.ts (71%) rename packages/spf/src/media/{features => primitives}/tests/select-tracks.test.ts (74%) create mode 100644 packages/spf/src/media/tsconfig.json rename packages/spf/src/{dom => }/network/chunked-stream-iterable.ts (100%) rename packages/spf/src/{dom => }/network/fetch.ts (82%) rename packages/spf/src/{dom => }/network/tests/chunked-stream-iterable.test.ts (100%) rename packages/spf/src/{dom => }/network/tests/fetch.test.ts (91%) create mode 100644 packages/spf/src/network/tsconfig.json rename packages/spf/src/{dom/features/segment-loader-actor.ts => playback/actors/dom/segment-loader.ts} (97%) rename packages/spf/src/{dom/media/source-buffer-actor.ts => playback/actors/dom/source-buffer.ts} (96%) rename packages/spf/src/{dom/media/tests/source-buffer-actor.test.ts => playback/actors/dom/tests/source-buffer.test.ts} (99%) rename packages/spf/src/{dom/features => playback/actors/dom}/tests/text-track-mode-cue-preservation.test.ts (100%) rename packages/spf/src/{dom/features/tests/text-track-segment-loader-actor.test.ts => playback/actors/dom/tests/text-track-segment-loader.test.ts} (80%) rename packages/spf/src/{dom/features/tests/text-tracks-actor.test.ts => playback/actors/dom/tests/text-tracks.test.ts} (98%) rename packages/spf/src/{dom/features/text-tracks-actor.ts => playback/actors/dom/text-tracks.ts} (59%) create mode 100644 packages/spf/src/playback/actors/dom/tsconfig.json rename packages/spf/src/{dom/features/text-track-segment-loader-actor.ts => playback/actors/text-track-segment-loader.ts} (60%) create mode 100644 packages/spf/src/playback/actors/text-tracks.ts create mode 100644 packages/spf/src/playback/actors/tsconfig.json rename packages/spf/src/{media/features => playback/behaviors}/calculate-presentation-duration.ts (91%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/end-of-stream.ts (92%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/load-segments.ts (94%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/setup-mediasource.ts (72%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/setup-sourcebuffer.ts (88%) create mode 100644 packages/spf/src/playback/behaviors/dom/setup-text-track-actors.ts rename packages/spf/src/{dom/features => playback/behaviors/dom}/sync-text-tracks.ts (93%) rename packages/spf/src/{dom/tests/end-of-stream.test.ts => playback/behaviors/dom/tests/end-of-stream-partial-resolution.test.ts} (96%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/end-of-stream.test.ts (95%) rename packages/spf/src/{ => playback/behaviors}/dom/tests/load-segments-track-switch.test.ts (94%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/load-segments.test.ts (99%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/load-text-track-cues.test.ts (71%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/setup-mediasource.test.ts (70%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/setup-sourcebuffer.test.ts (92%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/sync-text-tracks.test.ts (99%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/track-current-time.test.ts (99%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/track-playback-initiated.test.ts (98%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/track-playback-rate.test.ts (98%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/tests/update-duration.test.ts (97%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/track-current-time.ts (94%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/track-playback-initiated.ts (92%) rename packages/spf/src/{dom/features => playback/behaviors/dom}/track-playback-rate.ts (90%) create mode 100644 packages/spf/src/playback/behaviors/dom/tsconfig.json rename packages/spf/src/{dom/features => playback/behaviors/dom}/update-duration.ts (87%) create mode 100644 packages/spf/src/playback/behaviors/load-text-track-cues.ts rename packages/spf/src/{media/features => playback/behaviors}/quality-switching.ts (92%) rename packages/spf/src/{media/features => playback/behaviors}/resolve-presentation.ts (67%) rename packages/spf/src/{media/features => playback/behaviors}/resolve-track.ts (88%) create mode 100644 packages/spf/src/playback/behaviors/select-tracks.ts rename packages/spf/src/{media/features => playback/behaviors}/sync-preload-attribute.ts (89%) rename packages/spf/src/{media/features => playback/behaviors}/tests/calculate-presentation-duration.test.ts (99%) rename packages/spf/src/{media/features => playback/behaviors}/tests/quality-switching.test.ts (99%) rename packages/spf/src/{media/features => playback/behaviors}/tests/resolve-presentation.test.ts (85%) rename packages/spf/src/{media/features => playback/behaviors}/tests/resolve-track.test.ts (97%) create mode 100644 packages/spf/src/playback/behaviors/tests/select-tracks.test.ts rename packages/spf/src/{media/features => playback/behaviors}/tests/sync-preload-attribute.test.ts (80%) create mode 100644 packages/spf/src/playback/behaviors/tsconfig.json rename packages/spf/src/{dom/playback-engine => playback/engines/hls}/adapter.ts (79%) rename packages/spf/src/{dom/playback-engine/hls-engine.ts => playback/engines/hls/engine.ts} (59%) create mode 100644 packages/spf/src/playback/engines/hls/index.ts rename packages/spf/src/{dom/playback-engine => playback/engines/hls}/tests/adapter.test.ts (87%) rename packages/spf/src/{dom/tests/playback-engine.test.ts => playback/engines/hls/tests/engine.test.ts} (97%) create mode 100644 packages/spf/src/playback/engines/hls/tsconfig.json create mode 100644 packages/utils/src/string/generate-id.ts rename packages/{spf/src/core/utils => utils/src/string}/tests/generate-id.test.ts (100%) diff --git a/apps/sandbox/templates/spf-segment-loading/main.ts b/apps/sandbox/templates/spf-segment-loading/main.ts index 38560192..938d6b5e 100644 --- a/apps/sandbox/templates/spf-segment-loading/main.ts +++ b/apps/sandbox/templates/spf-segment-loading/main.ts @@ -9,8 +9,8 @@ import '@app/styles.css'; // preload=auto|metadata|none Initial preload mode import { effect } from '@videojs/spf'; -import type { HlsPlaybackEngineState } from '@videojs/spf/playback-engine'; -import { createHlsPlaybackEngine } from '@videojs/spf/playback-engine'; +import type { SimpleHlsEngineState } from '@videojs/spf/hls'; +import { createSimpleHlsEngine } from '@videojs/spf/hls'; // ── DOM refs ────────────────────────────────────────────────────────────────── const video = document.getElementById('video') as HTMLVideoElement; @@ -58,7 +58,7 @@ function formatBandwidth(bps: number): string { return `${Math.round(bps / 1000)} Kbps`; } -function getVideoTracks(state: HlsPlaybackEngineState) { +function getVideoTracks(state: SimpleHlsEngineState) { return state.presentation?.selectionSets?.find((s) => s.type === 'video')?.switchingSets[0]?.tracks ?? []; } @@ -265,14 +265,14 @@ function inspectState() { log('=== SPF Segment Loading POC Test ==='); log(`Stream: ${INITIAL_SRC}`); -let engine: ReturnType; +let engine: ReturnType; let cleanupEffects: () => void = () => {}; function startEngine(src: string) { cleanupEffects(); if (engine) engine.destroy(); - engine = createHlsPlaybackEngine({ initialBandwidth: 1_000_000 }); + engine = createSimpleHlsEngine({ initialBandwidth: 1_000_000 }); (window as any).engine = engine; (window as any).state = () => engine.state.get(); (window as any).owners = () => engine.owners.get(); diff --git a/packages/core/src/dom/media/simple-hls/index.ts b/packages/core/src/dom/media/simple-hls/index.ts index 4f69276f..8ca5afe2 100644 --- a/packages/core/src/dom/media/simple-hls/index.ts +++ b/packages/core/src/dom/media/simple-hls/index.ts @@ -1,4 +1,4 @@ -import { SpfMediaMixin } from '@videojs/spf/dom'; +import { SimpleHlsMediaMixin } from '@videojs/spf/hls'; import { HTMLVideoElementHost } from '../video-host'; -export class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {} +export class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {} diff --git a/packages/react/src/media/simple-hls-video/index.tsx b/packages/react/src/media/simple-hls-video/index.tsx index 732492cf..94a87a69 100644 --- a/packages/react/src/media/simple-hls-video/index.tsx +++ b/packages/react/src/media/simple-hls-video/index.tsx @@ -1,8 +1,8 @@ 'use client'; import { SimpleHlsMedia } from '@videojs/core/dom/media/simple-hls'; -import type { SpfMediaProps } from '@videojs/spf/dom'; -import { spfMediaDefaultProps } from '@videojs/spf/dom'; +import type { SimpleHlsMediaProps } from '@videojs/spf/hls'; +import { simpleHlsMediaDefaultProps } from '@videojs/spf/hls'; import type { ReactNode, VideoHTMLAttributes } from 'react'; import { forwardRef } from 'react'; import { useAttachMedia } from '../../utils/use-attach-media'; @@ -11,8 +11,8 @@ import { useMediaInstance } from '../../utils/use-media-instance'; import { useSyncProps } from '../../utils/use-sync-props'; export interface SimpleHlsVideoProps - extends Omit, keyof SpfMediaProps>, - Partial { + extends Omit, keyof SimpleHlsMediaProps>, + Partial { children?: ReactNode; } @@ -23,7 +23,7 @@ export const SimpleHlsVideo = forwardRef( const media = useMediaInstance(SimpleHlsMedia); const attachRef = useAttachMedia(media); const composedRef = useComposedRefs(attachRef, ref); - const htmlProps = useSyncProps(media, props, spfMediaDefaultProps); + const htmlProps = useSyncProps(media, props, simpleHlsMediaDefaultProps); return (