@videojs/spf 10.0.0-beta.26 → 10.0.0-beta.28
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/dist/default/core/signals/when.js +38 -0
- package/dist/default/core/signals/when.js.map +1 -0
- package/dist/default/core/tasks/delayed-reschedule.js +31 -0
- package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/default/core/tasks/task.js +133 -3
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls-audio.js +3 -0
- package/dist/default/hls-background-video.js +3 -0
- package/dist/default/hls-video.js +4 -0
- package/dist/default/hls.js +6 -5
- package/dist/default/media/dom/capabilities.js +2 -0
- package/dist/default/media/dom/capabilities.js.map +1 -1
- package/dist/default/media/errors.js +62 -0
- package/dist/default/media/errors.js.map +1 -0
- package/dist/default/media/hls/parse-media-playlist.js +167 -16
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +14 -3
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/reload-policy.js +71 -0
- package/dist/default/media/hls/reload-policy.js.map +1 -0
- package/dist/default/media/live-window.js +36 -0
- package/dist/default/media/live-window.js.map +1 -0
- package/dist/default/media/primitives/select-tracks.js +1 -24
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/types/index.js +15 -1
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/tracks.js +14 -0
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/mux-audio.js +3 -0
- package/dist/default/mux-background-video.js +3 -0
- package/dist/default/mux-video.js +3 -0
- package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-audio/media.js +9 -0
- package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media.js +11 -0
- package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-audio/media.js +26 -0
- package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/media.js +23 -0
- package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/default/playback/behaviors/collect-errors.js +77 -0
- package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +29 -7
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +78 -4
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +29 -8
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +18 -9
- package/dist/default/playback/behaviors/track-switching.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +9 -5
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/default/playback/engines/hls/engine.js +19 -6
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/error-messages.js +39 -0
- package/dist/default/playback/primitives/error-messages.js.map +1 -0
- package/dist/default/playback/primitives/live-window.js +66 -0
- package/dist/default/playback/primitives/live-window.js.map +1 -0
- package/dist/default/playback/primitives/report-track-conditions.js +86 -0
- package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
- package/dist/dev/core/signals/when.js +38 -0
- package/dist/dev/core/signals/when.js.map +1 -0
- package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
- package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/dev/core/tasks/task.d.ts +52 -1
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js +133 -3
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls-audio.d.ts +3 -0
- package/dist/dev/hls-audio.js +3 -0
- package/dist/dev/hls-background-video.d.ts +3 -0
- package/dist/dev/hls-background-video.js +3 -0
- package/dist/dev/hls-video.d.ts +5 -0
- package/dist/dev/hls-video.js +4 -0
- package/dist/dev/hls.d.ts +6 -5
- package/dist/dev/hls.js +6 -5
- package/dist/dev/media/dom/capabilities.js +2 -0
- package/dist/dev/media/dom/capabilities.js.map +1 -1
- package/dist/dev/media/errors.d.ts +82 -0
- package/dist/dev/media/errors.d.ts.map +1 -0
- package/dist/dev/media/errors.js +62 -0
- package/dist/dev/media/errors.js.map +1 -0
- package/dist/dev/media/hls/parse-media-playlist.js +167 -16
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +14 -3
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/reload-policy.js +71 -0
- package/dist/dev/media/hls/reload-policy.js.map +1 -0
- package/dist/dev/media/live-window.js +36 -0
- package/dist/dev/media/live-window.js.map +1 -0
- package/dist/dev/media/primitives/select-tracks.js +1 -24
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/types/index.d.ts +125 -9
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js +15 -1
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +14 -0
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/mux-audio.d.ts +4 -0
- package/dist/dev/mux-audio.js +3 -0
- package/dist/dev/mux-background-video.d.ts +3 -0
- package/dist/dev/mux-background-video.js +3 -0
- package/dist/dev/mux-video.d.ts +4 -0
- package/dist/dev/mux-video.js +3 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
- package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.js +11 -0
- package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
- package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.js +23 -0
- package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/dev/playback/behaviors/collect-errors.js +77 -0
- package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +29 -7
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +5 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.js +78 -4
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +29 -8
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.js +18 -9
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +34 -17
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.js +9 -5
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +7 -7
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
- package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/dev/playback/engines/hls/engine.d.ts +43 -15
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +19 -6
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/error-messages.js +39 -0
- package/dist/dev/playback/primitives/error-messages.js.map +1 -0
- package/dist/dev/playback/primitives/live-window.js +66 -0
- package/dist/dev/playback/primitives/live-window.js.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
- package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
- package/package.json +32 -6
- package/dist/default/background-video.js +0 -3
- package/dist/default/playback/engines/background-video/adapter.js +0 -158
- package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/default/playback/engines/background-video/engine.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -124
- package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter.js +0 -120
- package/dist/default/playback/engines/hls/adapter.js.map +0 -1
- package/dist/dev/background-video.d.ts +0 -3
- package/dist/dev/background-video.js +0 -3
- package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -59
- package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/adapter.js +0 -158
- package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -46
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -124
- package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.d.ts +0 -42
- package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.js +0 -120
- package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SVTA_UNSUPPORTED_AUDIO_FORMAT, SVTA_UNSUPPORTED_DRM_SYSTEM, SVTA_UNSUPPORTED_VIDEO_FORMAT } from "../../../media/errors.js";
|
|
2
|
+
//#region src/playback/adapters/hls-video/error-surface.ts
|
|
3
|
+
/**
|
|
4
|
+
* `message`, plus the alternative-Media sentence when `media`'s class names one.
|
|
5
|
+
*
|
|
6
|
+
* Read off the class rather than passed in so a subclass can point at a
|
|
7
|
+
* better-equipped sibling — the SPF Mux Medias name the hls.js-backed one, which
|
|
8
|
+
* plays the MPEG-TS and DRM sources SPF doesn't — without either adapter knowing
|
|
9
|
+
* that sibling exists.
|
|
10
|
+
*/
|
|
11
|
+
function withAlternativeMediaSuggestion(message, media) {
|
|
12
|
+
const { alternativeMediaSuggestion } = media.constructor;
|
|
13
|
+
const suggestion = alternativeMediaSuggestion?.trim();
|
|
14
|
+
return suggestion ? `${message} ${suggestion}` : message;
|
|
15
|
+
}
|
|
16
|
+
/** The first condition `fatalCodes` covers — the root cause, not its consequences. */
|
|
17
|
+
function firstFatal(errors, fatalCodes) {
|
|
18
|
+
return errors?.find((error) => fatalCodes.has(error.code));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The causes that mean the engine has no pipeline for what the source delivers —
|
|
22
|
+
* a container it can't append, or encryption it can't decrypt.
|
|
23
|
+
*
|
|
24
|
+
* What these have in common is that no retry, no other CDN, and no other
|
|
25
|
+
* rendition of the same source fixes them: the source needs a capability this
|
|
26
|
+
* engine doesn't have. That's the distinction the surfaced code exists to draw,
|
|
27
|
+
* and it's why the set is these three rather than "every cause".
|
|
28
|
+
*/
|
|
29
|
+
const UNSUPPORTED_FEATURE_CAUSES = /* @__PURE__ */ new Set([
|
|
30
|
+
SVTA_UNSUPPORTED_VIDEO_FORMAT,
|
|
31
|
+
SVTA_UNSUPPORTED_AUDIO_FORMAT,
|
|
32
|
+
SVTA_UNSUPPORTED_DRM_SYSTEM
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Whether anything in the sequence is a cause of the "we don't implement this"
|
|
36
|
+
* kind.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately `some` over the whole sequence rather than a per-type match
|
|
39
|
+
* against the verdict. A verdict means one type's candidates emptied, but the
|
|
40
|
+
* *reason* the source is unplayable can sit on another type — an audio-only
|
|
41
|
+
* source whose sole rendition is encrypted empties the audio candidates, and a
|
|
42
|
+
* video source with encrypted video empties the video ones. Both are the same
|
|
43
|
+
* answer to the viewer, so both get the same code.
|
|
44
|
+
*/
|
|
45
|
+
function hasUnsupportedFeatureCause(errors) {
|
|
46
|
+
return errors?.some((error) => UNSUPPORTED_FEATURE_CAUSES.has(error.code)) ?? false;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { firstFatal, hasUnsupportedFeatureCause, withAlternativeMediaSuggestion };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=error-surface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-surface.js","names":[],"sources":["../../../../../src/playback/adapters/hls-video/error-surface.ts"],"sourcesContent":["/**\n * The shared half of promoting reported conditions onto a media surface.\n *\n * Both HLS adapters do the same things: pick the first condition they treat as\n * fatal, latch it so a later append doesn't re-fire, and name a better-equipped\n * Media when their own class points at one. Only the *policy* differs — which\n * codes are fatal, which the video adapter and the audio-only adapter answer\n * differently — so that stays with each adapter and everything else lives here.\n *\n * See `internal/design/spf/features/errors.md` for the causes-vs-verdicts split\n * this rests on.\n */\nimport type { ErrorLike } from '@videojs/media';\nimport {\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n type SvtaError,\n} from '../../../media/errors';\n\n/**\n * The error shape a media surface exposes: `@videojs/media`'s {@link ErrorLike}\n * plus the reporter context the engine carries alongside a condition.\n *\n * `code` is the **SVTA code**, not a `MediaError.MEDIA_ERR_*` value. Consumers\n * that map codes to copy currently only know 1–5, so an SVTA code falls through\n * to showing `message`; an extensible code lookup above the engine is the\n * follow-up that fixes it.\n */\nexport interface HlsVideoMediaError extends ErrorLike {\n /** Reporter context (which selection emptied, which track, …). */\n readonly data?: unknown;\n}\n\n/**\n * `message`, plus the alternative-Media sentence when `media`'s class names one.\n *\n * Read off the class rather than passed in so a subclass can point at a\n * better-equipped sibling — the SPF Mux Medias name the hls.js-backed one, which\n * plays the MPEG-TS and DRM sources SPF doesn't — without either adapter knowing\n * that sibling exists.\n */\nexport function withAlternativeMediaSuggestion(message: string, media: object): string {\n const { alternativeMediaSuggestion } = media.constructor as { alternativeMediaSuggestion?: string };\n const suggestion = alternativeMediaSuggestion?.trim();\n\n return suggestion ? `${message} ${suggestion}` : message;\n}\n\n/** The first condition `fatalCodes` covers — the root cause, not its consequences. */\nexport function firstFatal(\n errors: readonly SvtaError[] | undefined,\n fatalCodes: ReadonlySet<number>\n): SvtaError | undefined {\n return errors?.find((error) => fatalCodes.has(error.code));\n}\n\n/**\n * The causes that mean the engine has no pipeline for what the source delivers —\n * a container it can't append, or encryption it can't decrypt.\n *\n * What these have in common is that no retry, no other CDN, and no other\n * rendition of the same source fixes them: the source needs a capability this\n * engine doesn't have. That's the distinction the surfaced code exists to draw,\n * and it's why the set is these three rather than \"every cause\".\n */\nconst UNSUPPORTED_FEATURE_CAUSES: ReadonlySet<number> = new Set<number>([\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n]);\n\n/**\n * Whether anything in the sequence is a cause of the \"we don't implement this\"\n * kind.\n *\n * Deliberately `some` over the whole sequence rather than a per-type match\n * against the verdict. A verdict means one type's candidates emptied, but the\n * *reason* the source is unplayable can sit on another type — an audio-only\n * source whose sole rendition is encrypted empties the audio candidates, and a\n * video source with encrypted video empties the video ones. Both are the same\n * answer to the viewer, so both get the same code.\n */\nexport function hasUnsupportedFeatureCause(errors: readonly SvtaError[] | undefined): boolean {\n return errors?.some((error) => UNSUPPORTED_FEATURE_CAUSES.has(error.code)) ?? false;\n}\n"],"mappings":";;;;;;;;;;AA0CA,SAAgB,+BAA+B,SAAiB,OAAuB;CACrF,MAAM,EAAE,+BAA+B,MAAM;CAC7C,MAAM,aAAa,4BAA4B,KAAK;CAEpD,OAAO,aAAa,GAAG,QAAQ,GAAG,eAAe;AACnD;;AAGA,SAAgB,WACd,QACA,YACuB;CACvB,OAAO,QAAQ,MAAM,UAAU,WAAW,IAAI,MAAM,IAAI,CAAC;AAC3D;;;;;;;;;;AAWA,MAAM,6CAAkD,IAAI,IAAY;CACtE;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,SAAgB,2BAA2B,QAAmD;CAC5F,OAAO,QAAQ,MAAM,UAAU,2BAA2B,IAAI,MAAM,IAAI,CAAC,KAAK;AAChF"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { computed, untrack } from "../../../core/signals/primitives.js";
|
|
2
|
+
import { effect } from "../../../core/signals/effect.js";
|
|
3
|
+
import { dedupedAudioTracks, dedupedVideoTracks, findAudioTrackById, findVideoTrackById, frameRateToNumber, isSameAudioTrack, isSameVideoTrack, toUserAudioTrackSelection, toUserVideoTrackSelection } from "../../../media/media-tracks/media-tracks.js";
|
|
4
|
+
//#region src/playback/adapters/hls-video/media-tracks.ts
|
|
5
|
+
const toVideoKey = (rendition) => ({
|
|
6
|
+
width: rendition.width,
|
|
7
|
+
height: rendition.height,
|
|
8
|
+
bandwidth: rendition.bitrate
|
|
9
|
+
});
|
|
10
|
+
const toAudioKey = (track) => ({
|
|
11
|
+
language: track.language,
|
|
12
|
+
name: track.label
|
|
13
|
+
});
|
|
14
|
+
/** Two track lists carry the same set when their id sequences match. */
|
|
15
|
+
const sameIds = (a, b) => a.length === b.length && a.every((item, i) => item.id === b[i].id);
|
|
16
|
+
/**
|
|
17
|
+
* Projects the SPF engine's presentation onto the media element's
|
|
18
|
+
* `videoRenditions` / `audioTracks` lists, and wires user selection back to the
|
|
19
|
+
* engine's `userVideoTrackSelection` / `userAudioTrackSelection` signals.
|
|
20
|
+
*
|
|
21
|
+
* Requires the media-tracks mixin (track-list infrastructure) earlier in the
|
|
22
|
+
* chain so the host exposes `addVideoTrack`, `videoRenditions`, and friends.
|
|
23
|
+
*/
|
|
24
|
+
function HlsVideoMediaMediaTracksMixin(BaseClass) {
|
|
25
|
+
class HlsVideoMediaMediaTracks extends BaseClass {
|
|
26
|
+
#abort = new AbortController();
|
|
27
|
+
#destroyed = false;
|
|
28
|
+
#renditions = [];
|
|
29
|
+
#audioTracks = [];
|
|
30
|
+
constructor(...args) {
|
|
31
|
+
super(...args);
|
|
32
|
+
const { state } = this.engine;
|
|
33
|
+
const { signal } = this.#abort;
|
|
34
|
+
const renditionsSignal = computed(() => dedupedVideoTracks(state.presentation.get()), { equals: sameIds });
|
|
35
|
+
const audioTracksSignal = computed(() => dedupedAudioTracks(state.presentation.get()), { equals: sameIds });
|
|
36
|
+
const reflectRenditions = () => {
|
|
37
|
+
const renditions = renditionsSignal.get();
|
|
38
|
+
this.#renditions = renditions;
|
|
39
|
+
this.#removeVideoTracks();
|
|
40
|
+
if (!renditions.length) return;
|
|
41
|
+
const videoTrack = this.addVideoTrack("main");
|
|
42
|
+
videoTrack.selected = true;
|
|
43
|
+
const resolved = untrack(() => findVideoTrackById(state.presentation.get(), state.selectedVideoTrackId.get()));
|
|
44
|
+
for (const rendition of renditions) {
|
|
45
|
+
const domRendition = videoTrack.addRendition("", rendition.width, rendition.height, rendition.codecs.join(","), rendition.bandwidth, rendition.frameRate ? frameRateToNumber(rendition.frameRate) : void 0);
|
|
46
|
+
domRendition.id = rendition.id;
|
|
47
|
+
domRendition.active = isSameVideoTrack(toVideoKey(domRendition), resolved);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const reflectSelectedVideo = () => {
|
|
51
|
+
const resolved = findVideoTrackById(state.presentation.get(), state.selectedVideoTrackId.get());
|
|
52
|
+
for (const rendition of this.videoRenditions) rendition.active = isSameVideoTrack(toVideoKey(rendition), resolved);
|
|
53
|
+
};
|
|
54
|
+
const reflectAudioTracks = () => {
|
|
55
|
+
const tracks = audioTracksSignal.get();
|
|
56
|
+
this.#audioTracks = tracks;
|
|
57
|
+
this.#removeAudioTracks();
|
|
58
|
+
if (!tracks.length) return;
|
|
59
|
+
const resolved = untrack(() => findAudioTrackById(state.presentation.get(), state.selectedAudioTrackId.get()));
|
|
60
|
+
for (const track of tracks) {
|
|
61
|
+
const domTrack = this.addAudioTrack(track.default ? "main" : "alternative", track.name, track.language ?? "");
|
|
62
|
+
domTrack.id = track.id;
|
|
63
|
+
domTrack.enabled = isSameAudioTrack(toAudioKey(domTrack), resolved);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const reflectSelectedAudio = () => {
|
|
67
|
+
const resolved = findAudioTrackById(state.presentation.get(), state.selectedAudioTrackId.get());
|
|
68
|
+
for (const track of this.audioTracks) track.enabled = isSameAudioTrack(toAudioKey(track), resolved);
|
|
69
|
+
};
|
|
70
|
+
const sourceUrl = computed(() => state.presentation.get()?.url);
|
|
71
|
+
const resetSelectionOnSourceChange = () => {
|
|
72
|
+
sourceUrl.get();
|
|
73
|
+
state.userVideoTrackSelection.set(void 0);
|
|
74
|
+
state.userAudioTrackSelection.set(void 0);
|
|
75
|
+
};
|
|
76
|
+
const effectCleanups = [
|
|
77
|
+
effect(reflectRenditions),
|
|
78
|
+
effect(reflectSelectedVideo),
|
|
79
|
+
effect(reflectAudioTracks),
|
|
80
|
+
effect(reflectSelectedAudio),
|
|
81
|
+
effect(resetSelectionOnSourceChange)
|
|
82
|
+
];
|
|
83
|
+
this.videoRenditions.addEventListener("change", this.#selectRendition, { signal });
|
|
84
|
+
this.audioTracks.addEventListener("change", this.#selectAudio, { signal });
|
|
85
|
+
signal.addEventListener("abort", () => effectCleanups.forEach((cleanup) => cleanup()), { once: true });
|
|
86
|
+
}
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this.#destroyed) return;
|
|
89
|
+
this.#destroyed = true;
|
|
90
|
+
this.#abort.abort();
|
|
91
|
+
this.#removeVideoTracks();
|
|
92
|
+
this.#removeAudioTracks();
|
|
93
|
+
super.destroy?.();
|
|
94
|
+
}
|
|
95
|
+
#selectRendition = () => {
|
|
96
|
+
const { userVideoTrackSelection } = this.engine.state;
|
|
97
|
+
const index = this.videoRenditions.selectedIndex;
|
|
98
|
+
const domRendition = index < 0 ? void 0 : this.videoRenditions[index];
|
|
99
|
+
const rendition = this.#renditions.find((candidate) => candidate.id === domRendition?.id);
|
|
100
|
+
userVideoTrackSelection.set(toUserVideoTrackSelection(rendition));
|
|
101
|
+
};
|
|
102
|
+
#selectAudio = () => {
|
|
103
|
+
const { presentation, selectedAudioTrackId, userAudioTrackSelection } = this.engine.state;
|
|
104
|
+
const resolved = findAudioTrackById(presentation.get(), selectedAudioTrackId.get());
|
|
105
|
+
const current = [...this.audioTracks].find((track) => isSameAudioTrack(toAudioKey(track), resolved));
|
|
106
|
+
const enabled = [...this.audioTracks].filter((track) => track.enabled);
|
|
107
|
+
const target = enabled.find((track) => track !== current) ?? enabled[0];
|
|
108
|
+
if (!target) return;
|
|
109
|
+
for (const track of enabled) if (track !== target) track.enabled = false;
|
|
110
|
+
if (target === current) return;
|
|
111
|
+
const audioTrack = this.#audioTracks.find((candidate) => candidate.id === target.id);
|
|
112
|
+
userAudioTrackSelection.set(toUserAudioTrackSelection(audioTrack));
|
|
113
|
+
};
|
|
114
|
+
#removeVideoTracks() {
|
|
115
|
+
for (const videoTrack of [...this.videoTracks]) this.removeVideoTrack(videoTrack);
|
|
116
|
+
}
|
|
117
|
+
#removeAudioTracks() {
|
|
118
|
+
for (const audioTrack of [...this.audioTracks]) this.removeAudioTrack(audioTrack);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return HlsVideoMediaMediaTracks;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { HlsVideoMediaMediaTracksMixin };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=media-tracks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-tracks.js","names":["#abort","#renditions","#removeVideoTracks","#audioTracks","#removeAudioTracks","#selectRendition","#selectAudio","#destroyed"],"sources":["../../../../../src/playback/adapters/hls-video/media-tracks.ts"],"sourcesContent":["import type {\n AudioTrackLike,\n MediaAudioTrackCapability,\n MediaVideoRenditionCapability,\n MediaVideoTrackCapability,\n VideoRenditionLike,\n} from '@videojs/media';\nimport type { Constructor } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, untrack } from '../../../core/signals/primitives';\nimport {\n type AudioTrack,\n dedupedAudioTracks,\n dedupedVideoTracks,\n findAudioTrackById,\n findVideoTrackById,\n frameRateToNumber,\n isSameAudioTrack,\n isSameVideoTrack,\n toUserAudioTrackSelection,\n toUserVideoTrackSelection,\n type VideoTrack,\n} from '../../../media/media-tracks';\nimport type { HlsVideoEngineContext, HlsVideoEngineState } from '../../engines/hls/engine';\n\n// Translate a DOM rendition/track into the SPF dedupe-key shape\nconst toVideoKey = (rendition: VideoRenditionLike) => ({\n width: rendition.width,\n height: rendition.height,\n bandwidth: rendition.bitrate!,\n});\n\nconst toAudioKey = (track: AudioTrackLike) => ({ language: track.language, name: track.label });\n\ntype HlsVideoEngineHost = {\n readonly engine: Composition<HlsVideoEngineState, HlsVideoEngineContext>;\n destroy?(): void;\n};\n\ntype MediaTracksHost = HlsVideoEngineHost &\n MediaVideoTrackCapability &\n MediaAudioTrackCapability &\n MediaVideoRenditionCapability;\n\n/** Two track lists carry the same set when their id sequences match. */\nconst sameIds = (a: { id: string }[], b: { id: string }[]): boolean =>\n a.length === b.length && a.every((item, i) => item.id === b[i]!.id);\n\n/**\n * Projects the SPF engine's presentation onto the media element's\n * `videoRenditions` / `audioTracks` lists, and wires user selection back to the\n * engine's `userVideoTrackSelection` / `userAudioTrackSelection` signals.\n *\n * Requires the media-tracks mixin (track-list infrastructure) earlier in the\n * chain so the host exposes `addVideoTrack`, `videoRenditions`, and friends.\n */\nexport function HlsVideoMediaMediaTracksMixin<Base extends Constructor<MediaTracksHost>>(BaseClass: Base) {\n class HlsVideoMediaMediaTracks extends (BaseClass as Constructor<MediaTracksHost>) {\n #abort = new AbortController();\n #destroyed = false;\n // Memoized SPF model — the last `computed` result per type — so a DOM\n // selection maps back to the engine's match criteria by id via the SPF\n // selection helpers (the DOM lists carry only DOM-shape props).\n #renditions: VideoTrack[] = [];\n #audioTracks: AudioTrack[] = [];\n\n constructor(...args: any[]) {\n super(...args);\n\n const { state } = this.engine;\n const { signal } = this.#abort;\n\n const renditionsSignal = computed(() => dedupedVideoTracks(state.presentation.get()), { equals: sameIds });\n const audioTracksSignal = computed(() => dedupedAudioTracks(state.presentation.get()), { equals: sameIds });\n\n // Rebuild video renditions when the set changes; seed `active` from the\n // current resolved selection (read untracked — `active` deltas are the\n // reflection effect's job).\n const reflectRenditions = () => {\n const renditions = renditionsSignal.get();\n this.#renditions = renditions;\n this.#removeVideoTracks();\n if (!renditions.length) return;\n\n // \"video track\" here is the Media UI Extensions concept, which maps onto\n // the SPF model differently than the name suggests:\n // MUE track -> SPF selection set\n // MUE rendition -> SPF track\n // @see https://github.com/video-dev/media-ui-extensions/blob/main/proposals/0011-renditions-list.md\n //\n // Every rendition hangs off a single \"main\" video track. Multi-track\n // video (multiple camera angles, CMAF multi-track) isn't supported yet,\n // so exactly one video track is always exposed; when it lands, this\n // becomes one track (selection set) per angle.\n const videoTrack = this.addVideoTrack('main');\n videoTrack.selected = true;\n\n const resolved = untrack(() => findVideoTrackById(state.presentation.get(), state.selectedVideoTrackId.get()));\n for (const rendition of renditions) {\n const domRendition = videoTrack.addRendition(\n '',\n rendition.width,\n rendition.height,\n rendition.codecs.join(','),\n rendition.bandwidth,\n rendition.frameRate ? frameRateToNumber(rendition.frameRate) : undefined\n );\n domRendition.id = rendition.id;\n domRendition.active = isSameVideoTrack(toVideoKey(domRendition), resolved);\n }\n };\n\n const reflectSelectedVideo = () => {\n const resolved = findVideoTrackById(state.presentation.get(), state.selectedVideoTrackId.get());\n for (const rendition of this.videoRenditions) {\n rendition.active = isSameVideoTrack(toVideoKey(rendition), resolved);\n }\n };\n\n // Rebuild audio tracks when the set changes; seed `enabled` from the\n // current resolved selection.\n const reflectAudioTracks = () => {\n const tracks = audioTracksSignal.get();\n this.#audioTracks = tracks;\n this.#removeAudioTracks();\n if (!tracks.length) return;\n\n const resolved = untrack(() => findAudioTrackById(state.presentation.get(), state.selectedAudioTrackId.get()));\n for (const track of tracks) {\n const domTrack = this.addAudioTrack(track.default ? 'main' : 'alternative', track.name, track.language ?? '');\n domTrack.id = track.id;\n domTrack.enabled = isSameAudioTrack(toAudioKey(domTrack), resolved);\n }\n };\n\n const reflectSelectedAudio = () => {\n const resolved = findAudioTrackById(state.presentation.get(), state.selectedAudioTrackId.get());\n for (const track of this.audioTracks) {\n track.enabled = isSameAudioTrack(toAudioKey(track), resolved);\n }\n };\n\n // A source change (new manifest URL) invalidates any pinned selection:\n // rendition identity (width/height/bandwidth) and audio identity\n // (language/name) don't reliably carry across sources, so drop both pins\n // and let the engine re-pick for the new source.\n const sourceUrl = computed(() => state.presentation.get()?.url);\n const resetSelectionOnSourceChange = () => {\n sourceUrl.get();\n state.userVideoTrackSelection.set(undefined);\n state.userAudioTrackSelection.set(undefined);\n };\n\n const effectCleanups = [\n effect(reflectRenditions),\n effect(reflectSelectedVideo),\n effect(reflectAudioTracks),\n effect(reflectSelectedAudio),\n effect(resetSelectionOnSourceChange),\n ];\n\n this.videoRenditions.addEventListener('change', this.#selectRendition, { signal });\n this.audioTracks.addEventListener('change', this.#selectAudio, { signal });\n signal.addEventListener('abort', () => effectCleanups.forEach((cleanup) => cleanup()), { once: true });\n }\n\n destroy(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n this.#abort.abort();\n this.#removeVideoTracks();\n this.#removeAudioTracks();\n super.destroy?.();\n }\n\n // Manual quality pin → the chosen rendition's width/height/bandwidth as the\n // match criteria (the properties renditions were deduped on).\n // `selectedIndex === -1` (Auto) clears the pin, resuming ABR.\n #selectRendition = () => {\n const { userVideoTrackSelection } = this.engine.state;\n const index = this.videoRenditions.selectedIndex;\n const domRendition = index < 0 ? undefined : this.videoRenditions[index];\n const rendition = this.#renditions.find((candidate) => candidate.id === domRendition?.id);\n userVideoTrackSelection.set(toUserVideoTrackSelection(rendition));\n };\n\n #selectAudio = () => {\n const { presentation, selectedAudioTrackId, userAudioTrackSelection } = this.engine.state;\n const resolved = findAudioTrackById(presentation.get(), selectedAudioTrackId.get());\n const current = [...this.audioTracks].find((track) => isSameAudioTrack(toAudioKey(track), resolved));\n\n // `enabled` is not exclusive like video `selected`, so prefer a newly\n // enabled track over the one that is already playing.\n const enabled = [...this.audioTracks].filter((track) => track.enabled);\n const target = enabled.find((track) => track !== current) ?? enabled[0];\n if (!target) return;\n\n // Disable the rest so future change events resolve unambiguously.\n for (const track of enabled) {\n if (track !== target) track.enabled = false;\n }\n // Skip the write when the enabled track is already the resolved one\n // that's this projection's own reflection (or a no-op re-affirm), not a user switch.\n if (target === current) return;\n\n const audioTrack = this.#audioTracks.find((candidate) => candidate.id === target.id);\n userAudioTrackSelection.set(toUserAudioTrackSelection(audioTrack));\n };\n\n #removeVideoTracks() {\n for (const videoTrack of [...this.videoTracks]) this.removeVideoTrack(videoTrack);\n }\n\n #removeAudioTracks() {\n for (const audioTrack of [...this.audioTracks]) this.removeAudioTrack(audioTrack);\n }\n }\n\n return HlsVideoMediaMediaTracks as unknown as Base;\n}\n"],"mappings":";;;;AA2BA,MAAM,cAAc,eAAmC;CACrD,OAAO,UAAU;CACjB,QAAQ,UAAU;CAClB,WAAW,UAAU;AACvB;AAEA,MAAM,cAAc,WAA2B;CAAE,UAAU,MAAM;CAAU,MAAM,MAAM;AAAM;;AAa7F,MAAM,WAAW,GAAqB,MACpC,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,MAAM,MAAM,KAAK,OAAO,EAAE,EAAE,CAAE,EAAE;;;;;;;;;AAUpE,SAAgB,8BAAyE,WAAiB;CACxG,MAAM,iCAAkC,UAA2C;EACjF,SAAS,IAAI,gBAAgB;EAC7B,aAAa;EAIb,cAA4B,CAAC;EAC7B,eAA6B,CAAC;EAE9B,YAAY,GAAG,MAAa;GAC1B,MAAM,GAAG,IAAI;GAEb,MAAM,EAAE,UAAU,KAAK;GACvB,MAAM,EAAE,WAAW,KAAKA;GAExB,MAAM,mBAAmB,eAAe,mBAAmB,MAAM,aAAa,IAAI,CAAC,GAAG,EAAE,QAAQ,QAAQ,CAAC;GACzG,MAAM,oBAAoB,eAAe,mBAAmB,MAAM,aAAa,IAAI,CAAC,GAAG,EAAE,QAAQ,QAAQ,CAAC;GAK1G,MAAM,0BAA0B;IAC9B,MAAM,aAAa,iBAAiB,IAAI;IACxC,KAAKC,cAAc;IACnB,KAAKC,mBAAmB;IACxB,IAAI,CAAC,WAAW,QAAQ;IAYxB,MAAM,aAAa,KAAK,cAAc,MAAM;IAC5C,WAAW,WAAW;IAEtB,MAAM,WAAW,cAAc,mBAAmB,MAAM,aAAa,IAAI,GAAG,MAAM,qBAAqB,IAAI,CAAC,CAAC;IAC7G,KAAK,MAAM,aAAa,YAAY;KAClC,MAAM,eAAe,WAAW,aAC9B,IACA,UAAU,OACV,UAAU,QACV,UAAU,OAAO,KAAK,GAAG,GACzB,UAAU,WACV,UAAU,YAAY,kBAAkB,UAAU,SAAS,IAAI,KAAA,CACjE;KACA,aAAa,KAAK,UAAU;KAC5B,aAAa,SAAS,iBAAiB,WAAW,YAAY,GAAG,QAAQ;IAC3E;GACF;GAEA,MAAM,6BAA6B;IACjC,MAAM,WAAW,mBAAmB,MAAM,aAAa,IAAI,GAAG,MAAM,qBAAqB,IAAI,CAAC;IAC9F,KAAK,MAAM,aAAa,KAAK,iBAC3B,UAAU,SAAS,iBAAiB,WAAW,SAAS,GAAG,QAAQ;GAEvE;GAIA,MAAM,2BAA2B;IAC/B,MAAM,SAAS,kBAAkB,IAAI;IACrC,KAAKC,eAAe;IACpB,KAAKC,mBAAmB;IACxB,IAAI,CAAC,OAAO,QAAQ;IAEpB,MAAM,WAAW,cAAc,mBAAmB,MAAM,aAAa,IAAI,GAAG,MAAM,qBAAqB,IAAI,CAAC,CAAC;IAC7G,KAAK,MAAM,SAAS,QAAQ;KAC1B,MAAM,WAAW,KAAK,cAAc,MAAM,UAAU,SAAS,eAAe,MAAM,MAAM,MAAM,YAAY,EAAE;KAC5G,SAAS,KAAK,MAAM;KACpB,SAAS,UAAU,iBAAiB,WAAW,QAAQ,GAAG,QAAQ;IACpE;GACF;GAEA,MAAM,6BAA6B;IACjC,MAAM,WAAW,mBAAmB,MAAM,aAAa,IAAI,GAAG,MAAM,qBAAqB,IAAI,CAAC;IAC9F,KAAK,MAAM,SAAS,KAAK,aACvB,MAAM,UAAU,iBAAiB,WAAW,KAAK,GAAG,QAAQ;GAEhE;GAMA,MAAM,YAAY,eAAe,MAAM,aAAa,IAAI,CAAC,EAAE,GAAG;GAC9D,MAAM,qCAAqC;IACzC,UAAU,IAAI;IACd,MAAM,wBAAwB,IAAI,KAAA,CAAS;IAC3C,MAAM,wBAAwB,IAAI,KAAA,CAAS;GAC7C;GAEA,MAAM,iBAAiB;IACrB,OAAO,iBAAiB;IACxB,OAAO,oBAAoB;IAC3B,OAAO,kBAAkB;IACzB,OAAO,oBAAoB;IAC3B,OAAO,4BAA4B;GACrC;GAEA,KAAK,gBAAgB,iBAAiB,UAAU,KAAKC,kBAAkB,EAAE,OAAO,CAAC;GACjF,KAAK,YAAY,iBAAiB,UAAU,KAAKC,cAAc,EAAE,OAAO,CAAC;GACzE,OAAO,iBAAiB,eAAe,eAAe,SAAS,YAAY,QAAQ,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC;EACvG;EAEA,UAAgB;GACd,IAAI,KAAKC,YAAY;GACrB,KAAKA,aAAa;GAClB,KAAKP,OAAO,MAAM;GAClB,KAAKE,mBAAmB;GACxB,KAAKE,mBAAmB;GACxB,MAAM,UAAU;EAClB;EAKA,yBAAyB;GACvB,MAAM,EAAE,4BAA4B,KAAK,OAAO;GAChD,MAAM,QAAQ,KAAK,gBAAgB;GACnC,MAAM,eAAe,QAAQ,IAAI,KAAA,IAAY,KAAK,gBAAgB;GAClE,MAAM,YAAY,KAAKH,YAAY,MAAM,cAAc,UAAU,OAAO,cAAc,EAAE;GACxF,wBAAwB,IAAI,0BAA0B,SAAS,CAAC;EAClE;EAEA,qBAAqB;GACnB,MAAM,EAAE,cAAc,sBAAsB,4BAA4B,KAAK,OAAO;GACpF,MAAM,WAAW,mBAAmB,aAAa,IAAI,GAAG,qBAAqB,IAAI,CAAC;GAClF,MAAM,UAAU,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,MAAM,UAAU,iBAAiB,WAAW,KAAK,GAAG,QAAQ,CAAC;GAInG,MAAM,UAAU,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,QAAQ,UAAU,MAAM,OAAO;GACrE,MAAM,SAAS,QAAQ,MAAM,UAAU,UAAU,OAAO,KAAK,QAAQ;GACrE,IAAI,CAAC,QAAQ;GAGb,KAAK,MAAM,SAAS,SAClB,IAAI,UAAU,QAAQ,MAAM,UAAU;GAIxC,IAAI,WAAW,SAAS;GAExB,MAAM,aAAa,KAAKE,aAAa,MAAM,cAAc,UAAU,OAAO,OAAO,EAAE;GACnF,wBAAwB,IAAI,0BAA0B,UAAU,CAAC;EACnE;EAEA,qBAAqB;GACnB,KAAK,MAAM,cAAc,CAAC,GAAG,KAAK,WAAW,GAAG,KAAK,iBAAiB,UAAU;EAClF;EAEA,qBAAqB;GACnB,KAAK,MAAM,cAAc,CAAC,GAAG,KAAK,WAAW,GAAG,KAAK,iBAAiB,UAAU;EAClF;CACF;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HlsVideoMediaMixin } from "./adapter.js";
|
|
2
|
+
import { HlsVideoMediaMediaTracksMixin } from "./media-tracks.js";
|
|
3
|
+
import { HTMLVideoElementHost } from "@videojs/media/dom/video-host";
|
|
4
|
+
import { MediaTracksMixin } from "@videojs/media/media-tracks";
|
|
5
|
+
//#region src/playback/adapters/hls-video/media.ts
|
|
6
|
+
const HlsVideoMediaBase = HlsVideoMediaMediaTracksMixin(MediaTracksMixin(HlsVideoMediaMixin(HTMLVideoElementHost)));
|
|
7
|
+
var HlsVideoMedia = class extends HlsVideoMediaBase {};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { HlsVideoMedia };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","names":[],"sources":["../../../../../src/playback/adapters/hls-video/media.ts"],"sourcesContent":["import { HTMLVideoElementHost } from '@videojs/media/dom/video-host';\nimport { MediaTracksMixin } from '@videojs/media/media-tracks';\nimport { HlsVideoMediaMixin } from './adapter';\nimport { HlsVideoMediaMediaTracksMixin } from './media-tracks';\n\nconst HlsVideoMediaBase = HlsVideoMediaMediaTracksMixin(MediaTracksMixin(HlsVideoMediaMixin(HTMLVideoElementHost)));\n\nexport class HlsVideoMedia extends HlsVideoMediaBase {}\n"],"mappings":";;;;;AAKA,MAAM,oBAAoB,8BAA8B,iBAAiB,mBAAmB,oBAAoB,CAAC,CAAC;AAElH,IAAa,gBAAb,cAAmC,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HlsAudioMedia } from "../hls-audio/media.js";
|
|
2
|
+
import { MuxMediaMixin } from "../mux-video/adapter.js";
|
|
3
|
+
//#region src/playback/adapters/mux-audio/media.ts
|
|
4
|
+
const MuxAudioMediaBase = MuxMediaMixin(HlsAudioMedia);
|
|
5
|
+
/**
|
|
6
|
+
* The Mux Media over the SPF audio-only HLS engine.
|
|
7
|
+
*
|
|
8
|
+
* Same Mux surface as the video flavor — `src`, the structured `source`, and the
|
|
9
|
+
* derived `contentData` all come from the shared mixin — over the subtractive
|
|
10
|
+
* engine, so only the audio renditions of whatever the playback ID names are
|
|
11
|
+
* fetched. That is the one place this diverges from the hls.js-backed
|
|
12
|
+
* `<mux-audio>`, which runs the full engine and downloads video renditions it
|
|
13
|
+
* never shows.
|
|
14
|
+
*
|
|
15
|
+
* `contentData` is kept rather than dropped, for the same reason its hls.js
|
|
16
|
+
* counterpart has it: a playback ID played as audio is usually a *video* asset,
|
|
17
|
+
* whose poster and storyboard exist and which an audio skin may well want. The
|
|
18
|
+
* element ignores it either way. Mux publishes neither for a genuinely
|
|
19
|
+
* audio-only asset, so those URLs 404 — see the known shortcoming on the video
|
|
20
|
+
* flavor, which shares the derivation.
|
|
21
|
+
*/
|
|
22
|
+
var MuxAudioMedia = class extends MuxAudioMediaBase {};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MuxAudioMedia };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","names":[],"sources":["../../../../../src/playback/adapters/mux-audio/media.ts"],"sourcesContent":["// Deliberately the mixin's own module, not `../mux-video`: that barrel reaches\n// `MuxVideoMedia`, and through it `HlsVideoMedia` and the full HLS engine. The\n// mixin alone carries no engine, which is what keeps this entry point\n// audio-only. Same shape as `hls-audio` importing its error surface\n// from `../hls-video`.\n\nimport { HlsAudioMedia } from '../hls-audio/media';\nimport { MuxMediaMixin } from '../mux-video/adapter';\n\nconst MuxAudioMediaBase = MuxMediaMixin(HlsAudioMedia);\n\n/**\n * The Mux Media over the SPF audio-only HLS engine.\n *\n * Same Mux surface as the video flavor — `src`, the structured `source`, and the\n * derived `contentData` all come from the shared mixin — over the subtractive\n * engine, so only the audio renditions of whatever the playback ID names are\n * fetched. That is the one place this diverges from the hls.js-backed\n * `<mux-audio>`, which runs the full engine and downloads video renditions it\n * never shows.\n *\n * `contentData` is kept rather than dropped, for the same reason its hls.js\n * counterpart has it: a playback ID played as audio is usually a *video* asset,\n * whose poster and storyboard exist and which an audio skin may well want. The\n * element ignores it either way. Mux publishes neither for a genuinely\n * audio-only asset, so those URLs 404 — see the known shortcoming on the video\n * flavor, which shares the derivation.\n */\nexport class MuxAudioMedia extends MuxAudioMediaBase {}\n"],"mappings":";;;AASA,MAAM,oBAAoB,cAAc,aAAa;;;;;;;;;;;;;;;;;;AAmBrD,IAAa,gBAAb,cAAmC,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createMuxPosterURL, createMuxStoryboardURL, createMuxVideoURL, parseMuxVideoURL } from "@videojs/media/dom/mux/source";
|
|
2
|
+
//#region src/playback/adapters/mux-video/adapter.ts
|
|
3
|
+
const muxMediaDefaultProps = {
|
|
4
|
+
src: "",
|
|
5
|
+
source: null
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Mux identity over any SPF Media: the structured `source`, the `src` derived
|
|
9
|
+
* from it, and the image URLs it describes.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is Mux identity, so it carries no engine and both flavors get
|
|
12
|
+
* it unchanged — the video Media over the full HLS engine, the audio-only Media
|
|
13
|
+
* over the subtractive one. A mixin rather than a shared base class because each
|
|
14
|
+
* flavor extends a different SPF Media, so there is no common class to put this
|
|
15
|
+
* on, only a common `src` accessor to write through.
|
|
16
|
+
*
|
|
17
|
+
* Unlike the hls.js-backed `MuxMedia`, there is no inherited `source` to
|
|
18
|
+
* delegate to — the SPF Medias know only `src` — so this owns the structured
|
|
19
|
+
* source and dispatches `sourcechange` itself.
|
|
20
|
+
*
|
|
21
|
+
* @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.
|
|
22
|
+
*/
|
|
23
|
+
function MuxMediaMixin(BaseClass) {
|
|
24
|
+
class MuxMediaImpl extends BaseClass {
|
|
25
|
+
/**
|
|
26
|
+
* Named on the error copy when this engine can't play a source: the
|
|
27
|
+
* hls.js-backed Mux Media plays the MPEG-TS and DRM-protected sources that
|
|
28
|
+
* SPF does not, and it backs both `<mux-video>` and `<mux-audio>`.
|
|
29
|
+
*
|
|
30
|
+
* Names the flavor rather than an import path, because one Media is reached
|
|
31
|
+
* through three of them — `@videojs/html`, `@videojs/react`, and this package
|
|
32
|
+
* — and each has a different counterpart. The flavor suffix is the one thing
|
|
33
|
+
* common to the layers a consumer imports elements and components from.
|
|
34
|
+
*/
|
|
35
|
+
static get alternativeMediaSuggestion() {
|
|
36
|
+
return "Try the hls.js-backed Mux media instead: import the `hls-js` flavor in place of the `spf` one.";
|
|
37
|
+
}
|
|
38
|
+
#source = muxMediaDefaultProps.source;
|
|
39
|
+
/**
|
|
40
|
+
* Media source URL. Setting a Mux stream URL
|
|
41
|
+
* (`https://stream.mux.com/<playback-id>.m3u8?...`) extracts the playback ID
|
|
42
|
+
* and query params into `source`; other URLs are kept as a plain `source.src`.
|
|
43
|
+
*
|
|
44
|
+
* Only playback options carry over. Mux identity comes from the URL, and the
|
|
45
|
+
* signed `poster`, `storyboard`, and `drm` tokens are scoped to a playback ID,
|
|
46
|
+
* so carrying them onto a different source would build rejected URLs.
|
|
47
|
+
*/
|
|
48
|
+
get src() {
|
|
49
|
+
return super.src;
|
|
50
|
+
}
|
|
51
|
+
set src(value) {
|
|
52
|
+
if (super.src === value) return;
|
|
53
|
+
this.source = parseMuxVideoURL(value) ?? (value ? { src: value } : null);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Structured Mux source. Setting it derives `src` from the playback ID, custom
|
|
57
|
+
* domain, and `playback` params (appended as `snake_case` query params). A
|
|
58
|
+
* `playback.token` replaces all other params — signed URLs bake them into the
|
|
59
|
+
* token.
|
|
60
|
+
*/
|
|
61
|
+
get source() {
|
|
62
|
+
return this.#source;
|
|
63
|
+
}
|
|
64
|
+
set source(value) {
|
|
65
|
+
const source = value ?? null;
|
|
66
|
+
if (source === this.#source) return;
|
|
67
|
+
this.#source = source;
|
|
68
|
+
super.src = source && (createMuxVideoURL(source) ?? source.src) || "";
|
|
69
|
+
this.dispatchEvent?.(new Event("sourcechange"));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Image URLs `source` describes rather than plays: `poster` from its `poster`
|
|
73
|
+
* params, `storyboard` from its `storyboard` params.
|
|
74
|
+
*
|
|
75
|
+
* Read-only and re-derived on read, so read it again after `sourcechange`.
|
|
76
|
+
* Nothing here is applied for you.
|
|
77
|
+
*/
|
|
78
|
+
get contentData() {
|
|
79
|
+
const poster = createMuxPosterURL(this.#source);
|
|
80
|
+
const storyboard = createMuxStoryboardURL(this.#source);
|
|
81
|
+
return {
|
|
82
|
+
...poster && { poster },
|
|
83
|
+
...storyboard && { storyboard }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return MuxMediaImpl;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { MuxMediaMixin, muxMediaDefaultProps };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","names":["#source"],"sources":["../../../../../src/playback/adapters/mux-video/adapter.ts"],"sourcesContent":["import {\n createMuxPosterURL,\n createMuxStoryboardURL,\n createMuxVideoURL,\n type MuxContentData,\n type MuxSourceBase,\n parseMuxVideoURL,\n} from '@videojs/media/dom/mux/source';\nimport type { Constructor, MixinReturn } from '@videojs/utils/types';\n\nexport interface MuxMediaProps {\n src: string;\n source: MuxSourceBase | null;\n}\n\nexport const muxMediaDefaultProps: MuxMediaProps = {\n src: '',\n source: null,\n};\n\nexport interface MuxMediaAPI extends MuxMediaProps {\n readonly contentData: MuxContentData;\n}\n\n/**\n * Mux identity over any SPF Media: the structured `source`, the `src` derived\n * from it, and the image URLs it describes.\n *\n * Everything here is Mux identity, so it carries no engine and both flavors get\n * it unchanged — the video Media over the full HLS engine, the audio-only Media\n * over the subtractive one. A mixin rather than a shared base class because each\n * flavor extends a different SPF Media, so there is no common class to put this\n * on, only a common `src` accessor to write through.\n *\n * Unlike the hls.js-backed `MuxMedia`, there is no inherited `source` to\n * delegate to — the SPF Medias know only `src` — so this owns the structured\n * source and dispatches `sourcechange` itself.\n *\n * @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.\n */\nexport function MuxMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class MuxMediaImpl extends BaseClass {\n /**\n * Named on the error copy when this engine can't play a source: the\n * hls.js-backed Mux Media plays the MPEG-TS and DRM-protected sources that\n * SPF does not, and it backs both `<mux-video>` and `<mux-audio>`.\n *\n * Names the flavor rather than an import path, because one Media is reached\n * through three of them — `@videojs/html`, `@videojs/react`, and this package\n * — and each has a different counterpart. The flavor suffix is the one thing\n * common to the layers a consumer imports elements and components from.\n */\n static get alternativeMediaSuggestion(): string | undefined {\n return 'Try the hls.js-backed Mux media instead: import the `hls-js` flavor in place of the `spf` one.';\n }\n\n #source: MuxSourceBase | null = muxMediaDefaultProps.source;\n\n /**\n * Media source URL. Setting a Mux stream URL\n * (`https://stream.mux.com/<playback-id>.m3u8?...`) extracts the playback ID\n * and query params into `source`; other URLs are kept as a plain `source.src`.\n *\n * Only playback options carry over. Mux identity comes from the URL, and the\n * signed `poster`, `storyboard`, and `drm` tokens are scoped to a playback ID,\n * so carrying them onto a different source would build rejected URLs.\n */\n get src(): string {\n return super.src;\n }\n\n set src(value: string) {\n // A URL already describing the current source leaves it alone. The elements\n // reflect the derived `src` back to the host, and re-deriving would drop the\n // params a Mux URL does not carry, such as `poster`.\n if (super.src === value) return;\n\n this.source = parseMuxVideoURL(value) ?? (value ? { src: value } : null);\n }\n\n /**\n * Structured Mux source. Setting it derives `src` from the playback ID, custom\n * domain, and `playback` params (appended as `snake_case` query params). A\n * `playback.token` replaces all other params — signed URLs bake them into the\n * token.\n */\n get source(): MuxSourceBase | null {\n return this.#source;\n }\n\n set source(value: MuxSourceBase | null) {\n const source = value ?? null;\n // Changing anything takes a new object, so handing the same one back costs\n // nothing.\n if (source === this.#source) return;\n\n this.#source = source;\n super.src = (source && (createMuxVideoURL(source) ?? source.src)) || '';\n\n this.dispatchEvent?.(new Event('sourcechange'));\n }\n\n /**\n * Image URLs `source` describes rather than plays: `poster` from its `poster`\n * params, `storyboard` from its `storyboard` params.\n *\n * Read-only and re-derived on read, so read it again after `sourcechange`.\n * Nothing here is applied for you.\n */\n get contentData(): MuxContentData {\n const poster = createMuxPosterURL(this.#source);\n const storyboard = createMuxStoryboardURL(this.#source);\n\n return {\n ...(poster && { poster }),\n ...(storyboard && { storyboard }),\n };\n }\n }\n\n // `MixinReturn` sources statics from `Base`, so this mixin's own needs adding\n // back to the type or callers can't read it.\n return MuxMediaImpl as unknown as MixinReturn<Base, MuxMediaAPI> & {\n readonly alternativeMediaSuggestion: string | undefined;\n };\n}\n"],"mappings":";;AAeA,MAAa,uBAAsC;CACjD,KAAK;CACL,QAAQ;AACV;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;;;;;;;;;;;EAWnC,WAAW,6BAAiD;GAC1D,OAAO;EACT;EAEA,UAAgC,qBAAqB;;;;;;;;;;EAWrD,IAAI,MAAc;GAChB,OAAO,MAAM;EACf;EAEA,IAAI,IAAI,OAAe;GAIrB,IAAI,MAAM,QAAQ,OAAO;GAEzB,KAAK,SAAS,iBAAiB,KAAK,MAAM,QAAQ,EAAE,KAAK,MAAM,IAAI;EACrE;;;;;;;EAQA,IAAI,SAA+B;GACjC,OAAO,KAAKA;EACd;EAEA,IAAI,OAAO,OAA6B;GACtC,MAAM,SAAS,SAAS;GAGxB,IAAI,WAAW,KAAKA,SAAS;GAE7B,KAAKA,UAAU;GACf,MAAM,MAAO,WAAW,kBAAkB,MAAM,KAAK,OAAO,QAAS;GAErE,KAAK,gBAAgB,IAAI,MAAM,cAAc,CAAC;EAChD;;;;;;;;EASA,IAAI,cAA8B;GAChC,MAAM,SAAS,mBAAmB,KAAKA,OAAO;GAC9C,MAAM,aAAa,uBAAuB,KAAKA,OAAO;GAEtD,OAAO;IACL,GAAI,UAAU,EAAE,OAAO;IACvB,GAAI,cAAc,EAAE,WAAW;GACjC;EACF;CACF;CAIA,OAAO;AAGT"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { HlsVideoMedia } from "../hls-video/media.js";
|
|
2
|
+
import { MuxMediaMixin } from "./adapter.js";
|
|
3
|
+
//#region src/playback/adapters/mux-video/media.ts
|
|
4
|
+
const MuxVideoMediaBase = MuxMediaMixin(HlsVideoMedia);
|
|
5
|
+
/**
|
|
6
|
+
* The Mux Media over the SPF HLS engine.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors `@videojs/media/dom/mux`'s hls.js-backed `MuxMedia` — same class shape,
|
|
9
|
+
* same `src`/`source` relationship, same derived `contentData` — over a different
|
|
10
|
+
* engine, though named for the flavor rather than sharing that class's name. It
|
|
11
|
+
* carries no `engine` or `preferPlayback`: SPF publishes no
|
|
12
|
+
* engine-shaped config for a consumer to pass, so the source is Mux identity and
|
|
13
|
+
* nothing else.
|
|
14
|
+
*
|
|
15
|
+
* `source.drm` is accepted but inert. SPF prunes encrypted renditions and reports
|
|
16
|
+
* unsupported DRM, and `alternativeMediaSuggestion` points at the hls.js-backed
|
|
17
|
+
* import, so a protected source fails with copy that says where to go.
|
|
18
|
+
*/
|
|
19
|
+
var MuxVideoMedia = class extends MuxVideoMediaBase {};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { MuxVideoMedia };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","names":[],"sources":["../../../../../src/playback/adapters/mux-video/media.ts"],"sourcesContent":["import { HlsVideoMedia } from '../hls-video/media';\nimport { MuxMediaMixin } from './adapter';\n\nconst MuxVideoMediaBase = MuxMediaMixin(HlsVideoMedia);\n\n/**\n * The Mux Media over the SPF HLS engine.\n *\n * Mirrors `@videojs/media/dom/mux`'s hls.js-backed `MuxMedia` — same class shape,\n * same `src`/`source` relationship, same derived `contentData` — over a different\n * engine, though named for the flavor rather than sharing that class's name. It\n * carries no `engine` or `preferPlayback`: SPF publishes no\n * engine-shaped config for a consumer to pass, so the source is Mux identity and\n * nothing else.\n *\n * `source.drm` is accepted but inert. SPF prunes encrypted renditions and reports\n * unsupported DRM, and `alternativeMediaSuggestion` points at the hls.js-backed\n * import, so a protected source fails with copy that says where to go.\n */\nexport class MuxVideoMedia extends MuxVideoMediaBase {}\n"],"mappings":";;;AAGA,MAAM,oBAAoB,cAAc,aAAa;;;;;;;;;;;;;;;AAgBrD,IAAa,gBAAb,cAAmC,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { computed, update } from "../../core/signals/primitives.js";
|
|
2
|
+
import { defineBehavior } from "../../core/composition/create-composition.js";
|
|
3
|
+
import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
|
|
4
|
+
import { isResolvedPresentation } from "../../media/types/index.js";
|
|
5
|
+
//#region src/playback/behaviors/collect-errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* **Owns the engine's error sequence.** Reporters append through
|
|
8
|
+
* {@link emitError}; this behavior owns the slot and its per-source lifecycle,
|
|
9
|
+
* clearing it on exit so a new source starts clean and the sequence can't grow
|
|
10
|
+
* unbounded across a session.
|
|
11
|
+
*
|
|
12
|
+
* Same split as `setupFailoverMonitor` and `failedCdns`: writes come from
|
|
13
|
+
* wherever the condition is detected, one behavior owns the slot. Deliberately
|
|
14
|
+
* has no `effects` — it holds no policy and derives nothing. Severity is decided
|
|
15
|
+
* at the adapter, not here (see `internal/design/spf/features/errors.md`), which
|
|
16
|
+
* is why this is a lifecycle owner rather than an error *handler*.
|
|
17
|
+
*
|
|
18
|
+
* Clearing binds to *exit* of `presentation-resolved`, mirroring the sibling
|
|
19
|
+
* mixins' clear-on-teardown (`emptied` / `MEDIA_DETACHED`). A live reload swaps
|
|
20
|
+
* the presentation object without leaving the resolved state, so it doesn't
|
|
21
|
+
* clear — only an actual source change or destroy does. Known gap: a
|
|
22
|
+
* resolved→resolved source swap that never passes through unresolved carries the
|
|
23
|
+
* prior source's errors forward; `resolve-track` guards the same transition with
|
|
24
|
+
* a commit-time id check, and doing likewise here is a follow-up.
|
|
25
|
+
*
|
|
26
|
+
* The vocabulary itself ({@link SvtaError} and the codes) is DOM- and
|
|
27
|
+
* signal-free in `media/errors`; only the write seam lives here, with the slot
|
|
28
|
+
* it writes.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Append `error` to the engine's error sequence. No-op when no owner is
|
|
32
|
+
* composed. Replaces the array rather than mutating it, so signal consumers
|
|
33
|
+
* notify; duplicates are kept, since a repeated condition is a real observation.
|
|
34
|
+
* Writes go through `update` so concurrent reporters can't lose each other's
|
|
35
|
+
* appends.
|
|
36
|
+
*
|
|
37
|
+
* Every emission is logged, deliberately *before* the owner check. A condition
|
|
38
|
+
* emitted with no `collectErrors` composed is dropped on the floor — that's the
|
|
39
|
+
* case where a log is the only evidence it happened at all, so gating the log on
|
|
40
|
+
* the same check would hide exactly what's worth seeing. Emissions that *are*
|
|
41
|
+
* collected still get logged, because reaching `state.errors` is no guarantee of
|
|
42
|
+
* reaching a person: only *verdicts* are promoted to the media surface, so every
|
|
43
|
+
* cause (and any non-fatal notice) is otherwise invisible outside a debugger.
|
|
44
|
+
*
|
|
45
|
+
* Ungated rather than `__DEV__`-only, matching the other reporting paths in this
|
|
46
|
+
* package (`resolve-presentation`, `track-switching`, the segment actors).
|
|
47
|
+
*/
|
|
48
|
+
function emitError(state, error) {
|
|
49
|
+
console.error("[spf] reported condition", error);
|
|
50
|
+
if (!state.errors) return;
|
|
51
|
+
update(state.errors, (errors) => [...errors ?? [], error]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Own `errors` for the resolved source's lifetime.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const reactor = collectErrors.setup({ state });
|
|
58
|
+
*/
|
|
59
|
+
const collectErrors = defineBehavior({
|
|
60
|
+
stateKeys: ["presentation", "errors"],
|
|
61
|
+
contextKeys: [],
|
|
62
|
+
setup: ({ state }) => {
|
|
63
|
+
const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
|
|
64
|
+
return createMachineReactor({
|
|
65
|
+
initial: "presentation-unresolved",
|
|
66
|
+
monitor: () => derivedStateSignal.get(),
|
|
67
|
+
states: {
|
|
68
|
+
"presentation-unresolved": {},
|
|
69
|
+
"presentation-resolved": { entry: () => () => state.errors.set(void 0) }
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
export { collectErrors, emitError };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=collect-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-errors.js","names":[],"sources":["../../../../src/playback/behaviors/collect-errors.ts"],"sourcesContent":["/**\n * **Owns the engine's error sequence.** Reporters append through\n * {@link emitError}; this behavior owns the slot and its per-source lifecycle,\n * clearing it on exit so a new source starts clean and the sequence can't grow\n * unbounded across a session.\n *\n * Same split as `setupFailoverMonitor` and `failedCdns`: writes come from\n * wherever the condition is detected, one behavior owns the slot. Deliberately\n * has no `effects` — it holds no policy and derives nothing. Severity is decided\n * at the adapter, not here (see `internal/design/spf/features/errors.md`), which\n * is why this is a lifecycle owner rather than an error *handler*.\n *\n * Clearing binds to *exit* of `presentation-resolved`, mirroring the sibling\n * mixins' clear-on-teardown (`emptied` / `MEDIA_DETACHED`). A live reload swaps\n * the presentation object without leaving the resolved state, so it doesn't\n * clear — only an actual source change or destroy does. Known gap: a\n * resolved→resolved source swap that never passes through unresolved carries the\n * prior source's errors forward; `resolve-track` guards the same transition with\n * a commit-time id check, and doing likewise here is a follow-up.\n *\n * The vocabulary itself ({@link SvtaError} and the codes) is DOM- and\n * signal-free in `media/errors`; only the write seam lives here, with the slot\n * it writes.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { SvtaError } from '../../media/errors';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\n\nexport interface CollectErrorsState {\n presentation?: MaybeResolvedPresentation;\n errors?: SvtaError[];\n}\n\n/**\n * State an error reporter writes into. The slot is *optional*: a behavior reports\n * through this seam without declaring ownership in its own typed slice, and\n * emission no-ops when `collectErrors` isn't composed. Same contract as\n * `failedCdns` / `failoverFetch`.\n */\nexport interface ErrorEmitterState {\n errors?: Signal<SvtaError[] | undefined>;\n}\n\n/**\n * Append `error` to the engine's error sequence. No-op when no owner is\n * composed. Replaces the array rather than mutating it, so signal consumers\n * notify; duplicates are kept, since a repeated condition is a real observation.\n * Writes go through `update` so concurrent reporters can't lose each other's\n * appends.\n *\n * Every emission is logged, deliberately *before* the owner check. A condition\n * emitted with no `collectErrors` composed is dropped on the floor — that's the\n * case where a log is the only evidence it happened at all, so gating the log on\n * the same check would hide exactly what's worth seeing. Emissions that *are*\n * collected still get logged, because reaching `state.errors` is no guarantee of\n * reaching a person: only *verdicts* are promoted to the media surface, so every\n * cause (and any non-fatal notice) is otherwise invisible outside a debugger.\n *\n * Ungated rather than `__DEV__`-only, matching the other reporting paths in this\n * package (`resolve-presentation`, `track-switching`, the segment actors).\n */\nexport function emitError(state: ErrorEmitterState, error: SvtaError): void {\n console.error('[spf] reported condition', error);\n if (!state.errors) return;\n update(state.errors, (errors) => [...(errors ?? []), error]);\n}\n\n/**\n * Own `errors` for the resolved source's lifetime.\n *\n * @example\n * const reactor = collectErrors.setup({ state });\n */\nexport const collectErrors = defineBehavior({\n stateKeys: ['presentation', 'errors'],\n contextKeys: [],\n setup: ({\n state,\n }: {\n state: {\n presentation: ReadonlySignal<CollectErrorsState['presentation']>;\n errors: Signal<CollectErrorsState['errors']>;\n };\n }) => {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: reset for the next source on exit (src\n // unload + destroy).\n entry: () => () => state.errors.set(undefined),\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,UAAU,OAA0B,OAAwB;CAC1E,QAAQ,MAAM,4BAA4B,KAAK;CAC/C,IAAI,CAAC,MAAM,QAAQ;CACnB,OAAO,MAAM,SAAS,WAAW,CAAC,GAAI,UAAU,CAAC,GAAI,KAAK,CAAC;AAC7D;;;;;;;AAQA,MAAa,gBAAgB,eAAe;CAC1C,WAAW,CAAC,gBAAgB,QAAQ;CACpC,aAAa,CAAC;CACd,QAAQ,EACN,YAMI;EACJ,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;EAEA,OAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,IAAI;GACtC,QAAQ;IACN,2BAA2B,CAAC;IAC5B,yBAAyB,EAGvB,mBAAmB,MAAM,OAAO,IAAI,KAAA,CAAS,EAC/C;GACF;EACF,CAAC;CACH;AACF,CAAC"}
|
|
@@ -30,6 +30,7 @@ function deriveState(presentation, mediaSource, msIsOpen, videoBufferActor, audi
|
|
|
30
30
|
if (!initTrackId) return "preconditions-unmet";
|
|
31
31
|
const track = findTrackById(presentation, initTrackId);
|
|
32
32
|
if (!track || !isResolvedTrack(track)) return "preconditions-unmet";
|
|
33
|
+
if (!Number.isFinite(track.duration)) return "preconditions-unmet";
|
|
33
34
|
if (!isLastSegmentAppended(track.segments, appended)) return "preconditions-unmet";
|
|
34
35
|
if (track.segments.length > 0) {
|
|
35
36
|
const start = track.segments[track.segments.length - 1].startTime;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls\n * `MediaSource.endOfStream()` once the temporally last segments of every\n * active buffer actor's currently-loading track are fully appended and\n * the user has reached them — letting the browser finalize duration and\n * fire `ended` on the media element.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE\n * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource\n * back to `'open'`; seek-back replays and back-buffer refills that re-load\n * earlier segments take this path, so the behavior must call\n * `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:\n * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,\n * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next\n * `'open'` re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via\n * `initTrackId` (set on the most recent `append-init` message) and which\n * segments it has appended. `deriveState` iterates over the available\n * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),\n * resolves each to its track via `findTrackById`, and checks that track's\n * last segment is appended. This means audio-only / video-only / mixed\n * configurations compose uniformly — the body iterates whatever's in\n * scope. No reliance on `selectedTrackId` slots: the actor's view IS the\n * source of truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near\n * end-of-stream, the actor's `initTrackId` still reflects the *old* track\n * until the new init segment is appended. The reactor may fire\n * `endOfStream()` against the old track's last-segment-appended state,\n * then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally\n * correct (the browser re-fires `ended` after the re-arm), at the cost of\n * one extra call. Accepted as the price of dropping `selectedTrackId`\n * dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we\n * fire. `buffer.updating === false` alone is insufficient: for chunked\n * fMP4 streaming, `updateend` fires after each chunk while the actor's\n * for-await loop synchronously enqueues the next `appendBuffer()` — so\n * the buffer flips `!updating → updating` across a microtask boundary.\n * The actor's `'updating'` state spans the entire multi-chunk append, so\n * `actor.snapshot.value === 'idle'` is the canonical \"no more pending or\n * in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models\n * DOM state) is durable as long as the segment loader is the sole writer\n * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier\n * segments mid-`ended` would require a broader coordination story\n * between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want\n * re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached (within {@link LAST_SEGMENT_REACHED_SLACK})\n * at least one active track's last segment startTime. Prevents `'eos-ready'`\n * entry when a back-buffer `remove()` / `appendBuffer()` briefly re-opens the\n * MediaSource while the user is mid-stream. HLS rendition time-alignment means\n * any active track works as the reference. The slack absorbs the near-end\n * playhead freeze (see the constant) so a tiny final segment doesn't deadlock.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside\n * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to\n * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future\n * `MediaSourceActor` whose snapshot would expose the same signal — the\n * consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,\n * should already hold given the actor-idle gate in `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual\n * container timestamps (`endOfStream()` only clamps up implicitly;\n * setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait\n * on source unload, presentation replace, or behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration`\n * from `presentation.duration` once per source; this behavior writes the\n * final value from the buffered end. Decision domains don't overlap.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\n/**\n * Slack (seconds) on the \"playhead has reached the last segment\" gate. A tiny final\n * segment (e.g. Apple's ~44ms last segment) starts right at the buffered end, and the\n * browser freezes the playhead ~50–70ms short of that end (its render horizon), so a\n * strict `currentTime >= lastSegStart` would never open — deadlocking `endOfStream`\n * (the MediaSource stays `'open'`, so the browser keeps the playhead frozen waiting for\n * data/EOS that never comes). This slack lets a playhead stalled just short of the final\n * segment still finalize. Firing slightly early is harmless: the last segment is already\n * appended (the gate above), so no more data is expected.\n */\nconst LAST_SEGMENT_REACHED_SLACK = 0.5;\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n // Slack absorbs the near-end playhead freeze so a tiny final segment doesn't deadlock.\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart - LAST_SEGMENT_REACHED_SLACK) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n // See behavior definition for details on these optional context signals.\n videoBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n audioBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor?.get(),\n context.audioBufferActor?.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\n/**\n * `endOfStream` uses a manual `Behavior<>` literal (rather than\n * `defineBehavior`) because it reads `videoBufferActor` /\n * `audioBufferActor` defensively without declaring them in its\n * contextKeys — those slots are contributed by other behaviors and\n * compose conditionally per engine variant. The `Behavior<>` literal\n * opts out of the exhaustiveness check so the typed context shape can\n * include the optional fields used at runtime. See the comment on\n * `endOfStreamSetup`'s context param for the discipline.\n */\nexport const endOfStream: Behavior<\n {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n },\n { mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']> }\n> = {\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource'],\n setup: endOfStreamSetup,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiIA,MAAM,6BAA6B;AAEnC,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;CACrB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,UAAU,OAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,gBAAgB,CAAC,CAAC,QAAQ,MAA8B,MAAM,KAAA,CAAS;CAEzG,IAAI,OAAO,WAAW,GAAG,OAAO;CAKhC,IAAI;CAEJ,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,IAAI;EACpC,IAAI,SAAS,UAAU,QAAQ,OAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;EACrD,IAAI,CAAC,aAAa,OAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,WAAW;EACrD,IAAI,CAAC,SAAS,CAAC,gBAAgB,KAAK,GAAG,OAAO;EAC9C,IAAI,CAAC,sBAAsB,MAAM,UAAU,QAAQ,GAAG,OAAO;EAE7D,IAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,EAAE,CAAE;GACzD,IAAI,iBAAiB,KAAA,KAAa,QAAQ,cAAc,eAAe;EACzE;CACF;CAGA,IAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,eAAe,4BACpE,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,iBAAiB,EACxB,OACA,WAYa;CAMb,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,IAAI;EAC5C,IAAI,CAAC,aAAa;GAChB,SAAS,IAAI,KAAK;GAClB;EACF;EACA,SAAS,IAAI,YAAY,eAAe,MAAM;EAC9C,MAAM,aAAa,IAAI,gBAAgB;EACvC,8BAA8B,aAAa,WAAW,SAAS,OAAO;GACpE,SAAS,IAAI,OAAO,MAAM;EAC5B,CAAC;EACD,aAAa,WAAW,MAAM;CAChC,CAAC;CAED,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,IAAI,GACvB,QAAQ,YAAY,IAAI,GACxB,SAAS,IAAI,GACb,QAAQ,kBAAkB,IAAI,GAC9B,QAAQ,kBAAkB,IAAI,GAC9B,MAAM,YAAY,IAAI,CACxB,CACF;CAEA,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,IAAI;IAC5C,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,qBAAqB,YAAY;KAKrC,MAAM,0BAA0B,YAAY,eAAe,WAAW,MAAM;KAC5E,IAAI,WAAW,OAAO,SAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,aAAa;KAC/D,IAAI,cAAc,GAAG,YAAY,WAAW;KAE5C,YAAY,YAAY;IAC1B;IAEA,mBAAmB,CAAC,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,GAAG,CAAC;IAErF,aAAa,WAAW,MAAM;GAChC,EACF;EACF;CACF,CAAC;CAED,aAAa;EACX,kBAAkB;EAClB,QAAQ,QAAQ;CAClB;AACF;;;;;;;;;;;AAYA,MAAa,cAMT;CACF,WAAW,CAAC,gBAAgB,aAAa;CACzC,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls\n * `MediaSource.endOfStream()` once every active buffer actor's\n * currently-loading track is *complete* (finite `Track.duration` — the parser's\n * completeness signal), its temporally last segments are fully appended, and\n * the user has reached them — letting the browser finalize duration and\n * fire `ended` on the media element. The completeness gate keeps it inert for\n * ongoing live (`Track.duration === Infinity`), whose \"last segment\" is only\n * the rolling edge; a live stream opts in when its duration turns finite.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE\n * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource\n * back to `'open'`; seek-back replays and back-buffer refills that re-load\n * earlier segments take this path, so the behavior must call\n * `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:\n * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,\n * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next\n * `'open'` re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via\n * `initTrackId` (set on the most recent `append-init` message) and which\n * segments it has appended. `deriveState` iterates over the available\n * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),\n * resolves each to its track via `findTrackById`, and checks that track's\n * last segment is appended. This means audio-only / video-only / mixed\n * configurations compose uniformly — the body iterates whatever's in\n * scope. No reliance on `selectedTrackId` slots: the actor's view IS the\n * source of truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near\n * end-of-stream, the actor's `initTrackId` still reflects the *old* track\n * until the new init segment is appended. The reactor may fire\n * `endOfStream()` against the old track's last-segment-appended state,\n * then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally\n * correct (the browser re-fires `ended` after the re-arm), at the cost of\n * one extra call. Accepted as the price of dropping `selectedTrackId`\n * dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we\n * fire. `buffer.updating === false` alone is insufficient: for chunked\n * fMP4 streaming, `updateend` fires after each chunk while the actor's\n * for-await loop synchronously enqueues the next `appendBuffer()` — so\n * the buffer flips `!updating → updating` across a microtask boundary.\n * The actor's `'updating'` state spans the entire multi-chunk append, so\n * `actor.snapshot.value === 'idle'` is the canonical \"no more pending or\n * in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models\n * DOM state) is durable as long as the segment loader is the sole writer\n * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier\n * segments mid-`ended` would require a broader coordination story\n * between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want\n * re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached (within {@link LAST_SEGMENT_REACHED_SLACK})\n * at least one active track's last segment startTime. Prevents `'eos-ready'`\n * entry when a back-buffer `remove()` / `appendBuffer()` briefly re-opens the\n * MediaSource while the user is mid-stream. HLS rendition time-alignment means\n * any active track works as the reference. The slack absorbs the near-end\n * playhead freeze (see the constant) so a tiny final segment doesn't deadlock.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside\n * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to\n * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future\n * `MediaSourceActor` whose snapshot would expose the same signal — the\n * consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,\n * should already hold given the actor-idle gate in `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual\n * container timestamps (`endOfStream()` only clamps up implicitly;\n * setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait\n * on source unload, presentation replace, or behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration`\n * from `presentation.duration` once per source; this behavior writes the\n * final value from the buffered end. Decision domains don't overlap.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\n/**\n * Slack (seconds) on the \"playhead has reached the last segment\" gate. A tiny final\n * segment (e.g. Apple's ~44ms last segment) starts right at the buffered end, and the\n * browser freezes the playhead ~50–70ms short of that end (its render horizon), so a\n * strict `currentTime >= lastSegStart` would never open — deadlocking `endOfStream`\n * (the MediaSource stays `'open'`, so the browser keeps the playhead frozen waiting for\n * data/EOS that never comes). This slack lets a playhead stalled just short of the final\n * segment still finalize. Firing slightly early is harmless: the last segment is already\n * appended (the gate above), so no more data is expected.\n */\nconst LAST_SEGMENT_REACHED_SLACK = 0.5;\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n // Only a complete playlist has a true last segment: firing on an ongoing\n // live window would pin a finite (live-edge) duration and end the stream,\n // only for the next reload's appends to reopen it and re-fire on a loop.\n // Independent of the currentTime slack below — this asks \"is there an end at\n // all,\" the slack asks \"has the playhead effectively reached it.\"\n if (!Number.isFinite(track.duration)) return 'preconditions-unmet';\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n // Slack absorbs the near-end playhead freeze so a tiny final segment doesn't deadlock.\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart - LAST_SEGMENT_REACHED_SLACK) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n // See behavior definition for details on these optional context signals.\n videoBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n audioBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor?.get(),\n context.audioBufferActor?.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\n/**\n * `endOfStream` uses a manual `Behavior<>` literal (rather than\n * `defineBehavior`) because it reads `videoBufferActor` /\n * `audioBufferActor` defensively without declaring them in its\n * contextKeys — those slots are contributed by other behaviors and\n * compose conditionally per engine variant. The `Behavior<>` literal\n * opts out of the exhaustiveness check so the typed context shape can\n * include the optional fields used at runtime. See the comment on\n * `endOfStreamSetup`'s context param for the discipline.\n */\nexport const endOfStream: Behavior<\n {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n },\n { mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']> }\n> = {\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource'],\n setup: endOfStreamSetup,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoIA,MAAM,6BAA6B;AAEnC,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;CACrB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,UAAU,OAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,gBAAgB,CAAC,CAAC,QAAQ,MAA8B,MAAM,KAAA,CAAS;CAEzG,IAAI,OAAO,WAAW,GAAG,OAAO;CAKhC,IAAI;CAEJ,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,IAAI;EACpC,IAAI,SAAS,UAAU,QAAQ,OAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;EACrD,IAAI,CAAC,aAAa,OAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,WAAW;EACrD,IAAI,CAAC,SAAS,CAAC,gBAAgB,KAAK,GAAG,OAAO;EAM9C,IAAI,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG,OAAO;EAC7C,IAAI,CAAC,sBAAsB,MAAM,UAAU,QAAQ,GAAG,OAAO;EAE7D,IAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,EAAE,CAAE;GACzD,IAAI,iBAAiB,KAAA,KAAa,QAAQ,cAAc,eAAe;EACzE;CACF;CAGA,IAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,eAAe,4BACpE,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,iBAAiB,EACxB,OACA,WAYa;CAMb,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,IAAI;EAC5C,IAAI,CAAC,aAAa;GAChB,SAAS,IAAI,KAAK;GAClB;EACF;EACA,SAAS,IAAI,YAAY,eAAe,MAAM;EAC9C,MAAM,aAAa,IAAI,gBAAgB;EACvC,8BAA8B,aAAa,WAAW,SAAS,OAAO;GACpE,SAAS,IAAI,OAAO,MAAM;EAC5B,CAAC;EACD,aAAa,WAAW,MAAM;CAChC,CAAC;CAED,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,IAAI,GACvB,QAAQ,YAAY,IAAI,GACxB,SAAS,IAAI,GACb,QAAQ,kBAAkB,IAAI,GAC9B,QAAQ,kBAAkB,IAAI,GAC9B,MAAM,YAAY,IAAI,CACxB,CACF;CAEA,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,IAAI;IAC5C,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,qBAAqB,YAAY;KAKrC,MAAM,0BAA0B,YAAY,eAAe,WAAW,MAAM;KAC5E,IAAI,WAAW,OAAO,SAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,aAAa;KAC/D,IAAI,cAAc,GAAG,YAAY,WAAW;KAE5C,YAAY,YAAY;IAC1B;IAEA,mBAAmB,CAAC,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,GAAG,CAAC;IAErF,aAAa,WAAW,MAAM;GAChC,EACF;EACF;CACF,CAAC;CAED,aAAa;EACX,kBAAkB;EAClB,QAAQ,QAAQ;CAClB;AACF;;;;;;;;;;;AAYA,MAAa,cAMT;CACF,WAAW,CAAC,gBAAgB,aAAa;CACzC,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT"}
|