@videojs/spf 10.0.0-beta.10 → 10.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -40
- package/dist/default/core/abr/bandwidth-estimator.js +95 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +53 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +69 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +77 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +66 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +130 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/features/sync-preload-attribute.js +26 -0
- package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +82 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +214 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/signals/effect.js +41 -0
- package/dist/default/core/signals/effect.js.map +1 -0
- package/dist/default/core/signals/primitives.js +32 -0
- package/dist/default/core/signals/primitives.js.map +1 -0
- package/dist/default/core/task.js +122 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +39 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +145 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +192 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +161 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +196 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +48 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/setup-text-tracks.js +67 -0
- package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js +54 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/default/dom/features/sync-text-track-modes.js +33 -0
- package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +49 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +53 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/default/dom/features/track-playback-rate.js +32 -0
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +78 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +146 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +176 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +55 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +95 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +151 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +10 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
- package/dist/default/index.js.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +18 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +69 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +77 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.js +66 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.js +130 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/features/sync-preload-attribute.js +26 -0
- package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +82 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +214 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/signals/effect.d.ts +15 -0
- package/dist/dev/core/signals/effect.d.ts.map +1 -0
- package/dist/dev/core/signals/effect.js +41 -0
- package/dist/dev/core/signals/effect.js.map +1 -0
- package/dist/dev/core/signals/primitives.d.ts +10 -0
- package/dist/dev/core/signals/primitives.d.ts.map +1 -0
- package/dist/dev/core/signals/primitives.js +32 -0
- package/dist/dev/core/signals/primitives.js.map +1 -0
- package/dist/dev/core/task.js +122 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +173 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +39 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +145 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +192 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +161 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +196 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +48 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/setup-text-tracks.js +67 -0
- package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +54 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/dev/dom/features/sync-text-track-modes.js +33 -0
- package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +49 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +44 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +53 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +32 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +78 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +146 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +176 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +55 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +95 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +123 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +151 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +4 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +24 -23
- package/dist/adapter-25AtMN8s.d.ts +0 -719
- package/dist/adapter-25AtMN8s.d.ts.map +0 -1
- package/dist/adapter-CflgYzjF.js +0 -3556
- package/dist/adapter-CflgYzjF.js.map +0 -1
- package/dist/dom/index.d.ts +0 -221
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js +0 -38
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { effect } from "../../core/signals/effect.js";
|
|
2
|
+
import { computed } from "../../core/signals/primitives.js";
|
|
3
|
+
import { isResolvedTrack } from "../../core/types/index.js";
|
|
4
|
+
import { getSelectedTrack } from "../../core/utils/track-selection.js";
|
|
5
|
+
//#region src/dom/features/end-of-stream.ts
|
|
6
|
+
/**
|
|
7
|
+
* Check if the last segment of a track has been appended to a SourceBuffer.
|
|
8
|
+
*
|
|
9
|
+
* Checks by segment ID rather than a pipeline flag, so it is robust across
|
|
10
|
+
* quality switches (different tracks have different segment IDs) and
|
|
11
|
+
* back-buffer flushes (flushed segment IDs are removed from the model).
|
|
12
|
+
*/
|
|
13
|
+
function isLastSegmentAppended(segments, actor) {
|
|
14
|
+
if (segments.length === 0) return true;
|
|
15
|
+
const lastSeg = segments[segments.length - 1];
|
|
16
|
+
if (!lastSeg) return false;
|
|
17
|
+
return actor?.snapshot.get().context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if the last segment has been appended for each selected track.
|
|
21
|
+
*
|
|
22
|
+
* Handles video-only, audio-only, and video+audio scenarios.
|
|
23
|
+
* A track with no segments (e.g. unresolved) is considered not ready.
|
|
24
|
+
*/
|
|
25
|
+
function hasLastSegmentLoaded(state, owners) {
|
|
26
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
27
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
28
|
+
if (videoTrack && !isResolvedTrack(videoTrack)) return false;
|
|
29
|
+
if (audioTrack && !isResolvedTrack(audioTrack)) return false;
|
|
30
|
+
if (videoTrack && isResolvedTrack(videoTrack)) {
|
|
31
|
+
if (!isLastSegmentAppended(videoTrack.segments, owners.videoBufferActor)) return false;
|
|
32
|
+
}
|
|
33
|
+
if (audioTrack && isResolvedTrack(audioTrack)) {
|
|
34
|
+
if (!isLastSegmentAppended(audioTrack.segments, owners.audioBufferActor)) return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if we can call endOfStream.
|
|
40
|
+
*/
|
|
41
|
+
function canEndStream(state, owners) {
|
|
42
|
+
return !!(owners.mediaSource && state.presentation);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if we should call endOfStream.
|
|
46
|
+
*/
|
|
47
|
+
function shouldEndStream(state, owners) {
|
|
48
|
+
if (!canEndStream(state, owners)) return false;
|
|
49
|
+
const { mediaElement } = owners;
|
|
50
|
+
if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
|
|
51
|
+
if (mediaElement && mediaElement.readyState < HTMLMediaElement.HAVE_METADATA) return false;
|
|
52
|
+
const hasVideoTrack = !!state.selectedVideoTrackId;
|
|
53
|
+
const hasAudioTrack = !!state.selectedAudioTrackId;
|
|
54
|
+
if (hasVideoTrack && !owners.videoBuffer) return false;
|
|
55
|
+
if (hasAudioTrack && !owners.audioBuffer) return false;
|
|
56
|
+
if (owners.videoBufferActor?.snapshot.get().status === "updating") return false;
|
|
57
|
+
if (owners.audioBufferActor?.snapshot.get().status === "updating") return false;
|
|
58
|
+
if (!hasLastSegmentLoaded(state, owners)) return false;
|
|
59
|
+
if (mediaElement) {
|
|
60
|
+
const videoTrack = hasVideoTrack ? getSelectedTrack(state, "video") : void 0;
|
|
61
|
+
const audioTrack = hasAudioTrack ? getSelectedTrack(state, "audio") : void 0;
|
|
62
|
+
const refTrack = videoTrack && isResolvedTrack(videoTrack) ? videoTrack : audioTrack && isResolvedTrack(audioTrack) ? audioTrack : void 0;
|
|
63
|
+
if (refTrack && refTrack.segments.length > 0) {
|
|
64
|
+
const lastSeg = refTrack.segments[refTrack.segments.length - 1];
|
|
65
|
+
if (mediaElement.currentTime < lastSeg.startTime) return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Wait for all currently-updating SourceBufferActors to finish.
|
|
72
|
+
* Uses actor status rather than raw SourceBuffer.updating so the wait is
|
|
73
|
+
* aligned with the same abstraction that owns all buffer operations.
|
|
74
|
+
*/
|
|
75
|
+
function waitForSourceBuffersReady(owners) {
|
|
76
|
+
const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().status === "updating");
|
|
77
|
+
if (updatingActors.length === 0) return Promise.resolve();
|
|
78
|
+
return Promise.all(updatingActors.map((actor) => new Promise((resolve) => {
|
|
79
|
+
let cleanup;
|
|
80
|
+
let resolved = false;
|
|
81
|
+
cleanup = effect(() => {
|
|
82
|
+
if (actor.snapshot.get().status !== "updating") {
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
resolved = true;
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
queueMicrotask(() => cleanup?.());
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}))).then(() => void 0);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the highest buffered end time across all active SourceBuffers.
|
|
94
|
+
* Used to set the final duration from actual container timestamps rather
|
|
95
|
+
* than playlist metadata, which handles both shorter and longer cases.
|
|
96
|
+
*/
|
|
97
|
+
function getMaxBufferedEnd(owners) {
|
|
98
|
+
let max = 0;
|
|
99
|
+
for (const buf of [owners.videoBuffer, owners.audioBuffer]) if (buf && buf.buffered.length > 0) {
|
|
100
|
+
const end = buf.buffered.end(buf.buffered.length - 1);
|
|
101
|
+
if (end > max) max = end;
|
|
102
|
+
}
|
|
103
|
+
return max;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* End of stream task (module-level, pure).
|
|
107
|
+
* Sets the final duration from actual buffered end time, then calls endOfStream().
|
|
108
|
+
*/
|
|
109
|
+
const endOfStreamTask = async ({ currentOwners }, _context) => {
|
|
110
|
+
const { mediaSource } = currentOwners;
|
|
111
|
+
if (mediaSource.readyState === "ended") return;
|
|
112
|
+
await waitForSourceBuffersReady(currentOwners);
|
|
113
|
+
if (mediaSource.readyState !== "open") return;
|
|
114
|
+
const bufferedEnd = getMaxBufferedEnd(currentOwners);
|
|
115
|
+
if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;
|
|
116
|
+
mediaSource.endOfStream();
|
|
117
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Call endOfStream when the last segment has been appended.
|
|
121
|
+
* This signals to the browser that the stream is complete.
|
|
122
|
+
*
|
|
123
|
+
* Per the MSE spec, appendBuffer() remains valid after endOfStream() —
|
|
124
|
+
* seeks that require re-appending earlier segments will still work.
|
|
125
|
+
* What becomes blocked is calling endOfStream() again, addSourceBuffer(),
|
|
126
|
+
* and MediaSource.duration updates.
|
|
127
|
+
*/
|
|
128
|
+
function endOfStream({ state, owners }) {
|
|
129
|
+
const shouldEnd = computed(() => shouldEndStream(state.get(), owners.get()));
|
|
130
|
+
let hasEnded = false;
|
|
131
|
+
return effect(() => {
|
|
132
|
+
if (!shouldEnd.get()) return;
|
|
133
|
+
const currentOwners = owners.get();
|
|
134
|
+
if (hasEnded) {
|
|
135
|
+
if ((currentOwners.mediaSourceReadyState?.get() ?? currentOwners.mediaSource?.readyState) !== "open") return;
|
|
136
|
+
hasEnded = false;
|
|
137
|
+
}
|
|
138
|
+
hasEnded = true;
|
|
139
|
+
endOfStreamTask({ currentOwners }, {}).catch((error) => console.error("Failed to call endOfStream:", error));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { endOfStream };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=end-of-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../src/dom/features/end-of-stream.ts"],"sourcesContent":["import { effect } from '../../core/signals/effect';\nimport { computed, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport type { Presentation } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\n\nexport interface EndOfStreamState extends TrackSelectionState {\n presentation?: Presentation;\n}\n\nexport interface EndOfStreamOwners {\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n mediaElement?: HTMLMediaElement | undefined;\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ## When to call endOfStream()\n//\n// Per the MSE spec, endOfStream() should be called once the last media\n// segments — temporally speaking — have been completely appended to all\n// active SourceBuffers. Specifically it signals two things:\n// 1. The temporally latest segments for both audio and video have been\n// appended (i.e. the buffer covers the end of the stream content).\n// 2. The MediaSource will transition from 'open' to 'ended'. Appending\n// additional (earlier) segments after this — e.g. for a seek-back or\n// back-buffer refill — will re-open the MediaSource, at which point\n// endOfStream() must be called again once loading reaches the end.\n//\n// The browser uses this signal to finalise MediaSource.duration and allow\n// the media element to fire the `ended` event. Without it, playback stalls\n// at the end of the buffered range waiting for data that will never arrive.\n//\n// The \"any track\" qualifier is intentional: per the HLS spec, all renditions\n// in a switching set are time-aligned, so the last segment of any rendition\n// covers the same end-of-stream content. We don't need to be tied to the\n// currently selected track.\n//\n// The right long-term condition is therefore:\n// - The last segment of the video content (from any resolved video track)\n// has been completely appended to the video SourceBuffer, AND\n// - The last segment of the audio content (from any resolved audio track)\n// has been completely appended to the audio SourceBuffer (when active), AND\n// - currentTime is within the time range of that last segment.\n//\n// The currentTime gate prevents unnecessary re-invocations when back-buffer\n// refills or other mid-stream appends briefly re-open the MediaSource while\n// the user is far from the end.\n//\n/**\n * Check if the last segment of a track has been appended to a SourceBuffer.\n *\n * Checks by segment ID rather than a pipeline flag, so it is robust across\n * quality switches (different tracks have different segment IDs) and\n * back-buffer flushes (flushed segment IDs are removed from the model).\n */\nfunction isLastSegmentAppended(segments: readonly { id: string }[], actor: SourceBufferActor | undefined): boolean {\n if (segments.length === 0) return true;\n const lastSeg = segments[segments.length - 1];\n if (!lastSeg) return false;\n // A partial segment is still streaming — the last segment is not ready until\n // its entry is present and not marked partial.\n return actor?.snapshot.get().context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;\n}\n\n/**\n * Check if the last segment has been appended for each selected track.\n *\n * Handles video-only, audio-only, and video+audio scenarios.\n * A track with no segments (e.g. unresolved) is considered not ready.\n */\nexport function hasLastSegmentLoaded(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // An unresolved track means we don't yet know its segments — cannot be done.\n // Fast-paths the quality-switch window: when selectedVideoTrackId has changed\n // to a new (unresolved) track, we cannot yet determine if its last segment\n // is loaded.\n if (videoTrack && !isResolvedTrack(videoTrack)) return false;\n if (audioTrack && !isResolvedTrack(audioTrack)) return false;\n\n if (videoTrack && isResolvedTrack(videoTrack)) {\n if (!isLastSegmentAppended(videoTrack.segments, owners.videoBufferActor)) return false;\n }\n\n if (audioTrack && isResolvedTrack(audioTrack)) {\n if (!isLastSegmentAppended(audioTrack.segments, owners.audioBufferActor)) return false;\n }\n\n return true;\n}\n\n/**\n * Check if we can call endOfStream.\n */\nexport function canEndStream(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n return !!(owners.mediaSource && state.presentation);\n}\n\n/**\n * Check if we should call endOfStream.\n */\nexport function shouldEndStream(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n if (!canEndStream(state, owners)) return false;\n\n const { mediaElement } = owners;\n\n // MediaSource must be open — use reactive readyState signal when available\n // so the computed re-evaluates when readyState changes (e.g. 'ended' → 'open' on seek-back).\n const readyState = owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState;\n if (readyState !== 'open') return false;\n\n // CRITICAL: MediaElement must have metadata before calling endOfStream\n // Calling endOfStream before HAVE_METADATA causes DEMUXER_ERROR\n // https://github.com/chromium/chromium/blob/main/media/filters/chunk_demuxer.cc\n if (mediaElement && mediaElement.readyState < HTMLMediaElement.HAVE_METADATA) {\n return false;\n }\n\n // SourceBuffers must exist for selected tracks before we can end the stream\n // (otherwise we'd close the MediaSource before SourceBuffers are created)\n const hasVideoTrack = !!state.selectedVideoTrackId;\n const hasAudioTrack = !!state.selectedAudioTrackId;\n\n if (hasVideoTrack && !owners.videoBuffer) return false;\n if (hasAudioTrack && !owners.audioBuffer) return false;\n\n // SourceBufferActors must be idle — setting duration while a SourceBuffer is\n // updating throws InvalidStateError. The actor subscriber in endOfStream() will\n // re-evaluate when each actor transitions back to idle.\n if (owners.videoBufferActor?.snapshot.get().status === 'updating') return false;\n if (owners.audioBufferActor?.snapshot.get().status === 'updating') return false;\n\n // Last segment must be appended for each selected track\n if (!hasLastSegmentLoaded(state, owners)) return false;\n\n // currentTime must have reached the last segment. Guards against re-ending\n // the stream when a back-buffer remove() re-opens the MediaSource while the\n // user is far from the end (remove() re-opens 'ended' → 'open' per MSE spec,\n // same as appendBuffer()).\n if (mediaElement) {\n const videoTrack = hasVideoTrack ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = hasAudioTrack ? getSelectedTrack(state, 'audio') : undefined;\n const refTrack =\n videoTrack && isResolvedTrack(videoTrack)\n ? videoTrack\n : audioTrack && isResolvedTrack(audioTrack)\n ? audioTrack\n : undefined;\n if (refTrack && refTrack.segments.length > 0) {\n const lastSeg = refTrack.segments[refTrack.segments.length - 1]!;\n if (mediaElement.currentTime < lastSeg.startTime) return false;\n }\n }\n\n return true;\n}\n\n/**\n * Wait for all currently-updating SourceBufferActors to finish.\n * Uses actor status rather than raw SourceBuffer.updating so the wait is\n * aligned with the same abstraction that owns all buffer operations.\n */\nfunction waitForSourceBuffersReady(owners: EndOfStreamOwners): Promise<void> {\n const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter(\n (actor): actor is SourceBufferActor => actor !== undefined && actor.snapshot.get().status === 'updating'\n );\n\n if (updatingActors.length === 0) return Promise.resolve();\n\n return Promise.all(\n updatingActors.map(\n (actor) =>\n new Promise<void>((resolve) => {\n // effect() runs its body synchronously on creation, then re-runs on the\n // next microtask after any dependency changes. If the actor is already\n // idle by the time effect() is called, resolve fires immediately and\n // cleanup is scheduled via queueMicrotask to avoid unwatching during\n // the watcher notification cycle.\n let cleanup: (() => void) | undefined;\n let resolved = false;\n cleanup = effect(() => {\n if (actor.snapshot.get().status !== 'updating') {\n if (!resolved) {\n resolved = true;\n resolve();\n }\n queueMicrotask(() => cleanup?.());\n }\n });\n })\n )\n ).then(() => undefined);\n}\n\n/**\n * Get the highest buffered end time across all active SourceBuffers.\n * Used to set the final duration from actual container timestamps rather\n * than playlist metadata, which handles both shorter and longer cases.\n */\nfunction getMaxBufferedEnd(owners: EndOfStreamOwners): number {\n let max = 0;\n for (const buf of [owners.videoBuffer, owners.audioBuffer]) {\n if (buf && buf.buffered.length > 0) {\n const end = buf.buffered.end(buf.buffered.length - 1);\n if (end > max) max = end;\n }\n }\n return max;\n}\n\n/**\n * End of stream task (module-level, pure).\n * Sets the final duration from actual buffered end time, then calls endOfStream().\n */\nconst endOfStreamTask = async (\n { currentOwners }: { currentOwners: EndOfStreamOwners },\n _context: object\n): Promise<void> => {\n const { mediaSource } = currentOwners;\n\n // Double-check MediaSource isn't already ended (in case of race)\n if (mediaSource!.readyState === 'ended') {\n return;\n }\n\n // Wait for any in-progress SourceBuffer operations to finish before calling\n // endOfStream() — the MSE spec forbids it while any buffer has updating === true.\n await waitForSourceBuffersReady(currentOwners);\n\n // Re-check after the async wait\n if (mediaSource!.readyState !== 'open') return;\n\n // Set the final duration from actual buffered container timestamps.\n // This is more accurate than the playlist-derived duration and correctly\n // handles both shorter (common with CMAF) and longer actual media durations.\n // Per MSE spec, endOfStream() will only *increase* duration if needed, so\n // setting it here first ensures the value from the buffer wins in all cases.\n const bufferedEnd = getMaxBufferedEnd(currentOwners);\n if (bufferedEnd > 0) {\n mediaSource!.duration = bufferedEnd;\n }\n\n mediaSource!.endOfStream();\n\n // Wait a frame to allow async state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n/**\n * Call endOfStream when the last segment has been appended.\n * This signals to the browser that the stream is complete.\n *\n * Per the MSE spec, appendBuffer() remains valid after endOfStream() —\n * seeks that require re-appending earlier segments will still work.\n * What becomes blocked is calling endOfStream() again, addSourceBuffer(),\n * and MediaSource.duration updates.\n */\nexport function endOfStream<S extends EndOfStreamState, O extends EndOfStreamOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n // Derived condition. Transitively tracks through owners into each actor's\n // snapshot signal — when owners changes and points to a new actor, this computed\n // re-tracks to the new actor's signal on next evaluation.\n // shouldEndStream calls actor.snapshot.get() inside the computed body, so those\n // reads are automatically tracked by the Signal.Computed dependency graph.\n const shouldEnd = computed(() => shouldEndStream(state.get(), owners.get()));\n\n let hasEnded = false;\n\n const cleanupEffect = effect(() => {\n if (!shouldEnd.get()) return;\n const currentOwners = owners.get();\n if (hasEnded) {\n // Per the MSE spec, calling appendBuffer() on a SourceBuffer when\n // readyState is 'ended' automatically transitions it back to 'open'.\n // This happens on seek-back after end-of-stream — allow endOfStream()\n // to be called again once the last segment is reloaded.\n const readyState = currentOwners.mediaSourceReadyState?.get() ?? currentOwners.mediaSource?.readyState;\n if (readyState !== 'open') return;\n hasEnded = false;\n }\n\n // Set flag before awaiting to close the re-entry window between\n // endOfStream() being called and the async task completing.\n hasEnded = true;\n endOfStreamTask({ currentOwners }, {}).catch((error) => console.error('Failed to call endOfStream:', error));\n });\n\n return cleanupEffect;\n}\n"],"mappings":";;;;;;;;;;;;AA6DA,SAAS,sBAAsB,UAAqC,OAA+C;AACjH,KAAI,SAAS,WAAW,EAAG,QAAO;CAClC,MAAM,UAAU,SAAS,SAAS,SAAS;AAC3C,KAAI,CAAC,QAAS,QAAO;AAGrB,QAAO,OAAO,SAAS,KAAK,CAAC,QAAQ,SAAS,MAAM,MAAM,EAAE,OAAO,QAAQ,MAAM,CAAC,EAAE,QAAQ,IAAI;;;;;;;;AASlG,SAAgB,qBAAqB,OAAyB,QAAoC;CAChG,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;AAMnF,KAAI,cAAc,CAAC,gBAAgB,WAAW,CAAE,QAAO;AACvD,KAAI,cAAc,CAAC,gBAAgB,WAAW,CAAE,QAAO;AAEvD,KAAI,cAAc,gBAAgB,WAAW;MACvC,CAAC,sBAAsB,WAAW,UAAU,OAAO,iBAAiB,CAAE,QAAO;;AAGnF,KAAI,cAAc,gBAAgB,WAAW;MACvC,CAAC,sBAAsB,WAAW,UAAU,OAAO,iBAAiB,CAAE,QAAO;;AAGnF,QAAO;;;;;AAMT,SAAgB,aAAa,OAAyB,QAAoC;AACxF,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM;;;;;AAMxC,SAAgB,gBAAgB,OAAyB,QAAoC;AAC3F,KAAI,CAAC,aAAa,OAAO,OAAO,CAAE,QAAO;CAEzC,MAAM,EAAE,iBAAiB;AAKzB,MADmB,OAAO,uBAAuB,KAAK,IAAI,OAAO,aAAa,gBAC3D,OAAQ,QAAO;AAKlC,KAAI,gBAAgB,aAAa,aAAa,iBAAiB,cAC7D,QAAO;CAKT,MAAM,gBAAgB,CAAC,CAAC,MAAM;CAC9B,MAAM,gBAAgB,CAAC,CAAC,MAAM;AAE9B,KAAI,iBAAiB,CAAC,OAAO,YAAa,QAAO;AACjD,KAAI,iBAAiB,CAAC,OAAO,YAAa,QAAO;AAKjD,KAAI,OAAO,kBAAkB,SAAS,KAAK,CAAC,WAAW,WAAY,QAAO;AAC1E,KAAI,OAAO,kBAAkB,SAAS,KAAK,CAAC,WAAW,WAAY,QAAO;AAG1E,KAAI,CAAC,qBAAqB,OAAO,OAAO,CAAE,QAAO;AAMjD,KAAI,cAAc;EAChB,MAAM,aAAa,gBAAgB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;EACtE,MAAM,aAAa,gBAAgB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;EACtE,MAAM,WACJ,cAAc,gBAAgB,WAAW,GACrC,aACA,cAAc,gBAAgB,WAAW,GACvC,aACA,KAAA;AACR,MAAI,YAAY,SAAS,SAAS,SAAS,GAAG;GAC5C,MAAM,UAAU,SAAS,SAAS,SAAS,SAAS,SAAS;AAC7D,OAAI,aAAa,cAAc,QAAQ,UAAW,QAAO;;;AAI7D,QAAO;;;;;;;AAQT,SAAS,0BAA0B,QAA0C;CAC3E,MAAM,iBAAiB,CAAC,OAAO,kBAAkB,OAAO,iBAAiB,CAAC,QACvE,UAAsC,UAAU,KAAA,KAAa,MAAM,SAAS,KAAK,CAAC,WAAW,WAC/F;AAED,KAAI,eAAe,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEzD,QAAO,QAAQ,IACb,eAAe,KACZ,UACC,IAAI,SAAe,YAAY;EAM7B,IAAI;EACJ,IAAI,WAAW;AACf,YAAU,aAAa;AACrB,OAAI,MAAM,SAAS,KAAK,CAAC,WAAW,YAAY;AAC9C,QAAI,CAAC,UAAU;AACb,gBAAW;AACX,cAAS;;AAEX,yBAAqB,WAAW,CAAC;;IAEnC;GACF,CACL,CACF,CAAC,WAAW,KAAA,EAAU;;;;;;;AAQzB,SAAS,kBAAkB,QAAmC;CAC5D,IAAI,MAAM;AACV,MAAK,MAAM,OAAO,CAAC,OAAO,aAAa,OAAO,YAAY,CACxD,KAAI,OAAO,IAAI,SAAS,SAAS,GAAG;EAClC,MAAM,MAAM,IAAI,SAAS,IAAI,IAAI,SAAS,SAAS,EAAE;AACrD,MAAI,MAAM,IAAK,OAAM;;AAGzB,QAAO;;;;;;AAOT,MAAM,kBAAkB,OACtB,EAAE,iBACF,aACkB;CAClB,MAAM,EAAE,gBAAgB;AAGxB,KAAI,YAAa,eAAe,QAC9B;AAKF,OAAM,0BAA0B,cAAc;AAG9C,KAAI,YAAa,eAAe,OAAQ;CAOxC,MAAM,cAAc,kBAAkB,cAAc;AACpD,KAAI,cAAc,EAChB,aAAa,WAAW;AAG1B,aAAa,aAAa;AAG1B,OAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;;;;;;;;;;;AAYhE,SAAgB,YAAqE,EACnF,OACA,UAIa;CAMb,MAAM,YAAY,eAAe,gBAAgB,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;CAE5E,IAAI,WAAW;AAqBf,QAnBsB,aAAa;AACjC,MAAI,CAAC,UAAU,KAAK,CAAE;EACtB,MAAM,gBAAgB,OAAO,KAAK;AAClC,MAAI,UAAU;AAMZ,QADmB,cAAc,uBAAuB,KAAK,IAAI,cAAc,aAAa,gBACzE,OAAQ;AAC3B,cAAW;;AAKb,aAAW;AACX,kBAAgB,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC,OAAO,UAAU,QAAQ,MAAM,+BAA+B,MAAM,CAAC;GAC5G"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { sampleBandwidth } from "../../core/abr/bandwidth-estimator.js";
|
|
2
|
+
import { DEFAULT_FORWARD_BUFFER_CONFIG } from "../../core/buffer/forward-buffer.js";
|
|
3
|
+
import { effect } from "../../core/signals/effect.js";
|
|
4
|
+
import { computed, signal } from "../../core/signals/primitives.js";
|
|
5
|
+
import { isResolvedTrack } from "../../core/types/index.js";
|
|
6
|
+
import { getSelectedTrack } from "../../core/utils/track-selection.js";
|
|
7
|
+
import { ChunkedStreamIterable } from "../network/chunked-stream-iterable.js";
|
|
8
|
+
import { fetchResolvable } from "../network/fetch.js";
|
|
9
|
+
import { createSegmentLoaderActor } from "./segment-loader-actor.js";
|
|
10
|
+
//#region src/dom/features/load-segments.ts
|
|
11
|
+
const ActorKeyByType = {
|
|
12
|
+
video: "videoBufferActor",
|
|
13
|
+
audio: "audioBufferActor"
|
|
14
|
+
};
|
|
15
|
+
function createTrackedFetch(throughput, onSample) {
|
|
16
|
+
return async (addressable, options) => {
|
|
17
|
+
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
18
|
+
const response = await fetchResolvable(addressable, fetchOptions);
|
|
19
|
+
if (!response.body) throw new Error("Response has no body");
|
|
20
|
+
const body = response.body;
|
|
21
|
+
return { [Symbol.asyncIterator]: async function* () {
|
|
22
|
+
let chunkStart = performance.now();
|
|
23
|
+
for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
|
|
24
|
+
const elapsed = performance.now() - chunkStart;
|
|
25
|
+
const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);
|
|
26
|
+
throughput.set(next);
|
|
27
|
+
onSample?.(next);
|
|
28
|
+
yield chunk;
|
|
29
|
+
chunkStart = performance.now();
|
|
30
|
+
}
|
|
31
|
+
} };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Non-tracking fetch: eagerly starts the request and returns the response body
|
|
36
|
+
* as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
|
|
37
|
+
* Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
|
|
38
|
+
* (equivalent to arrayBuffer() but through the same streaming path).
|
|
39
|
+
*/
|
|
40
|
+
async function fetchStream(addressable, options) {
|
|
41
|
+
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
42
|
+
const response = await fetchResolvable(addressable, fetchOptions);
|
|
43
|
+
if (!response.body) throw new Error("Response has no body");
|
|
44
|
+
return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
|
|
45
|
+
}
|
|
46
|
+
function selectLoadingInputs([segmentsCanLoad, state], type) {
|
|
47
|
+
const { playbackInitiated, preload, currentTime } = state;
|
|
48
|
+
return {
|
|
49
|
+
playbackInitiated,
|
|
50
|
+
preload,
|
|
51
|
+
currentTime,
|
|
52
|
+
track: getSelectedTrack(state, type),
|
|
53
|
+
segmentsCanLoad
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Equality function encoding the condition hierarchy for relevant changes.
|
|
58
|
+
*
|
|
59
|
+
* Pre-play (!playbackInitiated):
|
|
60
|
+
* Only preload changes matter. currentTime and resolvedTrackId are ignored
|
|
61
|
+
* (track changes not supported pre-play; currentTime value is used at
|
|
62
|
+
* trigger time but changes don't re-trigger).
|
|
63
|
+
*
|
|
64
|
+
* playbackInitiated transition:
|
|
65
|
+
* Always fires (handled in the subscriber; preload='auto' suppression
|
|
66
|
+
* applied there since equality functions have no memory of prior values).
|
|
67
|
+
*
|
|
68
|
+
* Post-play (playbackInitiated):
|
|
69
|
+
* resolvedTrackId changes (track switch or previously-unresolved track
|
|
70
|
+
* resolving) and currentTime changes both trigger. preload is irrelevant.
|
|
71
|
+
*/
|
|
72
|
+
const segmentStartFor = (currentTime, track) => {
|
|
73
|
+
if (currentTime == null) return void 0;
|
|
74
|
+
return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Returns true when the inputs are equal (no meaningful change — don't fire).
|
|
78
|
+
* Returns false when the inputs differ in a way that requires a new message.
|
|
79
|
+
*
|
|
80
|
+
* This IS the shouldLoadSegments logic, expressed as an equality function.
|
|
81
|
+
*/
|
|
82
|
+
function loadingInputsEq(prevState, curState) {
|
|
83
|
+
if (!curState.segmentsCanLoad) return true;
|
|
84
|
+
if (!curState.playbackInitiated) {
|
|
85
|
+
if (curState.preload === "none") return true;
|
|
86
|
+
return curState.preload === prevState.preload;
|
|
87
|
+
}
|
|
88
|
+
if (!prevState.playbackInitiated && curState.playbackInitiated) {
|
|
89
|
+
if (prevState.preload !== "auto") return false;
|
|
90
|
+
}
|
|
91
|
+
if (!curState.track || !isResolvedTrack(curState.track)) return true;
|
|
92
|
+
if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
|
|
93
|
+
return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Load segments orchestration — Reactor layer.
|
|
97
|
+
*
|
|
98
|
+
* Sends typed load messages to a SegmentLoaderActor when relevant conditions
|
|
99
|
+
* change. Uses targeted subscriptions rather than broad combineLatest so only
|
|
100
|
+
* meaningful state changes trigger evaluation.
|
|
101
|
+
*
|
|
102
|
+
* Condition hierarchy (see SegmentLoadingKey for detail):
|
|
103
|
+
*
|
|
104
|
+
* !playbackInitiated
|
|
105
|
+
* preload==='none' (or unset) → dormant; no trigger
|
|
106
|
+
* preload==='metadata' → trigger on transition to 'metadata'
|
|
107
|
+
* preload==='auto' → trigger on transition to 'auto'
|
|
108
|
+
*
|
|
109
|
+
* !playbackInitiated → playbackInitiated
|
|
110
|
+
* preload !== 'auto' → trigger (message shape changes)
|
|
111
|
+
* preload === 'auto' → suppressed (was already full-range mode;
|
|
112
|
+
* let segmentStart take over post-play)
|
|
113
|
+
* KNOWN LIMITATION: seek-before-play with
|
|
114
|
+
* preload='auto' is not supported — if the
|
|
115
|
+
* user seeks before pressing play, the
|
|
116
|
+
* first re-send is delayed until the next
|
|
117
|
+
* segment boundary crossing post-play.
|
|
118
|
+
*
|
|
119
|
+
* playbackInitiated
|
|
120
|
+
* resolvedTrackId changes → trigger
|
|
121
|
+
* segmentStart(currentTime) changes → trigger (segment boundary only)
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* const cleanup = loadSegments({ state, owners }, { type: 'video' });
|
|
125
|
+
*/
|
|
126
|
+
function loadSegments({ state, owners }, config) {
|
|
127
|
+
const { type } = config;
|
|
128
|
+
const actorKey = ActorKeyByType[type];
|
|
129
|
+
const initialBandwidth = state.get().bandwidthState;
|
|
130
|
+
const throughput = signal(initialBandwidth ?? {
|
|
131
|
+
fastEstimate: 0,
|
|
132
|
+
fastTotalWeight: 0,
|
|
133
|
+
slowEstimate: 0,
|
|
134
|
+
slowTotalWeight: 0,
|
|
135
|
+
bytesSampled: 0
|
|
136
|
+
});
|
|
137
|
+
const fetchBytes = type === "video" ? createTrackedFetch(throughput, initialBandwidth !== void 0 ? (next) => {
|
|
138
|
+
state.set(Object.assign({}, state.get(), { bandwidthState: next }));
|
|
139
|
+
} : void 0) : fetchStream;
|
|
140
|
+
const segmentLoader = signal(void 0);
|
|
141
|
+
const actorSource = computed(() => owners.get()[actorKey]);
|
|
142
|
+
let currentLoader;
|
|
143
|
+
const cleanupActorLifecycle = effect(() => {
|
|
144
|
+
const actor = actorSource.get();
|
|
145
|
+
if (currentLoader) {
|
|
146
|
+
currentLoader.destroy();
|
|
147
|
+
segmentLoader.set(void 0);
|
|
148
|
+
currentLoader = void 0;
|
|
149
|
+
}
|
|
150
|
+
if (actor) {
|
|
151
|
+
const loader = createSegmentLoaderActor(actor, fetchBytes);
|
|
152
|
+
currentLoader = loader;
|
|
153
|
+
segmentLoader.set(loader);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const segmentsCanLoad = computed(() => {
|
|
157
|
+
const track = getSelectedTrack(state.get(), type);
|
|
158
|
+
return !!track && isResolvedTrack(track) && !!segmentLoader.get();
|
|
159
|
+
});
|
|
160
|
+
const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));
|
|
161
|
+
let prevInputs;
|
|
162
|
+
const cleanupLoadEffect = effect(() => {
|
|
163
|
+
const inputs = loadingInputs.get();
|
|
164
|
+
if (prevInputs !== void 0 && loadingInputsEq(prevInputs, inputs)) return;
|
|
165
|
+
const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;
|
|
166
|
+
if (!canLoad) return;
|
|
167
|
+
prevInputs = inputs;
|
|
168
|
+
if (!(preload === "auto" || !!playbackInitiated))
|
|
169
|
+
/** @ts-expect-error */
|
|
170
|
+
segmentLoader.get()?.send({
|
|
171
|
+
type: "load",
|
|
172
|
+
track
|
|
173
|
+
});
|
|
174
|
+
else segmentLoader.get()?.send({
|
|
175
|
+
type: "load",
|
|
176
|
+
track,
|
|
177
|
+
range: {
|
|
178
|
+
start: currentTime,
|
|
179
|
+
end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
return () => {
|
|
184
|
+
cleanupActorLifecycle();
|
|
185
|
+
cleanupLoadEffect();
|
|
186
|
+
currentLoader?.destroy();
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { loadSegments };
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=load-segments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-segments.js","names":[],"sources":["../../../../src/dom/features/load-segments.ts"],"sourcesContent":["import { type BandwidthState, sampleBandwidth } from '../../core/abr/bandwidth-estimator';\nimport { DEFAULT_FORWARD_BUFFER_CONFIG } from '../../core/buffer/forward-buffer';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, signal } from '../../core/signals/primitives';\nimport type { AddressableObject, Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from '../network/chunked-stream-iterable';\nimport { fetchResolvable } from '../network/fetch';\nimport {\n type BufferState,\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SourceBufferState,\n} from './segment-loader-actor';\nimport type { MediaTrackType } from './setup-sourcebuffer';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// Map track type to SourceBufferActor owner property key.\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n// ============================================================================\n// TRACKED FETCH\n// ============================================================================\n\n/**\n * Creates a fetch function that eagerly starts the HTTP request (TTFB is\n * awaited), then returns a lazy iterable over the response body that\n * transparently samples bandwidth per chunk.\n *\n * Separating connection start from body reading ensures `fetch()` is called\n * as soon as the task begins — not deferred until the actor's append loop\n * first pulls a chunk. This makes fetch timing predictable and observable\n * (e.g. in tests that record fetched URLs) regardless of downstream consumers.\n *\n * `onSample` bridges throughput state outward; see Phase 2 comment for detail.\n */\ntype FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nfunction createTrackedFetch(\n throughput: Signal<BandwidthState>,\n onSample?: (next: BandwidthState) => void\n): (addressable: AddressableObject, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>> {\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);\n throughput.set(next);\n onSample?.(next);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n\n/**\n * Non-tracking fetch: eagerly starts the request and returns the response body\n * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.\n * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk\n * (equivalent to arrayBuffer() but through the same streaming path).\n */\nasync function fetchStream(addressable: AddressableObject, options?: FetchOptions): Promise<AsyncIterable<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 return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * State shape for segment loading.\n */\nexport interface SegmentLoadingState extends TrackSelectionState {\n presentation?: Presentation;\n preload?: string;\n bandwidthState?: BandwidthState;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once the user has initiated playback. Allows segment loading regardless of preload setting. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Owners shape for segment loading.\n */\nexport interface SegmentLoadingOwners {\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ============================================================================\n// STATE SELECTOR\n// ============================================================================\n\n/**\n * The subset of state that drives segment loading decisions.\n * A plain \"pick\" with one derived field (resolved track ID).\n */\ntype LoadingInputs = {\n playbackInitiated: boolean | undefined;\n preload: string | undefined;\n currentTime: number | undefined;\n /** @TODO cleanup type precision via inference+generics */\n track: ReturnType<typeof getSelectedTrack>;\n segmentsCanLoad: boolean;\n};\n\nfunction selectLoadingInputs(\n [segmentsCanLoad, state]: [boolean, SegmentLoadingState],\n type: MediaTrackType\n): LoadingInputs {\n const { playbackInitiated, preload, currentTime } = state;\n const track = getSelectedTrack(state, type);\n return {\n playbackInitiated,\n preload,\n currentTime,\n track,\n segmentsCanLoad,\n };\n}\n\n/**\n * Equality function encoding the condition hierarchy for relevant changes.\n *\n * Pre-play (!playbackInitiated):\n * Only preload changes matter. currentTime and resolvedTrackId are ignored\n * (track changes not supported pre-play; currentTime value is used at\n * trigger time but changes don't re-trigger).\n *\n * playbackInitiated transition:\n * Always fires (handled in the subscriber; preload='auto' suppression\n * applied there since equality functions have no memory of prior values).\n *\n * Post-play (playbackInitiated):\n * resolvedTrackId changes (track switch or previously-unresolved track\n * resolving) and currentTime changes both trigger. preload is irrelevant.\n */\nconst segmentStartFor = (currentTime: number | undefined, track: ResolvedTrack | undefined) => {\n if (currentTime == null) return undefined;\n return track?.segments.find(\n ({ startTime, duration }, i, segments) =>\n currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1)\n )?.startTime;\n};\n\n/**\n * Returns true when the inputs are equal (no meaningful change — don't fire).\n * Returns false when the inputs differ in a way that requires a new message.\n *\n * This IS the shouldLoadSegments logic, expressed as an equality function.\n */\nfunction loadingInputsEq(prevState: LoadingInputs, curState: LoadingInputs): boolean {\n if (!curState.segmentsCanLoad) return true;\n // Haven't started playback. Only care about preload changes.\n if (!curState.playbackInitiated) {\n if (curState.preload === 'none') return true; // blocked — equal, don't fire\n return curState.preload === prevState.preload; // equal if preload unchanged\n }\n // Transition: !playbackInitiated → playbackInitiated\n if (!prevState.playbackInitiated && curState.playbackInitiated) {\n if (prevState.preload !== 'auto') return false; // fire — message shape changes\n // preload was 'auto': fall through to segment comparison (suppress if same position)\n }\n\n // Don't treat as a change if we don't (yet) have a currently resolved track.\n if (!curState.track || !isResolvedTrack(curState.track)) return true;\n\n // If we *do* have a currently resolved track, treat as a change if the track ids have changed\n if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;\n\n // Finally, if playback has initiated *and* we have a resolved track, check if currentTime\n // has changed \"significantly\" (based on segment time range boundaries) and treat as a change\n // if so (regardless of whether or not the track has changed)\n return (\n segmentStartFor(prevState.currentTime, curState.track as ResolvedTrack) ===\n segmentStartFor(curState.currentTime, curState.track as ResolvedTrack)\n );\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\n/**\n * Load segments orchestration — Reactor layer.\n *\n * Sends typed load messages to a SegmentLoaderActor when relevant conditions\n * change. Uses targeted subscriptions rather than broad combineLatest so only\n * meaningful state changes trigger evaluation.\n *\n * Condition hierarchy (see SegmentLoadingKey for detail):\n *\n * !playbackInitiated\n * preload==='none' (or unset) → dormant; no trigger\n * preload==='metadata' → trigger on transition to 'metadata'\n * preload==='auto' → trigger on transition to 'auto'\n *\n * !playbackInitiated → playbackInitiated\n * preload !== 'auto' → trigger (message shape changes)\n * preload === 'auto' → suppressed (was already full-range mode;\n * let segmentStart take over post-play)\n * KNOWN LIMITATION: seek-before-play with\n * preload='auto' is not supported — if the\n * user seeks before pressing play, the\n * first re-send is delayed until the next\n * segment boundary crossing post-play.\n *\n * playbackInitiated\n * resolvedTrackId changes → trigger\n * segmentStart(currentTime) changes → trigger (segment boundary only)\n *\n * @example\n * const cleanup = loadSegments({ state, owners }, { type: 'video' });\n */\nexport function loadSegments<S extends SegmentLoadingState, O extends SegmentLoadingOwners>(\n { state, owners }: { state: Signal<S>; owners: Signal<O> },\n config: { type: MediaTrackType }\n): () => void {\n const { type } = config;\n const actorKey = ActorKeyByType[type];\n\n // Local throughput signal — used by createTrackedFetch to sample bandwidth\n // per chunk and propagate back to the shared state for ABR.\n const initialBandwidth = state.get().bandwidthState;\n const throughput = signal<BandwidthState>(\n initialBandwidth ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n }\n );\n\n const fetchBytes =\n type === 'video'\n ? createTrackedFetch(\n throughput,\n initialBandwidth !== undefined\n ? (next) => {\n state.set(Object.assign({}, state.get(), { bandwidthState: next }) as S);\n }\n : undefined\n )\n : fetchStream;\n\n // Local segment loader — signal so segmentsCanLoad can track it reactively\n const segmentLoader = signal<SegmentLoaderActor | undefined>(undefined);\n\n // Computed: isolates the specific actor key so the lifecycle effect only\n // re-runs when that actor reference changes, not on unrelated owners updates.\n const actorSource = computed<SourceBufferActor | undefined>(() => owners.get()[actorKey]);\n\n // Actor lifecycle — destroy and recreate SegmentLoaderActor when the\n // upstream SourceBufferActor is replaced (quality switch) or removed.\n let currentLoader: SegmentLoaderActor | undefined;\n const cleanupActorLifecycle = effect(() => {\n const actor = actorSource.get();\n\n if (currentLoader) {\n currentLoader.destroy();\n segmentLoader.set(undefined);\n currentLoader = undefined;\n }\n\n if (actor) {\n const loader = createSegmentLoaderActor(actor, fetchBytes);\n currentLoader = loader;\n segmentLoader.set(loader);\n }\n });\n\n // Derived: true only when we have both a resolved track and a ready loader\n const segmentsCanLoad = computed(() => {\n const track = getSelectedTrack(state.get(), type);\n return !!track && isResolvedTrack(track) && !!segmentLoader.get();\n });\n\n // Loading inputs — selectLoadingInputs is cheap; let the effect body apply\n // loadingInputsEq rather than the computed, so the first run always fires\n // regardless of the initial state.\n const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));\n\n // Load effect — prevInputs guards against redundant messages using the same\n // equality semantics as the former combineLatest selector subscription.\n // prevInputs is updated only when we actually send a message so that the\n // transition segmentsCanLoad: false → true always fires (loadingInputsEq\n // returns true for that transition, so we must not skip it here).\n let prevInputs: LoadingInputs | undefined;\n const cleanupLoadEffect = effect(() => {\n const inputs = loadingInputs.get();\n if (prevInputs !== undefined && loadingInputsEq(prevInputs, inputs)) return;\n\n const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;\n if (!canLoad) return;\n\n prevInputs = inputs;\n\n const fullMode = preload === 'auto' || !!playbackInitiated;\n if (!fullMode) {\n // Metadata mode: init only, no range.\n /** @ts-expect-error */\n segmentLoader.get()?.send({ type: 'load', track });\n } else {\n segmentLoader.get()?.send({\n type: 'load',\n /** @ts-expect-error */\n track,\n range: {\n start: currentTime as number,\n end: (currentTime as number) + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration,\n },\n });\n }\n });\n\n return () => {\n cleanupActorLifecycle();\n cleanupLoadEffect();\n currentLoader?.destroy();\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
|
|
2
|
+
import { effect } from "../../core/signals/effect.js";
|
|
3
|
+
import { computed } from "../../core/signals/primitives.js";
|
|
4
|
+
import { isResolvedTrack } from "../../core/types/index.js";
|
|
5
|
+
import { parseVttSegment } from "../text/parse-vtt-segment.js";
|
|
6
|
+
//#region src/dom/features/load-text-track-cues.ts
|
|
7
|
+
const CueKeys = [
|
|
8
|
+
"startTime",
|
|
9
|
+
"endTime",
|
|
10
|
+
"text"
|
|
11
|
+
];
|
|
12
|
+
function isDuplicateCue(cue, existingCues) {
|
|
13
|
+
return Array.prototype.some.call(existingCues ?? [], (existingCue) => {
|
|
14
|
+
return CueKeys.every((k) => existingCue[k] === cue[k]);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const loadVttSegmentTask = async ({ segment }, { textTrack }) => {
|
|
18
|
+
(await parseVttSegment(segment.url)).forEach((cue) => {
|
|
19
|
+
if (isDuplicateCue(cue, textTrack.cues)) return;
|
|
20
|
+
textTrack.addCue(cue);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Load text track cues task (composite - orchestrates VTT segment subtasks).
|
|
25
|
+
*/
|
|
26
|
+
const loadTextTrackCuesTask = async ({ currentState }, context) => {
|
|
27
|
+
const track = findSelectedTextTrack(currentState);
|
|
28
|
+
if (!track || !isResolvedTrack(track)) return;
|
|
29
|
+
const { segments } = track;
|
|
30
|
+
if (segments.length === 0) return;
|
|
31
|
+
const trackId = track.id;
|
|
32
|
+
const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));
|
|
33
|
+
const segmentsToLoad = getSegmentsToLoad(segments, segments.filter((s) => loadedIds.has(s.id)), currentState.currentTime ?? 0).filter((s) => !loadedIds.has(s.id));
|
|
34
|
+
if (segmentsToLoad.length === 0) return;
|
|
35
|
+
for (const segment of segmentsToLoad) {
|
|
36
|
+
if (context.signal.aborted) break;
|
|
37
|
+
try {
|
|
38
|
+
await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
|
|
39
|
+
const latestState = context.state.get();
|
|
40
|
+
const latest = latestState.textBufferState ?? {};
|
|
41
|
+
const trackState = latest[trackId] ?? { segments: [] };
|
|
42
|
+
context.state.set({
|
|
43
|
+
...latestState,
|
|
44
|
+
textBufferState: {
|
|
45
|
+
...latest,
|
|
46
|
+
[trackId]: { segments: [...trackState.segments, { id: segment.id }] }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof Error && error.name === "AbortError") break;
|
|
51
|
+
console.error("Failed to load VTT segment:", error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (context.textTrack.mode === "showing" && context.textTrack.cues) Array.from(context.textTrack.cues).forEach((cue) => {
|
|
55
|
+
context.textTrack.addCue(cue);
|
|
56
|
+
});
|
|
57
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Find the selected text track in the presentation.
|
|
61
|
+
*/
|
|
62
|
+
function findSelectedTextTrack(state) {
|
|
63
|
+
if (!state.presentation || !state.selectedTextTrackId) return;
|
|
64
|
+
const textSet = state.presentation.selectionSets.find((set) => set.type === "text");
|
|
65
|
+
if (!textSet?.switchingSets?.[0]?.tracks) return;
|
|
66
|
+
return textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the browser's TextTrack object for the selected text track.
|
|
70
|
+
*
|
|
71
|
+
* Retrieves the live TextTrack interface from the track element in owners,
|
|
72
|
+
* which is used for adding cues, checking mode, and managing track state.
|
|
73
|
+
*
|
|
74
|
+
* Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
|
|
75
|
+
* not the presentation Track metadata type.
|
|
76
|
+
*
|
|
77
|
+
* @param state - Current playback state (track selection)
|
|
78
|
+
* @param owners - DOM owners containing track elements map
|
|
79
|
+
* @returns DOM TextTrack interface or undefined if not found
|
|
80
|
+
*/
|
|
81
|
+
function getSelectedTextTrackFromOwners(state, owners) {
|
|
82
|
+
const trackId = state.selectedTextTrackId;
|
|
83
|
+
if (!trackId || !owners.textTracks) return;
|
|
84
|
+
return owners.textTracks.get(trackId)?.track;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if we can load text track cues.
|
|
88
|
+
*
|
|
89
|
+
* Requires:
|
|
90
|
+
* - Selected text track ID exists
|
|
91
|
+
* - Track elements map exists
|
|
92
|
+
* - Track element exists for selected track
|
|
93
|
+
*/
|
|
94
|
+
function canLoadTextTrackCues(state, owners) {
|
|
95
|
+
return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if we should load text track cues.
|
|
99
|
+
*
|
|
100
|
+
* Only load if:
|
|
101
|
+
* - Track is resolved (has segments)
|
|
102
|
+
* - Track has at least one segment
|
|
103
|
+
* - Track element exists
|
|
104
|
+
*/
|
|
105
|
+
function shouldLoadTextTrackCues(state, owners) {
|
|
106
|
+
if (!canLoadTextTrackCues(state, owners)) return false;
|
|
107
|
+
const track = findSelectedTextTrack(state);
|
|
108
|
+
if (!track || !isResolvedTrack(track) || track.segments.length === 0) return false;
|
|
109
|
+
if (!getSelectedTextTrackFromOwners(state, owners)) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Load text track cues orchestration.
|
|
114
|
+
*
|
|
115
|
+
* Triggers when:
|
|
116
|
+
* - Text track is selected
|
|
117
|
+
* - Track is resolved (has segments)
|
|
118
|
+
* - Track element exists
|
|
119
|
+
*
|
|
120
|
+
* Fetches and parses VTT segments within the forward buffer window, then adds
|
|
121
|
+
* cues to the track incrementally. Continues on segment errors to provide
|
|
122
|
+
* partial subtitles.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* const cleanup = loadTextTrackCues({ state, owners });
|
|
126
|
+
*/
|
|
127
|
+
function loadTextTrackCues({ state, owners }) {
|
|
128
|
+
let currentTask = null;
|
|
129
|
+
let abortController = null;
|
|
130
|
+
let lastTrackId;
|
|
131
|
+
const selectedTrackId = computed(() => state.get().selectedTextTrackId);
|
|
132
|
+
const cleanupEffect = effect(() => {
|
|
133
|
+
const s = state.get();
|
|
134
|
+
const o = owners.get();
|
|
135
|
+
if (selectedTrackId.get() !== lastTrackId) {
|
|
136
|
+
lastTrackId = selectedTrackId.get();
|
|
137
|
+
abortController?.abort();
|
|
138
|
+
currentTask = null;
|
|
139
|
+
}
|
|
140
|
+
if (currentTask) return;
|
|
141
|
+
if (!shouldLoadTextTrackCues(s, o)) return;
|
|
142
|
+
const textTrack = getSelectedTextTrackFromOwners(s, o);
|
|
143
|
+
if (!textTrack) return;
|
|
144
|
+
abortController = new AbortController();
|
|
145
|
+
currentTask = loadTextTrackCuesTask({ currentState: s }, {
|
|
146
|
+
signal: abortController.signal,
|
|
147
|
+
textTrack,
|
|
148
|
+
state
|
|
149
|
+
}).finally(() => {
|
|
150
|
+
currentTask = null;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
return () => {
|
|
154
|
+
abortController?.abort();
|
|
155
|
+
cleanupEffect();
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { loadTextTrackCues };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=load-text-track-cues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-text-track-cues.js","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"sourcesContent":["import { getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal } from '../../core/signals/primitives';\nimport type { Presentation, Segment, TextTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { parseVttSegment } from '../text/parse-vtt-segment';\n\nconst CueKeys = ['startTime', 'endTime', 'text'] as const;\nfunction isDuplicateCue(cue: VTTCue, existingCues: globalThis.TextTrack['cues']): boolean {\n return Array.prototype.some.call(existingCues ?? [], (existingCue) => {\n return CueKeys.every((k) => existingCue[k] === cue[k]);\n });\n}\n\nconst loadVttSegmentTask = async (\n { segment }: { segment: Segment },\n { textTrack }: { textTrack: globalThis.TextTrack }\n): Promise<void> => {\n const cues = await parseVttSegment(segment.url);\n cues.forEach((cue) => {\n if (isDuplicateCue(cue, textTrack.cues)) return;\n textTrack.addCue(cue);\n });\n};\n\n// ============================================================================\n// MAIN TASK (composite - orchestrates subtasks)\n// ============================================================================\n\n/**\n * Load text track cues task (composite - orchestrates VTT segment subtasks).\n */\nconst loadTextTrackCuesTask = async <S extends TextTrackCueLoadingState>(\n { currentState }: { currentState: S },\n context: {\n signal: AbortSignal;\n textTrack: globalThis.TextTrack;\n state: Signal<S>;\n }\n): Promise<void> => {\n const track = findSelectedTextTrack(currentState);\n if (!track || !isResolvedTrack(track)) return;\n\n const { segments } = track;\n if (segments.length === 0) return;\n\n const trackId = track.id;\n\n // Resolve segments already recorded in the state model for this track.\n // Keyed by track ID so multiple text tracks don't interfere with each other.\n const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));\n const alreadyLoaded = segments.filter((s) => loadedIds.has(s.id));\n\n // Apply the same forward buffer window as audio/video segment loading.\n const currentTime = currentState.currentTime ?? 0;\n const segmentsToLoad = getSegmentsToLoad(segments, alreadyLoaded, currentTime).filter((s) => !loadedIds.has(s.id));\n\n if (segmentsToLoad.length === 0) return;\n\n // Execute subtasks sequentially, recording each loaded segment in state.\n for (const segment of segmentsToLoad) {\n if (context.signal.aborted) break;\n\n try {\n await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });\n\n // Record the loaded segment in shared state — mirrors bufferState for\n // audio/video and supports N text tracks keyed by track ID.\n const latestState = context.state.get();\n const latest = latestState.textBufferState ?? {};\n const trackState = latest[trackId] ?? { segments: [] };\n context.state.set({\n ...latestState,\n textBufferState: {\n ...latest,\n [trackId]: { segments: [...trackState.segments, { id: segment.id }] },\n },\n } as S);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') break;\n console.error('Failed to load VTT segment:', error);\n // Continue to next segment (graceful degradation)\n }\n }\n\n // Chrome bug: after a track goes through mode='disabled' (which clears cues) and back\n // to 'showing', cues added to the track aren't activated. Re-adding all cues forces\n // Chrome to re-process them. Safe no-op in other browsers.\n // Mirrors the workaround in HlsMediaTextTracksMixin (packages/core/src/dom/media/hls/text-tracks.ts).\n if (context.textTrack.mode === 'showing' && context.textTrack.cues) {\n Array.from(context.textTrack.cues).forEach((cue) => {\n context.textTrack.addCue(cue);\n });\n }\n\n // Wait a frame before completing to allow state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * Loaded-segment record for a single text track.\n */\nexport interface TextTrackSegmentState {\n segments: Array<{ id: string }>;\n}\n\n/**\n * Buffer model for text track cues — keyed by track ID.\n *\n * Using a per-track-ID map (rather than fixed 'video'/'audio' keys) because\n * there can be N text tracks — one per language/subtitle variant.\n */\nexport type TextTrackBufferState = Record<string, TextTrackSegmentState>;\n\n/**\n * State shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingState {\n selectedTextTrackId?: string;\n presentation?: Presentation;\n /** Current playback position — used to gate VTT segment fetching to the forward buffer window. */\n currentTime?: number;\n /** Loaded-segment model for text tracks, keyed by track ID. */\n textBufferState?: TextTrackBufferState;\n}\n\n/**\n * Owners shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Find the selected text track in the presentation.\n */\nfunction findSelectedTextTrack(state: TextTrackCueLoadingState): TextTrack | undefined {\n if (!state.presentation || !state.selectedTextTrackId) {\n return undefined;\n }\n\n const textSet = state.presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) {\n return undefined;\n }\n\n const track = textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);\n\n return track as TextTrack | undefined;\n}\n\n/**\n * Get the browser's TextTrack object for the selected text track.\n *\n * Retrieves the live TextTrack interface from the track element in owners,\n * which is used for adding cues, checking mode, and managing track state.\n *\n * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),\n * not the presentation Track metadata type.\n *\n * @param state - Current playback state (track selection)\n * @param owners - DOM owners containing track elements map\n * @returns DOM TextTrack interface or undefined if not found\n */\nfunction getSelectedTextTrackFromOwners(\n state: TextTrackCueLoadingState,\n owners: TextTrackCueLoadingOwners\n): globalThis.TextTrack | undefined {\n const trackId = state.selectedTextTrackId;\n if (!trackId || !owners.textTracks) {\n return undefined;\n }\n\n const trackElement = owners.textTracks.get(trackId);\n return trackElement?.track;\n}\n\n/**\n * Check if we can load text track cues.\n *\n * Requires:\n * - Selected text track ID exists\n * - Track elements map exists\n * - Track element exists for selected track\n */\nexport function canLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);\n}\n\n/**\n * Check if we should load text track cues.\n *\n * Only load if:\n * - Track is resolved (has segments)\n * - Track has at least one segment\n * - Track element exists\n */\nexport function shouldLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n if (!canLoadTextTrackCues(state, owners)) {\n return false;\n }\n\n const track = findSelectedTextTrack(state);\n if (!track || !isResolvedTrack(track) || track.segments.length === 0) {\n return false;\n }\n\n const textTrack = getSelectedTextTrackFromOwners(state, owners);\n if (!textTrack) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Load text track cues orchestration.\n *\n * Triggers when:\n * - Text track is selected\n * - Track is resolved (has segments)\n * - Track element exists\n *\n * Fetches and parses VTT segments within the forward buffer window, then adds\n * cues to the track incrementally. Continues on segment errors to provide\n * partial subtitles.\n *\n * @example\n * const cleanup = loadTextTrackCues({ state, owners });\n */\nexport function loadTextTrackCues<S extends TextTrackCueLoadingState, O extends TextTrackCueLoadingOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n let currentTask: Promise<void> | null = null;\n let abortController: AbortController | null = null;\n let lastTrackId: string | undefined;\n\n const selectedTrackId = computed(() => state.get().selectedTextTrackId);\n\n const cleanupEffect = effect(() => {\n const s = state.get();\n const o = owners.get();\n\n // Abort any in-progress task when the selected track changes.\n // The new track's textBufferState entry will be empty, so the task\n // naturally starts fresh without needing any explicit reset.\n if (selectedTrackId.get() !== lastTrackId) {\n lastTrackId = selectedTrackId.get();\n abortController?.abort();\n currentTask = null;\n }\n\n if (currentTask) return;\n if (!shouldLoadTextTrackCues(s, o)) return;\n\n const textTrack = getSelectedTextTrackFromOwners(s, o);\n if (!textTrack) return;\n\n abortController = new AbortController();\n currentTask = loadTextTrackCuesTask(\n { currentState: s },\n { signal: abortController.signal, textTrack, state }\n ).finally(() => {\n currentTask = null;\n });\n });\n\n return () => {\n abortController?.abort();\n cleanupEffect();\n };\n}\n"],"mappings":"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"}
|