refactor: Rename SPF background-looping-video into background-video (#1731)

This commit is contained in:
Santiago Puppo
2026-06-24 11:09:20 -03:00
committed by GitHub
parent a5a7953603
commit a74b8c0993
17 changed files with 144 additions and 144 deletions
@@ -133,11 +133,11 @@ actual use.
consolidate into a single use-case doc with a *Variant-decision signal
source* section covering both paths. The Case-1 feature doc does not
exist separately in those cases. Not every use case has a Case-1
sibling — `background-looping-video` has constituent features but no
sibling — `background-video` has constituent features but no
single Case-1 axis-counterpart.
- **Conflating sibling use cases** — e.g., `video-only-mode-override` and
`background-looping-video` both touch the video-only delivery
`background-video` both touch the video-only delivery
composition but address different delivery scenarios (video-without-audio
delivery vs Mux background-video product). Check at Step 5: distinct
customer story? distinct composition specifics? If yes, separate docs
@@ -207,7 +207,7 @@ Common shapes:
video-only-mode-override) → constituent features include the parallel
Case-1 source-shape feature plus the baseline playback / buffer /
selection features.
- Product-scenario use cases (background-looping-video, shorts-player) →
- Product-scenario use cases (background-video, shorts-player) →
constituent features include multiple capability features the variant
assembles.
@@ -404,7 +404,7 @@ phases in, ask: is this really one use case, or is the variant actually
a decomposition into multiple use cases? Heuristic: a slice belongs in
its own doc if it has (a) a distinct customer story, (b) distinct
composition specifics, or (c) independent timeline. Worked example:
`video-only-mode-override` vs `background-looping-video` — both
`video-only-mode-override` vs `background-video` — both
exercise video-only delivery composition, distinct customer
stories, distinct composition specifics.
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox — SPF Background Looping Video</title>
<title>Sandbox — SPF Background Video</title>
<style>
html,
body {
@@ -223,7 +223,7 @@
<div class="scrim"></div>
<div class="hero">
<h1>Background Looping Video</h1>
<h1>Background Video</h1>
<p>
Subtractive engine variant — audio, text tracks, ABR, and preload monitoring are composed out; a max-resolution
picker is composed in. The SPF foundation for <code>&lt;mux-background-video&gt;</code>.
@@ -1,9 +1,9 @@
import '@app/styles.css';
// SPF Background Looping Video — sandbox demo
// http://localhost:5173/spf-background-looping-video/
// SPF Background Video — sandbox demo
// http://localhost:5173/spf-background-video/
//
// Drives `BackgroundLoopingVideoMediaElement`. The diagnostic strip
// Drives `BackgroundVideoMediaElement`. The diagnostic strip
// surfaces three signals reviewers should verify:
// - loadActivated is true from frame 0 (no preload-gate or play-event needed)
// - the picker honors `maxResolution` (defaults to the highest variant)
@@ -19,8 +19,8 @@ import '@app/styles.css';
import { SOURCES } from '@app/shared/sources';
import { effect, snapshot } from '@videojs/spf';
import type { BackgroundLoopingVideoEngineState } from '@videojs/spf/background-looping-video';
import { BackgroundLoopingVideoMediaElement } from '@videojs/spf/background-looping-video';
import type { BackgroundVideoEngineState } from '@videojs/spf/background-video';
import { BackgroundVideoMediaElement } from '@videojs/spf/background-video';
// ── DOM refs ──────────────────────────────────────────────────────────────────
const video = document.getElementById('bg-video') as HTMLVideoElement;
@@ -50,7 +50,7 @@ for (const id of HLS_SOURCE_IDS) {
}
// ── Renditions ────────────────────────────────────────────────────────────────
type MaybePresentation = BackgroundLoopingVideoEngineState['presentation'];
type MaybePresentation = BackgroundVideoEngineState['presentation'];
function videoTracksOf(presentation: MaybePresentation) {
return presentation?.selectionSets?.find((s) => s.type === 'video')?.switchingSets[0]?.tracks ?? [];
@@ -75,7 +75,7 @@ function trackDimensions(track: VideoTrack): { w: number; h: number } {
// ── Adapter lifecycle ─────────────────────────────────────────────────────────
let currentSourceId: keyof typeof SOURCES = DEFAULT_ID;
let currentMaxResolution: string | undefined;
let adapter!: BackgroundLoopingVideoMediaElement;
let adapter!: BackgroundVideoMediaElement;
let stopDiag: () => void = () => {};
function rebuildAdapter(): void {
@@ -84,7 +84,7 @@ function rebuildAdapter(): void {
video.pause();
adapter?.destroy();
adapter = new BackgroundLoopingVideoMediaElement({ config: { maxResolution: currentMaxResolution } });
adapter = new BackgroundVideoMediaElement({ config: { maxResolution: currentMaxResolution } });
// src before attach: the engine starts resolving the presentation before
// play() (called inside attach) runs, so no teardown races the play promise.
adapter.src = SOURCES[currentSourceId].url;
@@ -270,7 +270,7 @@ snapshot between operations rather than awaiting `send()` directly.
candidate (audio-only-tuned buffer-management behavior).
- **[`video-only-mode-override`](../use-cases/video-only-mode-override.md)**
*(coarse)* — Phase 1 baseline constituent (used as-is). Loop-
friendly buffer fetching is the peer `[background-looping-
friendly buffer fetching is the peer `[background-
video]` use case's concern, not this one's; video-only-mode-
override's Phase 3 may surface other defaults but not
loop-around.
+1 -1
View File
@@ -92,7 +92,7 @@ A feature's *implementation shape* falls along a spectrum:
|---|---|---|---|
| **Policy** | Pure config / function variation consumed by an existing behavior. No new behaviors | Inside an existing behavior | *(no documented examples yet — shape: a height cap consumed by a `selectQuality` config)* |
| **Middle pattern** | A new state-producing behavior monitors an external signal and updates state; existing consumer behaviors update to respect that state. Heavier than pure policy but lighter than composition | New behavior + targeted edits to consumers | *(no documented examples yet — shapes: `ResizeObserver` → cap state → switching; CDN-tracking → selection state → failover; buffer-state monitor → ended signal; `initPTS` detection → offset state → append)* |
| **Composition** | A different composed engine. Alternative compositions assemble a variant via one or more composition mechanisms (subtract / add / alternative-implementation / alternative-default-configuration), in any combination, to handle different *modes* or *delivery scenarios*. See [`use-cases/README.md`](../use-cases/README.md) for the full doc-type, mechanism taxonomy, and decomposition rubric | At the Adapter level, on initial conditions | *(no instance docs yet; candidates in [`use-cases/README.md`](../use-cases/README.md) Index — `audio-only-mode-override`, `video-only-mode-override`, `background-looping-video`)* |
| **Composition** | A different composed engine. Alternative compositions assemble a variant via one or more composition mechanisms (subtract / add / alternative-implementation / alternative-default-configuration), in any combination, to handle different *modes* or *delivery scenarios*. See [`use-cases/README.md`](../use-cases/README.md) for the full doc-type, mechanism taxonomy, and decomposition rubric | At the Adapter level, on initial conditions | *(no instance docs yet; candidates in [`use-cases/README.md`](../use-cases/README.md) Index — `audio-only-mode-override`, `video-only-mode-override`, `background-video`)* |
Composition is bounded to **modes** and **delivery scenarios** — see [`use-cases/README.md`](../use-cases/README.md) for the doc-type that captures composition variants. Most "feels like composition" items actually fit the middle pattern.
+1 -1
View File
@@ -140,7 +140,7 @@ Initially empty; populated as docs land. Candidates flagged in source material (
- [`audio-only-mode-override`](./audio-only-mode-override.md) *(partial — Phase 1 landed)* — audio-only delivery. Covers both truly-audio-only HLS sources and mixed-manifest sources delivered as audio-only via the same shared engine factory (`createHlsAudioOnlyEngine`). Subsumes what Notion originally framed as separate epics #4a (Basic Audio-only) and #4b (Audio-only Mode Override).
- [`video-only-mode-override`](./video-only-mode-override.md) *(coarse)* — video-only delivery. Inverse-axis sibling of [`audio-only-mode-override`](./audio-only-mode-override.md); same shape. Subsumes Notion epics NEW-A (Basic Video-only) and NEW-B (Video-only Composition).
- `[background-looping-video]` — Mux's background-video product scenario: loop + autoplay-muted + GPU/thermal-aware caps + likely silent-video delivery. **Distinct from `video-only-mode-override`** despite shared Mux consumer context; both may share constituent features but address different delivery scenarios. *[GitHub #873](https://github.com/videojs/v10/issues/873); [`mux-background-video`](https://github.com/muxinc/mux-background-video) prior art.*
- `[background-video]` — Mux's background-video product scenario: loop + autoplay-muted + GPU/thermal-aware caps + likely silent-video delivery. **Distinct from `video-only-mode-override`** despite shared Mux consumer context; both may share constituent features but address different delivery scenarios. *[GitHub #873](https://github.com/videojs/v10/issues/873); [`mux-background-video`](https://github.com/muxinc/mux-background-video) prior art.*
- Further candidates surfaced in source material but not yet scoped: picture-in-picture, short-form / shorts-player, audio-podcast mode, cast/remote-display compositions, ambient/decorative video.
## See also
@@ -213,7 +213,7 @@ artifact; how the variant is signaled is orthogonal.
- **Adapter shape proliferation.** Each use-case composition that gets its own
adapter multiplies the adapter surface. As more use cases land
(background-looping-video, video-only-mode-override, audio-podcast mode,
(background-video, video-only-mode-override, audio-podcast mode,
etc.), the inventory of `SimpleXHlsMediaElement` classes grows. Worth flagging
as a registry-level concern; may surface a future "use-case adapter factory"
pattern.
@@ -401,7 +401,7 @@ configuration drives end-of-stream correctly with no per-type changes.
- **[`video-only-mode-override`](./video-only-mode-override.md)** *(coarse)*
inverse-axis sibling. Same shape, video-side instead of audio-side: subtract
audio behaviors from any source to deliver video-only. Distinct from
`[background-looping-video]` despite shared constituent features.
`[background-video]` despite shared constituent features.
## See also
@@ -4,7 +4,7 @@ date: 2026-05-22
definition: technical
---
# Background-looping video
# Background video
Engine variant for silent, autoplay-looping video on ambient/decorative
surfaces (hero backgrounds, GIF-replacement loops, editorial previews).
@@ -18,7 +18,7 @@ demonstrates SPF composability by *removing* behaviors from the standard
playback engine rather than adding new logic.
Distinct from [`video-only-mode-override`](./video-only-mode-override.md):
both subtract audio from mixed-source manifests, but background-looping-
both subtract audio from mixed-source manifests, but background-
video also commits to single-rendition playback, loop semantics,
autoplay-via-initial-state, and a product-shaped adapter — `mode-override`
is the narrower "deliver video-only despite mixed source" use case.
@@ -26,9 +26,9 @@ is the narrower "deliver video-only despite mixed source" use case.
## Status
Phase 1 implemented ([#1586](https://github.com/videojs/v10/issues/1586)):
`createBackgroundLoopingVideoEngine`, `BackgroundLoopingVideoMediaElement`,
`createBackgroundVideoEngine`, `BackgroundVideoMediaElement`,
and the `pickMaxResolutionVideoTrack` primitive ship under
`@videojs/spf/background-looping-video`. Phases 2-3 (decorator composition
`@videojs/spf/background-video`. Phases 2-3 (decorator composition
of audio and preload) and Phase 4 (Video.js component shell — out of SPF
scope) stay coarser.
@@ -82,7 +82,7 @@ tree-shakes out the ABR code path."* — exactly the affordance Phase 1 wants.
Independent adapter parallel to `SimpleHlsMediaElement`:
```ts
const bgPlayer = new BackgroundLoopingVideoMediaElement({ picker: maxResolutionPicker });
const bgPlayer = new BackgroundVideoMediaElement({ picker: maxResolutionPicker });
bgPlayer.src = sourceUrl;
bgPlayer.loop = true; // native HTMLMediaElement.loop
bgPlayer.muted = true; // browser autoplay policy
@@ -97,7 +97,7 @@ Video.js component shell) live above the SPF engine.
## Variant-decision signal source
**Adapter-upfront.** Selecting `BackgroundLoopingVideoMediaElement` *is*
**Adapter-upfront.** Selecting `BackgroundVideoMediaElement` *is*
the variant choice — no parser detection, no runtime config branch. Same
resolution as [`video-only-mode-override`](./video-only-mode-override.md)
and [`audio-only-mode-override`](./audio-only-mode-override.md): Case-2
@@ -135,7 +135,7 @@ Phase 2 (decorations TBD): **[`audio-playback`](../features/audio-playback.md)**
- **Sampling-strip alt-impl Path A vs B.** Likely Path B per [`README.md` § Implementation note](./README.md#implementation-note-customizing-behaviors-for-use-cases).
- **GPU/thermal-aware quality caps boundary.** Engine-variant (compose a thermal-aware behavior) or adapter (cap the picker candidate set). Likely engine-variant given the product context.
Resolved Phase 1: ~~picker location~~ (`pickMaxResolutionVideoTrack` ships in [`media/primitives/select-tracks.ts`](../../../../packages/spf/src/media/primitives/select-tracks.ts) next to `pickFirstTrackId`); ~~adapter naming~~ (`BackgroundLoopingVideoMediaElement`; product-shell naming `<mux-background-video>` lives in the adapter layer).
Resolved Phase 1: ~~picker location~~ (`pickMaxResolutionVideoTrack` ships in [`media/primitives/select-tracks.ts`](../../../../packages/spf/src/media/primitives/select-tracks.ts) next to `pickFirstTrackId`); ~~adapter naming~~ (`BackgroundVideoMediaElement`; product-shell naming `<mux-background-video>` lives in the adapter layer).
## See also
@@ -21,7 +21,7 @@ epic NEW-B "Video-only Composition" — delivery-mode choice) into a single
composition with two variant-decision-source paths
(see *Variant-decision signal source* below).
[`background-looping-video`](./README.md#index) *(forward-ref; not yet
[`background-video`](./README.md#index) *(forward-ref; not yet
documented)* is a related-but-distinct Mux product scenario that may
compose this use case as part of a broader assembly (loop +
autoplay-muted + GPU/thermal-aware caps). See
@@ -47,7 +47,7 @@ for product context.
same engine factory and adapter, distinguished only by which signal
sources the variant decision.
- **Mux relevance:** [GitHub #873 (mux-background-video)](https://github.com/videojs/v10/issues/873)
is product-adjacent — distinct use case (`background-looping-video`) that
is product-adjacent — distinct use case (`background-video`) that
shares constituent features but addresses a different delivery scenario.
## Target delivery context
@@ -125,7 +125,7 @@ variant-decision behavior is needed.
be adapter-layer rather than engine-variant — boundary question.
- **GPU/thermal-aware quality caps** — for low-attention consumer surfaces
(ambient video, background tabs), cap video quality based on GPU/thermal
signals. Likely shared with `background-looping-video`'s broader scenario.
signals. Likely shared with `background-video`'s broader scenario.
- **Buffer-target tuning** — depends on consumer surface. Background/ambient
video may want shorter buffers (lower priority); foreground muted-autoplay
may want normal buffers. Probably consumer-policy rather than engine
@@ -152,7 +152,7 @@ Phase 1 baseline:
Phase 1 verification covers this.
- **[`buffer-management`](../features/buffer-management.md)** — used as-is in
Phase 1; Phase 3 surfaces alternative defaults / candidate Path-B behaviors
(loop-friendly buffer fetching is `background-looping-video`'s concern,
(loop-friendly buffer fetching is `background-video`'s concern,
not this use case's).
Phase 2 (when relevant):
@@ -225,11 +225,11 @@ implementation pass landed the shared-factory pattern (see that doc's
- **Adapter shape proliferation.** Each use-case composition that gets its
own adapter multiplies the adapter surface. Shared concern with
`audio-only-mode-override` and forthcoming use cases.
- **Relationship with `background-looping-video`.** Distinct use case (Mux's
- **Relationship with `background-video`.** Distinct use case (Mux's
background-video product scenario); shared constituent features but
different delivery scenario. Background-looping composes loop + autoplay-
different delivery scenario. Background composes loop + autoplay-
muted + GPU/thermal-aware caps + likely silent-video delivery; this use
case is the narrower "deliver video-only" piece. Background-looping-video
case is the narrower "deliver video-only" piece. Background-video
may compose this use case's engine factory as one of its building blocks,
or share constituents at the feature level — cross-link as peer use cases.
- **Test coverage for mixed-source video-only delivery.** New test coverage
@@ -265,8 +265,8 @@ implementation pass landed the shared-factory pattern (see that doc's
muted-default behavior) or adapter level (adapter sets `muted = true` on
the underlying media element). Likely adapter.
- **GPU/thermal-aware quality caps boundary.** Likely shared with
`background-looping-video`; may belong at the use-case level there rather
than here. Worth scoping in the background-looping-video doc when it
`background-video`; may belong at the use-case level there rather
than here. Worth scoping in the background-video doc when it
lands.
- **Mux-billing integration.** Same shape as audio-only-mode-override —
billing context likely surfaces at adapter / consumer level.
@@ -294,11 +294,11 @@ implementation pass landed the shared-factory pattern (see that doc's
inverse-axis sibling. Same shape, audio-side instead of video-side. The
joint resolution of the "engine variant factory shape" open question
applies to both.
- **`[background-looping-video]`** *(forward-ref; not yet documented)*
- **`[background-video]`** *(forward-ref; not yet documented)*
peer use case. Mux's background-video product scenario: loop +
autoplay-muted + GPU/thermal-aware caps + likely silent-video delivery.
**Distinct from this use case** despite shared Mux consumer context and
overlapping constituent features. Background-looping-video may compose
overlapping constituent features. Background-video may compose
this use case's engine factory as one of its building blocks, or compose
shared constituents directly — resolution depends on whether
"use-case-composing-use-case" emerges as a pattern. Cross-link as peer.
+4 -4
View File
@@ -33,10 +33,10 @@
"development": "./dist/dev/hls.js",
"default": "./dist/default/hls.js"
},
"./background-looping-video": {
"types": "./dist/dev/background-looping-video.d.ts",
"development": "./dist/dev/background-looping-video.js",
"default": "./dist/default/background-looping-video.js"
"./background-video": {
"types": "./dist/dev/background-video.d.ts",
"development": "./dist/dev/background-video.js",
"default": "./dist/default/background-video.js"
}
},
"main": "dist/default/index.js",
@@ -1,13 +0,0 @@
export type { BackgroundLoopingVideoMediaAPI, BackgroundLoopingVideoMediaProps } from './adapter';
export {
BackgroundLoopingVideoMediaElement,
BackgroundLoopingVideoMediaMixin,
backgroundLoopingVideoMediaDefaultProps,
} from './adapter';
export type {
BackgroundLoopingVideoEngineConfig,
BackgroundLoopingVideoEngineContext,
BackgroundLoopingVideoEngineSignals,
BackgroundLoopingVideoEngineState,
} from './engine';
export { createBackgroundLoopingVideoEngine } from './engine';
@@ -7,14 +7,14 @@ import {
} from '../../../media/primitives/select-tracks';
import type { VideoSelectionSet } from '../../../media/types';
import {
type BackgroundLoopingVideoEngineConfig,
type BackgroundLoopingVideoEngineContext,
type BackgroundLoopingVideoEngineSignals,
type BackgroundLoopingVideoEngineState,
createBackgroundLoopingVideoEngine,
type BackgroundVideoEngineConfig,
type BackgroundVideoEngineContext,
type BackgroundVideoEngineSignals,
type BackgroundVideoEngineState,
createBackgroundVideoEngine,
} from './engine';
export interface BackgroundLoopingVideoMediaProps {
export interface BackgroundVideoMediaProps {
src: string;
preload: '' | 'none' | 'metadata' | 'auto';
loop: boolean;
@@ -23,7 +23,7 @@ export interface BackgroundLoopingVideoMediaProps {
maxResolution: string | number | undefined;
}
export const backgroundLoopingVideoMediaDefaultProps: BackgroundLoopingVideoMediaProps = {
export const backgroundVideoMediaDefaultProps: BackgroundVideoMediaProps = {
src: '',
preload: 'auto',
loop: true,
@@ -32,8 +32,8 @@ export const backgroundLoopingVideoMediaDefaultProps: BackgroundLoopingVideoMedi
maxResolution: undefined,
};
export interface BackgroundLoopingVideoMediaAPI extends BackgroundLoopingVideoMediaProps {
readonly engine: Composition<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext>;
export interface BackgroundVideoMediaAPI extends BackgroundVideoMediaProps {
readonly engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;
attach(mediaElement: HTMLMediaElement): void;
detach(): void;
destroy(): void;
@@ -41,7 +41,7 @@ export interface BackgroundLoopingVideoMediaAPI extends BackgroundLoopingVideoMe
}
/**
* Mixin that adds the background-looping-video SPF playback engine to any
* Mixin that adds the background-video SPF playback engine to any
* base class.
*
* Implements the WHATWG HTML media element contract (`src`, `preload`,
@@ -53,7 +53,7 @@ export interface BackgroundLoopingVideoMediaAPI extends BackgroundLoopingVideoMe
* passthroughs, all defaulting to `true` the use case is silent
* autoplay-looping video, so muted + autoplay satisfy browser autoplay
* policies and loop is the defining behavior;
* - drives the underlying engine with the background-looping-video
* - drives the underlying engine with the background-video
* composition (single-rendition, video-only, autoplay-from-construction).
*
* A new engine is created on every src assignment this fully tears down
@@ -63,22 +63,22 @@ export interface BackgroundLoopingVideoMediaAPI extends BackgroundLoopingVideoMe
* automatically.
*
* @example
* class BackgroundLoopingVideoMedia extends BackgroundLoopingVideoMediaMixin(HTMLVideoElementHost) {}
* class BackgroundVideoMedia extends BackgroundVideoMediaMixin(HTMLVideoElementHost) {}
*
* const media = new BackgroundLoopingVideoMedia();
* const media = new BackgroundVideoMedia();
* media.attach(document.querySelector('video'));
* media.src = 'https://stream.mux.com/abc123.m3u8';
* media.play();
*/
export function BackgroundLoopingVideoMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {
class BackgroundLoopingVideoMediaImpl extends BaseClass {
#engine: Composition<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext>;
#config: BackgroundLoopingVideoEngineConfig;
#signals!: BackgroundLoopingVideoEngineSignals;
#preload: '' | 'none' | 'metadata' | 'auto' = backgroundLoopingVideoMediaDefaultProps.preload;
#loop: boolean = backgroundLoopingVideoMediaDefaultProps.loop;
#muted: boolean = backgroundLoopingVideoMediaDefaultProps.muted;
#autoplay: boolean = backgroundLoopingVideoMediaDefaultProps.autoplay;
export function BackgroundVideoMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {
class BackgroundVideoMediaImpl extends BaseClass {
#engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;
#config: BackgroundVideoEngineConfig;
#signals!: BackgroundVideoEngineSignals;
#preload: '' | 'none' | 'metadata' | 'auto' = backgroundVideoMediaDefaultProps.preload;
#loop: boolean = backgroundVideoMediaDefaultProps.loop;
#muted: boolean = backgroundVideoMediaDefaultProps.muted;
#autoplay: boolean = backgroundVideoMediaDefaultProps.autoplay;
#maxResolution: string | number | undefined;
/** Pending loadstart listener from a deferred play() retry, if any. */
@@ -94,7 +94,7 @@ export function BackgroundLoopingVideoMediaMixin<Base extends Constructor<any>>(
this.#engine = this.#createEngine();
}
get engine(): Composition<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext> {
get engine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
return this.#engine;
}
@@ -216,7 +216,7 @@ export function BackgroundLoopingVideoMediaMixin<Base extends Constructor<any>>(
async play(): Promise<void> {
const mediaElement = this.#signals.context.mediaElement.get();
if (!mediaElement) {
return Promise.reject(new Error('BackgroundLoopingVideoMediaElement: no media element attached'));
return Promise.reject(new Error('BackgroundVideoMediaElement: no media element attached'));
}
try {
@@ -243,14 +243,14 @@ export function BackgroundLoopingVideoMediaMixin<Base extends Constructor<any>>(
// Private
// -------------------------------------------------------------------------
#createEngine(): Composition<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext> {
#createEngine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
const adapterPicker: TrackPicker = (presentation) => {
const videoSet = presentation.selectionSets?.find((s) => s.type === 'video') as VideoSelectionSet | undefined;
const tracks = videoSet?.switchingSets[0]?.tracks ?? [];
return pickTrackUnderPixelArea(tracks, maxResolutionToPixelArea(this.#maxResolution))?.id;
};
return createBackgroundLoopingVideoEngine({
return createBackgroundVideoEngine({
picker: adapterPicker,
...this.#config,
onSignalsReady: (signals) => {
@@ -267,8 +267,8 @@ export function BackgroundLoopingVideoMediaMixin<Base extends Constructor<any>>(
}
}
return BackgroundLoopingVideoMediaImpl as unknown as MixinReturn<Base, BackgroundLoopingVideoMediaAPI>;
return BackgroundVideoMediaImpl as unknown as MixinReturn<Base, BackgroundVideoMediaAPI>;
}
/** Standalone SPF background-looping-video adapter with no base class. */
export class BackgroundLoopingVideoMediaElement extends BackgroundLoopingVideoMediaMixin(class {}) {}
/** Standalone SPF background-video adapter with no base class. */
export class BackgroundVideoMediaElement extends BackgroundVideoMediaMixin(class {}) {}
@@ -23,11 +23,11 @@ import { resolveVideoTrack } from '../../behaviors/resolve-track';
import { type SelectVideoTrackConfig, selectVideoTrack } from '../../behaviors/select-tracks';
// ============================================================================
// Background-looping-video engine state & context
// Background-video engine state & context
// ============================================================================
/**
* State shape for the background-looping-video playback engine.
* State shape for the background-video playback engine.
*
* Narrower than `SimpleHlsEngineState`: audio/text track slots are absent
* because their selection/resolution behaviors are subtracted. `bandwidthState`
@@ -35,7 +35,7 @@ import { type SelectVideoTrackConfig, selectVideoTrack } from '../../behaviors/s
* samples into it (wasted work in this variant a Phase 3 alt-impl will skip
* sampling).
*/
export interface BackgroundLoopingVideoEngineState {
export interface BackgroundVideoEngineState {
/**
* The presentation being played. A caller writes `{ url }`;
* `resolvePresentation` parses the manifest and populates the rest.
@@ -47,9 +47,9 @@ export interface BackgroundLoopingVideoEngineState {
}
/**
* Context shape for the background-looping-video engine.
* Context shape for the background-video engine.
*/
export interface BackgroundLoopingVideoEngineContext {
export interface BackgroundVideoEngineContext {
mediaElement?: HTMLMediaElement | undefined;
mediaSource?: MediaSource;
videoBufferActor?: SourceBufferActor;
@@ -62,28 +62,28 @@ export interface BackgroundLoopingVideoEngineContext {
* state (reads) without touching `composition.state` / `composition.context`
* directly.
*/
export type BackgroundLoopingVideoEngineSignals = {
state: StateSignals<BackgroundLoopingVideoEngineState>;
context: ContextSignals<BackgroundLoopingVideoEngineContext>;
export type BackgroundVideoEngineSignals = {
state: StateSignals<BackgroundVideoEngineState>;
context: ContextSignals<BackgroundVideoEngineContext>;
};
/**
* Configuration for the background-looping-video engine.
* Configuration for the background-video engine.
*
* Each option is consumed by the appropriate behavior the engine itself
* has no config beyond what its behaviors read. Compared to
* `SimpleHlsEngineConfig`, audio/text/ABR/bandwidth/quality knobs are
* dropped: the variant subtracts the behaviors that read them.
*/
export interface BackgroundLoopingVideoEngineConfig
extends ShareSignalsConfig<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext> {
export interface BackgroundVideoEngineConfig
extends ShareSignalsConfig<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
/**
* Track picker handed to `selectVideoTrack`. Default:
* `pickHighestResolutionVideoTrack` picks the highest-resolution variant on
* presentation resolve and pins it for the session. Override for
* mobile-aware or content-aware caps.
*
* Adapters (e.g. `BackgroundLoopingVideoMediaElement`) install their own
* Adapters (e.g. `BackgroundVideoMediaElement`) install their own
* picker; this default applies when the engine is constructed directly.
*/
picker?: TrackPicker<SelectVideoTrackConfig>;
@@ -95,13 +95,13 @@ export interface BackgroundLoopingVideoEngineConfig
}
// ============================================================================
// Background-looping-video playback engine
// Background-video playback engine
// ============================================================================
const shareSignals = makeShareSignals<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext>();
const shareSignals = makeShareSignals<BackgroundVideoEngineState, BackgroundVideoEngineContext>();
/**
* Create a background-looping-video playback engine.
* Create a background-video playback engine.
*
* Subtractive composition over the HLS engine baseline:
* audio-side, text-side, ABR-driven, preload-monitoring, and play/seek
@@ -113,12 +113,12 @@ const shareSignals = makeShareSignals<BackgroundLoopingVideoEngineState, Backgro
* surfaces that should start loading the moment a src is set.
*
* Native `loop` / `muted` / `autoplay` are adapter concerns and live on
* `BackgroundLoopingVideoMediaElement` rather than the engine.
* `BackgroundVideoMediaElement` rather than the engine.
*
* @example
* ```ts
* let signals: BackgroundLoopingVideoEngineSignals;
* const engine = createBackgroundLoopingVideoEngine({
* let signals: BackgroundVideoEngineSignals;
* const engine = createBackgroundVideoEngine({
* onSignalsReady: (refs) => {
* signals = refs;
* },
@@ -130,9 +130,9 @@ const shareSignals = makeShareSignals<BackgroundLoopingVideoEngineState, Backgro
* await engine.destroy();
* ```
*/
export function createBackgroundLoopingVideoEngine(
config: BackgroundLoopingVideoEngineConfig = {}
): Composition<BackgroundLoopingVideoEngineState, BackgroundLoopingVideoEngineContext> {
export function createBackgroundVideoEngine(
config: BackgroundVideoEngineConfig = {}
): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
const finalConfig = {
...config,
picker: config.picker ?? pickHighestResolutionVideoTrack,
@@ -0,0 +1,13 @@
export type { BackgroundVideoMediaAPI, BackgroundVideoMediaProps } from './adapter';
export {
BackgroundVideoMediaElement,
BackgroundVideoMediaMixin,
backgroundVideoMediaDefaultProps,
} from './adapter';
export type {
BackgroundVideoEngineConfig,
BackgroundVideoEngineContext,
BackgroundVideoEngineSignals,
BackgroundVideoEngineState,
} from './engine';
export { createBackgroundVideoEngine } from './engine';
@@ -1,5 +1,5 @@
/**
* BackgroundLoopingVideoMediaElement adapter tests.
* BackgroundVideoMediaElement adapter tests.
*
* Covers the HTMLMediaElement-compatible contract for src, preload, loop,
* muted, and play(). Adapter-shape parallels SimpleHlsMediaElement; the
@@ -8,9 +8,9 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MaybeResolvedPresentation } from '../../../../media/types';
import { BackgroundLoopingVideoMediaElement } from '../adapter';
import { BackgroundVideoMediaElement } from '../adapter';
describe('BackgroundLoopingVideoMediaElement', () => {
describe('BackgroundVideoMediaElement', () => {
beforeEach(() => {
vi.stubGlobal(
'fetch',
@@ -24,24 +24,24 @@ describe('BackgroundLoopingVideoMediaElement', () => {
describe('src', () => {
it('returns empty string before any src is set', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
expect(media.src).toBe('');
});
it('reflects the set value synchronously', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.src = 'https://example.com/v.m3u8';
expect(media.src).toBe('https://example.com/v.m3u8');
});
it('synchronously updates engine presentation state when src is set', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.src = 'https://example.com/v.m3u8';
expect(media.engine.state.presentation.get()?.url).toBe('https://example.com/v.m3u8');
});
it('clears engine presentation state when src is set to empty string', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.src = 'https://example.com/v.m3u8';
media.src = '';
expect(media.engine.state.presentation.get()?.url).toBeFalsy();
@@ -50,12 +50,12 @@ describe('BackgroundLoopingVideoMediaElement', () => {
describe('attach / detach', () => {
it('exposes the engine immediately (created at construction)', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
expect(media.engine).not.toBeNull();
});
it('reuses the same engine instance across attach calls', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const engineBefore = media.engine;
media.attach(document.createElement('video'));
media.attach(document.createElement('video'));
@@ -63,7 +63,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it('re-attaches the media element to the new engine when src changes', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
media.attach(el);
media.src = 'https://example.com/v1.m3u8';
@@ -71,21 +71,21 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it('sets mediaElement in context when attached', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
media.attach(el);
expect(media.engine.context.mediaElement.get()).toBe(el);
});
it('clears mediaElement in context when detached', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.attach(document.createElement('video'));
media.detach();
expect(media.engine.context.mediaElement.get()).toBeUndefined();
});
it('detach does not destroy the engine', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.attach(document.createElement('video'));
const spy = vi.spyOn(media.engine, 'destroy');
media.detach();
@@ -95,17 +95,17 @@ describe('BackgroundLoopingVideoMediaElement', () => {
describe('loop / muted defaults', () => {
it('defaults loop to true', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
expect(media.loop).toBe(true);
});
it('defaults muted to true', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
expect(media.muted).toBe(true);
});
it('applies loop / muted defaults to the media element on attach', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
// start the element in the opposite state so we can confirm attach overrides it
el.loop = false;
@@ -117,7 +117,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
// attach modifies native props; changing src doesn't
it('preserves loop / muted to the preserved element on src change', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
media.attach(el);
el.loop = false;
@@ -131,7 +131,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
// pins loop=true / muted=true for the autoplay-looping use case). These
// assert functional setters — unskip when the setters are implemented.
it.skip('mirrors loop changes onto the attached element', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
media.attach(el);
media.loop = false;
@@ -140,7 +140,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it.skip('mirrors muted changes onto the attached element', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const el = document.createElement('video');
media.attach(el);
media.muted = false;
@@ -149,7 +149,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it.skip('stores loop / muted updates made before attach and applies them on attach', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.loop = false;
media.muted = false;
const el = document.createElement('video');
@@ -161,7 +161,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
describe('play()', () => {
it('returns a Promise', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.attach(document.createElement('video'));
const result = media.play();
expect(result).toBeInstanceOf(Promise);
@@ -169,14 +169,14 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it('rejects when no media element is attached', async () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
await expect(media.play()).rejects.toThrow('no media element attached');
});
});
describe('destroy()', () => {
it('destroys the underlying engine', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const spy = vi.spyOn(media.engine, 'destroy');
media.destroy();
expect(spy).toHaveBeenCalledOnce();
@@ -185,25 +185,25 @@ describe('BackgroundLoopingVideoMediaElement', () => {
describe('maxResolution', () => {
it('defaults to undefined', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
expect(media.maxResolution).toBeUndefined();
});
it('reflects the value passed via constructor config', () => {
const media = new BackgroundLoopingVideoMediaElement({
const media = new BackgroundVideoMediaElement({
config: { maxResolution: '720p' },
});
expect(media.maxResolution).toBe('720p');
});
it('reflects setter writes', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.maxResolution = '1080p';
expect(media.maxResolution).toBe('1080p');
});
it('does not rebuild the engine on setter writes — closure picker reads the field live', () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
const firstEngine = media.engine;
media.maxResolution = '720p';
expect(media.engine).toBe(firstEngine);
@@ -240,7 +240,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
it('the closure picker reads maxResolution at pick time', async () => {
// Construct without a cap. If the closure captured at creation, the
// pick would use `undefined` (→ 1440p). It uses the current field instead.
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.maxResolution = '720p';
media.engine.state.presentation.set(presentationWithFourTracks());
await new Promise<void>((resolve) => queueMicrotask(resolve));
@@ -249,7 +249,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it('setter writes are reflected on the next presentation cycle', async () => {
const media = new BackgroundLoopingVideoMediaElement();
const media = new BackgroundVideoMediaElement();
media.engine.state.presentation.set(presentationWithFourTracks());
await new Promise<void>((resolve) => queueMicrotask(resolve));
expect(media.engine.state.selectedVideoTrackId.get()).toBe('1440p');
@@ -268,7 +268,7 @@ describe('BackgroundLoopingVideoMediaElement', () => {
});
it('honors a user-supplied picker, overriding the closure default', async () => {
const media = new BackgroundLoopingVideoMediaElement({
const media = new BackgroundVideoMediaElement({
config: { maxResolution: '720p', picker: () => '1440p' },
});
media.engine.state.presentation.set(presentationWithFourTracks());
@@ -1,5 +1,5 @@
/**
* createBackgroundLoopingVideoEngine tests.
* createBackgroundVideoEngine tests.
*
* The variant subtracts audio, text, ABR, and preload-monitoring behaviors
* from the simple HLS engine, then seeds `loadActivated: true` so the
@@ -10,13 +10,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { snapshot } from '../../../../core/signals/primitives';
import type { MaybeResolvedPresentation } from '../../../../media/types';
import { createBackgroundLoopingVideoEngine } from '../engine';
import { createBackgroundVideoEngine } from '../engine';
vi.mock('../../../../media/dom/mse/append-segment', () => ({
appendSegment: vi.fn().mockResolvedValue(undefined),
}));
describe('createBackgroundLoopingVideoEngine', () => {
describe('createBackgroundVideoEngine', () => {
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
@@ -28,7 +28,7 @@ describe('createBackgroundLoopingVideoEngine', () => {
});
it('creates an engine with state, context, and destroy()', () => {
const engine = createBackgroundLoopingVideoEngine();
const engine = createBackgroundVideoEngine();
expect(engine.state).toBeDefined();
expect(engine.context).toBeDefined();
@@ -38,13 +38,13 @@ describe('createBackgroundLoopingVideoEngine', () => {
});
it('seeds loadActivated: true so preload gates pass from frame 0', () => {
const engine = createBackgroundLoopingVideoEngine();
const engine = createBackgroundVideoEngine();
expect(engine.state.loadActivated.get()).toBe(true);
engine.destroy();
});
it('omits subtracted state slots — no audio/text/userVideoTrackSelection signals', () => {
const engine = createBackgroundLoopingVideoEngine();
const engine = createBackgroundVideoEngine();
const state = snapshot(engine.state) as Record<string, unknown>;
// selectedAudioTrackId is declared by calculatePresentationDuration so
@@ -61,7 +61,7 @@ describe('createBackgroundLoopingVideoEngine', () => {
});
it('omits subtracted context slots — no audio segment loader / text actors', () => {
const engine = createBackgroundLoopingVideoEngine();
const engine = createBackgroundVideoEngine();
const context = snapshot(engine.context) as Record<string, unknown>;
// `audioBufferActor` IS declared by `endOfStream` (cross-type EOS
@@ -79,7 +79,7 @@ describe('createBackgroundLoopingVideoEngine', () => {
});
it('defaults the picker to pickHighestResolutionVideoTrack', async () => {
const engine = createBackgroundLoopingVideoEngine();
const engine = createBackgroundVideoEngine();
const presentation: MaybeResolvedPresentation = {
id: 'p',
@@ -136,7 +136,7 @@ describe('createBackgroundLoopingVideoEngine', () => {
});
it('honors a custom picker override from config', async () => {
const engine = createBackgroundLoopingVideoEngine({
const engine = createBackgroundVideoEngine({
picker: () => 'forced-pick',
});
+1 -1
View File
@@ -8,7 +8,7 @@ const createConfig = (mode: PackageBuildMode): UserConfig => ({
index: 'src/index.ts',
dom: 'src/dom.ts',
hls: 'src/playback/engines/hls/index.ts',
'background-looping-video': 'src/playback/engines/background-looping-video/index.ts',
'background-video': 'src/playback/engines/background-video/index.ts',
},
});