@videojs/spf 10.0.0-beta.14 → 10.0.0-beta.16
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/buffer/forward-buffer.js.map +1 -1
- package/dist/default/core/create-machine-actor.js +91 -0
- package/dist/default/core/create-machine-actor.js.map +1 -0
- package/dist/default/core/create-machine-reactor.js +83 -0
- package/dist/default/core/create-machine-reactor.js.map +1 -0
- package/dist/default/core/create-transition-actor.js +46 -0
- package/dist/default/core/create-transition-actor.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +42 -46
- package/dist/default/core/features/resolve-presentation.js.map +1 -1
- package/dist/default/core/features/resolve-track.js +1 -1
- package/dist/default/core/machine.js +26 -0
- package/dist/default/core/machine.js.map +1 -0
- package/dist/default/core/signals/primitives.js +1 -1
- package/dist/default/core/task.js +76 -7
- package/dist/default/core/task.js.map +1 -1
- package/dist/default/core/types/index.js.map +1 -1
- package/dist/default/dom/features/end-of-stream.js +5 -5
- package/dist/default/dom/features/end-of-stream.js.map +1 -1
- package/dist/default/dom/features/load-text-track-cues.js +80 -137
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -1
- package/dist/default/dom/features/segment-loader-actor.js +150 -103
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -1
- package/dist/default/dom/features/sync-text-tracks.js +95 -0
- package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/text-tracks-actor.js +42 -0
- package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +51 -38
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -1
- package/dist/default/dom/media/source-buffer-actor.js +52 -91
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -1
- package/dist/default/dom/network/fetch.js +0 -1
- package/dist/default/dom/network/fetch.js.map +1 -1
- package/dist/default/dom/playback-engine/engine.js +4 -13
- package/dist/default/dom/playback-engine/engine.js.map +1 -1
- package/dist/dev/core/actor.d.ts +19 -10
- package/dist/dev/core/actor.d.ts.map +1 -1
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -1
- package/dist/dev/core/create-machine-actor.d.ts +12 -0
- package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-actor.js +91 -0
- package/dist/dev/core/create-machine-actor.js.map +1 -0
- package/dist/dev/core/create-machine-reactor.d.ts +8 -0
- package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-reactor.js +83 -0
- package/dist/dev/core/create-machine-reactor.js.map +1 -0
- package/dist/dev/core/create-transition-actor.d.ts +23 -0
- package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
- package/dist/dev/core/create-transition-actor.js +46 -0
- package/dist/dev/core/create-transition-actor.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +42 -46
- package/dist/dev/core/features/resolve-presentation.js.map +1 -1
- package/dist/dev/core/features/resolve-track.js +1 -1
- package/dist/dev/core/machine.d.ts +21 -0
- package/dist/dev/core/machine.d.ts.map +1 -0
- package/dist/dev/core/machine.js +26 -0
- package/dist/dev/core/machine.js.map +1 -0
- package/dist/dev/core/signals/primitives.js +1 -1
- package/dist/dev/core/task.js +76 -7
- package/dist/dev/core/task.js.map +1 -1
- package/dist/dev/core/types/index.d.ts +1 -8
- package/dist/dev/core/types/index.d.ts.map +1 -1
- package/dist/dev/core/types/index.js.map +1 -1
- package/dist/dev/dom/features/end-of-stream.js +5 -5
- package/dist/dev/dom/features/end-of-stream.js.map +1 -1
- package/dist/dev/dom/features/load-text-track-cues.d.ts +50 -26
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -1
- package/dist/dev/dom/features/load-text-track-cues.js +80 -137
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -1
- package/dist/dev/dom/features/segment-loader-actor.js +150 -103
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -1
- package/dist/dev/dom/features/sync-text-tracks.js +95 -0
- package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.js +42 -0
- package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +11 -9
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -1
- package/dist/dev/dom/features/track-playback-initiated.js +51 -38
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -1
- package/dist/dev/dom/media/source-buffer-actor.d.ts +14 -8
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -1
- package/dist/dev/dom/media/source-buffer-actor.js +52 -91
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -1
- package/dist/dev/dom/network/fetch.js +0 -1
- package/dist/dev/dom/network/fetch.js.map +1 -1
- package/dist/dev/dom/playback-engine/engine.d.ts +4 -3
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -1
- package/dist/dev/dom/playback-engine/engine.js +4 -13
- package/dist/dev/dom/playback-engine/engine.js.map +1 -1
- package/package.json +5 -4
- package/dist/default/dom/features/setup-text-tracks.js +0 -67
- package/dist/default/dom/features/setup-text-tracks.js.map +0 -1
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js +0 -54
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +0 -1
- package/dist/default/dom/features/sync-text-track-modes.js +0 -33
- package/dist/default/dom/features/sync-text-track-modes.js.map +0 -1
- package/dist/dev/dom/features/setup-text-tracks.js +0 -67
- package/dist/dev/dom/features/setup-text-tracks.js.map +0 -1
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +0 -54
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +0 -1
- package/dist/dev/dom/features/sync-text-track-modes.js +0 -33
- package/dist/dev/dom/features/sync-text-track-modes.js.map +0 -1
|
@@ -1,51 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { computed, update } from "../../core/signals/primitives.js";
|
|
2
|
+
import { createMachineReactor } from "../../core/create-machine-reactor.js";
|
|
3
3
|
import { listen } from "@videojs/utils/dom";
|
|
4
4
|
//#region src/dom/features/track-playback-initiated.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* FSM states for playback initiation tracking.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* 'preconditions-unmet' ──── element + URL ────→ 'monitoring'
|
|
10
|
+
* ↑ ↑ │
|
|
11
|
+
* │ preconditions lost │ play / !paused
|
|
12
|
+
* │ │ ↓
|
|
13
|
+
* └────────────── 'playback-initiated' ←──────┘
|
|
14
|
+
* (exit cleanup resets state.playbackInitiated → false)
|
|
7
15
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
16
|
+
* any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function deriveState(state, owners) {
|
|
20
|
+
if (!owners.mediaElement || !state.presentation?.url) return "preconditions-unmet";
|
|
21
|
+
if (state.playbackInitiated) return "playback-initiated";
|
|
22
|
+
return "monitoring";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Track whether playback has been initiated for the current presentation URL.
|
|
11
26
|
*
|
|
12
|
-
* A
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
27
|
+
* A three-state Reactor FSM driven by `state.playbackInitiated` and the
|
|
28
|
+
* `deriveState` pattern:
|
|
29
|
+
* - `'preconditions-unmet'` — no element or URL yet; no effects.
|
|
30
|
+
* - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.
|
|
31
|
+
* - `'playback-initiated'` — tracks element and URL; exit cleanup resets
|
|
32
|
+
* `state.playbackInitiated` to `false` on any change or lost preconditions.
|
|
15
33
|
*
|
|
16
34
|
* @example
|
|
17
|
-
* const
|
|
35
|
+
* const reactor = trackPlaybackInitiated({ state, owners });
|
|
36
|
+
* // later:
|
|
37
|
+
* reactor.destroy();
|
|
18
38
|
*/
|
|
19
39
|
function trackPlaybackInitiated({ state, owners }) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (pi === void 0) return;
|
|
42
|
-
if (state.get().playbackInitiated !== pi) update(state, { playbackInitiated: pi });
|
|
40
|
+
const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));
|
|
41
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
42
|
+
const urlSignal = computed(() => state.get().presentation?.url);
|
|
43
|
+
return createMachineReactor({
|
|
44
|
+
initial: "preconditions-unmet",
|
|
45
|
+
monitor: () => derivedStateSignal.get(),
|
|
46
|
+
states: {
|
|
47
|
+
"preconditions-unmet": {},
|
|
48
|
+
monitoring: { entry: () => {
|
|
49
|
+
const el = mediaElementSignal.get();
|
|
50
|
+
update(state, { playbackInitiated: !el.paused });
|
|
51
|
+
return listen(el, "play", () => {
|
|
52
|
+
update(state, { playbackInitiated: !el.paused });
|
|
53
|
+
});
|
|
54
|
+
} },
|
|
55
|
+
"playback-initiated": { effects: () => {
|
|
56
|
+
mediaElementSignal.get();
|
|
57
|
+
urlSignal.get();
|
|
58
|
+
return () => update(state, { playbackInitiated: false });
|
|
59
|
+
} }
|
|
60
|
+
}
|
|
43
61
|
});
|
|
44
|
-
return () => {
|
|
45
|
-
cleanupResetEffect();
|
|
46
|
-
cleanupPlayEffect();
|
|
47
|
-
cleanupMergeEffect();
|
|
48
|
-
};
|
|
49
62
|
}
|
|
50
63
|
//#endregion
|
|
51
64
|
export { trackPlaybackInitiated };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-playback-initiated.js","names":[],"sources":["../../../../src/dom/features/track-playback-initiated.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport {
|
|
1
|
+
{"version":3,"file":"track-playback-initiated.js","names":[],"sources":["../../../../src/dom/features/track-playback-initiated.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { Reactor } from '../../core/create-machine-reactor';\nimport { createMachineReactor } from '../../core/create-machine-reactor';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once play has been requested for the current presentation URL. */\n playbackInitiated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: { url?: string };\n}\n\n/**\n * Owners shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * FSM states for playback initiation tracking.\n *\n * ```\n * 'preconditions-unmet' ──── element + URL ────→ 'monitoring'\n * ↑ ↑ │\n * │ preconditions lost │ play / !paused\n * │ │ ↓\n * └────────────── 'playback-initiated' ←──────┘\n * (exit cleanup resets state.playbackInitiated → false)\n *\n * any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'\n * ```\n */\nfunction deriveState(\n state: PlaybackInitiatedState,\n owners: PlaybackInitiatedOwners\n): 'preconditions-unmet' | 'monitoring' | 'playback-initiated' {\n if (!owners.mediaElement || !state.presentation?.url) return 'preconditions-unmet';\n if (state.playbackInitiated) return 'playback-initiated';\n return 'monitoring';\n}\n\n/**\n * Track whether playback has been initiated for the current presentation URL.\n *\n * A three-state Reactor FSM driven by `state.playbackInitiated` and the\n * `deriveState` pattern:\n * - `'preconditions-unmet'` — no element or URL yet; no effects.\n * - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.\n * - `'playback-initiated'` — tracks element and URL; exit cleanup resets\n * `state.playbackInitiated` to `false` on any change or lost preconditions.\n *\n * @example\n * const reactor = trackPlaybackInitiated({ state, owners });\n * // later:\n * reactor.destroy();\n */\nexport function trackPlaybackInitiated<S extends PlaybackInitiatedState, O extends PlaybackInitiatedOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): Reactor<'preconditions-unmet' | 'monitoring' | 'playback-initiated' | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n const urlSignal = computed(() => state.get().presentation?.url);\n\n return createMachineReactor<'preconditions-unmet' | 'monitoring' | 'playback-initiated'>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // Entry: check if already playing; otherwise listen for play.\n // The fn body is automatically untracked — el is read at entry time only.\n entry: () => {\n const el = mediaElementSignal.get()!;\n update(state, { playbackInitiated: !el.paused } as Partial<S>);\n return listen(el, 'play', () => {\n update(state, { playbackInitiated: !el.paused } as Partial<S>);\n });\n },\n },\n\n 'playback-initiated': {\n // Reaction: tracks element and URL while initiated. When either changes,\n // the effect re-runs — the exit cleanup fires first, resetting\n // state.playbackInitiated to false. deriveState then returns 'monitoring'\n // on the next microtask, and the always monitor drives the transition.\n //\n // This covers both the preconditions-lost path (element/URL → undefined,\n // which also triggers a deriveState → 'preconditions-unmet' transition)\n // and the URL-change / element-swap path (preconditions still met but\n // values changed, handled entirely by this effect's cleanup).\n effects: () => {\n mediaElementSignal.get(); // tracked — re-run on element change\n urlSignal.get(); // tracked — re-run on URL change\n return () => update(state, { playbackInitiated: false } as Partial<S>);\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoCA,SAAS,YACP,OACA,QAC6D;AAC7D,KAAI,CAAC,OAAO,gBAAgB,CAAC,MAAM,cAAc,IAAK,QAAO;AAC7D,KAAI,MAAM,kBAAmB,QAAO;AACpC,QAAO;;;;;;;;;;;;;;;;;AAkBT,SAAgB,uBAA4F,EAC1G,OACA,UAIoG;CACpG,MAAM,qBAAqB,eAAe,YAAY,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;CACjF,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CACpE,MAAM,YAAY,eAAe,MAAM,KAAK,CAAC,cAAc,IAAI;AAE/D,QAAO,qBAAkF;EACvF,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,YAAY,EAGV,aAAa;IACX,MAAM,KAAK,mBAAmB,KAAK;AACnC,WAAO,OAAO,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAe;AAC9D,WAAO,OAAO,IAAI,cAAc;AAC9B,YAAO,OAAO,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAe;MAC9D;MAEL;GAED,sBAAsB,EAUpB,eAAe;AACb,uBAAmB,KAAK;AACxB,cAAU,KAAK;AACf,iBAAa,OAAO,OAAO,EAAE,mBAAmB,OAAO,CAAe;MAEzE;GACF;EACF,CAAC"}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMachineActor } from "../../core/create-machine-actor.js";
|
|
2
|
+
import { SerialRunner, Task } from "../../core/task.js";
|
|
2
3
|
import { appendSegment } from "./append-segment.js";
|
|
3
4
|
import { flushBuffer } from "./buffer-flusher.js";
|
|
4
|
-
import { SerialRunner, Task } from "../../core/task.js";
|
|
5
5
|
//#region src/dom/media/source-buffer-actor.ts
|
|
6
|
-
/**
|
|
7
|
-
* Thrown when a message is sent to the actor in a state that does not
|
|
8
|
-
* accept messages (currently: 'updating').
|
|
9
|
-
*/
|
|
10
|
-
var SourceBufferActorError = class extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "SourceBufferActorError";
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
6
|
function snapshotBuffered(buffered) {
|
|
17
7
|
const ranges = [];
|
|
18
8
|
for (let i = 0; i < buffered.length; i++) ranges.push({
|
|
@@ -21,25 +11,25 @@ function snapshotBuffered(buffered) {
|
|
|
21
11
|
});
|
|
22
12
|
return ranges;
|
|
23
13
|
}
|
|
24
|
-
function appendInitTask(message, {
|
|
14
|
+
function appendInitTask(message, { getContext, sourceBuffer }) {
|
|
25
15
|
return new Task(async (taskSignal) => {
|
|
26
|
-
const ctx =
|
|
16
|
+
const ctx = getContext();
|
|
27
17
|
if (taskSignal.aborted) return ctx;
|
|
28
18
|
await appendSegment(sourceBuffer, message.data);
|
|
29
19
|
return {
|
|
30
20
|
...ctx,
|
|
31
21
|
initTrackId: message.meta.trackId
|
|
32
22
|
};
|
|
33
|
-
}
|
|
23
|
+
});
|
|
34
24
|
}
|
|
35
|
-
function appendSegmentTask(message, {
|
|
25
|
+
function appendSegmentTask(message, { getContext, sourceBuffer, setContext }) {
|
|
36
26
|
return new Task(async (taskSignal) => {
|
|
37
|
-
const ctx =
|
|
27
|
+
const ctx = getContext();
|
|
38
28
|
if (taskSignal.aborted) return ctx;
|
|
39
29
|
const { meta } = message;
|
|
40
30
|
const EPSILON = 1e-4;
|
|
41
31
|
const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);
|
|
42
|
-
if (!(message.data instanceof ArrayBuffer))
|
|
32
|
+
if (!(message.data instanceof ArrayBuffer)) setContext({
|
|
43
33
|
...ctx,
|
|
44
34
|
segments: [...filtered, {
|
|
45
35
|
id: meta.id,
|
|
@@ -63,11 +53,11 @@ function appendSegmentTask(message, { signal, getCtx, sourceBuffer, onPartialCon
|
|
|
63
53
|
}],
|
|
64
54
|
bufferedRanges: snapshotBuffered(sourceBuffer.buffered)
|
|
65
55
|
};
|
|
66
|
-
}
|
|
56
|
+
});
|
|
67
57
|
}
|
|
68
|
-
function removeTask(message, {
|
|
58
|
+
function removeTask(message, { getContext, sourceBuffer }) {
|
|
69
59
|
return new Task(async (taskSignal) => {
|
|
70
|
-
const ctx =
|
|
60
|
+
const ctx = getContext();
|
|
71
61
|
if (taskSignal.aborted) return ctx;
|
|
72
62
|
await flushBuffer(sourceBuffer, message.start, message.end);
|
|
73
63
|
const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);
|
|
@@ -80,7 +70,7 @@ function removeTask(message, { signal, getCtx, sourceBuffer }) {
|
|
|
80
70
|
segments: filtered,
|
|
81
71
|
bufferedRanges
|
|
82
72
|
};
|
|
83
|
-
}
|
|
73
|
+
});
|
|
84
74
|
}
|
|
85
75
|
const messageTaskFactories = {
|
|
86
76
|
"append-init": appendInitTask,
|
|
@@ -92,83 +82,54 @@ function messageToTask(message, options) {
|
|
|
92
82
|
return factory(message, options);
|
|
93
83
|
}
|
|
94
84
|
function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
95
|
-
const
|
|
96
|
-
|
|
85
|
+
const handleError = (e) => {
|
|
86
|
+
if (!(e instanceof Error && e.name === "AbortError")) console.error("SourceBuffer operation failed:", e);
|
|
87
|
+
};
|
|
88
|
+
const onMessage = (msg, { transition, setContext, getContext, runner }) => {
|
|
89
|
+
transition("updating");
|
|
90
|
+
const task = messageToTask(msg, {
|
|
91
|
+
getContext,
|
|
92
|
+
sourceBuffer,
|
|
93
|
+
setContext
|
|
94
|
+
});
|
|
95
|
+
runner.schedule(task).then(setContext, handleError);
|
|
96
|
+
};
|
|
97
|
+
return createMachineActor({
|
|
98
|
+
runner: () => new SerialRunner(),
|
|
99
|
+
initial: "idle",
|
|
97
100
|
context: {
|
|
98
101
|
segments: [],
|
|
99
102
|
bufferedRanges: [],
|
|
100
103
|
initTrackId: void 0,
|
|
101
104
|
...initialContext
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
const runner = new SerialRunner();
|
|
105
|
-
function applyResult(newContext) {
|
|
106
|
-
const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
|
|
107
|
-
snapshotSignal.set({
|
|
108
|
-
status,
|
|
109
|
-
context: newContext
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function handleError(e) {
|
|
113
|
-
update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
|
|
114
|
-
throw e;
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
get snapshot() {
|
|
118
|
-
return snapshotSignal;
|
|
119
105
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
status: "updating",
|
|
145
|
-
context: ctx
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
for (const message of messages.slice(0, -1)) {
|
|
149
|
-
const task = messageToTask(message, {
|
|
150
|
-
signal,
|
|
151
|
-
getCtx: () => workingCtx,
|
|
152
|
-
sourceBuffer,
|
|
153
|
-
onPartialContext
|
|
154
|
-
});
|
|
155
|
-
runner.schedule(task).then((newCtx) => {
|
|
156
|
-
workingCtx = newCtx;
|
|
157
|
-
});
|
|
106
|
+
states: {
|
|
107
|
+
idle: { on: {
|
|
108
|
+
"append-init": onMessage,
|
|
109
|
+
"append-segment": onMessage,
|
|
110
|
+
remove: onMessage,
|
|
111
|
+
batch: (msg, { transition, setContext, getContext, runner }) => {
|
|
112
|
+
const { messages } = msg;
|
|
113
|
+
if (messages.length === 0) return;
|
|
114
|
+
transition("updating");
|
|
115
|
+
messages.forEach((msg) => {
|
|
116
|
+
const task = messageToTask(msg, {
|
|
117
|
+
getContext,
|
|
118
|
+
sourceBuffer,
|
|
119
|
+
setContext
|
|
120
|
+
});
|
|
121
|
+
runner.schedule(task).then(setContext, handleError);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} },
|
|
125
|
+
updating: {
|
|
126
|
+
onSettled: "idle",
|
|
127
|
+
on: { cancel: (_, { runner }) => {
|
|
128
|
+
runner.abortAll();
|
|
129
|
+
} }
|
|
158
130
|
}
|
|
159
|
-
const lastTask = messageToTask(messages[messages.length - 1], {
|
|
160
|
-
signal,
|
|
161
|
-
getCtx: () => workingCtx,
|
|
162
|
-
sourceBuffer,
|
|
163
|
-
onPartialContext
|
|
164
|
-
});
|
|
165
|
-
return runner.schedule(lastTask).then(applyResult).catch(handleError);
|
|
166
|
-
},
|
|
167
|
-
destroy() {
|
|
168
|
-
update(snapshotSignal, { status: "destroyed" });
|
|
169
|
-
runner.destroy();
|
|
170
131
|
}
|
|
171
|
-
};
|
|
132
|
+
});
|
|
172
133
|
}
|
|
173
134
|
//#endregion
|
|
174
135
|
export { createSourceBufferActor };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-buffer-actor.js","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"sourcesContent":["import type { ActorSnapshot, SignalActor } from '../../core/actor';\nimport { type ReadonlySignal, signal, update } from '../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { Segment, Track } from '../../core/types';\nimport { type AppendData, appendSegment } from './append-segment';\nimport { flushBuffer } from './buffer-flusher';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type { AppendData };\n\nexport type AppendInitMessage = { type: 'append-init'; data: AppendData; meta: { trackId: Track['id'] } };\nexport type AppendSegmentMessage = { type: 'append-segment'; data: AppendData; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type SourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\n\n/** Finite (bounded) operational modes of the actor. */\nexport type SourceBufferActorStatus = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n segments: Array<\n Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n trackBandwidth?: number;\n /**\n * True while a streaming append is in progress for this segment.\n * The segment's data is partially present in the SourceBuffer.\n * Downstream code must not treat a partial segment as fully buffered.\n */\n partial?: boolean;\n }\n >;\n bufferedRanges: BufferedRange[];\n}\n\n/** Complete snapshot of a SourceBufferActor. */\nexport type SourceBufferActorSnapshot = ActorSnapshot<SourceBufferActorStatus, SourceBufferActorContext>;\n\n/**\n * Thrown when a message is sent to the actor in a state that does not\n * accept messages (currently: 'updating').\n */\nexport class SourceBufferActorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceBufferActorError';\n }\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport interface SourceBufferActor extends SignalActor<SourceBufferActorStatus, SourceBufferActorContext> {\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void>;\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void>;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getCtx at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n signal: AbortSignal;\n getCtx: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n /**\n * Called when a streaming append transitions to a partial state — i.e.\n * the first chunk of an AsyncIterable has been committed and the segment\n * now has data in the SourceBuffer but is not yet complete. Not called for\n * full ArrayBuffer appends (which are atomic).\n */\n onPartialContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId };\n },\n { signal }\n );\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { signal, getCtx, sourceBuffer, onPartialContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. The epsilon guards against floating-point drift in\n // parsed timestamps.\n const EPSILON = 0.0001;\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);\n\n // For streaming data: emit partial state before the first chunk so\n // downstream code can see the in-progress segment and treat it as\n // incomplete. ArrayBuffer appends are atomic so no partial state is\n // needed — context is updated once at task completion.\n if (!(message.data instanceof ArrayBuffer)) {\n onPartialContext({\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n partial: true,\n },\n ],\n bufferedRanges: ctx.bufferedRanges,\n });\n }\n\n await appendSegment(sourceBuffer, message.data, taskSignal);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n },\n { signal }\n );\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n },\n { signal }\n );\n}\n\ntype MessageTaskFactory<T extends SourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies { [K in SourceBufferMessage['type']]: MessageTaskFactory<Extract<SourceBufferMessage, { type: K }>> };\n\nfunction messageToTask(message: SourceBufferMessage, options: MessageTaskOptions): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n const snapshotSignal = signal<SourceBufferActorSnapshot>({\n status: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n });\n\n const runner = new SerialRunner();\n\n // Applies the completed context atomically with the idle transition.\n // If the actor was destroyed while the operation was in flight, preserve\n // 'destroyed' — do not regress to 'idle'.\n function applyResult(newContext: SourceBufferActorContext): void {\n const status = snapshotSignal.get().status === 'destroyed' ? 'destroyed' : 'idle';\n snapshotSignal.set({ status, context: newContext });\n }\n\n function handleError(e: unknown): never {\n // TODO: QuotaExceededError and other SourceBuffer errors leave the physical\n // buffer in an unknown partial state while context goes unchanged. A future\n // improvement should detect QuotaExceededError specifically and use total\n // bytes-in-buffer as a heuristic to identify the effective buffer capacity,\n // enabling targeted flush-and-retry rather than silent model drift.\n const status = snapshotSignal.get().status === 'destroyed' ? 'destroyed' : 'idle';\n update(snapshotSignal, { status });\n throw e;\n }\n\n return {\n get snapshot(): ReadonlySignal<SourceBufferActorSnapshot> {\n return snapshotSignal;\n },\n\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void> {\n if (snapshotSignal.get().status !== 'idle') {\n return Promise.reject(\n new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`)\n );\n }\n\n // Transition synchronously so any subsequent send/batch within the same\n // tick is rejected — the actor is now committed to this operation.\n update(snapshotSignal, { status: 'updating' });\n\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n snapshotSignal.set({ status: 'updating', context: ctx });\n };\n\n const task = messageToTask(message, {\n signal,\n getCtx: () => snapshotSignal.get().context,\n sourceBuffer,\n onPartialContext,\n });\n\n return runner.schedule(task).then(applyResult).catch(handleError);\n },\n\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void> {\n if (snapshotSignal.get().status !== 'idle') {\n return Promise.reject(\n new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`)\n );\n }\n\n if (messages.length === 0) return Promise.resolve();\n\n // Transition synchronously — the entire batch is one 'updating' period.\n update(snapshotSignal, { status: 'updating' });\n\n // Each message is its own Task on the runner, executed in submission order.\n // workingCtx threads the result of each task into the next without\n // writing to the signal between steps — context is only written atomically\n // when the last task completes.\n //\n // workingCtx is captured here (synchronously after status → 'updating'),\n // so it reflects the current context at the moment the batch was accepted.\n // This is correct: status is now 'updating' so no other sender can modify\n // context between this line and the first task executing.\n //\n // NOTE: if an intermediate task fails (e.g. SourceBuffer error event),\n // workingCtx is not updated for that step and subsequent tasks in the\n // batch will operate on a stale context. This is an edge case — happy-path\n // appends do not fail — but worth revisiting if MSE error recovery lands.\n let workingCtx = snapshotSignal.get().context;\n\n // Partial context updates from streaming appends write to the signal\n // directly so external subscribers see in-progress state, but workingCtx\n // is only advanced on task completion to preserve batch context threading.\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n snapshotSignal.set({ status: 'updating', context: ctx });\n };\n\n for (const message of messages.slice(0, -1)) {\n const task = messageToTask(message, { signal, getCtx: () => workingCtx, sourceBuffer, onPartialContext });\n const result = runner.schedule(task);\n result.then((newCtx) => {\n workingCtx = newCtx;\n });\n }\n\n const lastTask = messageToTask(messages[messages.length - 1]!, {\n signal,\n getCtx: () => workingCtx,\n sourceBuffer,\n onPartialContext,\n });\n return runner.schedule(lastTask).then(applyResult).catch(handleError);\n },\n\n destroy(): void {\n update(snapshotSignal, { status: 'destroyed' });\n runner.destroy();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"source-buffer-actor.js","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../core/create-machine-actor';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { Segment, Track } from '../../core/types';\nimport { type AppendData, appendSegment } from './append-segment';\nimport { flushBuffer } from './buffer-flusher';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type { AppendData };\n\nexport type AppendInitMessage = { type: 'append-init'; data: AppendData; meta: { trackId: Track['id'] } };\nexport type AppendSegmentMessage = { type: 'append-segment'; data: AppendData; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type IndividualSourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\nexport type BatchMessage = { type: 'batch'; messages: IndividualSourceBufferMessage[] };\nexport type CancelMessage = { type: 'cancel' };\n\n/** All messages accepted by a SourceBufferActor. */\nexport type SourceBufferMessage = IndividualSourceBufferMessage | BatchMessage | CancelMessage;\n\n/** Finite states of the actor. */\nexport type SourceBufferActorState = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n segments: Array<\n Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n trackBandwidth?: number;\n /**\n * True while a streaming append is in progress for this segment.\n * The segment's data is partially present in the SourceBuffer.\n * Downstream code must not treat a partial segment as fully buffered.\n */\n partial?: boolean;\n }\n >;\n bufferedRanges: BufferedRange[];\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport type SourceBufferActor = MessageActor<SourceBufferActorState, SourceBufferActorContext, SourceBufferMessage>;\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getContext at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n getContext: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n setContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId };\n });\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { getContext, sourceBuffer, setContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. The epsilon guards against floating-point drift in\n // parsed timestamps.\n const EPSILON = 0.0001;\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);\n\n // For streaming data: emit partial state before the first chunk so\n // downstream code can see the in-progress segment and treat it as\n // incomplete. ArrayBuffer appends are atomic so no partial state is\n // needed — context is updated once at task completion.\n if (!(message.data instanceof ArrayBuffer)) {\n setContext({\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n partial: true,\n },\n ],\n bufferedRanges: ctx.bufferedRanges,\n });\n }\n\n await appendSegment(sourceBuffer, message.data, taskSignal);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n });\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n });\n}\n\ntype MessageTaskFactory<T extends IndividualSourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies {\n [K in IndividualSourceBufferMessage['type']]: MessageTaskFactory<Extract<IndividualSourceBufferMessage, { type: K }>>;\n};\n\nfunction messageToTask(\n message: IndividualSourceBufferMessage,\n options: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n type UserState = Exclude<SourceBufferActorState, 'destroyed'>;\n\n const handleError = (e: unknown): void => {\n if (!(e instanceof Error && e.name === 'AbortError')) {\n console.error('SourceBuffer operation failed:', e);\n }\n };\n\n type Ctx = HandlerContext<UserState, SourceBufferActorContext, () => SerialRunner>;\n\n const onMessage = (msg: IndividualSourceBufferMessage, { transition, setContext, getContext, runner }: Ctx): void => {\n transition('updating');\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n };\n\n return createMachineActor<UserState, SourceBufferActorContext, SourceBufferMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n states: {\n idle: {\n on: {\n 'append-init': onMessage,\n 'append-segment': onMessage,\n remove: onMessage,\n batch: (msg, { transition, setContext, getContext, runner }) => {\n const { messages } = msg;\n if (messages.length === 0) return;\n\n transition('updating');\n messages.forEach((msg) => {\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n });\n },\n },\n },\n updating: {\n // Automatically return to idle once all scheduled tasks settle.\n onSettled: 'idle',\n on: {\n // Abort all in-progress and pending tasks. onSettled handles → 'idle'\n // once the aborted tasks drain.\n cancel: (_, { runner }) => {\n runner.abortAll();\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","names":[],"sources":["../../../../src/dom/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n * 3. fetchResolvableStream() - Stream body as Uint8Array chunks\n */\n\nimport type { AddressableObject } from '../../core/types';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: AddressableObject,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"fetch.js","names":[],"sources":["../../../../src/dom/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n * 3. fetchResolvableStream() - Stream body as Uint8Array chunks\n */\n\nimport type { AddressableObject } from '../../core/types';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: AddressableObject,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,gBAAgB,aAAgC,SAA0C;CAC9G,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAmDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM"}
|
|
@@ -13,9 +13,7 @@ import { syncPreloadAttribute } from "../../core/features/sync-preload-attribute
|
|
|
13
13
|
import { endOfStream } from "../features/end-of-stream.js";
|
|
14
14
|
import { setupMediaSource } from "../features/setup-mediasource.js";
|
|
15
15
|
import { setupSourceBuffers } from "../features/setup-sourcebuffer.js";
|
|
16
|
-
import {
|
|
17
|
-
import { syncSelectedTextTrackFromDom } from "../features/sync-selected-text-track-from-dom.js";
|
|
18
|
-
import { syncTextTrackModes } from "../features/sync-text-track-modes.js";
|
|
16
|
+
import { syncTextTracks } from "../features/sync-text-tracks.js";
|
|
19
17
|
import { updateDuration } from "../features/update-duration.js";
|
|
20
18
|
//#region src/dom/playback-engine/engine.ts
|
|
21
19
|
/**
|
|
@@ -119,15 +117,7 @@ function createPlaybackEngine(config = {}) {
|
|
|
119
117
|
state,
|
|
120
118
|
owners
|
|
121
119
|
}),
|
|
122
|
-
|
|
123
|
-
state,
|
|
124
|
-
owners
|
|
125
|
-
}),
|
|
126
|
-
syncTextTrackModes({
|
|
127
|
-
state,
|
|
128
|
-
owners
|
|
129
|
-
}),
|
|
130
|
-
syncSelectedTextTrackFromDom({
|
|
120
|
+
syncTextTracks({
|
|
131
121
|
state,
|
|
132
122
|
owners
|
|
133
123
|
}),
|
|
@@ -140,7 +130,8 @@ function createPlaybackEngine(config = {}) {
|
|
|
140
130
|
state,
|
|
141
131
|
owners,
|
|
142
132
|
destroy: () => {
|
|
143
|
-
cleanups.forEach((cleanup) => cleanup());
|
|
133
|
+
cleanups.forEach((cleanup) => typeof cleanup === "function" ? cleanup() : cleanup.destroy());
|
|
134
|
+
for (const value of Object.values(owners.get())) if (value !== null && typeof value === "object" && typeof value.destroy === "function") value.destroy();
|
|
144
135
|
destroyVttParser();
|
|
145
136
|
}
|
|
146
137
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","names":[],"sources":["../../../../src/dom/playback-engine/engine.ts"],"sourcesContent":["import type { BandwidthState } from '../../core/abr/bandwidth-estimator';\nimport { calculatePresentationDuration } from '../../core/features/calculate-presentation-duration';\nimport { switchQuality } from '../../core/features/quality-switching';\nimport { resolvePresentation } from '../../core/features/resolve-presentation';\nimport { resolveTrack } from '../../core/features/resolve-track';\nimport { selectAudioTrack, selectTextTrack, selectVideoTrack } from '../../core/features/select-tracks';\nimport { syncPreloadAttribute } from '../../core/features/sync-preload-attribute';\nimport type { ReadonlySignal, Signal } from '../../core/signals/primitives';\nimport { signal } from '../../core/signals/primitives';\nimport { endOfStream } from '../features/end-of-stream';\nimport { loadSegments } from '../features/load-segments';\nimport type { TextTrackBufferState } from '../features/load-text-track-cues';\nimport { loadTextTrackCues } from '../features/load-text-track-cues';\nimport { setupMediaSource } from '../features/setup-mediasource';\nimport { setupSourceBuffers } from '../features/setup-sourcebuffer';\nimport { setupTextTracks } from '../features/setup-text-tracks';\nimport { syncSelectedTextTrackFromDom } from '../features/sync-selected-text-track-from-dom';\nimport { syncTextTrackModes } from '../features/sync-text-track-modes';\nimport { trackCurrentTime } from '../features/track-current-time';\nimport { trackPlaybackInitiated } from '../features/track-playback-initiated';\nimport { updateDuration } from '../features/update-duration';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { destroyVttParser } from '../text/parse-vtt-segment';\n\n/**\n * Configuration for the playback engine.\n */\nexport interface PlaybackEngineConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching text track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n */\n enableDefaultTrack?: boolean;\n}\n\n/**\n * Combined state shape for the playback engine.\n * Includes all state needed by orchestrations.\n */\nexport interface PlaybackEngineState {\n // Presentation state\n presentation?: any;\n preload?: 'auto' | 'metadata' | 'none';\n\n // Track selection state\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n // NOTE: Text Tracks (subtitles/ccs) can be unselected\n selectedTextTrackId?: string;\n\n // Bandwidth estimation state\n bandwidthState?: BandwidthState;\n\n // ABR control — set to true to suppress automatic quality switching (manual selection mode).\n // TODO: replace with separate manualVideoTrackId / abrVideoTrackId fields so the two\n // concerns don't share a field; see quality-switching.ts for the full design note.\n abrDisabled?: boolean;\n\n // Text track buffer state (tracks loaded VTT segments per text track ID)\n textBufferState?: TextTrackBufferState;\n\n // Current playback position (mirrored from mediaElement via trackCurrentTime)\n currentTime?: number;\n\n // True once the user has initiated playback (enables segment loading regardless of preload)\n playbackInitiated?: boolean;\n}\n\n/**\n * Combined owners shape for the playback engine.\n * Includes all mutable platform objects.\n */\nexport interface PlaybackEngineOwners {\n // Media element\n mediaElement?: HTMLMediaElement | undefined;\n\n // MediaSource\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n\n // SourceBuffers and their actors (created together by setupSourceBuffer)\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n\n // Text tracks (track elements by ID)\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Playback engine instance.\n */\nexport interface PlaybackEngine {\n /**\n * Reactive state signal (for inspection/testing).\n */\n state: Signal<PlaybackEngineState>;\n\n /**\n * Mutable owners signal (for inspection/testing).\n */\n owners: Signal<PlaybackEngineOwners>;\n\n /**\n * Cleanup function to destroy all orchestrations.\n */\n destroy: () => void;\n}\n\n/**\n * Create a POC playback engine.\n *\n * Wires together all orchestrations to create a reactive playback pipeline:\n * 1. Resolve presentation (multivariant playlist)\n * 2. Select initial video and audio tracks\n * 3. Resolve selected tracks (media playlists)\n * 4. Setup MediaSource\n * 5. Setup SourceBuffers for video and audio\n *\n * Note: This is a POC - does not yet load/append segments.\n *\n * @param config - Playback engine configuration\n * @returns Playback engine instance with state, owners, and destroy function\n *\n * @example\n * const engine = createPlaybackEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * });\n *\n * // Initialize by setting state and owners\n * engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });\n * engine.state.set({\n * ...engine.state.get(),\n * presentation: { url: 'https://example.com/playlist.m3u8' },\n * preload: 'auto',\n * });\n *\n * // Inspect state\n * console.log(engine.state.get());\n *\n * // Cleanup\n * engine.destroy();\n */\nexport function createPlaybackEngine(config: PlaybackEngineConfig = {}): PlaybackEngine {\n // Create reactive state and owners as signals\n const state = signal<PlaybackEngineState>({\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n });\n const owners = signal<PlaybackEngineOwners>({});\n\n // Wire up orchestrations\n const cleanups = [\n // 0a. Sync preload attribute from mediaElement → state.preload\n // Normalises '' (absent attribute) to 'auto' (browser default).\n syncPreloadAttribute({ state, owners }),\n\n // 0b. Track media element play event → state.playbackInitiated\n // Enables preload=\"none\" resolution via native controls / element.play()\n trackPlaybackInitiated({ state, owners }),\n\n // 1. Resolve presentation (URL already in state)\n resolvePresentation({ state }),\n\n // 2. Select initial tracks (when presentation loads)\n selectVideoTrack(\n { state },\n {\n type: 'video',\n ...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),\n }\n ),\n selectAudioTrack(\n { state },\n {\n type: 'audio',\n ...(config.preferredAudioLanguage !== undefined && { preferredAudioLanguage: config.preferredAudioLanguage }),\n }\n ),\n selectTextTrack(\n { state },\n {\n type: 'text',\n ...(config.preferredSubtitleLanguage !== undefined && {\n preferredSubtitleLanguage: config.preferredSubtitleLanguage,\n }),\n ...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),\n ...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),\n }\n ),\n\n // 3. Resolve selected tracks (fetch media playlists)\n resolveTrack({ state }, { type: 'video' as const }),\n resolveTrack({ state }, { type: 'audio' as const }),\n resolveTrack({ state }, { type: 'text' as const }),\n\n // 3.5. Calculate presentation duration from resolved tracks\n calculatePresentationDuration({ state }),\n\n // 4. Setup MediaSource (when presentation loaded)\n setupMediaSource({ state, owners }),\n\n // 4.5. Update MediaSource duration (when presentation duration available)\n updateDuration({ state, owners }),\n\n // 5. Setup SourceBuffers (when MediaSource ready and all selected tracks resolved)\n // Both SourceBuffers are created in a single synchronous operation to guarantee\n // neither is visible to loadSegments before the other exists — preventing the\n // Firefox bug where appending video data before audio SB is created causes\n // mozHasAudio to be permanently false.\n setupSourceBuffers({ state, owners }),\n\n // 5.5. Track currentTime from mediaElement (feeds forward buffer management)\n //\n // NOTE: SourceBufferActor wiring is intentionally absent here in Phase 1.\n //\n // Attempting to wire actors into the engine at this layer revealed a\n // brittleness in the current architecture: every patch to `owners` —\n // regardless of which field changed — wakes up ALL combineLatest subscribers\n // (loadSegments, endOfStream, etc.). Storing actor references in owners\n // caused loadSegments to re-evaluate mid-task, store a spurious pending\n // state, and run a second loading cycle on completion.\n //\n // This means any future feature that needs to introduce new reactive state\n // alongside SourceBuffers faces the same hazard. The right fix is for\n // loadSegments (and other orchestrations) to route their MSE operations\n // through the actor directly, at which point the actor lifecycle is\n // co-located with its consumer rather than managed centrally here.\n //\n // Actor wiring will be introduced in Phase 2 as part of the loadSegments\n // refactor. See .claude/plans/spf/buffer-state-shadow-actual-model.md.\n trackCurrentTime({ state, owners }),\n\n // 5.75. ABR quality switching (reacts to bandwidth samples from loadSegments)\n switchQuality(\n { state },\n config.initialBandwidth !== undefined ? { defaultBandwidth: config.initialBandwidth } : {}\n ),\n\n // 6. Load segments (when SourceBuffer ready and track resolved)\n loadSegments({ state, owners }, { type: 'video' }),\n loadSegments({ state, owners }, { type: 'audio' }),\n\n // 6.5. Signal end of stream when all segments loaded\n endOfStream({ state, owners }),\n\n // 7. Setup text tracks (when mediaElement and presentation ready)\n setupTextTracks({ state, owners }),\n\n // 8. Sync text track modes (when track selected and track elements created)\n syncTextTrackModes({ state, owners }),\n\n // 8.5. Bridge DOM text track mode changes → selectedTextTrackId\n // Detects when external code (e.g. captions button via toggleSubtitles())\n // sets a subtitle/caption track to 'showing' and reflects that into SPF\n // state, which in turn drives loadTextTrackCues.\n syncSelectedTextTrackFromDom({ state, owners }),\n\n // 9. Load text track cues (when track resolved and mode set)\n loadTextTrackCues({ state, owners }),\n ];\n\n // Return engine instance\n return {\n state,\n owners,\n destroy: () => {\n cleanups.forEach((cleanup) => cleanup());\n destroyVttParser();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0KA,SAAgB,qBAAqB,SAA+B,EAAE,EAAkB;CAEtF,MAAM,QAAQ,OAA4B,EACxC,gBAAgB;EACd,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,EACF,CAAC;CACF,MAAM,SAAS,OAA6B,EAAE,CAAC;CAG/C,MAAM,WAAW;EAGf,qBAAqB;GAAE;GAAO;GAAQ,CAAC;EAIvC,uBAAuB;GAAE;GAAO;GAAQ,CAAC;EAGzC,oBAAoB,EAAE,OAAO,CAAC;EAG9B,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,qBAAqB,KAAA,KAAa,EAAE,kBAAkB,OAAO,kBAAkB;GAC3F,CACF;EACD,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,2BAA2B,KAAA,KAAa,EAAE,wBAAwB,OAAO,wBAAwB;GAC7G,CACF;EACD,gBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,8BAA8B,KAAA,KAAa,EACpD,2BAA2B,OAAO,2BACnC;GACD,GAAI,OAAO,wBAAwB,KAAA,KAAa,EAAE,qBAAqB,OAAO,qBAAqB;GACnG,GAAI,OAAO,uBAAuB,KAAA,KAAa,EAAE,oBAAoB,OAAO,oBAAoB;GACjG,CACF;EAGD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,QAAiB,CAAC;EAGlD,8BAA8B,EAAE,OAAO,CAAC;EAGxC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,eAAe;GAAE;GAAO;GAAQ,CAAC;EAOjC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAqBrC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,cACE,EAAE,OAAO,EACT,OAAO,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,OAAO,kBAAkB,GAAG,EAAE,CAC3F;EAGD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAClD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAGlD,YAAY;GAAE;GAAO;GAAQ,CAAC;EAG9B,gBAAgB;GAAE;GAAO;GAAQ,CAAC;EAGlC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAMrC,6BAA6B;GAAE;GAAO;GAAQ,CAAC;EAG/C,kBAAkB;GAAE;GAAO;GAAQ,CAAC;EACrC;AAGD,QAAO;EACL;EACA;EACA,eAAe;AACb,YAAS,SAAS,YAAY,SAAS,CAAC;AACxC,qBAAkB;;EAErB"}
|
|
1
|
+
{"version":3,"file":"engine.js","names":[],"sources":["../../../../src/dom/playback-engine/engine.ts"],"sourcesContent":["import type { BandwidthState } from '../../core/abr/bandwidth-estimator';\nimport { calculatePresentationDuration } from '../../core/features/calculate-presentation-duration';\nimport { switchQuality } from '../../core/features/quality-switching';\nimport { resolvePresentation } from '../../core/features/resolve-presentation';\nimport { resolveTrack } from '../../core/features/resolve-track';\nimport { selectAudioTrack, selectTextTrack, selectVideoTrack } from '../../core/features/select-tracks';\nimport { syncPreloadAttribute } from '../../core/features/sync-preload-attribute';\nimport type { ReadonlySignal, Signal } from '../../core/signals/primitives';\nimport { signal } from '../../core/signals/primitives';\nimport { endOfStream } from '../features/end-of-stream';\nimport { loadSegments } from '../features/load-segments';\nimport { loadTextTrackCues } from '../features/load-text-track-cues';\nimport { setupMediaSource } from '../features/setup-mediasource';\nimport { setupSourceBuffers } from '../features/setup-sourcebuffer';\nimport { syncTextTracks } from '../features/sync-text-tracks';\nimport type { TextTrackSegmentLoaderActor } from '../features/text-track-segment-loader-actor';\nimport type { TextTracksActor } from '../features/text-tracks-actor';\nimport { trackCurrentTime } from '../features/track-current-time';\nimport { trackPlaybackInitiated } from '../features/track-playback-initiated';\nimport { updateDuration } from '../features/update-duration';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { destroyVttParser } from '../text/parse-vtt-segment';\n\n/**\n * Configuration for the playback engine.\n */\nexport interface PlaybackEngineConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching text track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n */\n enableDefaultTrack?: boolean;\n}\n\n/**\n * Combined state shape for the playback engine.\n * Includes all state needed by orchestrations.\n */\nexport interface PlaybackEngineState {\n // Presentation state\n presentation?: any;\n preload?: 'auto' | 'metadata' | 'none';\n\n // Track selection state\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n // NOTE: Text Tracks (subtitles/ccs) can be unselected\n selectedTextTrackId?: string;\n\n // Bandwidth estimation state\n bandwidthState?: BandwidthState;\n\n // ABR control — set to true to suppress automatic quality switching (manual selection mode).\n // TODO: replace with separate manualVideoTrackId / abrVideoTrackId fields so the two\n // concerns don't share a field; see quality-switching.ts for the full design note.\n abrDisabled?: boolean;\n\n // Current playback position (mirrored from mediaElement via trackCurrentTime)\n currentTime?: number;\n\n // True once the user has initiated playback (enables segment loading regardless of preload)\n playbackInitiated?: boolean;\n}\n\n/**\n * Combined owners shape for the playback engine.\n * Includes all mutable platform objects.\n */\nexport interface PlaybackEngineOwners {\n // Media element\n mediaElement?: HTMLMediaElement | undefined;\n\n // MediaSource\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n\n // SourceBuffers and their actors (created together by setupSourceBuffer)\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n\n // Text track actors (written by loadTextTrackCues; destroyed by engine on destroy)\n textTracksActor?: TextTracksActor;\n segmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n/**\n * Playback engine instance.\n */\nexport interface PlaybackEngine {\n /**\n * Reactive state signal (for inspection/testing).\n */\n state: Signal<PlaybackEngineState>;\n\n /**\n * Mutable owners signal (for inspection/testing).\n */\n owners: Signal<PlaybackEngineOwners>;\n\n /**\n * Cleanup function to destroy all orchestrations.\n */\n destroy: () => void;\n}\n\n/**\n * Create a POC playback engine.\n *\n * Wires together all orchestrations to create a reactive playback pipeline:\n * 1. Resolve presentation (multivariant playlist)\n * 2. Select initial video and audio tracks\n * 3. Resolve selected tracks (media playlists)\n * 4. Setup MediaSource\n * 5. Setup SourceBuffers for video and audio\n *\n * Note: This is a POC - does not yet load/append segments.\n *\n * @param config - Playback engine configuration\n * @returns Playback engine instance with state, owners, and destroy function\n *\n * @example\n * const engine = createPlaybackEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * });\n *\n * // Initialize by setting state and owners\n * engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });\n * engine.state.set({\n * ...engine.state.get(),\n * presentation: { url: 'https://example.com/playlist.m3u8' },\n * preload: 'auto',\n * });\n *\n * // Inspect state\n * console.log(engine.state.get());\n *\n * // Cleanup\n * engine.destroy();\n */\nexport function createPlaybackEngine(config: PlaybackEngineConfig = {}): PlaybackEngine {\n // Create reactive state and owners as signals\n const state = signal<PlaybackEngineState>({\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n });\n const owners = signal<PlaybackEngineOwners>({});\n\n // Wire up orchestrations\n const cleanups = [\n // 0a. Sync preload attribute from mediaElement → state.preload\n // Normalises '' (absent attribute) to 'auto' (browser default).\n syncPreloadAttribute({ state, owners }),\n\n // 0b. Track media element play event → state.playbackInitiated\n // Enables preload=\"none\" resolution via native controls / element.play()\n trackPlaybackInitiated({ state, owners }),\n\n // 1. Resolve presentation (URL already in state)\n resolvePresentation({ state }),\n\n // 2. Select initial tracks (when presentation loads)\n selectVideoTrack(\n { state },\n {\n type: 'video',\n ...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),\n }\n ),\n selectAudioTrack(\n { state },\n {\n type: 'audio',\n ...(config.preferredAudioLanguage !== undefined && { preferredAudioLanguage: config.preferredAudioLanguage }),\n }\n ),\n selectTextTrack(\n { state },\n {\n type: 'text',\n ...(config.preferredSubtitleLanguage !== undefined && {\n preferredSubtitleLanguage: config.preferredSubtitleLanguage,\n }),\n ...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),\n ...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),\n }\n ),\n\n // 3. Resolve selected tracks (fetch media playlists)\n resolveTrack({ state }, { type: 'video' as const }),\n resolveTrack({ state }, { type: 'audio' as const }),\n resolveTrack({ state }, { type: 'text' as const }),\n\n // 3.5. Calculate presentation duration from resolved tracks\n calculatePresentationDuration({ state }),\n\n // 4. Setup MediaSource (when presentation loaded)\n setupMediaSource({ state, owners }),\n\n // 4.5. Update MediaSource duration (when presentation duration available)\n updateDuration({ state, owners }),\n\n // 5. Setup SourceBuffers (when MediaSource ready and all selected tracks resolved)\n // Both SourceBuffers are created in a single synchronous operation to guarantee\n // neither is visible to loadSegments before the other exists — preventing the\n // Firefox bug where appending video data before audio SB is created causes\n // mozHasAudio to be permanently false.\n setupSourceBuffers({ state, owners }),\n\n // 5.5. Track currentTime from mediaElement (feeds forward buffer management)\n //\n // NOTE: SourceBufferActor wiring is intentionally absent here in Phase 1.\n //\n // Attempting to wire actors into the engine at this layer revealed a\n // brittleness in the current architecture: every patch to `owners` —\n // regardless of which field changed — wakes up ALL combineLatest subscribers\n // (loadSegments, endOfStream, etc.). Storing actor references in owners\n // caused loadSegments to re-evaluate mid-task, store a spurious pending\n // state, and run a second loading cycle on completion.\n //\n // This means any future feature that needs to introduce new reactive state\n // alongside SourceBuffers faces the same hazard. The right fix is for\n // loadSegments (and other orchestrations) to route their MSE operations\n // through the actor directly, at which point the actor lifecycle is\n // co-located with its consumer rather than managed centrally here.\n //\n // Actor wiring will be introduced in Phase 2 as part of the loadSegments\n // refactor. See .claude/plans/spf/buffer-state-shadow-actual-model.md.\n trackCurrentTime({ state, owners }),\n\n // 5.75. ABR quality switching (reacts to bandwidth samples from loadSegments)\n switchQuality(\n { state },\n config.initialBandwidth !== undefined ? { defaultBandwidth: config.initialBandwidth } : {}\n ),\n\n // 6. Load segments (when SourceBuffer ready and track resolved)\n loadSegments({ state, owners }, { type: 'video' }),\n loadSegments({ state, owners }, { type: 'audio' }),\n\n // 6.5. Signal end of stream when all segments loaded\n endOfStream({ state, owners }),\n\n // 7-8.5. Text track sync: setup, mode sync, and DOM bridge.\n // Consolidates setupTextTracks, syncTextTrackModes, syncSelectedTextTrackFromDom.\n syncTextTracks({ state, owners }),\n\n // 9. Load text track cues (when track resolved and mode set)\n loadTextTrackCues({ state, owners }),\n ];\n\n // Return engine instance\n return {\n state,\n owners,\n destroy: () => {\n cleanups.forEach((cleanup) => (typeof cleanup === 'function' ? cleanup() : cleanup.destroy()));\n // Destroy any actors that orchestrations wrote into owners during their lifetime.\n for (const value of Object.values(owners.get())) {\n if (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as { destroy?: unknown }).destroy === 'function'\n ) {\n (value as { destroy(): void }).destroy();\n }\n }\n destroyVttParser();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuKA,SAAgB,qBAAqB,SAA+B,EAAE,EAAkB;CAEtF,MAAM,QAAQ,OAA4B,EACxC,gBAAgB;EACd,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,EACF,CAAC;CACF,MAAM,SAAS,OAA6B,EAAE,CAAC;CAG/C,MAAM,WAAW;EAGf,qBAAqB;GAAE;GAAO;GAAQ,CAAC;EAIvC,uBAAuB;GAAE;GAAO;GAAQ,CAAC;EAGzC,oBAAoB,EAAE,OAAO,CAAC;EAG9B,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,qBAAqB,KAAA,KAAa,EAAE,kBAAkB,OAAO,kBAAkB;GAC3F,CACF;EACD,iBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,2BAA2B,KAAA,KAAa,EAAE,wBAAwB,OAAO,wBAAwB;GAC7G,CACF;EACD,gBACE,EAAE,OAAO,EACT;GACE,MAAM;GACN,GAAI,OAAO,8BAA8B,KAAA,KAAa,EACpD,2BAA2B,OAAO,2BACnC;GACD,GAAI,OAAO,wBAAwB,KAAA,KAAa,EAAE,qBAAqB,OAAO,qBAAqB;GACnG,GAAI,OAAO,uBAAuB,KAAA,KAAa,EAAE,oBAAoB,OAAO,oBAAoB;GACjG,CACF;EAGD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,SAAkB,CAAC;EACnD,aAAa,EAAE,OAAO,EAAE,EAAE,MAAM,QAAiB,CAAC;EAGlD,8BAA8B,EAAE,OAAO,CAAC;EAGxC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,eAAe;GAAE;GAAO;GAAQ,CAAC;EAOjC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAqBrC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,cACE,EAAE,OAAO,EACT,OAAO,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,OAAO,kBAAkB,GAAG,EAAE,CAC3F;EAGD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAClD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAGlD,YAAY;GAAE;GAAO;GAAQ,CAAC;EAI9B,eAAe;GAAE;GAAO;GAAQ,CAAC;EAGjC,kBAAkB;GAAE;GAAO;GAAQ,CAAC;EACrC;AAGD,QAAO;EACL;EACA;EACA,eAAe;AACb,YAAS,SAAS,YAAa,OAAO,YAAY,aAAa,SAAS,GAAG,QAAQ,SAAS,CAAE;AAE9F,QAAK,MAAM,SAAS,OAAO,OAAO,OAAO,KAAK,CAAC,CAC7C,KACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY,WAEnD,OAA8B,SAAS;AAG5C,qBAAkB;;EAErB"}
|
package/dist/dev/core/actor.d.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Machine, MachineSnapshot } from "./machine.js";
|
|
2
2
|
|
|
3
3
|
//#region src/core/actor.d.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Complete actor snapshot: finite state + non-finite context.
|
|
6
|
+
* Extends `MachineSnapshot` with context — the non-finite data managed by the actor.
|
|
7
|
+
*/
|
|
8
|
+
interface ActorSnapshot<State extends string, Context extends object> extends MachineSnapshot<State> {
|
|
7
9
|
context: Context;
|
|
8
10
|
}
|
|
9
|
-
/** Generic actor interface: owns its snapshot
|
|
10
|
-
interface SignalActor<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/** Generic actor interface: owns its snapshot as a reactive signal. */
|
|
12
|
+
interface SignalActor<State extends string, Context extends object> extends Machine<ActorSnapshot<State, Context>> {}
|
|
13
|
+
/**
|
|
14
|
+
* A message-driven actor with no reactive snapshot.
|
|
15
|
+
*
|
|
16
|
+
* Use for actors that coordinate async work but have no state that external
|
|
17
|
+
* consumers need to observe. Analogous to XState's `fromCallback`.
|
|
18
|
+
*/
|
|
19
|
+
interface CallbackActor<Message extends {
|
|
20
|
+
type: string;
|
|
21
|
+
}> {
|
|
22
|
+
send(message: Message): void;
|
|
14
23
|
destroy(): void;
|
|
15
24
|
}
|
|
16
25
|
//#endregion
|
|
17
|
-
export { SignalActor };
|
|
26
|
+
export { ActorSnapshot, CallbackActor, SignalActor };
|
|
18
27
|
//# sourceMappingURL=actor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor.d.ts","names":[],"sources":["../../../src/core/actor.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"actor.d.ts","names":[],"sources":["../../../src/core/actor.ts"],"mappings":";;;;;;;UAeiB,aAAA,uDAAoE,eAAA,CAAgB,KAAA;EACnG,OAAA,EAAS,OAAA;AAAA;;UAIM,WAAA,uDACP,OAAA,CAAQ,aAAA,CAAc,KAAA,EAAO,OAAA;AADvC;;;;;;AAAA,UASiB,aAAA;EAAgC,IAAA;AAAA;EAC/C,IAAA,CAAK,OAAA,EAAS,OAAA;EACd,OAAA;AAAA"}
|