@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -40
- package/dist/default/core/abr/bandwidth-estimator.js +95 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/create-machine-actor.js +91 -0
- package/dist/default/core/create-machine-actor.js.map +1 -0
- package/dist/default/core/create-machine-reactor.js +83 -0
- package/dist/default/core/create-machine-reactor.js.map +1 -0
- package/dist/default/core/create-transition-actor.js +46 -0
- package/dist/default/core/create-transition-actor.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +53 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +69 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +73 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +66 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +130 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/features/sync-preload-attribute.js +26 -0
- package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +82 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +214 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/machine.js +26 -0
- package/dist/default/core/machine.js.map +1 -0
- package/dist/default/core/signals/effect.js +41 -0
- package/dist/default/core/signals/effect.js.map +1 -0
- package/dist/default/core/signals/primitives.js +32 -0
- package/dist/default/core/signals/primitives.js.map +1 -0
- package/dist/default/core/task.js +191 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +39 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +145 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +192 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +104 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +243 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +48 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/sync-text-tracks.js +95 -0
- package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/text-tracks-actor.js +42 -0
- package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +49 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +66 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/default/dom/features/track-playback-rate.js +32 -0
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +78 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +146 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +137 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +54 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +100 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +142 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +10 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
- package/dist/default/index.js.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +27 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/create-machine-actor.d.ts +12 -0
- package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-actor.js +91 -0
- package/dist/dev/core/create-machine-actor.js.map +1 -0
- package/dist/dev/core/create-machine-reactor.d.ts +8 -0
- package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-reactor.js +83 -0
- package/dist/dev/core/create-machine-reactor.js.map +1 -0
- package/dist/dev/core/create-transition-actor.d.ts +23 -0
- package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
- package/dist/dev/core/create-transition-actor.js +46 -0
- package/dist/dev/core/create-transition-actor.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +69 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +73 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.js +66 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.js +130 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/features/sync-preload-attribute.js +26 -0
- package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +82 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +214 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/machine.d.ts +21 -0
- package/dist/dev/core/machine.d.ts.map +1 -0
- package/dist/dev/core/machine.js +26 -0
- package/dist/dev/core/machine.js.map +1 -0
- package/dist/dev/core/signals/effect.d.ts +15 -0
- package/dist/dev/core/signals/effect.d.ts.map +1 -0
- package/dist/dev/core/signals/effect.js +41 -0
- package/dist/dev/core/signals/effect.js.map +1 -0
- package/dist/dev/core/signals/primitives.d.ts +10 -0
- package/dist/dev/core/signals/primitives.d.ts.map +1 -0
- package/dist/dev/core/signals/primitives.js +32 -0
- package/dist/dev/core/signals/primitives.js.map +1 -0
- package/dist/dev/core/task.js +191 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +166 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +39 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +145 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +192 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +104 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +243 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +48 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/sync-text-tracks.js +95 -0
- package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.js +42 -0
- package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +49 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +66 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +32 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +78 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +146 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +137 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +54 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +100 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +142 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +4 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +26 -24
- package/dist/adapter-CMw-rvbk.js +0 -3512
- package/dist/adapter-CMw-rvbk.js.map +0 -1
- package/dist/adapter-l8iJN_GI.d.ts +0 -689
- package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
- package/dist/dom/index.d.ts +0 -243
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js +0 -38
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/dom/network/fetch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Fetch resolvable from AddressableObject.
|
|
4
|
+
*
|
|
5
|
+
* Handles byte range requests if byteRange is present.
|
|
6
|
+
* Returns native fetch Response for composability (can extract text, stream, etc.).
|
|
7
|
+
*
|
|
8
|
+
* @param addressable - Resource to fetch (url + optional byteRange)
|
|
9
|
+
* @returns Promise resolving to Response
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
|
|
13
|
+
* const text = await getResponseText(response);
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With byte range
|
|
17
|
+
* const response = await fetchResolvable({
|
|
18
|
+
* url: 'https://example.com/file.mp4',
|
|
19
|
+
* byteRange: { start: 1000, end: 1999 }
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
async function fetchResolvable(addressable, options) {
|
|
23
|
+
const headers = new Headers(options?.headers);
|
|
24
|
+
if (addressable.byteRange) {
|
|
25
|
+
const { start, end } = addressable.byteRange;
|
|
26
|
+
headers.set("Range", `bytes=${start}-${end}`);
|
|
27
|
+
}
|
|
28
|
+
const request = new Request(addressable.url, {
|
|
29
|
+
method: "GET",
|
|
30
|
+
headers,
|
|
31
|
+
...options
|
|
32
|
+
});
|
|
33
|
+
return fetch(request);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extract text from Response.
|
|
37
|
+
*
|
|
38
|
+
* Accepts minimal Response-like object (just needs text() method).
|
|
39
|
+
* Returns promise from response.text().
|
|
40
|
+
*
|
|
41
|
+
* @param response - Response-like object with text() method
|
|
42
|
+
* @returns Promise resolving to text content
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const response = await fetchResolvable(addressable);
|
|
46
|
+
* const text = await getResponseText(response);
|
|
47
|
+
*/
|
|
48
|
+
function getResponseText(response) {
|
|
49
|
+
return response.text();
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { fetchResolvable, getResponseText };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","names":[],"sources":["../../../../src/dom/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n * 3. fetchResolvableStream() - Stream body as Uint8Array chunks\n */\n\nimport type { AddressableObject } from '../../core/types';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: AddressableObject,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,gBAAgB,aAAgC,SAA0C;CAC9G,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAmDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { update } from "../../core/signals/primitives.js";
|
|
2
|
+
import { createPlaybackEngine } from "./engine.js";
|
|
3
|
+
//#region src/dom/playback-engine/adapter.ts
|
|
4
|
+
/**
|
|
5
|
+
* Mixin that adds SPF playback engine behavior to any base class.
|
|
6
|
+
*
|
|
7
|
+
* Implements the src/play() contract per the WHATWG HTML spec so that SPF can
|
|
8
|
+
* be used anywhere a media element API is expected.
|
|
9
|
+
*
|
|
10
|
+
* A new engine is created on every src assignment — this fully tears down all
|
|
11
|
+
* state, SourceBuffers, and in-flight requests from the previous source before
|
|
12
|
+
* the next one begins. The media element reference is preserved across src
|
|
13
|
+
* changes and re-applied to the new engine automatically.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}
|
|
17
|
+
*
|
|
18
|
+
* const media = new SimpleHlsMedia();
|
|
19
|
+
* media.attach(document.querySelector('video'));
|
|
20
|
+
* media.src = 'https://stream.mux.com/abc123.m3u8';
|
|
21
|
+
*/
|
|
22
|
+
function SpfMediaMixin(BaseClass) {
|
|
23
|
+
class SpfMediaImpl extends BaseClass {
|
|
24
|
+
#engine;
|
|
25
|
+
#config;
|
|
26
|
+
#preload = "";
|
|
27
|
+
/** Pending loadstart listener from a deferred play() retry, if any. */
|
|
28
|
+
#loadstartListener = null;
|
|
29
|
+
constructor(...args) {
|
|
30
|
+
super(...args);
|
|
31
|
+
const { config } = args?.[0] ?? {};
|
|
32
|
+
this.#config = config;
|
|
33
|
+
this.#engine = createPlaybackEngine(config);
|
|
34
|
+
}
|
|
35
|
+
get engine() {
|
|
36
|
+
return this.#engine;
|
|
37
|
+
}
|
|
38
|
+
attach(mediaElement) {
|
|
39
|
+
super.attach?.(mediaElement);
|
|
40
|
+
update(this.#engine.owners, { mediaElement });
|
|
41
|
+
}
|
|
42
|
+
detach() {
|
|
43
|
+
this.#cancelPendingPlay();
|
|
44
|
+
update(this.#engine.owners, { mediaElement: void 0 });
|
|
45
|
+
super.detach?.();
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
this.#cancelPendingPlay();
|
|
49
|
+
this.#engine.destroy();
|
|
50
|
+
}
|
|
51
|
+
get preload() {
|
|
52
|
+
return this.#preload;
|
|
53
|
+
}
|
|
54
|
+
set preload(value) {
|
|
55
|
+
this.#preload = value;
|
|
56
|
+
if (value) update(this.#engine.state, { preload: value });
|
|
57
|
+
}
|
|
58
|
+
get src() {
|
|
59
|
+
return this.#engine.state.get().presentation?.url ?? "";
|
|
60
|
+
}
|
|
61
|
+
set src(value) {
|
|
62
|
+
const prevMediaElement = this.#engine.owners.get().mediaElement;
|
|
63
|
+
this.#cancelPendingPlay();
|
|
64
|
+
this.#engine.destroy();
|
|
65
|
+
this.#engine = createPlaybackEngine(this.#config);
|
|
66
|
+
if (this.#preload) update(this.#engine.state, { preload: this.#preload });
|
|
67
|
+
if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
|
|
68
|
+
if (value) update(this.#engine.state, { presentation: { url: value } });
|
|
69
|
+
}
|
|
70
|
+
play() {
|
|
71
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
72
|
+
if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
|
|
73
|
+
update(this.#engine.state, { playbackInitiated: true });
|
|
74
|
+
return mediaElement.play().catch((err) => {
|
|
75
|
+
if (this.src) return new Promise((resolve, reject) => {
|
|
76
|
+
const listener = () => {
|
|
77
|
+
this.#loadstartListener = null;
|
|
78
|
+
mediaElement.play().then(resolve, reject);
|
|
79
|
+
};
|
|
80
|
+
this.#loadstartListener = listener;
|
|
81
|
+
mediaElement.addEventListener("loadstart", listener, { once: true });
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
#cancelPendingPlay() {
|
|
87
|
+
if (!this.#loadstartListener) return;
|
|
88
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
89
|
+
mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
|
|
90
|
+
this.#loadstartListener = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return SpfMediaImpl;
|
|
94
|
+
}
|
|
95
|
+
/** Standalone SPF media adapter with no base class. */
|
|
96
|
+
var SpfMedia = class extends SpfMediaMixin(class {}) {};
|
|
97
|
+
//#endregion
|
|
98
|
+
export { SpfMedia, SpfMediaMixin };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\nexport interface SpfMediaAPI {\n readonly engine: PlaybackEngine;\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF playback engine behavior to any base class.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SpfMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SpfMediaImpl extends BaseClass {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n update(this.#engine.owners, { mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n update(this.#engine.owners, { mediaElement: undefined });\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n update(this.#engine.state, { preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.get().presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.get().mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before setting owners so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n update(this.#engine.state, { preload: this.#preload });\n }\n\n if (prevMediaElement) {\n update(this.#engine.owners, { mediaElement: prevMediaElement });\n }\n\n if (value) {\n update(this.#engine.state, { presentation: { url: value } });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n update(this.#engine.state, { playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SpfMediaImpl as unknown as MixinReturn<Base, SpfMediaAPI>;\n}\n\n/** Standalone SPF media adapter with no base class. */\nexport class SpfMedia extends SpfMediaMixin(class {}) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;EACnC;EACA;EACA,WAA8C;;EAG9C,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,qBAAqB,OAAO;;EAG7C,IAAI,SAAyB;AAC3B,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,CAAC;;EAG/C,SAAe;AACb,SAAA,mBAAyB;AACzB,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,KAAA,GAAW,CAAC;AACxD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;EAOxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,OAAO,CAAC;;EAalD,IAAI,MAAc;AAChB,UAAO,MAAA,OAAa,MAAM,KAAK,CAAC,cAAc,OAAO;;EAGvD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,OAAa,OAAO,KAAK,CAAC;AAEnD,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,qBAAqB,MAAA,OAAa;AAIjD,OAAI,MAAA,QACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,MAAA,SAAe,CAAC;AAGxD,OAAI,iBACF,QAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,kBAAkB,CAAC;AAGjE,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;EAShE,OAAsB;GACpB,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,UAAO,MAAA,OAAa,OAAO,EAAE,mBAAmB,MAAM,CAAC;AAEvD,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;GAC9B,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,iBAAc,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,WAAb,cAA8B,cAAc,MAAM,GAAG,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { signal } from "../../core/signals/primitives.js";
|
|
2
|
+
import { loadSegments } from "../features/load-segments.js";
|
|
3
|
+
import { destroyVttParser } from "../text/parse-vtt-segment.js";
|
|
4
|
+
import { loadTextTrackCues } from "../features/load-text-track-cues.js";
|
|
5
|
+
import { trackCurrentTime } from "../features/track-current-time.js";
|
|
6
|
+
import { trackPlaybackInitiated } from "../features/track-playback-initiated.js";
|
|
7
|
+
import { calculatePresentationDuration } from "../../core/features/calculate-presentation-duration.js";
|
|
8
|
+
import { switchQuality } from "../../core/features/quality-switching.js";
|
|
9
|
+
import { resolvePresentation } from "../../core/features/resolve-presentation.js";
|
|
10
|
+
import { resolveTrack } from "../../core/features/resolve-track.js";
|
|
11
|
+
import { selectAudioTrack, selectTextTrack, selectVideoTrack } from "../../core/features/select-tracks.js";
|
|
12
|
+
import { syncPreloadAttribute } from "../../core/features/sync-preload-attribute.js";
|
|
13
|
+
import { endOfStream } from "../features/end-of-stream.js";
|
|
14
|
+
import { setupMediaSource } from "../features/setup-mediasource.js";
|
|
15
|
+
import { setupSourceBuffers } from "../features/setup-sourcebuffer.js";
|
|
16
|
+
import { syncTextTracks } from "../features/sync-text-tracks.js";
|
|
17
|
+
import { updateDuration } from "../features/update-duration.js";
|
|
18
|
+
//#region src/dom/playback-engine/engine.ts
|
|
19
|
+
/**
|
|
20
|
+
* Create a POC playback engine.
|
|
21
|
+
*
|
|
22
|
+
* Wires together all orchestrations to create a reactive playback pipeline:
|
|
23
|
+
* 1. Resolve presentation (multivariant playlist)
|
|
24
|
+
* 2. Select initial video and audio tracks
|
|
25
|
+
* 3. Resolve selected tracks (media playlists)
|
|
26
|
+
* 4. Setup MediaSource
|
|
27
|
+
* 5. Setup SourceBuffers for video and audio
|
|
28
|
+
*
|
|
29
|
+
* Note: This is a POC - does not yet load/append segments.
|
|
30
|
+
*
|
|
31
|
+
* @param config - Playback engine configuration
|
|
32
|
+
* @returns Playback engine instance with state, owners, and destroy function
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const engine = createPlaybackEngine({
|
|
36
|
+
* initialBandwidth: 2_000_000,
|
|
37
|
+
* preferredAudioLanguage: 'en',
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Initialize by setting state and owners
|
|
41
|
+
* engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });
|
|
42
|
+
* engine.state.set({
|
|
43
|
+
* ...engine.state.get(),
|
|
44
|
+
* presentation: { url: 'https://example.com/playlist.m3u8' },
|
|
45
|
+
* preload: 'auto',
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Inspect state
|
|
49
|
+
* console.log(engine.state.get());
|
|
50
|
+
*
|
|
51
|
+
* // Cleanup
|
|
52
|
+
* engine.destroy();
|
|
53
|
+
*/
|
|
54
|
+
function createPlaybackEngine(config = {}) {
|
|
55
|
+
const state = signal({ bandwidthState: {
|
|
56
|
+
fastEstimate: 0,
|
|
57
|
+
fastTotalWeight: 0,
|
|
58
|
+
slowEstimate: 0,
|
|
59
|
+
slowTotalWeight: 0,
|
|
60
|
+
bytesSampled: 0
|
|
61
|
+
} });
|
|
62
|
+
const owners = signal({});
|
|
63
|
+
const cleanups = [
|
|
64
|
+
syncPreloadAttribute({
|
|
65
|
+
state,
|
|
66
|
+
owners
|
|
67
|
+
}),
|
|
68
|
+
trackPlaybackInitiated({
|
|
69
|
+
state,
|
|
70
|
+
owners
|
|
71
|
+
}),
|
|
72
|
+
resolvePresentation({ state }),
|
|
73
|
+
selectVideoTrack({ state }, {
|
|
74
|
+
type: "video",
|
|
75
|
+
...config.initialBandwidth !== void 0 && { initialBandwidth: config.initialBandwidth }
|
|
76
|
+
}),
|
|
77
|
+
selectAudioTrack({ state }, {
|
|
78
|
+
type: "audio",
|
|
79
|
+
...config.preferredAudioLanguage !== void 0 && { preferredAudioLanguage: config.preferredAudioLanguage }
|
|
80
|
+
}),
|
|
81
|
+
selectTextTrack({ state }, {
|
|
82
|
+
type: "text",
|
|
83
|
+
...config.preferredSubtitleLanguage !== void 0 && { preferredSubtitleLanguage: config.preferredSubtitleLanguage },
|
|
84
|
+
...config.includeForcedTracks !== void 0 && { includeForcedTracks: config.includeForcedTracks },
|
|
85
|
+
...config.enableDefaultTrack !== void 0 && { enableDefaultTrack: config.enableDefaultTrack }
|
|
86
|
+
}),
|
|
87
|
+
resolveTrack({ state }, { type: "video" }),
|
|
88
|
+
resolveTrack({ state }, { type: "audio" }),
|
|
89
|
+
resolveTrack({ state }, { type: "text" }),
|
|
90
|
+
calculatePresentationDuration({ state }),
|
|
91
|
+
setupMediaSource({
|
|
92
|
+
state,
|
|
93
|
+
owners
|
|
94
|
+
}),
|
|
95
|
+
updateDuration({
|
|
96
|
+
state,
|
|
97
|
+
owners
|
|
98
|
+
}),
|
|
99
|
+
setupSourceBuffers({
|
|
100
|
+
state,
|
|
101
|
+
owners
|
|
102
|
+
}),
|
|
103
|
+
trackCurrentTime({
|
|
104
|
+
state,
|
|
105
|
+
owners
|
|
106
|
+
}),
|
|
107
|
+
switchQuality({ state }, config.initialBandwidth !== void 0 ? { defaultBandwidth: config.initialBandwidth } : {}),
|
|
108
|
+
loadSegments({
|
|
109
|
+
state,
|
|
110
|
+
owners
|
|
111
|
+
}, { type: "video" }),
|
|
112
|
+
loadSegments({
|
|
113
|
+
state,
|
|
114
|
+
owners
|
|
115
|
+
}, { type: "audio" }),
|
|
116
|
+
endOfStream({
|
|
117
|
+
state,
|
|
118
|
+
owners
|
|
119
|
+
}),
|
|
120
|
+
syncTextTracks({
|
|
121
|
+
state,
|
|
122
|
+
owners
|
|
123
|
+
}),
|
|
124
|
+
loadTextTrackCues({
|
|
125
|
+
state,
|
|
126
|
+
owners
|
|
127
|
+
})
|
|
128
|
+
];
|
|
129
|
+
return {
|
|
130
|
+
state,
|
|
131
|
+
owners,
|
|
132
|
+
destroy: () => {
|
|
133
|
+
cleanups.forEach((cleanup) => typeof cleanup === "function" ? cleanup() : cleanup.destroy());
|
|
134
|
+
for (const value of Object.values(owners.get())) if (value !== null && typeof value === "object" && typeof value.destroy === "function") value.destroy();
|
|
135
|
+
destroyVttParser();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { createPlaybackEngine };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","names":[],"sources":["../../../../src/dom/playback-engine/engine.ts"],"sourcesContent":["import type { BandwidthState } from '../../core/abr/bandwidth-estimator';\nimport { calculatePresentationDuration } from '../../core/features/calculate-presentation-duration';\nimport { switchQuality } from '../../core/features/quality-switching';\nimport { resolvePresentation } from '../../core/features/resolve-presentation';\nimport { resolveTrack } from '../../core/features/resolve-track';\nimport { selectAudioTrack, selectTextTrack, selectVideoTrack } from '../../core/features/select-tracks';\nimport { syncPreloadAttribute } from '../../core/features/sync-preload-attribute';\nimport type { ReadonlySignal, Signal } from '../../core/signals/primitives';\nimport { signal } from '../../core/signals/primitives';\nimport { endOfStream } from '../features/end-of-stream';\nimport { loadSegments } from '../features/load-segments';\nimport { loadTextTrackCues } from '../features/load-text-track-cues';\nimport { setupMediaSource } from '../features/setup-mediasource';\nimport { setupSourceBuffers } from '../features/setup-sourcebuffer';\nimport { syncTextTracks } from '../features/sync-text-tracks';\nimport type { TextTrackSegmentLoaderActor } from '../features/text-track-segment-loader-actor';\nimport type { TextTracksActor } from '../features/text-tracks-actor';\nimport { trackCurrentTime } from '../features/track-current-time';\nimport { trackPlaybackInitiated } from '../features/track-playback-initiated';\nimport { updateDuration } from '../features/update-duration';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { destroyVttParser } from '../text/parse-vtt-segment';\n\n/**\n * Configuration for the playback engine.\n */\nexport interface PlaybackEngineConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching text track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n */\n enableDefaultTrack?: boolean;\n}\n\n/**\n * Combined state shape for the playback engine.\n * Includes all state needed by orchestrations.\n */\nexport interface PlaybackEngineState {\n // Presentation state\n presentation?: any;\n preload?: 'auto' | 'metadata' | 'none';\n\n // Track selection state\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n // NOTE: Text Tracks (subtitles/ccs) can be unselected\n selectedTextTrackId?: string;\n\n // Bandwidth estimation state\n bandwidthState?: BandwidthState;\n\n // ABR control — set to true to suppress automatic quality switching (manual selection mode).\n // TODO: replace with separate manualVideoTrackId / abrVideoTrackId fields so the two\n // concerns don't share a field; see quality-switching.ts for the full design note.\n abrDisabled?: boolean;\n\n // Current playback position (mirrored from mediaElement via trackCurrentTime)\n currentTime?: number;\n\n // True once the user has initiated playback (enables segment loading regardless of preload)\n playbackInitiated?: boolean;\n}\n\n/**\n * Combined owners shape for the playback engine.\n * Includes all mutable platform objects.\n */\nexport interface PlaybackEngineOwners {\n // Media element\n mediaElement?: HTMLMediaElement | undefined;\n\n // MediaSource\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n\n // SourceBuffers and their actors (created together by setupSourceBuffer)\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n\n // Text track actors (written by loadTextTrackCues; destroyed by engine on destroy)\n textTracksActor?: TextTracksActor;\n segmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n/**\n * Playback engine instance.\n */\nexport interface PlaybackEngine {\n /**\n * Reactive state signal (for inspection/testing).\n */\n state: Signal<PlaybackEngineState>;\n\n /**\n * Mutable owners signal (for inspection/testing).\n */\n owners: Signal<PlaybackEngineOwners>;\n\n /**\n * Cleanup function to destroy all orchestrations.\n */\n destroy: () => void;\n}\n\n/**\n * Create a POC playback engine.\n *\n * Wires together all orchestrations to create a reactive playback pipeline:\n * 1. Resolve presentation (multivariant playlist)\n * 2. Select initial video and audio tracks\n * 3. Resolve selected tracks (media playlists)\n * 4. Setup MediaSource\n * 5. Setup SourceBuffers for video and audio\n *\n * Note: This is a POC - does not yet load/append segments.\n *\n * @param config - Playback engine configuration\n * @returns Playback engine instance with state, owners, and destroy function\n *\n * @example\n * const engine = createPlaybackEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * });\n *\n * // Initialize by setting state and owners\n * engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });\n * engine.state.set({\n * ...engine.state.get(),\n * presentation: { url: 'https://example.com/playlist.m3u8' },\n * preload: 'auto',\n * });\n *\n * // Inspect state\n * console.log(engine.state.get());\n *\n * // Cleanup\n * engine.destroy();\n */\nexport function createPlaybackEngine(config: PlaybackEngineConfig = {}): PlaybackEngine {\n // Create reactive state and owners as signals\n const state = signal<PlaybackEngineState>({\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n });\n const owners = signal<PlaybackEngineOwners>({});\n\n // Wire up orchestrations\n const cleanups = [\n // 0a. Sync preload attribute from mediaElement → state.preload\n // Normalises '' (absent attribute) to 'auto' (browser default).\n syncPreloadAttribute({ state, owners }),\n\n // 0b. Track media element play event → state.playbackInitiated\n // Enables preload=\"none\" resolution via native controls / element.play()\n trackPlaybackInitiated({ state, owners }),\n\n // 1. Resolve presentation (URL already in state)\n resolvePresentation({ state }),\n\n // 2. Select initial tracks (when presentation loads)\n selectVideoTrack(\n { state },\n {\n type: 'video',\n ...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),\n }\n ),\n selectAudioTrack(\n { state },\n {\n type: 'audio',\n ...(config.preferredAudioLanguage !== undefined && { preferredAudioLanguage: config.preferredAudioLanguage }),\n }\n ),\n selectTextTrack(\n { state },\n {\n type: 'text',\n ...(config.preferredSubtitleLanguage !== undefined && {\n preferredSubtitleLanguage: config.preferredSubtitleLanguage,\n }),\n ...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),\n ...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),\n }\n ),\n\n // 3. Resolve selected tracks (fetch media playlists)\n resolveTrack({ state }, { type: 'video' as const }),\n resolveTrack({ state }, { type: 'audio' as const }),\n resolveTrack({ state }, { type: 'text' as const }),\n\n // 3.5. Calculate presentation duration from resolved tracks\n calculatePresentationDuration({ state }),\n\n // 4. Setup MediaSource (when presentation loaded)\n setupMediaSource({ state, owners }),\n\n // 4.5. Update MediaSource duration (when presentation duration available)\n updateDuration({ state, owners }),\n\n // 5. Setup SourceBuffers (when MediaSource ready and all selected tracks resolved)\n // Both SourceBuffers are created in a single synchronous operation to guarantee\n // neither is visible to loadSegments before the other exists — preventing the\n // Firefox bug where appending video data before audio SB is created causes\n // mozHasAudio to be permanently false.\n setupSourceBuffers({ state, owners }),\n\n // 5.5. Track currentTime from mediaElement (feeds forward buffer management)\n //\n // NOTE: SourceBufferActor wiring is intentionally absent here in Phase 1.\n //\n // Attempting to wire actors into the engine at this layer revealed a\n // brittleness in the current architecture: every patch to `owners` —\n // regardless of which field changed — wakes up ALL combineLatest subscribers\n // (loadSegments, endOfStream, etc.). Storing actor references in owners\n // caused loadSegments to re-evaluate mid-task, store a spurious pending\n // state, and run a second loading cycle on completion.\n //\n // This means any future feature that needs to introduce new reactive state\n // alongside SourceBuffers faces the same hazard. The right fix is for\n // loadSegments (and other orchestrations) to route their MSE operations\n // through the actor directly, at which point the actor lifecycle is\n // co-located with its consumer rather than managed centrally here.\n //\n // Actor wiring will be introduced in Phase 2 as part of the loadSegments\n // refactor. See .claude/plans/spf/buffer-state-shadow-actual-model.md.\n trackCurrentTime({ state, owners }),\n\n // 5.75. ABR quality switching (reacts to bandwidth samples from loadSegments)\n switchQuality(\n { state },\n config.initialBandwidth !== undefined ? { defaultBandwidth: config.initialBandwidth } : {}\n ),\n\n // 6. Load segments (when SourceBuffer ready and track resolved)\n loadSegments({ state, owners }, { type: 'video' }),\n loadSegments({ state, owners }, { type: 'audio' }),\n\n // 6.5. Signal end of stream when all segments loaded\n endOfStream({ state, owners }),\n\n // 7-8.5. Text track sync: setup, mode sync, and DOM bridge.\n // Consolidates setupTextTracks, syncTextTrackModes, syncSelectedTextTrackFromDom.\n syncTextTracks({ state, owners }),\n\n // 9. Load text track cues (when track resolved and mode set)\n loadTextTrackCues({ state, owners }),\n ];\n\n // Return engine instance\n return {\n state,\n owners,\n destroy: () => {\n cleanups.forEach((cleanup) => (typeof cleanup === 'function' ? cleanup() : cleanup.destroy()));\n // Destroy any actors that orchestrations wrote into owners during their lifetime.\n for (const value of Object.values(owners.get())) {\n if (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as { destroy?: unknown }).destroy === 'function'\n ) {\n (value as { destroy(): void }).destroy();\n }\n }\n destroyVttParser();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuKA,SAAgB,qBAAqB,SAA+B,EAAE,EAAkB;CAEtF,MAAM,QAAQ,OAA4B,EACxC,gBAAgB;EACd,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,EACF,CAAC;CACF,MAAM,SAAS,OAA6B,EAAE,CAAC;CAG/C,MAAM,WAAW;EAGf,qBAAqB;GAAE;GAAO;GAAQ,CAAC;EAIvC,uBAAuB;GAAE;GAAO;GAAQ,CAAC;EAGzC,oBAAoB,EAAE,OAAO,CAAC;EAG9B,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,qBAAqB,KAAA,KAAa,EAAE,kBAAkB,OAAO,kBAAkB;GAC3F,CACF;EACD,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,2BAA2B,KAAA,KAAa,EAAE,wBAAwB,OAAO,wBAAwB;GAC7G,CACF;EACD,gBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,8BAA8B,KAAA,KAAa,EACpD,2BAA2B,OAAO,2BACnC;GACD,GAAI,OAAO,wBAAwB,KAAA,KAAa,EAAE,qBAAqB,OAAO,qBAAqB;GACnG,GAAI,OAAO,uBAAuB,KAAA,KAAa,EAAE,oBAAoB,OAAO,oBAAoB;GACjG,CACF;EAGD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,QAAiB,CAAC;EAGlD,8BAA8B,EAAE,OAAO,CAAC;EAGxC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,eAAe;GAAE;GAAO;GAAQ,CAAC;EAOjC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAqBrC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,cACE,EAAE,OAAO,EACT,OAAO,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,OAAO,kBAAkB,GAAG,EAAE,CAC3F;EAGD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAClD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAGlD,YAAY;GAAE;GAAO;GAAQ,CAAC;EAI9B,eAAe;GAAE;GAAO;GAAQ,CAAC;EAGjC,kBAAkB;GAAE;GAAO;GAAQ,CAAC;EACrC;AAGD,QAAO;EACL;EACA;EACA,eAAe;AACb,YAAS,SAAS,YAAa,OAAO,YAAY,aAAa,SAAS,GAAG,QAAQ,SAAS,CAAE;AAE9F,QAAK,MAAM,SAAS,OAAO,OAAO,OAAO,KAAK,CAAC,CAC7C,KACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY,WAEnD,OAA8B,SAAS;AAG5C,qBAAkB;;EAErB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/dom/text/parse-vtt-segment.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse a VTT segment using browser's native parser.
|
|
4
|
+
*
|
|
5
|
+
* Creates a dummy video element with a track element to leverage
|
|
6
|
+
* the browser's optimized VTT parsing. Returns parsed VTTCue objects.
|
|
7
|
+
*/
|
|
8
|
+
let dummyVideo = null;
|
|
9
|
+
function ensureDummyVideo() {
|
|
10
|
+
if (!dummyVideo) {
|
|
11
|
+
dummyVideo = document.createElement("video");
|
|
12
|
+
dummyVideo.muted = true;
|
|
13
|
+
dummyVideo.preload = "none";
|
|
14
|
+
dummyVideo.style.display = "none";
|
|
15
|
+
dummyVideo.crossOrigin = "anonymous";
|
|
16
|
+
}
|
|
17
|
+
return dummyVideo;
|
|
18
|
+
}
|
|
19
|
+
function parseVttSegment(url) {
|
|
20
|
+
const video = ensureDummyVideo();
|
|
21
|
+
const track = document.createElement("track");
|
|
22
|
+
track.kind = "subtitles";
|
|
23
|
+
track.default = true;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const onLoad = () => {
|
|
26
|
+
const cues = [];
|
|
27
|
+
const textTrack = track.track;
|
|
28
|
+
if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
|
|
29
|
+
const cue = textTrack.cues[i];
|
|
30
|
+
if (cue) cues.push(cue);
|
|
31
|
+
}
|
|
32
|
+
cleanup();
|
|
33
|
+
resolve(cues);
|
|
34
|
+
};
|
|
35
|
+
const onError = () => {
|
|
36
|
+
cleanup();
|
|
37
|
+
reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
|
|
38
|
+
};
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
track.removeEventListener("load", onLoad);
|
|
41
|
+
track.removeEventListener("error", onError);
|
|
42
|
+
video.removeChild(track);
|
|
43
|
+
};
|
|
44
|
+
track.addEventListener("load", onLoad);
|
|
45
|
+
track.addEventListener("error", onError);
|
|
46
|
+
video.appendChild(track);
|
|
47
|
+
track.src = url;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function destroyVttParser() {
|
|
51
|
+
dummyVideo = null;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { destroyVttParser, parseVttSegment };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=parse-vtt-segment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-vtt-segment.js","names":[],"sources":["../../../../src/dom/text/parse-vtt-segment.ts"],"sourcesContent":["/**\n * Parse a VTT segment using browser's native parser.\n *\n * Creates a dummy video element with a track element to leverage\n * the browser's optimized VTT parsing. Returns parsed VTTCue objects.\n */\n\n// Singleton dummy video (reused across all parsing)\nlet dummyVideo: HTMLVideoElement | null = null;\n\nfunction ensureDummyVideo(): HTMLVideoElement {\n if (!dummyVideo) {\n dummyVideo = document.createElement('video');\n dummyVideo.muted = true;\n dummyVideo.preload = 'none';\n dummyVideo.style.display = 'none';\n dummyVideo.crossOrigin = 'anonymous';\n }\n return dummyVideo;\n}\n\nexport function parseVttSegment(url: string): Promise<VTTCue[]> {\n const video = ensureDummyVideo();\n const track = document.createElement('track');\n track.kind = 'subtitles';\n track.default = true;\n\n return new Promise((resolve, reject) => {\n const onLoad = (): void => {\n const cues: VTTCue[] = [];\n const textTrack = track.track;\n\n if (textTrack.cues) {\n for (let i = 0; i < textTrack.cues.length; i++) {\n const cue = textTrack.cues[i];\n if (cue) {\n cues.push(cue as VTTCue);\n }\n }\n }\n\n cleanup();\n resolve(cues);\n };\n\n const onError = (): void => {\n cleanup();\n reject(new Error(`Failed to load VTT segment: ${url}`));\n };\n\n const cleanup = (): void => {\n track.removeEventListener('load', onLoad);\n track.removeEventListener('error', onError);\n video.removeChild(track);\n };\n\n track.addEventListener('load', onLoad);\n track.addEventListener('error', onError);\n video.appendChild(track);\n track.src = url;\n });\n}\n\nexport function destroyVttParser(): void {\n dummyVideo = null;\n}\n"],"mappings":";;;;;;;AAQA,IAAI,aAAsC;AAE1C,SAAS,mBAAqC;AAC5C,KAAI,CAAC,YAAY;AACf,eAAa,SAAS,cAAc,QAAQ;AAC5C,aAAW,QAAQ;AACnB,aAAW,UAAU;AACrB,aAAW,MAAM,UAAU;AAC3B,aAAW,cAAc;;AAE3B,QAAO;;AAGT,SAAgB,gBAAgB,KAAgC;CAC9D,MAAM,QAAQ,kBAAkB;CAChC,MAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,OAAM,OAAO;AACb,OAAM,UAAU;AAEhB,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,eAAqB;GACzB,MAAM,OAAiB,EAAE;GACzB,MAAM,YAAY,MAAM;AAExB,OAAI,UAAU,KACZ,MAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,QAAQ,KAAK;IAC9C,MAAM,MAAM,UAAU,KAAK;AAC3B,QAAI,IACF,MAAK,KAAK,IAAc;;AAK9B,YAAS;AACT,WAAQ,KAAK;;EAGf,MAAM,gBAAsB;AAC1B,YAAS;AACT,0BAAO,IAAI,MAAM,+BAA+B,MAAM,CAAC;;EAGzD,MAAM,gBAAsB;AAC1B,SAAM,oBAAoB,QAAQ,OAAO;AACzC,SAAM,oBAAoB,SAAS,QAAQ;AAC3C,SAAM,YAAY,MAAM;;AAG1B,QAAM,iBAAiB,QAAQ,OAAO;AACtC,QAAM,iBAAiB,SAAS,QAAQ;AACxC,QAAM,YAAY,MAAM;AACxB,QAAM,MAAM;GACZ;;AAGJ,SAAgB,mBAAyB;AACvC,cAAa"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadSegments } from "./dom/features/load-segments.js";
|
|
2
|
+
import { destroyVttParser, parseVttSegment } from "./dom/text/parse-vtt-segment.js";
|
|
3
|
+
import { loadTextTrackCues } from "./dom/features/load-text-track-cues.js";
|
|
4
|
+
import { trackCurrentTime } from "./dom/features/track-current-time.js";
|
|
5
|
+
import { trackPlaybackInitiated } from "./dom/features/track-playback-initiated.js";
|
|
6
|
+
import { trackPlaybackRate } from "./dom/features/track-playback-rate.js";
|
|
7
|
+
import { appendSegment } from "./dom/media/append-segment.js";
|
|
8
|
+
import { flushBuffer } from "./dom/media/buffer-flusher.js";
|
|
9
|
+
import { SpfMedia, SpfMediaMixin } from "./dom/playback-engine/adapter.js";
|
|
10
|
+
export { SpfMedia, SpfMediaMixin, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["/**\n * Stream Processing Framework (SPF) for Video.js 10\n *\n * Bundle size target: < 20KB (minified + gzipped)\n * Primary use case: CMAF HLS playback with Video.js v10\n *\n * @packageDocumentation\n */\n\n// Public API exports will be added here as features are implemented\n\nexport const VERSION = '0.1.0';\n"],"mappings":";;;;;;;;;AAWA,MAAa,UAAU"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/core/abr/bandwidth-estimator.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dual EWMA Bandwidth Estimator
|
|
4
|
+
*
|
|
5
|
+
* Estimates available bandwidth using two EWMA calculations with different
|
|
6
|
+
* half-lives, taking the minimum of both. This approach (from Shaka Player):
|
|
7
|
+
*
|
|
8
|
+
* - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
|
|
9
|
+
* - **Slow EWMA** (5s half-life): Provides stability during fluctuations
|
|
10
|
+
* - **min(fast, slow)**: Adapts down quickly, up slowly
|
|
11
|
+
*
|
|
12
|
+
* This naturally provides asymmetric behavior needed for good QoE:
|
|
13
|
+
* avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Bandwidth estimator state.
|
|
17
|
+
*
|
|
18
|
+
* This state structure will be managed by O1 (State Container).
|
|
19
|
+
* Functions in this module operate on this state immutably.
|
|
20
|
+
*/
|
|
21
|
+
interface BandwidthState {
|
|
22
|
+
/** Fast-moving EWMA estimate (raw, uncorrected). */
|
|
23
|
+
fastEstimate: number;
|
|
24
|
+
/** Total weight accumulated in fast EWMA. */
|
|
25
|
+
fastTotalWeight: number;
|
|
26
|
+
/** Slow-moving EWMA estimate (raw, uncorrected). */
|
|
27
|
+
slowEstimate: number;
|
|
28
|
+
/** Total weight accumulated in slow EWMA. */
|
|
29
|
+
slowTotalWeight: number;
|
|
30
|
+
/** Total bytes sampled across all valid samples. */
|
|
31
|
+
bytesSampled: number;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { BandwidthState };
|
|
35
|
+
//# sourceMappingURL=bandwidth-estimator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bandwidth-estimator.d.ts","names":[],"sources":["../../../../src/core/abr/bandwidth-estimator.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;UAAiB,cAAA;;EAEf,YAAA;;EAEA,eAAA;;EAEA,YAAA;;EAEA,eAAA;;EAEA,YAAA;AAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { applyZeroFactor, calculateEwma } from "./ewma.js";
|
|
2
|
+
//#region src/core/abr/bandwidth-estimator.ts
|
|
3
|
+
/**
|
|
4
|
+
* Dual EWMA Bandwidth Estimator
|
|
5
|
+
*
|
|
6
|
+
* Estimates available bandwidth using two EWMA calculations with different
|
|
7
|
+
* half-lives, taking the minimum of both. This approach (from Shaka Player):
|
|
8
|
+
*
|
|
9
|
+
* - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
|
|
10
|
+
* - **Slow EWMA** (5s half-life): Provides stability during fluctuations
|
|
11
|
+
* - **min(fast, slow)**: Adapts down quickly, up slowly
|
|
12
|
+
*
|
|
13
|
+
* This naturally provides asymmetric behavior needed for good QoE:
|
|
14
|
+
* avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Default bandwidth estimator configuration.
|
|
18
|
+
*
|
|
19
|
+
* Values match Shaka Player defaults based on experimentation.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_BANDWIDTH_CONFIG = {
|
|
22
|
+
fastHalfLife: 2,
|
|
23
|
+
slowHalfLife: 5,
|
|
24
|
+
minTotalBytes: 128e3,
|
|
25
|
+
minBytes: 16e3,
|
|
26
|
+
minDuration: 5
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Add a bandwidth sample from a segment download.
|
|
30
|
+
*
|
|
31
|
+
* Samples are filtered based on:
|
|
32
|
+
* - Minimum bytes (filters TTFB-dominated small segments)
|
|
33
|
+
* - Minimum duration (filters cached responses)
|
|
34
|
+
*
|
|
35
|
+
* Valid samples update both fast and slow EWMA estimates.
|
|
36
|
+
*
|
|
37
|
+
* @param state - Current estimator state
|
|
38
|
+
* @param durationMs - Download duration in milliseconds
|
|
39
|
+
* @param numBytes - Number of bytes downloaded
|
|
40
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
41
|
+
* @returns New estimator state with sample incorporated (or unchanged if filtered)
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
|
|
45
|
+
* // Sample: 1MB in 1 second
|
|
46
|
+
* state = sampleBandwidth(state, 1000, 1_000_000);
|
|
47
|
+
*/
|
|
48
|
+
function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
49
|
+
const updatedBytesSampled = state.bytesSampled + numBytes;
|
|
50
|
+
if (numBytes < config.minBytes) return {
|
|
51
|
+
...state,
|
|
52
|
+
bytesSampled: updatedBytesSampled
|
|
53
|
+
};
|
|
54
|
+
if (durationMs < config.minDuration) return {
|
|
55
|
+
...state,
|
|
56
|
+
bytesSampled: updatedBytesSampled
|
|
57
|
+
};
|
|
58
|
+
const bandwidth = 8e3 * numBytes / durationMs;
|
|
59
|
+
const weight = durationMs / 1e3;
|
|
60
|
+
return {
|
|
61
|
+
fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),
|
|
62
|
+
fastTotalWeight: state.fastTotalWeight + weight,
|
|
63
|
+
slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),
|
|
64
|
+
slowTotalWeight: state.slowTotalWeight + weight,
|
|
65
|
+
bytesSampled: updatedBytesSampled
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the current bandwidth estimate.
|
|
70
|
+
*
|
|
71
|
+
* Returns the **minimum** of the fast and slow EWMA estimates.
|
|
72
|
+
* This provides the key asymmetric behavior:
|
|
73
|
+
* - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
|
|
74
|
+
* - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
|
|
75
|
+
*
|
|
76
|
+
* Uses default estimate until enough data has been sampled.
|
|
77
|
+
*
|
|
78
|
+
* @param state - Current estimator state
|
|
79
|
+
* @param defaultEstimate - Fallback estimate before sufficient samples (bps)
|
|
80
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
81
|
+
* @returns Bandwidth estimate in bits per second
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
|
|
85
|
+
*/
|
|
86
|
+
function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
87
|
+
if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
|
|
88
|
+
const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
|
|
89
|
+
const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
|
|
90
|
+
return Math.min(fastEstimate, slowEstimate);
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { getBandwidthEstimate, sampleBandwidth };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=bandwidth-estimator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../../src/core/abr/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled.\n *\n * @param state - Current estimator state\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;AAKjD,KAAI,WAAW,OAAO,SACpB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;AAMxD,KAAI,aAAa,OAAO,YACtB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;CAKxD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;AAE5B,QAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;EACf;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;AAER,KAAI,MAAM,eAAe,OAAO,cAC9B,QAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;CAEpG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;AAIpG,QAAO,KAAK,IAAI,cAAc,aAAa"}
|