@videojs/spf 10.0.0-beta.10 → 10.0.0-beta.11
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/all.js +16 -0
- package/dist/default/core/abr/bandwidth-estimator.js +96 -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/events/create-event-stream.js +48 -0
- package/dist/default/core/events/create-event-stream.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +51 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +68 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +99 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +77 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +147 -0
- package/dist/default/core/features/select-tracks.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 +83 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +215 -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/reactive/combine-latest.js +72 -0
- package/dist/default/core/reactive/combine-latest.js.map +1 -0
- package/dist/default/core/state/create-state.js +143 -0
- package/dist/default/core/state/create-state.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 +31 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +169 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +188 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +151 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +197 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +55 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +70 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/setup-text-tracks.js +88 -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 +63 -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 +38 -0
- package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +46 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +55 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/{dom/index.js → default/dom/features/track-playback-rate.js} +2 -4
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +72 -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 +154 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +186 -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 +56 -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 +176 -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 +11 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +2 -2
- package/dist/default/index.js.map +1 -0
- package/dist/dev/all.js +16 -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 +96 -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 +31 -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/events/create-event-stream.d.ts +48 -0
- package/dist/dev/core/events/create-event-stream.d.ts.map +1 -0
- package/dist/dev/core/events/create-event-stream.js +48 -0
- package/dist/dev/core/events/create-event-stream.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +51 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +68 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.d.ts +16 -0
- package/dist/dev/core/features/resolve-presentation.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +99 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.d.ts +13 -0
- package/dist/dev/core/features/resolve-track.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-track.js +77 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.d.ts +11 -0
- package/dist/dev/core/features/select-tracks.d.ts.map +1 -0
- package/dist/dev/core/features/select-tracks.js +147 -0
- package/dist/dev/core/features/select-tracks.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 +83 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +215 -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/reactive/combine-latest.js +72 -0
- package/dist/dev/core/reactive/combine-latest.js.map +1 -0
- package/dist/dev/core/state/create-state.d.ts +104 -0
- package/dist/dev/core/state/create-state.d.ts.map +1 -0
- package/dist/dev/core/state/create-state.js +143 -0
- package/dist/dev/core/state/create-state.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 +31 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +169 -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 +188 -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 +151 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +197 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +55 -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 +70 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/setup-text-tracks.js +88 -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 +63 -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 +38 -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 +46 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +53 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +55 -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 +36 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +72 -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 +154 -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 +186 -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 +56 -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 +135 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +176 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +3 -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 +11 -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 +22 -22
- 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.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,88 @@
|
|
|
1
|
+
import { combineLatest } from "../../core/reactive/combine-latest.js";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/features/setup-text-tracks.ts
|
|
4
|
+
/**
|
|
5
|
+
* Get all text tracks from presentation.
|
|
6
|
+
*/
|
|
7
|
+
function getTextTracks(presentation) {
|
|
8
|
+
if (!presentation?.selectionSets) return [];
|
|
9
|
+
const textSet = presentation.selectionSets.find((set) => set.type === "text");
|
|
10
|
+
if (!textSet?.switchingSets?.[0]?.tracks) return [];
|
|
11
|
+
return textSet.switchingSets[0].tracks;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check if we can setup text tracks.
|
|
15
|
+
*
|
|
16
|
+
* Requires:
|
|
17
|
+
* - mediaElement exists
|
|
18
|
+
* - presentation has text tracks to setup
|
|
19
|
+
*/
|
|
20
|
+
function canSetupTextTracks(state, owners) {
|
|
21
|
+
return !!owners.mediaElement && !!getTextTracks(state.presentation).length;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if we should setup text tracks (not already set up).
|
|
25
|
+
*/
|
|
26
|
+
function shouldSetupTextTracks(owners) {
|
|
27
|
+
return !owners.textTracks;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a track element for a text track.
|
|
31
|
+
*
|
|
32
|
+
* Note: We use DOM <track> elements instead of the TextTrack JS API
|
|
33
|
+
* because there's no way to remove TextTracks added via addTextTrack().
|
|
34
|
+
*/
|
|
35
|
+
function createTrackElement(track) {
|
|
36
|
+
const trackElement = document.createElement("track");
|
|
37
|
+
trackElement.id = track.id;
|
|
38
|
+
trackElement.kind = track.kind;
|
|
39
|
+
trackElement.label = track.label;
|
|
40
|
+
if (track.language) trackElement.srclang = track.language;
|
|
41
|
+
if (track.default) trackElement.default = true;
|
|
42
|
+
trackElement.src = track.url;
|
|
43
|
+
return trackElement;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Setup text tracks orchestration.
|
|
47
|
+
*
|
|
48
|
+
* Triggers when:
|
|
49
|
+
* - mediaElement exists
|
|
50
|
+
* - presentation is resolved (has text tracks)
|
|
51
|
+
*
|
|
52
|
+
* Creates <track> elements for all text tracks and adds them as children
|
|
53
|
+
* to the media element. This allows the browser's native text track rendering.
|
|
54
|
+
*
|
|
55
|
+
* Note: Uses DOM track elements instead of TextTrack API because tracks
|
|
56
|
+
* added via addTextTrack() cannot be removed.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const cleanup = setupTextTracks({ state, owners });
|
|
60
|
+
*/
|
|
61
|
+
function setupTextTracks({ state, owners }) {
|
|
62
|
+
let hasSetup = false;
|
|
63
|
+
let createdTracks = [];
|
|
64
|
+
const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]) => {
|
|
65
|
+
if (hasSetup) return;
|
|
66
|
+
if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;
|
|
67
|
+
hasSetup = true;
|
|
68
|
+
const textTracks = getTextTracks(s.presentation);
|
|
69
|
+
if (textTracks.length === 0) return;
|
|
70
|
+
const trackMap = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const track of textTracks) {
|
|
72
|
+
const trackElement = createTrackElement(track);
|
|
73
|
+
o.mediaElement.appendChild(trackElement);
|
|
74
|
+
trackMap.set(track.id, trackElement);
|
|
75
|
+
createdTracks.push(trackElement);
|
|
76
|
+
}
|
|
77
|
+
owners.patch({ textTracks: trackMap });
|
|
78
|
+
});
|
|
79
|
+
return () => {
|
|
80
|
+
for (const trackElement of createdTracks) trackElement.remove();
|
|
81
|
+
createdTracks = [];
|
|
82
|
+
unsubscribe();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { setupTextTracks };
|
|
88
|
+
//# sourceMappingURL=setup-text-tracks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-text-tracks.js","names":[],"sources":["../../../../src/dom/features/setup-text-tracks.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { PartiallyResolvedTextTrack, Presentation } from '../../core/types';\n\n/**\n * State shape for text track setup.\n */\nexport interface TextTrackState {\n presentation?: Presentation | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track setup.\n */\nexport interface TextTrackOwners {\n mediaElement?: HTMLMediaElement | undefined;\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Get all text tracks from presentation.\n */\nfunction getTextTracks(presentation?: Presentation): PartiallyResolvedTextTrack[] {\n if (!presentation?.selectionSets) return [];\n\n const textSet = presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) return [];\n\n return textSet.switchingSets[0].tracks as PartiallyResolvedTextTrack[];\n}\n\n/**\n * Check if we can setup text tracks.\n *\n * Requires:\n * - mediaElement exists\n * - presentation has text tracks to setup\n */\nexport function canSetupTextTracks(state: TextTrackState, owners: TextTrackOwners): boolean {\n return !!owners.mediaElement && !!getTextTracks(state.presentation).length;\n}\n\n/**\n * Check if we should setup text tracks (not already set up).\n */\nexport function shouldSetupTextTracks(owners: TextTrackOwners): boolean {\n return !owners.textTracks;\n}\n\n/**\n * Create a track element for a text track.\n *\n * Note: We use DOM <track> elements instead of the TextTrack JS API\n * because there's no way to remove TextTracks added via addTextTrack().\n */\nfunction createTrackElement(track: PartiallyResolvedTextTrack): HTMLTrackElement {\n const trackElement = document.createElement('track');\n\n trackElement.id = track.id;\n trackElement.kind = track.kind;\n trackElement.label = track.label;\n\n if (track.language) {\n trackElement.srclang = track.language;\n }\n\n if (track.default) {\n trackElement.default = true;\n }\n\n // Set src to track URL\n trackElement.src = track.url;\n\n return trackElement;\n}\n\n/**\n * Setup text tracks orchestration.\n *\n * Triggers when:\n * - mediaElement exists\n * - presentation is resolved (has text tracks)\n *\n * Creates <track> elements for all text tracks and adds them as children\n * to the media element. This allows the browser's native text track rendering.\n *\n * Note: Uses DOM track elements instead of TextTrack API because tracks\n * added via addTextTrack() cannot be removed.\n *\n * @example\n * const cleanup = setupTextTracks({ state, owners });\n */\nexport function setupTextTracks({\n state,\n owners,\n}: {\n state: WritableState<TextTrackState>;\n owners: WritableState<TextTrackOwners>;\n}): () => void {\n let hasSetup = false;\n let createdTracks: HTMLTrackElement[] = [];\n\n const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]: [TextTrackState, TextTrackOwners]) => {\n // Check orchestration conditions\n if (hasSetup) return; // Only run once\n if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;\n\n // Mark as setup before doing any work to prevent re-entry\n hasSetup = true;\n\n const textTracks = getTextTracks(s.presentation);\n if (textTracks.length === 0) return;\n\n const trackMap = new Map<string, HTMLTrackElement>();\n\n // Create track elements for all text tracks\n for (const track of textTracks) {\n const trackElement = createTrackElement(track);\n o.mediaElement!.appendChild(trackElement);\n trackMap.set(track.id, trackElement);\n createdTracks.push(trackElement);\n }\n\n // Update owners with track element map\n owners.patch({ textTracks: trackMap });\n });\n\n // Return cleanup function\n return () => {\n // Remove all created track elements\n for (const trackElement of createdTracks) {\n trackElement.remove();\n }\n createdTracks = [];\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;AAuBA,SAAS,cAAc,cAA2D;AAChF,KAAI,CAAC,cAAc,cAAe,QAAO,EAAE;CAE3C,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAQ,QAAO,EAAE;AAEnD,QAAO,QAAQ,cAAc,GAAG;;;;;;;;;AAUlC,SAAgB,mBAAmB,OAAuB,QAAkC;AAC1F,QAAO,CAAC,CAAC,OAAO,gBAAgB,CAAC,CAAC,cAAc,MAAM,aAAa,CAAC;;;;;AAMtE,SAAgB,sBAAsB,QAAkC;AACtE,QAAO,CAAC,OAAO;;;;;;;;AASjB,SAAS,mBAAmB,OAAqD;CAC/E,MAAM,eAAe,SAAS,cAAc,QAAQ;AAEpD,cAAa,KAAK,MAAM;AACxB,cAAa,OAAO,MAAM;AAC1B,cAAa,QAAQ,MAAM;AAE3B,KAAI,MAAM,SACR,cAAa,UAAU,MAAM;AAG/B,KAAI,MAAM,QACR,cAAa,UAAU;AAIzB,cAAa,MAAM,MAAM;AAEzB,QAAO;;;;;;;;;;;;;;;;;;AAmBT,SAAgB,gBAAgB,EAC9B,OACA,UAIa;CACb,IAAI,WAAW;CACf,IAAI,gBAAoC,EAAE;CAE1C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAA0C;AAE1G,MAAI,SAAU;AACd,MAAI,CAAC,mBAAmB,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAE;AAG5D,aAAW;EAEX,MAAM,aAAa,cAAc,EAAE,aAAa;AAChD,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,2BAAW,IAAI,KAA+B;AAGpD,OAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,eAAe,mBAAmB,MAAM;AAC9C,KAAE,aAAc,YAAY,aAAa;AACzC,YAAS,IAAI,MAAM,IAAI,aAAa;AACpC,iBAAc,KAAK,aAAa;;AAIlC,SAAO,MAAM,EAAE,YAAY,UAAU,CAAC;GACtC;AAGF,cAAa;AAEX,OAAK,MAAM,gBAAgB,cACzB,cAAa,QAAQ;AAEvB,kBAAgB,EAAE;AAClB,eAAa"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { listen } from "@videojs/utils/dom";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/features/sync-selected-text-track-from-dom.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sync selectedTextTrackId from DOM text track mode changes.
|
|
6
|
+
*
|
|
7
|
+
* Listens to the `change` event on `media.textTracks` and updates
|
|
8
|
+
* `selectedTextTrackId` when external code (e.g. the captions button via
|
|
9
|
+
* `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.
|
|
10
|
+
*
|
|
11
|
+
* This bridges the core store's `toggleSubtitles()` with SPF's reactive text
|
|
12
|
+
* track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this
|
|
13
|
+
* bridge, direct DOM mode changes would be immediately overridden by
|
|
14
|
+
* `syncTextTrackModes` on the next SPF state update.
|
|
15
|
+
*
|
|
16
|
+
* When a subtitle/caption track's mode is 'showing', its DOM `id` — which
|
|
17
|
+
* matches the SPF track ID set by `setupTextTracks` — is written to
|
|
18
|
+
* `selectedTextTrackId`. When no subtitle/caption track is 'showing',
|
|
19
|
+
* `selectedTextTrackId` is cleared along with the deselected track's
|
|
20
|
+
* `textBufferState` entry — setting mode to 'disabled' clears native cues from
|
|
21
|
+
* the track element, so the buffer must be reset to re-fetch cues on re-enable.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const cleanup = syncSelectedTextTrackFromDom({ state, owners });
|
|
25
|
+
*/
|
|
26
|
+
function syncSelectedTextTrackFromDom({ state, owners }) {
|
|
27
|
+
let lastMediaElement;
|
|
28
|
+
let removeListener = null;
|
|
29
|
+
const unsubscribe = owners.subscribe((currentOwners) => {
|
|
30
|
+
const { mediaElement } = currentOwners;
|
|
31
|
+
if (mediaElement === lastMediaElement) return;
|
|
32
|
+
removeListener?.();
|
|
33
|
+
removeListener = null;
|
|
34
|
+
lastMediaElement = mediaElement;
|
|
35
|
+
if (!mediaElement) return;
|
|
36
|
+
const sync = () => {
|
|
37
|
+
const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
|
|
38
|
+
const current = state.current;
|
|
39
|
+
if (current.selectedTextTrackId === newId) return;
|
|
40
|
+
if (newId) state.patch({ selectedTextTrackId: newId });
|
|
41
|
+
else {
|
|
42
|
+
const prevId = current.selectedTextTrackId;
|
|
43
|
+
if (prevId && current.textBufferState?.[prevId]) {
|
|
44
|
+
const next = { ...current.textBufferState };
|
|
45
|
+
delete next[prevId];
|
|
46
|
+
state.patch({
|
|
47
|
+
selectedTextTrackId: void 0,
|
|
48
|
+
textBufferState: next
|
|
49
|
+
});
|
|
50
|
+
} else state.patch({ selectedTextTrackId: void 0 });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
removeListener = listen(mediaElement.textTracks, "change", sync);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
removeListener?.();
|
|
57
|
+
unsubscribe();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { syncSelectedTextTrackFromDom };
|
|
63
|
+
//# sourceMappingURL=sync-selected-text-track-from-dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-selected-text-track-from-dom.js","names":[],"sources":["../../../../src/dom/features/sync-selected-text-track-from-dom.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { TextTrackBufferState } from './load-text-track-cues';\n\n/**\n * State shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomState {\n selectedTextTrackId?: string | undefined;\n textBufferState?: TextTrackBufferState | undefined;\n}\n\n/**\n * Owners shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Sync selectedTextTrackId from DOM text track mode changes.\n *\n * Listens to the `change` event on `media.textTracks` and updates\n * `selectedTextTrackId` when external code (e.g. the captions button via\n * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.\n *\n * This bridges the core store's `toggleSubtitles()` with SPF's reactive text\n * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this\n * bridge, direct DOM mode changes would be immediately overridden by\n * `syncTextTrackModes` on the next SPF state update.\n *\n * When a subtitle/caption track's mode is 'showing', its DOM `id` — which\n * matches the SPF track ID set by `setupTextTracks` — is written to\n * `selectedTextTrackId`. When no subtitle/caption track is 'showing',\n * `selectedTextTrackId` is cleared along with the deselected track's\n * `textBufferState` entry — setting mode to 'disabled' clears native cues from\n * the track element, so the buffer must be reset to re-fetch cues on re-enable.\n *\n * @example\n * const cleanup = syncSelectedTextTrackFromDom({ state, owners });\n */\nexport function syncSelectedTextTrackFromDom({\n state,\n owners,\n}: {\n state: WritableState<SelectedTextTrackFromDomState>;\n owners: WritableState<SelectedTextTrackFromDomOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n const sync = () => {\n const showingTrack = Array.from(mediaElement.textTracks).find(\n (t) => t.mode === 'showing' && (t.kind === 'subtitles' || t.kind === 'captions')\n );\n\n // showingTrack.id is set from the SPF presentation track ID by setupTextTracks.\n // Fall back to undefined for empty-string IDs (non-SPF-managed tracks).\n const newId = showingTrack?.id || undefined;\n const current = state.current;\n\n // Guard against redundant patches — e.g. syncTextTrackModes confirming the\n // current selection, which would otherwise create a feedback loop.\n if (current.selectedTextTrackId === newId) return;\n\n if (newId) {\n state.patch({ selectedTextTrackId: newId });\n } else {\n // When deselecting, clear the textBufferState entry for the previous track.\n // Setting mode to 'disabled' (as toggleSubtitles() does) clears native cues\n // from the track element, so the buffer must be reset to allow re-fetching\n // on re-enable.\n const prevId = current.selectedTextTrackId;\n if (prevId && current.textBufferState?.[prevId]) {\n const next = { ...current.textBufferState };\n delete next[prevId];\n state.patch({ selectedTextTrackId: undefined, textBufferState: next });\n } else {\n state.patch({ selectedTextTrackId: undefined });\n }\n }\n };\n\n removeListener = listen(mediaElement.textTracks, 'change', sync);\n });\n\n return () => {\n removeListener?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,6BAA6B,EAC3C,OACA,UAIa;CACb,IAAI;CACJ,IAAI,iBAAsC;CAE1C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;EAEnB,MAAM,aAAa;GAOjB,MAAM,QANe,MAAM,KAAK,aAAa,WAAW,CAAC,MACtD,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,eAAe,EAAE,SAAS,YACtE,EAI2B,MAAM;GAClC,MAAM,UAAU,MAAM;AAItB,OAAI,QAAQ,wBAAwB,MAAO;AAE3C,OAAI,MACF,OAAM,MAAM,EAAE,qBAAqB,OAAO,CAAC;QACtC;IAKL,MAAM,SAAS,QAAQ;AACvB,QAAI,UAAU,QAAQ,kBAAkB,SAAS;KAC/C,MAAM,OAAO,EAAE,GAAG,QAAQ,iBAAiB;AAC3C,YAAO,KAAK;AACZ,WAAM,MAAM;MAAE,qBAAqB;MAAW,iBAAiB;MAAM,CAAC;UAEtE,OAAM,MAAM,EAAE,qBAAqB,QAAW,CAAC;;;AAKrD,mBAAiB,OAAO,aAAa,YAAY,UAAU,KAAK;GAChE;AAEF,cAAa;AACX,oBAAkB;AAClB,eAAa"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { combineLatest } from "../../core/reactive/combine-latest.js";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/features/sync-text-track-modes.ts
|
|
4
|
+
/**
|
|
5
|
+
* Check if we can sync text track modes.
|
|
6
|
+
*
|
|
7
|
+
* Requires:
|
|
8
|
+
* - textTracks map exists (track elements created)
|
|
9
|
+
*/
|
|
10
|
+
function canSyncTextTrackModes(owners) {
|
|
11
|
+
return !!owners.textTracks && owners.textTracks.size > 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sync text track modes orchestration.
|
|
15
|
+
*
|
|
16
|
+
* Manages track element modes based on selectedTextTrackId:
|
|
17
|
+
* - Selected track: mode = "showing"
|
|
18
|
+
* - Other tracks: mode = "hidden"
|
|
19
|
+
* - No selection: all tracks mode = "hidden"
|
|
20
|
+
*
|
|
21
|
+
* Note: Uses "hidden" instead of "disabled" for non-selected tracks
|
|
22
|
+
* so they remain available in the browser's track menu.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const cleanup = syncTextTrackModes({ state, owners });
|
|
26
|
+
*/
|
|
27
|
+
function syncTextTrackModes({ state, owners }) {
|
|
28
|
+
return combineLatest([state, owners]).subscribe(([s, o]) => {
|
|
29
|
+
if (!canSyncTextTrackModes(o)) return;
|
|
30
|
+
const selectedId = s.selectedTextTrackId;
|
|
31
|
+
for (const [trackId, trackElement] of o.textTracks) if (trackId === selectedId) trackElement.track.mode = "showing";
|
|
32
|
+
else trackElement.track.mode = "hidden";
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { syncTextTrackModes };
|
|
38
|
+
//# sourceMappingURL=sync-text-track-modes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-text-track-modes.js","names":[],"sources":["../../../../src/dom/features/sync-text-track-modes.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for text track mode synchronization.\n */\nexport interface TextTrackModeState {\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track mode synchronization.\n */\nexport interface TextTrackModeOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Check if we can sync text track modes.\n *\n * Requires:\n * - textTracks map exists (track elements created)\n */\nexport function canSyncTextTrackModes(owners: TextTrackModeOwners): boolean {\n return !!owners.textTracks && owners.textTracks.size > 0;\n}\n\n/**\n * Sync text track modes orchestration.\n *\n * Manages track element modes based on selectedTextTrackId:\n * - Selected track: mode = \"showing\"\n * - Other tracks: mode = \"hidden\"\n * - No selection: all tracks mode = \"hidden\"\n *\n * Note: Uses \"hidden\" instead of \"disabled\" for non-selected tracks\n * so they remain available in the browser's track menu.\n *\n * @example\n * const cleanup = syncTextTrackModes({ state, owners });\n */\nexport function syncTextTrackModes({\n state,\n owners,\n}: {\n state: WritableState<TextTrackModeState>;\n owners: WritableState<TextTrackModeOwners>;\n}): () => void {\n return combineLatest([state, owners]).subscribe(([s, o]: [TextTrackModeState, TextTrackModeOwners]) => {\n // Check orchestration conditions\n if (!canSyncTextTrackModes(o)) return;\n\n const selectedId = s.selectedTextTrackId;\n\n // Update all track element modes\n for (const [trackId, trackElement] of o.textTracks!) {\n if (trackId === selectedId) {\n // Selected track: show it\n trackElement.track.mode = 'showing';\n } else {\n // Other tracks: hide them (but keep available in menu)\n trackElement.track.mode = 'hidden';\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;AAuBA,SAAgB,sBAAsB,QAAsC;AAC1E,QAAO,CAAC,CAAC,OAAO,cAAc,OAAO,WAAW,OAAO;;;;;;;;;;;;;;;;AAiBzD,SAAgB,mBAAmB,EACjC,OACA,UAIa;AACb,QAAO,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAAkD;AAErG,MAAI,CAAC,sBAAsB,EAAE,CAAE;EAE/B,MAAM,aAAa,EAAE;AAGrB,OAAK,MAAM,CAAC,SAAS,iBAAiB,EAAE,WACtC,KAAI,YAAY,WAEd,cAAa,MAAM,OAAO;MAG1B,cAAa,MAAM,OAAO;GAG9B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { listen } from "@videojs/utils/dom";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/features/track-current-time.ts
|
|
4
|
+
/**
|
|
5
|
+
* Track current playback position from the media element.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors `mediaElement.currentTime` into reactive state on:
|
|
8
|
+
* - `timeupdate` — fires during playback (~4 Hz)
|
|
9
|
+
* - `seeking` — fires when a seek begins; per spec, `currentTime` is
|
|
10
|
+
* already at the new position when this event dispatches, so buffer
|
|
11
|
+
* management can react immediately rather than waiting for `timeupdate`,
|
|
12
|
+
* which does not fire while paused.
|
|
13
|
+
*
|
|
14
|
+
* Also syncs immediately when a media element becomes available.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const cleanup = trackCurrentTime({ state, owners });
|
|
18
|
+
*/
|
|
19
|
+
function trackCurrentTime({ state, owners }) {
|
|
20
|
+
let lastMediaElement;
|
|
21
|
+
let removeListeners = null;
|
|
22
|
+
const unsubscribe = owners.subscribe((currentOwners) => {
|
|
23
|
+
const { mediaElement } = currentOwners;
|
|
24
|
+
if (mediaElement === lastMediaElement) return;
|
|
25
|
+
removeListeners?.();
|
|
26
|
+
removeListeners = null;
|
|
27
|
+
lastMediaElement = mediaElement;
|
|
28
|
+
if (!mediaElement) return;
|
|
29
|
+
state.patch({ currentTime: mediaElement.currentTime });
|
|
30
|
+
const sync = () => state.patch({ currentTime: mediaElement.currentTime });
|
|
31
|
+
const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
|
|
32
|
+
const removeSeeking = listen(mediaElement, "seeking", sync);
|
|
33
|
+
removeListeners = () => {
|
|
34
|
+
removeTimeupdate();
|
|
35
|
+
removeSeeking();
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
removeListeners?.();
|
|
40
|
+
unsubscribe();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { trackCurrentTime };
|
|
46
|
+
//# sourceMappingURL=track-current-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../src/dom/features/track-current-time.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for current time tracking.\n */\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\n/**\n * Owners shape for current time tracking.\n */\nexport interface CurrentTimeOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track current time.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackCurrentTime(owners: CurrentTimeOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track current playback position from the media element.\n *\n * Mirrors `mediaElement.currentTime` into reactive state on:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is\n * already at the new position when this event dispatches, so buffer\n * management can react immediately rather than waiting for `timeupdate`,\n * which does not fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackCurrentTime({ state, owners });\n */\nexport function trackCurrentTime({\n state,\n owners,\n}: {\n state: WritableState<CurrentTimeState>;\n owners: WritableState<CurrentTimeOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListeners: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListeners?.();\n removeListeners = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n // Sync immediately so consumers don't wait for the first event\n state.patch({ currentTime: mediaElement.currentTime });\n\n const sync = () => state.patch({ currentTime: mediaElement.currentTime });\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n removeListeners = () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n\n return () => {\n removeListeners?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAI;CACJ,IAAI,kBAAuC;CAE3C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,qBAAmB;AACnB,oBAAkB;AAClB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAGnB,QAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EAEtD,MAAM,aAAa,MAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EACzE,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,0BAAwB;AACtB,qBAAkB;AAClB,kBAAe;;GAEjB;AAEF,cAAa;AACX,qBAAmB;AACnB,eAAa"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { listen } from "@videojs/utils/dom";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/features/track-playback-initiated.ts
|
|
4
|
+
/**
|
|
5
|
+
* Track whether playback has been initiated by the user.
|
|
6
|
+
*
|
|
7
|
+
* Sets `state.playbackInitiated = true` when the media element fires a `play`
|
|
8
|
+
* event (via `element.play()`, native controls, or autoplay) and simultaneously
|
|
9
|
+
* dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`
|
|
10
|
+
* can react.
|
|
11
|
+
*
|
|
12
|
+
* Resets `state.playbackInitiated = false` when `presentation.url` changes,
|
|
13
|
+
* so a new source with `preload="none"` won't load segments until play is
|
|
14
|
+
* triggered again.
|
|
15
|
+
*
|
|
16
|
+
* This flag is used by `shouldLoadSegments` to allow segment loading after
|
|
17
|
+
* play is initiated regardless of the initial `preload` setting — `preload`
|
|
18
|
+
* is a startup hint, not a runtime gate.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const cleanup = trackPlaybackInitiated({ state, owners, events });
|
|
22
|
+
*/
|
|
23
|
+
function trackPlaybackInitiated({ state, owners, events }) {
|
|
24
|
+
let lastMediaElement;
|
|
25
|
+
let removeListener = null;
|
|
26
|
+
let lastPresentationUrl;
|
|
27
|
+
const unsubscribeState = state.subscribe((currentState) => {
|
|
28
|
+
const url = currentState.presentation?.url;
|
|
29
|
+
if (url !== lastPresentationUrl) {
|
|
30
|
+
if (lastPresentationUrl !== void 0) state.patch({ playbackInitiated: false });
|
|
31
|
+
lastPresentationUrl = url;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const unsubscribeOwners = owners.subscribe((currentOwners) => {
|
|
35
|
+
const { mediaElement } = currentOwners;
|
|
36
|
+
if (mediaElement === lastMediaElement) return;
|
|
37
|
+
removeListener?.();
|
|
38
|
+
removeListener = null;
|
|
39
|
+
lastMediaElement = mediaElement;
|
|
40
|
+
if (!mediaElement) return;
|
|
41
|
+
removeListener = listen(mediaElement, "play", () => {
|
|
42
|
+
state.patch({ playbackInitiated: true });
|
|
43
|
+
events.dispatch({ type: "play" });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
removeListener?.();
|
|
48
|
+
unsubscribeState();
|
|
49
|
+
unsubscribeOwners();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { trackPlaybackInitiated };
|
|
55
|
+
//# sourceMappingURL=track-playback-initiated.js.map
|
|
@@ -0,0 +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 type { EventStream } from '../../core/events/create-event-stream';\nimport type { PresentationAction } from '../../core/features/resolve-presentation';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once the user has initiated playback (play event fired). */\n playbackInitiated?: boolean;\n /** Current presentation — watched for URL changes to reset playbackInitiated. */\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 * Track whether playback has been initiated by the user.\n *\n * Sets `state.playbackInitiated = true` when the media element fires a `play`\n * event (via `element.play()`, native controls, or autoplay) and simultaneously\n * dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`\n * can react.\n *\n * Resets `state.playbackInitiated = false` when `presentation.url` changes,\n * so a new source with `preload=\"none\"` won't load segments until play is\n * triggered again.\n *\n * This flag is used by `shouldLoadSegments` to allow segment loading after\n * play is initiated regardless of the initial `preload` setting — `preload`\n * is a startup hint, not a runtime gate.\n *\n * @example\n * const cleanup = trackPlaybackInitiated({ state, owners, events });\n */\nexport function trackPlaybackInitiated({\n state,\n owners,\n events,\n}: {\n state: WritableState<PlaybackInitiatedState>;\n owners: WritableState<PlaybackInitiatedOwners>;\n events: EventStream<PresentationAction>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n let lastPresentationUrl: string | undefined;\n\n // Watch for presentation URL changes to reset playbackInitiated\n const unsubscribeState = state.subscribe((currentState) => {\n const url = currentState.presentation?.url;\n if (url !== lastPresentationUrl) {\n if (lastPresentationUrl !== undefined) {\n // URL changed to a new value — reset so new source requires play again\n state.patch({ playbackInitiated: false });\n }\n lastPresentationUrl = url;\n }\n });\n\n // Bridge media element play event → state flag + event stream\n const unsubscribeOwners = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n removeListener = listen(mediaElement, 'play', () => {\n state.patch({ playbackInitiated: true });\n events.dispatch({ type: 'play' });\n });\n });\n\n return () => {\n removeListener?.();\n unsubscribeState();\n unsubscribeOwners();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,uBAAuB,EACrC,OACA,QACA,UAKa;CACb,IAAI;CACJ,IAAI,iBAAsC;CAC1C,IAAI;CAGJ,MAAM,mBAAmB,MAAM,WAAW,iBAAiB;EACzD,MAAM,MAAM,aAAa,cAAc;AACvC,MAAI,QAAQ,qBAAqB;AAC/B,OAAI,wBAAwB,OAE1B,OAAM,MAAM,EAAE,mBAAmB,OAAO,CAAC;AAE3C,yBAAsB;;GAExB;CAGF,MAAM,oBAAoB,OAAO,WAAW,kBAAkB;EAC5D,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAEnB,mBAAiB,OAAO,cAAc,cAAc;AAClD,SAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AACxC,UAAO,SAAS,EAAE,MAAM,QAAQ,CAAC;IACjC;GACF;AAEF,cAAa;AACX,oBAAkB;AAClB,oBAAkB;AAClB,qBAAmB"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import "../src-DRsS_H80.js";
|
|
2
|
-
import { a as trackPlaybackInitiated, c as destroyVttParser, i as appendSegment, l as parseVttSegment, o as trackCurrentTime, r as flushBuffer, s as loadTextTrackCues, t as SpfMedia, u as loadSegments } from "../adapter-CflgYzjF.js";
|
|
3
1
|
import { listen } from "@videojs/utils/dom";
|
|
4
2
|
|
|
5
3
|
//#region src/dom/features/track-playback-rate.ts
|
|
@@ -34,5 +32,5 @@ function trackPlaybackRate({ state, owners }) {
|
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
//#endregion
|
|
37
|
-
export {
|
|
38
|
-
//# sourceMappingURL=
|
|
35
|
+
export { trackPlaybackRate };
|
|
36
|
+
//# sourceMappingURL=track-playback-rate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../src/dom/features/track-playback-rate.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for playback rate tracking.\n */\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\n/**\n * Owners shape for playback rate tracking.\n */\nexport interface PlaybackRateOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track playback rate.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackPlaybackRate(owners: PlaybackRateOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track playback rate from the media element.\n *\n * Mirrors `mediaElement.playbackRate` into reactive state on each `ratechange`\n * event. Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackPlaybackRate({ state, owners });\n */\nexport function trackPlaybackRate({\n state,\n owners,\n}: {\n state: WritableState<PlaybackRateState>;\n owners: WritableState<PlaybackRateOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n state.patch({ playbackRate: mediaElement.playbackRate });\n\n removeListener = listen(mediaElement, 'ratechange', () => {\n state.patch({ playbackRate: mediaElement.playbackRate });\n });\n });\n\n return () => {\n removeListener?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;AAoCA,SAAgB,kBAAkB,EAChC,OACA,UAIa;CACb,IAAI;CACJ,IAAI,iBAAsC;CAE1C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAEnB,QAAM,MAAM,EAAE,cAAc,aAAa,cAAc,CAAC;AAExD,mBAAiB,OAAO,cAAc,oBAAoB;AACxD,SAAM,MAAM,EAAE,cAAc,aAAa,cAAc,CAAC;IACxD;GACF;AAEF,cAAa;AACX,oBAAkB;AAClB,eAAa"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { hasPresentationDuration } from "../../core/types/index.js";
|
|
2
|
+
import { combineLatest } from "../../core/reactive/combine-latest.js";
|
|
3
|
+
|
|
4
|
+
//#region src/dom/features/update-duration.ts
|
|
5
|
+
/**
|
|
6
|
+
* Check if we can update MediaSource duration (have required data).
|
|
7
|
+
*/
|
|
8
|
+
function canUpdateDuration(state, owners) {
|
|
9
|
+
return !!(owners.mediaSource && state.presentation && hasPresentationDuration(state.presentation));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the maximum buffered end time across all SourceBuffers.
|
|
13
|
+
*/
|
|
14
|
+
function getMaxBufferedEnd(owners) {
|
|
15
|
+
let maxEnd = 0;
|
|
16
|
+
const buffers = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter((buf) => buf !== void 0);
|
|
17
|
+
for (const buffer of buffers) {
|
|
18
|
+
const { buffered } = buffer;
|
|
19
|
+
if (buffered.length > 0) {
|
|
20
|
+
const end = buffered.end(buffered.length - 1);
|
|
21
|
+
if (end > maxEnd) maxEnd = end;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return maxEnd;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if we should update MediaSource duration (conditions met).
|
|
28
|
+
*/
|
|
29
|
+
function shouldUpdateDuration(state, owners) {
|
|
30
|
+
if (!canUpdateDuration(state, owners)) return false;
|
|
31
|
+
const { mediaSource } = owners;
|
|
32
|
+
const { presentation } = state;
|
|
33
|
+
if (mediaSource.readyState !== "open") return false;
|
|
34
|
+
const duration = presentation.duration;
|
|
35
|
+
if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;
|
|
36
|
+
return Number.isNaN(mediaSource.duration);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wait for all currently-updating SourceBuffers to finish.
|
|
40
|
+
*
|
|
41
|
+
* The MSE spec forbids setting MediaSource.duration while any attached
|
|
42
|
+
* SourceBuffer has updating === true. This defers until all are idle.
|
|
43
|
+
*/
|
|
44
|
+
function waitForSourceBuffersReady(owners) {
|
|
45
|
+
const updating = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter((buf) => buf?.updating === true);
|
|
46
|
+
if (updating.length === 0) return Promise.resolve();
|
|
47
|
+
return Promise.all(updating.map((buf) => new Promise((resolve) => buf.addEventListener("updateend", () => resolve(), { once: true })))).then(() => void 0);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Update MediaSource duration when presentation duration becomes available.
|
|
51
|
+
*/
|
|
52
|
+
function updateDuration({ state, owners }) {
|
|
53
|
+
let destroyed = false;
|
|
54
|
+
const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
|
|
55
|
+
if (!shouldUpdateDuration(currentState, currentOwners)) return;
|
|
56
|
+
const { mediaSource } = currentOwners;
|
|
57
|
+
await waitForSourceBuffersReady(currentOwners);
|
|
58
|
+
if (destroyed || mediaSource.readyState !== "open") return;
|
|
59
|
+
let duration = currentState.presentation.duration;
|
|
60
|
+
const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
|
|
61
|
+
if (maxBufferedEnd > duration) duration = maxBufferedEnd;
|
|
62
|
+
mediaSource.duration = duration;
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
destroyed = true;
|
|
66
|
+
unsubscribe();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { updateDuration };
|
|
72
|
+
//# sourceMappingURL=update-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-duration.js","names":[],"sources":["../../../../src/dom/features/update-duration.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { hasPresentationDuration } from '../../core/types';\n\nexport interface DurationUpdateState {\n presentation?: Presentation;\n}\n\nexport interface DurationUpdateOwners {\n mediaSource?: MediaSource;\n videoSourceBuffer?: SourceBuffer;\n audioSourceBuffer?: SourceBuffer;\n}\n\n/**\n * Check if we can update MediaSource duration (have required data).\n */\nexport function canUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n return !!(owners.mediaSource && state.presentation && hasPresentationDuration(state.presentation));\n}\n\n/**\n * Get the maximum buffered end time across all SourceBuffers.\n */\nexport function getMaxBufferedEnd(owners: DurationUpdateOwners): number {\n let maxEnd = 0;\n\n const buffers = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf !== undefined\n );\n\n for (const buffer of buffers) {\n const { buffered } = buffer;\n if (buffered.length > 0) {\n const end = buffered.end(buffered.length - 1);\n if (end > maxEnd) {\n maxEnd = end;\n }\n }\n }\n\n return maxEnd;\n}\n\n/**\n * Check if we should update MediaSource duration (conditions met).\n */\nexport function shouldUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n if (!canUpdateDuration(state, owners)) return false;\n\n const { mediaSource } = owners;\n const { presentation } = state;\n\n // MediaSource must be open\n if (mediaSource!.readyState !== 'open') return false;\n\n const duration = presentation!.duration!;\n\n // Validate duration: finite, positive, not NaN\n if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;\n\n // Only set duration on initial MediaSource setup, when it hasn't been set yet.\n // A freshly opened MediaSource has duration === NaN. Once set (either by this\n // task or by endOfStreamTask from buffered.end()), we leave it alone — attempting\n // to re-sync a slight drift between mediaSource.duration and presentation.duration\n // races with concurrent appendBuffer() calls from loadSegmentsTask.\n return Number.isNaN(mediaSource!.duration);\n}\n\n/**\n * Wait for all currently-updating SourceBuffers to finish.\n *\n * The MSE spec forbids setting MediaSource.duration while any attached\n * SourceBuffer has updating === true. This defers until all are idle.\n */\nfunction waitForSourceBuffersReady(owners: DurationUpdateOwners): Promise<void> {\n const updating = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf?.updating === true\n );\n\n if (updating.length === 0) return Promise.resolve();\n\n return Promise.all(\n updating.map(\n (buf) => new Promise<void>((resolve) => buf.addEventListener('updateend', () => resolve(), { once: true }))\n )\n ).then(() => undefined);\n}\n\n/**\n * Update MediaSource duration when presentation duration becomes available.\n */\nexport function updateDuration({\n state,\n owners,\n}: {\n state: WritableState<DurationUpdateState>;\n owners: WritableState<DurationUpdateOwners>;\n}): () => void {\n let destroyed = false;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [DurationUpdateState, DurationUpdateOwners]) => {\n if (!shouldUpdateDuration(currentState, currentOwners)) return;\n\n const { mediaSource } = currentOwners;\n\n // MSE spec: duration cannot be set while any SourceBuffer is updating\n await waitForSourceBuffersReady(currentOwners);\n\n // Re-check after async wait: destroyed, or readyState changed (e.g. endOfStream\n // already called endOfStream(), transitioning 'open' → 'ended').\n if (destroyed || mediaSource!.readyState !== 'open') return;\n\n let duration = currentState.presentation!.duration!;\n\n // Get max buffered end time across all SourceBuffers\n const maxBufferedEnd = getMaxBufferedEnd(currentOwners);\n\n // MSE spec: duration cannot be less than any buffered range\n // If buffered ranges exceed calculated duration, extend to match\n if (maxBufferedEnd > duration) {\n duration = maxBufferedEnd;\n }\n\n // Set duration on MediaSource\n mediaSource!.duration = duration;\n }\n );\n\n return () => {\n destroyed = true;\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;AAkBA,SAAgB,kBAAkB,OAA4B,QAAuC;AACnG,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM,gBAAgB,wBAAwB,MAAM,aAAa;;;;;AAMnG,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,SAAS;CAEb,MAAM,UAAU,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QAClE,QAA6B,QAAQ,OACvC;AAED,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,EAAE,aAAa;AACrB,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,MAAM,SAAS,IAAI,SAAS,SAAS,EAAE;AAC7C,OAAI,MAAM,OACR,UAAS;;;AAKf,QAAO;;;;;AAMT,SAAgB,qBAAqB,OAA4B,QAAuC;AACtG,KAAI,CAAC,kBAAkB,OAAO,OAAO,CAAE,QAAO;CAE9C,MAAM,EAAE,gBAAgB;CACxB,MAAM,EAAE,iBAAiB;AAGzB,KAAI,YAAa,eAAe,OAAQ,QAAO;CAE/C,MAAM,WAAW,aAAc;AAG/B,KAAI,CAAC,OAAO,SAAS,SAAS,IAAI,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG,QAAO;AAOlF,QAAO,OAAO,MAAM,YAAa,SAAS;;;;;;;;AAS5C,SAAS,0BAA0B,QAA6C;CAC9E,MAAM,WAAW,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QACnE,QAA6B,KAAK,aAAa,KACjD;AAED,KAAI,SAAS,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEnD,QAAO,QAAQ,IACb,SAAS,KACN,QAAQ,IAAI,SAAe,YAAY,IAAI,iBAAiB,mBAAmB,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC,CAAC,CAC5G,CACF,CAAC,WAAW,OAAU;;;;;AAMzB,SAAgB,eAAe,EAC7B,OACA,UAIa;CACb,IAAI,YAAY;CAEhB,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAAgE;AACpF,MAAI,CAAC,qBAAqB,cAAc,cAAc,CAAE;EAExD,MAAM,EAAE,gBAAgB;AAGxB,QAAM,0BAA0B,cAAc;AAI9C,MAAI,aAAa,YAAa,eAAe,OAAQ;EAErD,IAAI,WAAW,aAAa,aAAc;EAG1C,MAAM,iBAAiB,kBAAkB,cAAc;AAIvD,MAAI,iBAAiB,SACnB,YAAW;AAIb,cAAa,WAAW;GAE3B;AAED,cAAa;AACX,cAAY;AACZ,eAAa"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/dom/media/append-segment.ts
|
|
2
|
+
/**
|
|
3
|
+
* Append media data to a SourceBuffer.
|
|
4
|
+
*
|
|
5
|
+
* Accepts either a full ArrayBuffer (single append) or an AsyncIterable of
|
|
6
|
+
* Uint8Array chunks (one append per chunk, in order). Waits for `updateend`
|
|
7
|
+
* between each call so appends are serialized correctly.
|
|
8
|
+
*
|
|
9
|
+
* Errors from the SourceBuffer (`error` event) or from the iterable are
|
|
10
|
+
* propagated as rejections.
|
|
11
|
+
*/
|
|
12
|
+
async function appendSegment(sourceBuffer, data, signal) {
|
|
13
|
+
if (data instanceof ArrayBuffer) await appendChunk(sourceBuffer, data);
|
|
14
|
+
else try {
|
|
15
|
+
for await (const chunk of data) {
|
|
16
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
17
|
+
await appendChunk(sourceBuffer, chunk);
|
|
18
|
+
}
|
|
19
|
+
} catch (e) {
|
|
20
|
+
if (e instanceof DOMException && e.name === "AbortError" && !sourceBuffer.updating) try {
|
|
21
|
+
sourceBuffer.abort();
|
|
22
|
+
} catch {}
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function appendChunk(sourceBuffer, data) {
|
|
27
|
+
if (sourceBuffer.updating) await new Promise((resolve) => {
|
|
28
|
+
const onUpdateEnd = () => {
|
|
29
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
33
|
+
});
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const onUpdateEnd = () => {
|
|
36
|
+
cleanup();
|
|
37
|
+
resolve();
|
|
38
|
+
};
|
|
39
|
+
const onError = (event) => {
|
|
40
|
+
cleanup();
|
|
41
|
+
reject(/* @__PURE__ */ new Error(`SourceBuffer append error: ${event.type}`));
|
|
42
|
+
};
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
45
|
+
sourceBuffer.removeEventListener("error", onError);
|
|
46
|
+
};
|
|
47
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
48
|
+
sourceBuffer.addEventListener("error", onError);
|
|
49
|
+
try {
|
|
50
|
+
sourceBuffer.appendBuffer(data);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
cleanup();
|
|
53
|
+
reject(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { appendSegment };
|
|
60
|
+
//# sourceMappingURL=append-segment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append-segment.js","names":[],"sources":["../../../../src/dom/media/append-segment.ts"],"sourcesContent":["/**\n * Segment appender helper.\n *\n * Appends media data (ArrayBuffer or AsyncIterable<Uint8Array> stream) to a\n * SourceBuffer, waiting for `updateend` between calls so the browser can\n * process each append before the next one arrives.\n */\n\n/** Data accepted by appendSegment — a full buffer or an async chunk stream. */\nexport type AppendData = ArrayBuffer | AsyncIterable<Uint8Array>;\n\n/**\n * Append media data to a SourceBuffer.\n *\n * Accepts either a full ArrayBuffer (single append) or an AsyncIterable of\n * Uint8Array chunks (one append per chunk, in order). Waits for `updateend`\n * between each call so appends are serialized correctly.\n *\n * Errors from the SourceBuffer (`error` event) or from the iterable are\n * propagated as rejections.\n */\nexport async function appendSegment(sourceBuffer: SourceBuffer, data: AppendData, signal?: AbortSignal): Promise<void> {\n if (data instanceof ArrayBuffer) {\n await appendChunk(sourceBuffer, data);\n } else {\n try {\n for await (const chunk of data) {\n // Check between chunks so an abort can stop streaming before the next\n // appendBuffer call. The current chunk (if any) has already landed in the\n // SourceBuffer; the partial: true flag in the actor model reflects this.\n if (signal?.aborted) throw signal.reason ?? new DOMException('Aborted', 'AbortError');\n await appendChunk(sourceBuffer, chunk);\n }\n } catch (e) {\n // Reset the MSE segment parser on any abort to discard partial fMP4 box\n // data from the SourceBuffer's internal byte buffer. Two paths reach here:\n //\n // 1. Explicit abort check above (signal.aborted between chunks) — the\n // last appended chunk may have left the parser mid-fragment.\n //\n // 2. The fetch was cancelled (signal fired), causing the underlying\n // ReadableStream to error and the for-await loop to throw directly\n // without ever passing through the signal check above. Same cleanup\n // is needed: partial box data must be cleared before the next append.\n //\n // Without this, the next appendBuffer call sees stale partial data and\n // Chrome throws CHUNK_DEMUXER_ERROR_APPEND_FAILED.\n if (e instanceof DOMException && e.name === 'AbortError' && !sourceBuffer.updating) {\n try {\n sourceBuffer.abort();\n } catch {\n // Thrown if the MediaSource is not \"open\" (e.g. during teardown).\n }\n }\n throw e;\n }\n }\n}\n\nasync function appendChunk(sourceBuffer: SourceBuffer, data: ArrayBuffer | Uint8Array<ArrayBufferLike>): Promise<void> {\n if (sourceBuffer.updating) {\n await new Promise<void>((resolve) => {\n const onUpdateEnd = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n resolve();\n };\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n });\n }\n\n return new Promise<void>((resolve, reject) => {\n const onUpdateEnd = () => {\n cleanup();\n resolve();\n };\n\n const onError = (event: Event) => {\n cleanup();\n reject(new Error(`SourceBuffer append error: ${event.type}`));\n };\n\n const cleanup = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n sourceBuffer.removeEventListener('error', onError);\n };\n\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n sourceBuffer.addEventListener('error', onError);\n\n try {\n sourceBuffer.appendBuffer(data as ArrayBuffer);\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,eAAsB,cAAc,cAA4B,MAAkB,QAAqC;AACrH,KAAI,gBAAgB,YAClB,OAAM,YAAY,cAAc,KAAK;KAErC,KAAI;AACF,aAAW,MAAM,SAAS,MAAM;AAI9B,OAAI,QAAQ,QAAS,OAAM,OAAO,UAAU,IAAI,aAAa,WAAW,aAAa;AACrF,SAAM,YAAY,cAAc,MAAM;;UAEjC,GAAG;AAcV,MAAI,aAAa,gBAAgB,EAAE,SAAS,gBAAgB,CAAC,aAAa,SACxE,KAAI;AACF,gBAAa,OAAO;UACd;AAIV,QAAM;;;AAKZ,eAAe,YAAY,cAA4B,MAAgE;AACrH,KAAI,aAAa,SACf,OAAM,IAAI,SAAe,YAAY;EACnC,MAAM,oBAAoB;AACxB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,YAAS;;AAEX,eAAa,iBAAiB,aAAa,YAAY;GACvD;AAGJ,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,oBAAoB;AACxB,YAAS;AACT,YAAS;;EAGX,MAAM,WAAW,UAAiB;AAChC,YAAS;AACT,0BAAO,IAAI,MAAM,8BAA8B,MAAM,OAAO,CAAC;;EAG/D,MAAM,gBAAgB;AACpB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,gBAAa,oBAAoB,SAAS,QAAQ;;AAGpD,eAAa,iBAAiB,aAAa,YAAY;AACvD,eAAa,iBAAiB,SAAS,QAAQ;AAE/C,MAAI;AACF,gBAAa,aAAa,KAAoB;WACvC,OAAO;AACd,YAAS;AACT,UAAO,MAAM;;GAEf"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/dom/media/buffer-flusher.ts
|
|
2
|
+
/**
|
|
3
|
+
* Buffer flusher helper (P12)
|
|
4
|
+
*
|
|
5
|
+
* Removes a time range from a SourceBuffer to manage memory.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Remove a time range from a SourceBuffer.
|
|
9
|
+
*
|
|
10
|
+
* Waits for the SourceBuffer to be ready (not updating), then removes
|
|
11
|
+
* the specified range. Returns a promise that resolves when removal completes.
|
|
12
|
+
*
|
|
13
|
+
* @param sourceBuffer - The SourceBuffer to remove data from
|
|
14
|
+
* @param start - Start of the time range to remove (seconds)
|
|
15
|
+
* @param end - End of the time range to remove (seconds)
|
|
16
|
+
* @returns Promise that resolves when removal completes
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* await flushBuffer(videoSourceBuffer, 0, 30);
|
|
20
|
+
*/
|
|
21
|
+
async function flushBuffer(sourceBuffer, start, end) {
|
|
22
|
+
if (sourceBuffer.updating) await new Promise((resolve) => {
|
|
23
|
+
const onUpdateEnd = () => {
|
|
24
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
28
|
+
});
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const onUpdateEnd = () => {
|
|
31
|
+
cleanup();
|
|
32
|
+
resolve();
|
|
33
|
+
};
|
|
34
|
+
const onError = (event) => {
|
|
35
|
+
cleanup();
|
|
36
|
+
reject(/* @__PURE__ */ new Error(`SourceBuffer remove error: ${event.type}`));
|
|
37
|
+
};
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
40
|
+
sourceBuffer.removeEventListener("error", onError);
|
|
41
|
+
};
|
|
42
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
43
|
+
sourceBuffer.addEventListener("error", onError);
|
|
44
|
+
try {
|
|
45
|
+
sourceBuffer.remove(start, end);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { flushBuffer };
|
|
55
|
+
//# sourceMappingURL=buffer-flusher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer-flusher.js","names":[],"sources":["../../../../src/dom/media/buffer-flusher.ts"],"sourcesContent":["/**\n * Buffer flusher helper (P12)\n *\n * Removes a time range from a SourceBuffer to manage memory.\n */\n\n/**\n * Remove a time range from a SourceBuffer.\n *\n * Waits for the SourceBuffer to be ready (not updating), then removes\n * the specified range. Returns a promise that resolves when removal completes.\n *\n * @param sourceBuffer - The SourceBuffer to remove data from\n * @param start - Start of the time range to remove (seconds)\n * @param end - End of the time range to remove (seconds)\n * @returns Promise that resolves when removal completes\n *\n * @example\n * await flushBuffer(videoSourceBuffer, 0, 30);\n */\nexport async function flushBuffer(sourceBuffer: SourceBuffer, start: number, end: number): Promise<void> {\n // Wait for SourceBuffer to be ready (not currently updating)\n if (sourceBuffer.updating) {\n await new Promise<void>((resolve) => {\n const onUpdateEnd = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n resolve();\n };\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n });\n }\n\n return new Promise<void>((resolve, reject) => {\n const onUpdateEnd = () => {\n cleanup();\n resolve();\n };\n\n const onError = (event: Event) => {\n cleanup();\n reject(new Error(`SourceBuffer remove error: ${event.type}`));\n };\n\n const cleanup = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n sourceBuffer.removeEventListener('error', onError);\n };\n\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n sourceBuffer.addEventListener('error', onError);\n\n try {\n sourceBuffer.remove(start, end);\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,YAAY,cAA4B,OAAe,KAA4B;AAEvG,KAAI,aAAa,SACf,OAAM,IAAI,SAAe,YAAY;EACnC,MAAM,oBAAoB;AACxB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,YAAS;;AAEX,eAAa,iBAAiB,aAAa,YAAY;GACvD;AAGJ,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,oBAAoB;AACxB,YAAS;AACT,YAAS;;EAGX,MAAM,WAAW,UAAiB;AAChC,YAAS;AACT,0BAAO,IAAI,MAAM,8BAA8B,MAAM,OAAO,CAAC;;EAG/D,MAAM,gBAAgB;AACpB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,gBAAa,oBAAoB,SAAS,QAAQ;;AAGpD,eAAa,iBAAiB,aAAa,YAAY;AACvD,eAAa,iBAAiB,SAAS,QAAQ;AAE/C,MAAI;AACF,gBAAa,OAAO,OAAO,IAAI;WACxB,OAAO;AACd,YAAS;AACT,UAAO,MAAM;;GAEf"}
|