@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -40
- package/dist/default/core/abr/bandwidth-estimator.js +95 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/create-machine-actor.js +91 -0
- package/dist/default/core/create-machine-actor.js.map +1 -0
- package/dist/default/core/create-machine-reactor.js +83 -0
- package/dist/default/core/create-machine-reactor.js.map +1 -0
- package/dist/default/core/create-transition-actor.js +46 -0
- package/dist/default/core/create-transition-actor.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +53 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +69 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +73 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +66 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +130 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/features/sync-preload-attribute.js +26 -0
- package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +82 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +214 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/machine.js +26 -0
- package/dist/default/core/machine.js.map +1 -0
- package/dist/default/core/signals/effect.js +41 -0
- package/dist/default/core/signals/effect.js.map +1 -0
- package/dist/default/core/signals/primitives.js +32 -0
- package/dist/default/core/signals/primitives.js.map +1 -0
- package/dist/default/core/task.js +191 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +39 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +145 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +192 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +104 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +243 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +48 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/sync-text-tracks.js +95 -0
- package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/text-tracks-actor.js +42 -0
- package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +49 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +66 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/default/dom/features/track-playback-rate.js +32 -0
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +78 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +146 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +137 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +54 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +100 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +142 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +10 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
- package/dist/default/index.js.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +27 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/create-machine-actor.d.ts +12 -0
- package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-actor.js +91 -0
- package/dist/dev/core/create-machine-actor.js.map +1 -0
- package/dist/dev/core/create-machine-reactor.d.ts +8 -0
- package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-reactor.js +83 -0
- package/dist/dev/core/create-machine-reactor.js.map +1 -0
- package/dist/dev/core/create-transition-actor.d.ts +23 -0
- package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
- package/dist/dev/core/create-transition-actor.js +46 -0
- package/dist/dev/core/create-transition-actor.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +69 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +73 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.js +66 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.js +130 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/features/sync-preload-attribute.js +26 -0
- package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +82 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +214 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/machine.d.ts +21 -0
- package/dist/dev/core/machine.d.ts.map +1 -0
- package/dist/dev/core/machine.js +26 -0
- package/dist/dev/core/machine.js.map +1 -0
- package/dist/dev/core/signals/effect.d.ts +15 -0
- package/dist/dev/core/signals/effect.d.ts.map +1 -0
- package/dist/dev/core/signals/effect.js +41 -0
- package/dist/dev/core/signals/effect.js.map +1 -0
- package/dist/dev/core/signals/primitives.d.ts +10 -0
- package/dist/dev/core/signals/primitives.d.ts.map +1 -0
- package/dist/dev/core/signals/primitives.js +32 -0
- package/dist/dev/core/signals/primitives.js.map +1 -0
- package/dist/dev/core/task.js +191 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +166 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +39 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +145 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +192 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +104 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +243 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +48 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/sync-text-tracks.js +95 -0
- package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.js +42 -0
- package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +49 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +66 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +32 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +78 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +146 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +137 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +54 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +100 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +142 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +4 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +26 -24
- package/dist/adapter-CMw-rvbk.js +0 -3512
- package/dist/adapter-CMw-rvbk.js.map +0 -1
- package/dist/adapter-l8iJN_GI.d.ts +0 -689
- package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
- package/dist/dom/index.d.ts +0 -243
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js +0 -38
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { effect } from "../../core/signals/effect.js";
|
|
2
|
+
import { computed } from "../../core/signals/primitives.js";
|
|
3
|
+
import { attachMediaSource, createMediaSource, observeMediaSourceReadyState } from "../media/mediasource-setup.js";
|
|
4
|
+
//#region src/dom/features/setup-mediasource.ts
|
|
5
|
+
/**
|
|
6
|
+
* Setup MediaSource orchestration.
|
|
7
|
+
*
|
|
8
|
+
* Creates and attaches MediaSource when:
|
|
9
|
+
* - mediaElement exists in owners
|
|
10
|
+
* - presentation.url exists in state
|
|
11
|
+
*
|
|
12
|
+
* Updates owners.mediaSource after successful setup.
|
|
13
|
+
*/
|
|
14
|
+
function setupMediaSource({ state, owners }) {
|
|
15
|
+
const abortController = new AbortController();
|
|
16
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
17
|
+
const presentationUrlSignal = computed(() => state.get().presentation?.url);
|
|
18
|
+
const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());
|
|
19
|
+
const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);
|
|
20
|
+
const mediaSourceSignal = computed(() => owners.get().mediaSource);
|
|
21
|
+
const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());
|
|
22
|
+
const cleanupEffect = effect(() => {
|
|
23
|
+
if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
|
|
24
|
+
const mediaElement = mediaElementSignal.get();
|
|
25
|
+
const { signal } = abortController;
|
|
26
|
+
const mediaSource = createMediaSource({ preferManaged: true });
|
|
27
|
+
const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);
|
|
28
|
+
attachMediaSource(mediaSource, mediaElement);
|
|
29
|
+
const cleanupOwnersUpdateEffect = effect(() => {
|
|
30
|
+
if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== "open") return;
|
|
31
|
+
owners.set(Object.assign({}, owners.get(), {
|
|
32
|
+
mediaSource,
|
|
33
|
+
mediaSourceReadyState
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
cleanupOwnersUpdateEffect();
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
abortController?.abort();
|
|
42
|
+
cleanupEffect();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { setupMediaSource };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=setup-mediasource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../src/dom/features/setup-mediasource.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 { attachMediaSource, createMediaSource, observeMediaSourceReadyState } from '../media/mediasource-setup';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: Presentation;\n}\n\n/**\n * Owners shape for MediaSource setup.\n */\nexport interface MediaSourceOwners {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n}\n\n/**\n * Setup MediaSource orchestration.\n *\n * Creates and attaches MediaSource when:\n * - mediaElement exists in owners\n * - presentation.url exists in state\n *\n * Updates owners.mediaSource after successful setup.\n */\nexport function setupMediaSource<S extends MediaSourceState, O extends MediaSourceOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const abortController = new AbortController();\n\n // Get the latest mediaElement (even if nullish)\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n // Get the latest presentationUrl (even if nullish)\n const presentationUrlSignal = computed(() => state.get().presentation?.url);\n\n const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());\n\n const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);\n const mediaSourceSignal = computed(() => owners.get().mediaSource);\n const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());\n\n // NOTE: This should be cleaner and less brittle if/when Reactors have their own internal finite state. This is planned as followup work.\n // (here, e.g. something like: \"preconditions_unmet\"|\"pending\"|\"setting_up\"|\"tearing_down\"|\"set_up\")\n // This should also avoid needing nested effect().\n const cleanupEffect = effect(() => {\n if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;\n const mediaElement = mediaElementSignal.get() as HTMLMediaElement;\n const { signal } = abortController;\n\n const mediaSource = createMediaSource({ preferManaged: true });\n // NOTE: Consider making MediaSource an Actor and using this in it.\n const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);\n attachMediaSource(mediaSource, mediaElement);\n\n const cleanupOwnersUpdateEffect = effect(() => {\n // If we already have a MediaSource or the *internal* mediaSource is not yet fully attached, wait to add it to owners;\n if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== 'open') return;\n owners.set(Object.assign({}, owners.get(), { mediaSource, mediaSourceReadyState }) as O);\n });\n\n return () => {\n cleanupOwnersUpdateEffect();\n };\n });\n\n return () => {\n abortController?.abort();\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,SAAgB,iBAA0E,EACxF,OACA,UAIa;CACb,MAAM,kBAAkB,IAAI,iBAAiB;CAG7C,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CAEpE,MAAM,wBAAwB,eAAe,MAAM,KAAK,CAAC,cAAc,IAAI;CAE3E,MAAM,iBAAiB,eAAe,CAAC,CAAC,mBAAmB,KAAK,IAAI,CAAC,CAAC,sBAAsB,KAAK,CAAC;CAElG,MAAM,wBAAwB,eAAe,mBAAmB,KAAK,EAAE,IAAI;CAC3E,MAAM,oBAAoB,eAAe,OAAO,KAAK,CAAC,YAAY;CAClE,MAAM,oBAAoB,eAAe,CAAC,sBAAsB,KAAK,CAAC;CAKtE,MAAM,gBAAgB,aAAa;AACjC,MAAI,CAAC,eAAe,KAAK,IAAI,CAAC,kBAAkB,KAAK,CAAE;EACvD,MAAM,eAAe,mBAAmB,KAAK;EAC7C,MAAM,EAAE,WAAW;EAEnB,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;EAE9D,MAAM,wBAAwB,6BAA6B,aAAa,OAAO;AAC/E,oBAAkB,aAAa,aAAa;EAE5C,MAAM,4BAA4B,aAAa;AAE7C,OAAI,CAAC,CAAC,kBAAkB,KAAK,IAAI,sBAAsB,KAAK,KAAK,OAAQ;AACzE,UAAO,IAAI,OAAO,OAAO,EAAE,EAAE,OAAO,KAAK,EAAE;IAAE;IAAa;IAAuB,CAAC,CAAM;IACxF;AAEF,eAAa;AACX,8BAA2B;;GAE7B;AAEF,cAAa;AACX,mBAAiB,OAAO;AACxB,iBAAe"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { effect } from "../../core/signals/effect.js";
|
|
2
|
+
import { computed, update } from "../../core/signals/primitives.js";
|
|
3
|
+
import { isResolvedTrack } from "../../core/types/index.js";
|
|
4
|
+
import { BufferKeyByType, getSelectedTrack } from "../../core/utils/track-selection.js";
|
|
5
|
+
import { createSourceBuffer } from "../media/mediasource-setup.js";
|
|
6
|
+
import { createSourceBufferActor } from "../media/source-buffer-actor.js";
|
|
7
|
+
//#region src/dom/features/setup-sourcebuffer.ts
|
|
8
|
+
const ActorKeyByType = {
|
|
9
|
+
video: "videoBufferActor",
|
|
10
|
+
audio: "audioBufferActor"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build MIME codec string from track metadata.
|
|
14
|
+
*
|
|
15
|
+
* @param track - Resolved track with mimeType and codecs
|
|
16
|
+
* @returns MIME codec string (e.g., 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"')
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })
|
|
20
|
+
* // => 'video/mp4; codecs="avc1.42E01E"'
|
|
21
|
+
*/
|
|
22
|
+
function buildMimeCodec(track) {
|
|
23
|
+
const codecString = track.codecs?.join(",") ?? "";
|
|
24
|
+
return `${track.mimeType}; codecs="${codecString}"`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Setup all needed SourceBuffers as a single coordinated operation.
|
|
28
|
+
*
|
|
29
|
+
* Waits until ALL media tracks in the presentation are resolved with codecs,
|
|
30
|
+
* then creates every SourceBuffer in one synchronous block before setting
|
|
31
|
+
* owners. This guarantees that downstream consumers (e.g. loadSegments) never
|
|
32
|
+
* see a partial set of SourceBuffers — preventing the Firefox bug where
|
|
33
|
+
* appending to a video SourceBuffer before the audio SourceBuffer exists
|
|
34
|
+
* causes mozHasAudio to be permanently false.
|
|
35
|
+
*
|
|
36
|
+
* Handles video-only, audio-only, and combined presentations correctly:
|
|
37
|
+
* track types are derived from the presentation rather than hardcoded.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const cleanup = setupSourceBuffers({ state, owners });
|
|
41
|
+
*/
|
|
42
|
+
function setupSourceBuffers({ state, owners }) {
|
|
43
|
+
const presentationTypesSignal = computed(() => {
|
|
44
|
+
const { presentation } = state.get();
|
|
45
|
+
if (!presentation || !("selectionSets" in presentation)) return [];
|
|
46
|
+
return presentation.selectionSets.map(({ type }) => type).filter((type) => type === "video" || type === "audio");
|
|
47
|
+
});
|
|
48
|
+
const canSetupSignal = computed(() => {
|
|
49
|
+
const types = presentationTypesSignal.get();
|
|
50
|
+
if (!owners.get().mediaSource || types.length === 0) return false;
|
|
51
|
+
const s = state.get();
|
|
52
|
+
return types.every((type) => {
|
|
53
|
+
const track = getSelectedTrack(s, type);
|
|
54
|
+
return track && isResolvedTrack(track) && !!track.codecs?.length;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
const shouldSetupSignal = computed(() => {
|
|
58
|
+
const o = owners.get();
|
|
59
|
+
return presentationTypesSignal.get().every((type) => !o[BufferKeyByType[type]]);
|
|
60
|
+
});
|
|
61
|
+
return effect(() => {
|
|
62
|
+
if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
|
|
63
|
+
const s = state.get();
|
|
64
|
+
const o = owners.get();
|
|
65
|
+
const patch = {};
|
|
66
|
+
for (const type of presentationTypesSignal.get()) {
|
|
67
|
+
const track = getSelectedTrack(s, type);
|
|
68
|
+
const buffer = createSourceBuffer(o.mediaSource, buildMimeCodec(track));
|
|
69
|
+
patch[BufferKeyByType[type]] = buffer;
|
|
70
|
+
patch[ActorKeyByType[type]] = createSourceBufferActor(buffer);
|
|
71
|
+
}
|
|
72
|
+
update(owners, patch);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { setupSourceBuffers };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=setup-sourcebuffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-sourcebuffer.js","names":[],"sources":["../../../../src/dom/features/setup-sourcebuffer.ts"],"sourcesContent":["import { effect } from '../../core/signals/effect';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\nimport type { Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { BufferKeyByType, getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport { createSourceBuffer } from '../media/mediasource-setup';\nimport { createSourceBufferActor, type SourceBufferActor } from '../media/source-buffer-actor';\n\n/**\n * Media track type for SourceBuffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const satisfies Record<MediaTrackType, keyof SourceBufferOwners>;\n\n/**\n * State shape for SourceBuffer setup.\n */\nexport interface SourceBufferState extends TrackSelectionState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Owners shape for SourceBuffer setup.\n */\nexport interface SourceBufferOwners {\n mediaSource?: MediaSource;\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n/**\n * Build MIME codec string from track metadata.\n *\n * @param track - Resolved track with mimeType and codecs\n * @returns MIME codec string (e.g., 'video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"')\n *\n * @example\n * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })\n * // => 'video/mp4; codecs=\"avc1.42E01E\"'\n */\nexport function buildMimeCodec(track: ResolvedTrack): string {\n const codecString = track.codecs?.join(',') ?? '';\n return `${track.mimeType}; codecs=\"${codecString}\"`;\n}\n\n/**\n * Setup all needed SourceBuffers as a single coordinated operation.\n *\n * Waits until ALL media tracks in the presentation are resolved with codecs,\n * then creates every SourceBuffer in one synchronous block before setting\n * owners. This guarantees that downstream consumers (e.g. loadSegments) never\n * see a partial set of SourceBuffers — preventing the Firefox bug where\n * appending to a video SourceBuffer before the audio SourceBuffer exists\n * causes mozHasAudio to be permanently false.\n *\n * Handles video-only, audio-only, and combined presentations correctly:\n * track types are derived from the presentation rather than hardcoded.\n *\n * @example\n * const cleanup = setupSourceBuffers({ state, owners });\n */\nexport function setupSourceBuffers<S extends SourceBufferState, O extends SourceBufferOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n // Derive which media track types this presentation actually contains\n const presentationTypesSignal = computed((): MediaTrackType[] => {\n const { presentation } = state.get();\n if (!presentation || !('selectionSets' in presentation)) return [];\n return presentation.selectionSets\n .map(({ type }) => type)\n .filter((type): type is MediaTrackType => type === 'video' || type === 'audio');\n });\n\n // All presentation track types must have a selected, resolved track with codecs\n const canSetupSignal = computed(() => {\n const types = presentationTypesSignal.get();\n if (!owners.get().mediaSource || types.length === 0) return false;\n const s = state.get();\n return types.every((type) => {\n const track = getSelectedTrack(s, type);\n return track && isResolvedTrack(track) && !!track.codecs?.length;\n });\n });\n\n // DOM-observable \"already done\" guard — once all presentation buffers exist in owners, setup has run\n const shouldSetupSignal = computed(() => {\n const o = owners.get();\n return presentationTypesSignal.get().every((type) => !o[BufferKeyByType[type]]);\n });\n\n const cleanupEffect = effect(() => {\n if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;\n\n const s = state.get();\n const o = owners.get();\n\n // Create all SourceBuffers synchronously — no await between addSourceBuffer\n // calls — then set owners once so subscribers see all buffers simultaneously.\n const patch: Partial<SourceBufferOwners> = {};\n\n for (const type of presentationTypesSignal.get()) {\n const track = getSelectedTrack(s, type) as ResolvedTrack;\n const buffer = createSourceBuffer(o.mediaSource!, buildMimeCodec(track));\n patch[BufferKeyByType[type]] = buffer;\n patch[ActorKeyByType[type]] = createSourceBufferActor(buffer);\n }\n\n update(owners, patch);\n });\n\n return cleanupEffect;\n}\n"],"mappings":";;;;;;;AAcA,MAAM,iBAAiB;CACrB,OAAO;CACP,OAAO;CACR;;;;;;;;;;;AAgCD,SAAgB,eAAe,OAA8B;CAC3D,MAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC/C,QAAO,GAAG,MAAM,SAAS,YAAY,YAAY;;;;;;;;;;;;;;;;;;AAmBnD,SAAgB,mBAA8E,EAC5F,OACA,UAIa;CAEb,MAAM,0BAA0B,eAAiC;EAC/D,MAAM,EAAE,iBAAiB,MAAM,KAAK;AACpC,MAAI,CAAC,gBAAgB,EAAE,mBAAmB,cAAe,QAAO,EAAE;AAClE,SAAO,aAAa,cACjB,KAAK,EAAE,WAAW,KAAK,CACvB,QAAQ,SAAiC,SAAS,WAAW,SAAS,QAAQ;GACjF;CAGF,MAAM,iBAAiB,eAAe;EACpC,MAAM,QAAQ,wBAAwB,KAAK;AAC3C,MAAI,CAAC,OAAO,KAAK,CAAC,eAAe,MAAM,WAAW,EAAG,QAAO;EAC5D,MAAM,IAAI,MAAM,KAAK;AACrB,SAAO,MAAM,OAAO,SAAS;GAC3B,MAAM,QAAQ,iBAAiB,GAAG,KAAK;AACvC,UAAO,SAAS,gBAAgB,MAAM,IAAI,CAAC,CAAC,MAAM,QAAQ;IAC1D;GACF;CAGF,MAAM,oBAAoB,eAAe;EACvC,MAAM,IAAI,OAAO,KAAK;AACtB,SAAO,wBAAwB,KAAK,CAAC,OAAO,SAAS,CAAC,EAAE,gBAAgB,OAAO;GAC/E;AAsBF,QApBsB,aAAa;AACjC,MAAI,CAAC,eAAe,KAAK,IAAI,CAAC,kBAAkB,KAAK,CAAE;EAEvD,MAAM,IAAI,MAAM,KAAK;EACrB,MAAM,IAAI,OAAO,KAAK;EAItB,MAAM,QAAqC,EAAE;AAE7C,OAAK,MAAM,QAAQ,wBAAwB,KAAK,EAAE;GAChD,MAAM,QAAQ,iBAAiB,GAAG,KAAK;GACvC,MAAM,SAAS,mBAAmB,EAAE,aAAc,eAAe,MAAM,CAAC;AACxE,SAAM,gBAAgB,SAAS;AAC/B,SAAM,eAAe,SAAS,wBAAwB,OAAO;;AAG/D,SAAO,QAAQ,MAAM;GACrB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { computed, untrack, update } from "../../core/signals/primitives.js";
|
|
2
|
+
import { createMachineReactor } from "../../core/create-machine-reactor.js";
|
|
3
|
+
import { listen } from "@videojs/utils/dom";
|
|
4
|
+
//#region src/dom/features/sync-text-tracks.ts
|
|
5
|
+
function createTrackElement(track) {
|
|
6
|
+
const el = document.createElement("track");
|
|
7
|
+
el.id = track.id;
|
|
8
|
+
el.kind = track.kind;
|
|
9
|
+
el.label = track.label;
|
|
10
|
+
el.toggleAttribute("data-src-track", true);
|
|
11
|
+
if (track.language) el.srclang = track.language;
|
|
12
|
+
if (track.default) el.default = true;
|
|
13
|
+
return el;
|
|
14
|
+
}
|
|
15
|
+
function getModelTextTracks(presentation) {
|
|
16
|
+
return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
|
|
17
|
+
}
|
|
18
|
+
function syncModes(textTracks, selectedId) {
|
|
19
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
20
|
+
const track = textTracks[i];
|
|
21
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
22
|
+
track.mode = track.id === selectedId ? "showing" : "disabled";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Text track sync orchestration.
|
|
27
|
+
*
|
|
28
|
+
* A single `always` monitor keeps the reactor in sync with preconditions.
|
|
29
|
+
* `'set-up'` owns the full lifecycle of `<track>` elements:
|
|
30
|
+
*
|
|
31
|
+
* - **Effect 1** — creates `<track>` elements on entry; exit cleanup removes
|
|
32
|
+
* them and clears `selectedTextTrackId` on any outbound transition.
|
|
33
|
+
* - **Effect 2** — owns mode sync, the Chromium settling-window guard, and
|
|
34
|
+
* the `'change'` listener that bridges DOM state back to
|
|
35
|
+
* `selectedTextTrackId`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const reactor = syncTextTracks({ state, owners });
|
|
39
|
+
* // later:
|
|
40
|
+
* reactor.destroy();
|
|
41
|
+
*/
|
|
42
|
+
function syncTextTracks({ state, owners }) {
|
|
43
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
44
|
+
const modelTextTracksSignal = computed(() => getModelTextTracks(state.get().presentation), { equals(prevTextTracks, nextTextTracks) {
|
|
45
|
+
if (prevTextTracks === nextTextTracks) return true;
|
|
46
|
+
if (typeof prevTextTracks !== typeof nextTextTracks) return false;
|
|
47
|
+
if (prevTextTracks?.length !== nextTextTracks?.length) return false;
|
|
48
|
+
return !!nextTextTracks && nextTextTracks.every((nextTextTrack) => prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id));
|
|
49
|
+
} });
|
|
50
|
+
const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);
|
|
51
|
+
const preconditionsMetSignal = computed(() => !!mediaElementSignal.get() && !!modelTextTracksSignal.get()?.length);
|
|
52
|
+
return createMachineReactor({
|
|
53
|
+
initial: "preconditions-unmet",
|
|
54
|
+
monitor: () => preconditionsMetSignal.get() ? "set-up" : "preconditions-unmet",
|
|
55
|
+
states: {
|
|
56
|
+
"preconditions-unmet": {},
|
|
57
|
+
"set-up": {
|
|
58
|
+
entry: () => {
|
|
59
|
+
const mediaElement = mediaElementSignal.get();
|
|
60
|
+
modelTextTracksSignal.get().forEach((track) => mediaElement.appendChild(createTrackElement(track)));
|
|
61
|
+
return () => {
|
|
62
|
+
mediaElement.querySelectorAll("track[data-src-track]:is([kind=\"subtitles\"],[kind=\"captions\"]").forEach((trackEl) => trackEl.remove());
|
|
63
|
+
update(state, { selectedTextTrackId: void 0 });
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
effects: () => {
|
|
67
|
+
const mediaElement = untrack(() => mediaElementSignal.get());
|
|
68
|
+
const selectedId = selectedTextTrackIdSignal.get();
|
|
69
|
+
syncModes(mediaElement.textTracks, selectedId);
|
|
70
|
+
let syncTimeout = setTimeout(() => {
|
|
71
|
+
syncTimeout = void 0;
|
|
72
|
+
}, 0);
|
|
73
|
+
const onChange = () => {
|
|
74
|
+
if (syncTimeout) {
|
|
75
|
+
syncModes(mediaElement.textTracks, untrack(() => selectedTextTrackIdSignal.get()));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id;
|
|
79
|
+
if (newId === untrack(() => selectedTextTrackIdSignal.get())) return;
|
|
80
|
+
update(state, { selectedTextTrackId: newId });
|
|
81
|
+
};
|
|
82
|
+
const unlisten = listen(mediaElement.textTracks, "change", onChange);
|
|
83
|
+
return () => {
|
|
84
|
+
clearTimeout(syncTimeout ?? void 0);
|
|
85
|
+
unlisten();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { syncTextTracks };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=sync-text-tracks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../src/dom/features/sync-text-tracks.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { Reactor } from '../../core/create-machine-reactor';\nimport { createMachineReactor } from '../../core/create-machine-reactor';\nimport { computed, type Signal, untrack, update } from '../../core/signals/primitives';\nimport type { PartiallyResolvedTextTrack, Presentation, TextTrack } from '../../core/types';\n\n/**\n * FSM states for text track sync.\n *\n * ```\n * 'preconditions-unmet' ──── mediaElement + tracks available ────→ 'set-up'\n * ↑ |\n * └──────────────── preconditions lost (exit cleanup) ──────────┘\n *\n * any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'\n * ```\n */\n/**\n * State shape for text track sync.\n */\nexport interface TextTrackSyncState {\n presentation?: Presentation | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track sync.\n */\nexport interface TextTrackSyncOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction createTrackElement(track: PartiallyResolvedTextTrack | TextTrack): HTMLTrackElement {\n const el = document.createElement('track');\n el.id = track.id;\n el.kind = track.kind;\n el.label = track.label;\n el.toggleAttribute('data-src-track', true);\n if (track.language) el.srclang = track.language;\n if (track.default) el.default = true;\n return el;\n}\n\nfunction getModelTextTracks(\n presentation: Presentation | undefined\n): (PartiallyResolvedTextTrack | TextTrack)[] | undefined {\n return presentation?.selectionSets?.find((s) => s.type === 'text')?.switchingSets[0]?.tracks;\n}\n\nfunction syncModes(textTracks: TextTrackList, selectedId: string | undefined): void {\n for (let i = 0; i < textTracks.length; i++) {\n const track = textTracks[i]!;\n if (track.kind !== 'subtitles' && track.kind !== 'captions') continue;\n track.mode = track.id === selectedId ? 'showing' : 'disabled';\n }\n}\n\n// ============================================================================\n// Main export\n// ============================================================================\n\n/**\n * Text track sync orchestration.\n *\n * A single `always` monitor keeps the reactor in sync with preconditions.\n * `'set-up'` owns the full lifecycle of `<track>` elements:\n *\n * - **Effect 1** — creates `<track>` elements on entry; exit cleanup removes\n * them and clears `selectedTextTrackId` on any outbound transition.\n * - **Effect 2** — owns mode sync, the Chromium settling-window guard, and\n * the `'change'` listener that bridges DOM state back to\n * `selectedTextTrackId`.\n *\n * @example\n * const reactor = syncTextTracks({ state, owners });\n * // later:\n * reactor.destroy();\n */\nexport function syncTextTracks<S extends TextTrackSyncState, O extends TextTrackSyncOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): Reactor<'preconditions-unmet' | 'set-up' | 'destroying' | 'destroyed'> {\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n const modelTextTracksSignal = computed(() => getModelTextTracks(state.get().presentation), {\n /** @TODO Make generic and abstract away for Array<T> | undefined (CJP) */\n equals(prevTextTracks, nextTextTracks) {\n if (prevTextTracks === nextTextTracks) return true;\n if (typeof prevTextTracks !== typeof nextTextTracks) return false;\n if (prevTextTracks?.length !== nextTextTracks?.length) return false;\n // NOTE: This could probably be optimized, but the set should generally be small (CJP)\n return (\n !!nextTextTracks &&\n nextTextTracks.every((nextTextTrack) =>\n prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id)\n )\n );\n },\n });\n const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);\n const preconditionsMetSignal = computed(() => !!mediaElementSignal.get() && !!modelTextTracksSignal.get()?.length);\n\n return createMachineReactor<'preconditions-unmet' | 'set-up'>({\n initial: 'preconditions-unmet',\n monitor: () => (preconditionsMetSignal.get() ? 'set-up' : 'preconditions-unmet'),\n states: {\n 'preconditions-unmet': {},\n\n 'set-up': {\n // Entry: create <track> elements once on state entry; exit cleanup removes\n // them and clears selectedTextTrackId on any outbound transition.\n // The fn body is automatically untracked — no untrack() needed.\n entry: () => {\n const mediaElement = mediaElementSignal.get() as HTMLMediaElement;\n const modelTextTracks = modelTextTracksSignal.get() as PartiallyResolvedTextTrack[];\n modelTextTracks.forEach((track) => mediaElement.appendChild(createTrackElement(track)));\n return () => {\n mediaElement\n .querySelectorAll('track[data-src-track]:is([kind=\"subtitles\"],[kind=\"captions\"]')\n .forEach((trackEl) => trackEl.remove());\n update(state, { selectedTextTrackId: undefined } as Partial<S>);\n };\n },\n\n // Reaction: mode sync + DOM change listener; re-runs when selectedId changes.\n // mediaElement is read with untrack() since element changes go through the\n // always monitor (preconditions-unmet path), not this effect.\n effects: () => {\n const mediaElement = untrack(() => mediaElementSignal.get() as HTMLMediaElement);\n const selectedId = selectedTextTrackIdSignal.get();\n\n syncModes(mediaElement.textTracks, selectedId);\n let syncTimeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n syncTimeout = undefined;\n }, 0);\n\n const onChange = () => {\n if (syncTimeout) {\n // Inside the settling window: browser auto-selection is overriding our\n // modes. Re-apply to restore the intended state without touching state.\n // change events are queued as tasks (async), so no re-entrancy risk.\n syncModes(\n mediaElement.textTracks,\n untrack(() => selectedTextTrackIdSignal.get())\n );\n return;\n }\n\n const showingTrack = Array.from(mediaElement.textTracks).find(\n (t) => t.mode === 'showing' && (t.kind === 'subtitles' || t.kind === 'captions')\n );\n\n // showingTrack.id matches the SPF track ID set by createTrackElement above.\n // Fall back to undefined for empty-string IDs (non-SPF-managed tracks).\n const newId = showingTrack?.id;\n const currentModelId = untrack(() => selectedTextTrackIdSignal.get());\n if (newId === currentModelId) return;\n update(state, { selectedTextTrackId: newId } as Partial<S>);\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n clearTimeout(syncTimeout ?? undefined);\n unlisten();\n };\n },\n },\n },\n });\n}\n"],"mappings":";;;;AAoCA,SAAS,mBAAmB,OAAiE;CAC3F,MAAM,KAAK,SAAS,cAAc,QAAQ;AAC1C,IAAG,KAAK,MAAM;AACd,IAAG,OAAO,MAAM;AAChB,IAAG,QAAQ,MAAM;AACjB,IAAG,gBAAgB,kBAAkB,KAAK;AAC1C,KAAI,MAAM,SAAU,IAAG,UAAU,MAAM;AACvC,KAAI,MAAM,QAAS,IAAG,UAAU;AAChC,QAAO;;AAGT,SAAS,mBACP,cACwD;AACxD,QAAO,cAAc,eAAe,MAAM,MAAM,EAAE,SAAS,OAAO,EAAE,cAAc,IAAI;;AAGxF,SAAS,UAAU,YAA2B,YAAsC;AAClF,MAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,QAAQ,WAAW;AACzB,MAAI,MAAM,SAAS,eAAe,MAAM,SAAS,WAAY;AAC7D,QAAM,OAAO,MAAM,OAAO,aAAa,YAAY;;;;;;;;;;;;;;;;;;;;AAyBvD,SAAgB,eAA4E,EAC1F,OACA,UAIyE;CACzE,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CACpE,MAAM,wBAAwB,eAAe,mBAAmB,MAAM,KAAK,CAAC,aAAa,EAAE,EAEzF,OAAO,gBAAgB,gBAAgB;AACrC,MAAI,mBAAmB,eAAgB,QAAO;AAC9C,MAAI,OAAO,mBAAmB,OAAO,eAAgB,QAAO;AAC5D,MAAI,gBAAgB,WAAW,gBAAgB,OAAQ,QAAO;AAE9D,SACE,CAAC,CAAC,kBACF,eAAe,OAAO,kBACpB,gBAAgB,MAAM,kBAAkB,cAAc,OAAO,cAAc,GAAG,CAC/E;IAGN,CAAC;CACF,MAAM,4BAA4B,eAAe,MAAM,KAAK,CAAC,oBAAoB;CACjF,MAAM,yBAAyB,eAAe,CAAC,CAAC,mBAAmB,KAAK,IAAI,CAAC,CAAC,sBAAsB,KAAK,EAAE,OAAO;AAElH,QAAO,qBAAuD;EAC5D,SAAS;EACT,eAAgB,uBAAuB,KAAK,GAAG,WAAW;EAC1D,QAAQ;GACN,uBAAuB,EAAE;GAEzB,UAAU;IAIR,aAAa;KACX,MAAM,eAAe,mBAAmB,KAAK;AACrB,2BAAsB,KAAK,CACnC,SAAS,UAAU,aAAa,YAAY,mBAAmB,MAAM,CAAC,CAAC;AACvF,kBAAa;AACX,mBACG,iBAAiB,oEAAgE,CACjF,SAAS,YAAY,QAAQ,QAAQ,CAAC;AACzC,aAAO,OAAO,EAAE,qBAAqB,KAAA,GAAW,CAAe;;;IAOnE,eAAe;KACb,MAAM,eAAe,cAAc,mBAAmB,KAAK,CAAqB;KAChF,MAAM,aAAa,0BAA0B,KAAK;AAElD,eAAU,aAAa,YAAY,WAAW;KAC9C,IAAI,cAAyD,iBAAiB;AAC5E,oBAAc,KAAA;QACb,EAAE;KAEL,MAAM,iBAAiB;AACrB,UAAI,aAAa;AAIf,iBACE,aAAa,YACb,cAAc,0BAA0B,KAAK,CAAC,CAC/C;AACD;;MASF,MAAM,QANe,MAAM,KAAK,aAAa,WAAW,CAAC,MACtD,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,eAAe,EAAE,SAAS,YACtE,EAI2B;AAE5B,UAAI,UADmB,cAAc,0BAA0B,KAAK,CAAC,CACvC;AAC9B,aAAO,OAAO,EAAE,qBAAqB,OAAO,CAAe;;KAG7D,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,SAAS;AAEpE,kBAAa;AACX,mBAAa,eAAe,KAAA,EAAU;AACtC,gBAAU;;;IAGf;GACF;EACF,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
|
|
2
|
+
import { untrack } from "../../core/signals/primitives.js";
|
|
3
|
+
import { SerialRunner, Task } from "../../core/task.js";
|
|
4
|
+
import { parseVttSegment } from "../text/parse-vtt-segment.js";
|
|
5
|
+
//#region src/dom/features/text-track-segment-loader-actor.ts
|
|
6
|
+
/**
|
|
7
|
+
* Loads VTT segments for a text track and delegates cue management to a
|
|
8
|
+
* TextTracksActor. Mirrors the SegmentLoaderActor/SourceBufferActor pattern
|
|
9
|
+
* for the text track equivalent.
|
|
10
|
+
*
|
|
11
|
+
* Planning is done in the load handler: segments already recorded in
|
|
12
|
+
* TextTracksActor's context are skipped. Each load preempts in-flight work
|
|
13
|
+
* via abortAll() before scheduling fresh tasks.
|
|
14
|
+
*/
|
|
15
|
+
function createTextTrackSegmentLoaderActor(textTracksActor) {
|
|
16
|
+
const runner = new SerialRunner();
|
|
17
|
+
let destroyed = false;
|
|
18
|
+
return {
|
|
19
|
+
send({ track, currentTime }) {
|
|
20
|
+
if (destroyed) return;
|
|
21
|
+
const trackId = track.id;
|
|
22
|
+
const bufferedSegments = untrack(() => textTracksActor.snapshot.get().context.segments[trackId] ?? []);
|
|
23
|
+
const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime);
|
|
24
|
+
runner.abortAll();
|
|
25
|
+
for (const segment of segmentsToLoad) runner.schedule(new Task(async (signal) => {
|
|
26
|
+
if (signal.aborted) return;
|
|
27
|
+
try {
|
|
28
|
+
const cues = await parseVttSegment(segment.url);
|
|
29
|
+
if (signal.aborted) return;
|
|
30
|
+
textTracksActor.send({
|
|
31
|
+
type: "add-cues",
|
|
32
|
+
meta: {
|
|
33
|
+
trackId,
|
|
34
|
+
id: segment.id,
|
|
35
|
+
startTime: segment.startTime,
|
|
36
|
+
duration: segment.duration
|
|
37
|
+
},
|
|
38
|
+
cues
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Failed to load VTT segment:", error);
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
destroy() {
|
|
46
|
+
if (destroyed) return;
|
|
47
|
+
destroyed = true;
|
|
48
|
+
runner.destroy();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { createTextTrackSegmentLoaderActor };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=text-track-segment-loader-actor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-track-segment-loader-actor.js","names":[],"sources":["../../../../src/dom/features/text-track-segment-loader-actor.ts"],"sourcesContent":["import type { CallbackActor } from '../../core/actor';\nimport { getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { untrack } from '../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { TextTrack } from '../../core/types';\nimport { parseVttSegment } from '../text/parse-vtt-segment';\nimport type { TextTracksActor } from './text-tracks-actor';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type TextTrackSegmentLoaderMessage = {\n type: 'load';\n track: TextTrack;\n currentTime: number;\n};\n\nexport type TextTrackSegmentLoaderActor = CallbackActor<TextTrackSegmentLoaderMessage>;\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Loads VTT segments for a text track and delegates cue management to a\n * TextTracksActor. Mirrors the SegmentLoaderActor/SourceBufferActor pattern\n * for the text track equivalent.\n *\n * Planning is done in the load handler: segments already recorded in\n * TextTracksActor's context are skipped. Each load preempts in-flight work\n * via abortAll() before scheduling fresh tasks.\n */\nexport function createTextTrackSegmentLoaderActor(textTracksActor: TextTracksActor): TextTrackSegmentLoaderActor {\n const runner = new SerialRunner();\n let destroyed = false;\n\n return {\n send({ track, currentTime }: TextTrackSegmentLoaderMessage): void {\n if (destroyed) return;\n const trackId = track.id;\n const bufferedSegments = untrack(() => textTracksActor.snapshot.get().context.segments[trackId] ?? []);\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime);\n\n runner.abortAll();\n for (const segment of segmentsToLoad) {\n runner.schedule(\n new Task(async (signal) => {\n if (signal.aborted) return;\n try {\n const cues = await parseVttSegment(segment.url);\n if (signal.aborted) return;\n textTracksActor.send({\n type: 'add-cues',\n meta: { trackId, id: segment.id, startTime: segment.startTime, duration: segment.duration },\n cues,\n });\n } catch (error) {\n // Graceful degradation: log and continue to the next segment.\n console.error('Failed to load VTT segment:', error);\n }\n })\n );\n }\n },\n\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n runner.destroy();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgB,kCAAkC,iBAA+D;CAC/G,MAAM,SAAS,IAAI,cAAc;CACjC,IAAI,YAAY;AAEhB,QAAO;EACL,KAAK,EAAE,OAAO,eAAoD;AAChE,OAAI,UAAW;GACf,MAAM,UAAU,MAAM;GACtB,MAAM,mBAAmB,cAAc,gBAAgB,SAAS,KAAK,CAAC,QAAQ,SAAS,YAAY,EAAE,CAAC;GACtG,MAAM,iBAAiB,kBAAkB,MAAM,UAAU,kBAAkB,YAAY;AAEvF,UAAO,UAAU;AACjB,QAAK,MAAM,WAAW,eACpB,QAAO,SACL,IAAI,KAAK,OAAO,WAAW;AACzB,QAAI,OAAO,QAAS;AACpB,QAAI;KACF,MAAM,OAAO,MAAM,gBAAgB,QAAQ,IAAI;AAC/C,SAAI,OAAO,QAAS;AACpB,qBAAgB,KAAK;MACnB,MAAM;MACN,MAAM;OAAE;OAAS,IAAI,QAAQ;OAAI,WAAW,QAAQ;OAAW,UAAU,QAAQ;OAAU;MAC3F;MACD,CAAC;aACK,OAAO;AAEd,aAAQ,MAAM,+BAA+B,MAAM;;KAErD,CACH;;EAIL,UAAgB;AACd,OAAI,UAAW;AACf,eAAY;AACZ,UAAO,SAAS;;EAEnB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createTransitionActor } from "../../core/create-transition-actor.js";
|
|
2
|
+
//#region src/dom/features/text-tracks-actor.ts
|
|
3
|
+
function isDuplicateCue(cue, existing) {
|
|
4
|
+
return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);
|
|
5
|
+
}
|
|
6
|
+
/** TextTrack actor: wraps all text tracks on a media element, owns cue operations. */
|
|
7
|
+
function createTextTracksActor(mediaElement) {
|
|
8
|
+
return createTransitionActor({
|
|
9
|
+
loaded: {},
|
|
10
|
+
segments: {}
|
|
11
|
+
}, (context, message) => {
|
|
12
|
+
const { meta, cues } = message;
|
|
13
|
+
const { trackId, id: segmentId, startTime, duration } = meta;
|
|
14
|
+
const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);
|
|
15
|
+
if (!textTrack) return context;
|
|
16
|
+
const existingCues = context.loaded[trackId] ?? [];
|
|
17
|
+
const existingSegments = context.segments[trackId] ?? [];
|
|
18
|
+
const prunedCues = cues.filter((cue) => !isDuplicateCue(cue, existingCues));
|
|
19
|
+
const segmentAlreadyLoaded = existingSegments.some((s) => s.id === segmentId);
|
|
20
|
+
if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;
|
|
21
|
+
for (const cue of prunedCues) textTrack.addCue(cue);
|
|
22
|
+
return {
|
|
23
|
+
...context,
|
|
24
|
+
loaded: {
|
|
25
|
+
...context.loaded,
|
|
26
|
+
[trackId]: [...existingCues, ...prunedCues]
|
|
27
|
+
},
|
|
28
|
+
segments: segmentAlreadyLoaded ? context.segments : {
|
|
29
|
+
...context.segments,
|
|
30
|
+
[trackId]: [...existingSegments, {
|
|
31
|
+
id: segmentId,
|
|
32
|
+
startTime,
|
|
33
|
+
duration
|
|
34
|
+
}]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { createTextTracksActor };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=text-tracks-actor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-tracks-actor.js","names":[],"sources":["../../../../src/dom/features/text-tracks-actor.ts"],"sourcesContent":["import type { TransitionActor } from '../../core/create-transition-actor';\nimport { createTransitionActor } from '../../core/create-transition-actor';\nimport type { Segment } from '../../core/types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Minimal cue record — enough for deduplication and snapshot observability. */\nexport interface CueRecord {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/** Segment identity and timing — mirrors AppendSegmentMeta without trackId (keyed separately). */\nexport type CueSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & { trackId: string };\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface TextTracksActorContext {\n /** Cues added per track ID. Used for duplicate detection and snapshot observability. */\n loaded: Record<string, CueRecord[]>;\n /** Segments whose cues have been fully added, keyed by track ID. Used for load planning. */\n segments: Record<string, Array<Pick<Segment, 'id' | 'startTime' | 'duration'>>>;\n}\n\nexport type AddCuesMessage = { type: 'add-cues'; meta: CueSegmentMeta; cues: VTTCue[] };\nexport type TextTracksActorMessage = AddCuesMessage;\n\nexport type TextTracksActor = TransitionActor<TextTracksActorContext, TextTracksActorMessage>;\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction isDuplicateCue(cue: VTTCue, existing: CueRecord[]): boolean {\n return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/** TextTrack actor: wraps all text tracks on a media element, owns cue operations. */\nexport function createTextTracksActor(mediaElement: HTMLMediaElement): TextTracksActor {\n return createTransitionActor({ loaded: {}, segments: {} } as TextTracksActorContext, (context, message) => {\n // NOTE: Currently assumes cues are applied to a non-disabled TextTrack. Discuss different approaches here, including:\n // - Making the message responsible for auto-selection of the textTrack (changes logic in sync-text-tracks)\n // - Silent gating/console warning + early bail\n // - throwing a domain-specific error\n // - accepting as is (which would result in errors, but also \"shouldn't ever happen\" unless a bug is introduced)\n // (CJP)\n const { meta, cues } = message;\n const { trackId, id: segmentId, startTime, duration } = meta;\n const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);\n if (!textTrack) return context;\n\n const existingCues = context.loaded[trackId] ?? [];\n const existingSegments = context.segments[trackId] ?? [];\n const prunedCues = cues.filter((cue) => !isDuplicateCue(cue, existingCues));\n const segmentAlreadyLoaded = existingSegments.some((s) => s.id === segmentId);\n\n if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;\n\n for (const cue of prunedCues) textTrack.addCue(cue);\n return {\n ...context,\n loaded: {\n ...context.loaded,\n [trackId]: [...existingCues, ...prunedCues],\n },\n segments: segmentAlreadyLoaded\n ? context.segments\n : {\n ...context.segments,\n [trackId]: [...existingSegments, { id: segmentId, startTime, duration }],\n },\n };\n });\n}\n"],"mappings":";;AAmCA,SAAS,eAAe,KAAa,UAAgC;AACnE,QAAO,SAAS,MAAM,MAAM,EAAE,cAAc,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,EAAE,SAAS,IAAI,KAAK;;;AAQhH,SAAgB,sBAAsB,cAAiD;AACrF,QAAO,sBAAsB;EAAE,QAAQ,EAAE;EAAE,UAAU,EAAE;EAAE,GAA6B,SAAS,YAAY;EAOzG,MAAM,EAAE,MAAM,SAAS;EACvB,MAAM,EAAE,SAAS,IAAI,WAAW,WAAW,aAAa;EACxD,MAAM,YAAY,MAAM,KAAK,aAAa,WAAW,CAAC,MAAM,MAAM,EAAE,OAAO,QAAQ;AACnF,MAAI,CAAC,UAAW,QAAO;EAEvB,MAAM,eAAe,QAAQ,OAAO,YAAY,EAAE;EAClD,MAAM,mBAAmB,QAAQ,SAAS,YAAY,EAAE;EACxD,MAAM,aAAa,KAAK,QAAQ,QAAQ,CAAC,eAAe,KAAK,aAAa,CAAC;EAC3E,MAAM,uBAAuB,iBAAiB,MAAM,MAAM,EAAE,OAAO,UAAU;AAE7E,MAAI,WAAW,WAAW,KAAK,qBAAsB,QAAO;AAE5D,OAAK,MAAM,OAAO,WAAY,WAAU,OAAO,IAAI;AACnD,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,cAAc,GAAG,WAAW;IAC5C;GACD,UAAU,uBACN,QAAQ,WACR;IACE,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,kBAAkB;KAAE,IAAI;KAAW;KAAW;KAAU,CAAC;IACzE;GACN;GACD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { effect } from "../../core/signals/effect.js";
|
|
2
|
+
import { update } from "../../core/signals/primitives.js";
|
|
3
|
+
import { listen } from "@videojs/utils/dom";
|
|
4
|
+
//#region src/dom/features/track-current-time.ts
|
|
5
|
+
/**
|
|
6
|
+
* Track current playback position from the media element.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors `mediaElement.currentTime` into reactive state on:
|
|
9
|
+
* - `timeupdate` — fires during playback (~4 Hz)
|
|
10
|
+
* - `seeking` — fires when a seek begins; per spec, `currentTime` is
|
|
11
|
+
* already at the new position when this event dispatches, so buffer
|
|
12
|
+
* management can react immediately rather than waiting for `timeupdate`,
|
|
13
|
+
* which does not fire while paused.
|
|
14
|
+
*
|
|
15
|
+
* Also syncs immediately when a media element becomes available.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const cleanup = trackCurrentTime({ state, owners });
|
|
19
|
+
*/
|
|
20
|
+
function trackCurrentTime({ state, owners }) {
|
|
21
|
+
let lastMediaElement;
|
|
22
|
+
let removeListeners = null;
|
|
23
|
+
const cleanupEffect = effect(() => {
|
|
24
|
+
const { mediaElement } = owners.get();
|
|
25
|
+
if (mediaElement === lastMediaElement) return;
|
|
26
|
+
removeListeners?.();
|
|
27
|
+
removeListeners = null;
|
|
28
|
+
lastMediaElement = mediaElement;
|
|
29
|
+
if (!mediaElement) return;
|
|
30
|
+
const sync = () => {
|
|
31
|
+
update(state, { currentTime: mediaElement.currentTime });
|
|
32
|
+
};
|
|
33
|
+
sync();
|
|
34
|
+
const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
|
|
35
|
+
const removeSeeking = listen(mediaElement, "seeking", sync);
|
|
36
|
+
removeListeners = () => {
|
|
37
|
+
removeTimeupdate();
|
|
38
|
+
removeSeeking();
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
removeListeners?.();
|
|
43
|
+
cleanupEffect();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { trackCurrentTime };
|
|
48
|
+
|
|
49
|
+
//# 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 { effect } from '../../core/signals/effect';\nimport { type Signal, update } from '../../core/signals/primitives';\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<S extends CurrentTimeState, O extends CurrentTimeOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListeners: (() => void) | null = null;\n\n const cleanupEffect = effect(() => {\n const { mediaElement } = owners.get();\n\n if (mediaElement === lastMediaElement) return;\n\n removeListeners?.();\n removeListeners = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n const sync = () => {\n const patch: Partial<CurrentTimeState> = { currentTime: mediaElement.currentTime };\n update(state, patch);\n };\n\n // Sync immediately so consumers don't wait for the first event\n sync();\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 cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,iBAA0E,EACxF,OACA,UAIa;CACb,IAAI;CACJ,IAAI,kBAAuC;CAE3C,MAAM,gBAAgB,aAAa;EACjC,MAAM,EAAE,iBAAiB,OAAO,KAAK;AAErC,MAAI,iBAAiB,iBAAkB;AAEvC,qBAAmB;AACnB,oBAAkB;AAClB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;EAEnB,MAAM,aAAa;AAEjB,UAAO,OADkC,EAAE,aAAa,aAAa,aAAa,CAC9D;;AAItB,QAAM;EACN,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,iBAAe"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { computed, update } from "../../core/signals/primitives.js";
|
|
2
|
+
import { createMachineReactor } from "../../core/create-machine-reactor.js";
|
|
3
|
+
import { listen } from "@videojs/utils/dom";
|
|
4
|
+
//#region src/dom/features/track-playback-initiated.ts
|
|
5
|
+
/**
|
|
6
|
+
* FSM states for playback initiation tracking.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* 'preconditions-unmet' ──── element + URL ────→ 'monitoring'
|
|
10
|
+
* ↑ ↑ │
|
|
11
|
+
* │ preconditions lost │ play / !paused
|
|
12
|
+
* │ │ ↓
|
|
13
|
+
* └────────────── 'playback-initiated' ←──────┘
|
|
14
|
+
* (exit cleanup resets state.playbackInitiated → false)
|
|
15
|
+
*
|
|
16
|
+
* any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function deriveState(state, owners) {
|
|
20
|
+
if (!owners.mediaElement || !state.presentation?.url) return "preconditions-unmet";
|
|
21
|
+
if (state.playbackInitiated) return "playback-initiated";
|
|
22
|
+
return "monitoring";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Track whether playback has been initiated for the current presentation URL.
|
|
26
|
+
*
|
|
27
|
+
* A three-state Reactor FSM driven by `state.playbackInitiated` and the
|
|
28
|
+
* `deriveState` pattern:
|
|
29
|
+
* - `'preconditions-unmet'` — no element or URL yet; no effects.
|
|
30
|
+
* - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.
|
|
31
|
+
* - `'playback-initiated'` — tracks element and URL; exit cleanup resets
|
|
32
|
+
* `state.playbackInitiated` to `false` on any change or lost preconditions.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const reactor = trackPlaybackInitiated({ state, owners });
|
|
36
|
+
* // later:
|
|
37
|
+
* reactor.destroy();
|
|
38
|
+
*/
|
|
39
|
+
function trackPlaybackInitiated({ state, owners }) {
|
|
40
|
+
const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));
|
|
41
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
42
|
+
const urlSignal = computed(() => state.get().presentation?.url);
|
|
43
|
+
return createMachineReactor({
|
|
44
|
+
initial: "preconditions-unmet",
|
|
45
|
+
monitor: () => derivedStateSignal.get(),
|
|
46
|
+
states: {
|
|
47
|
+
"preconditions-unmet": {},
|
|
48
|
+
monitoring: { entry: () => {
|
|
49
|
+
const el = mediaElementSignal.get();
|
|
50
|
+
update(state, { playbackInitiated: !el.paused });
|
|
51
|
+
return listen(el, "play", () => {
|
|
52
|
+
update(state, { playbackInitiated: !el.paused });
|
|
53
|
+
});
|
|
54
|
+
} },
|
|
55
|
+
"playback-initiated": { effects: () => {
|
|
56
|
+
mediaElementSignal.get();
|
|
57
|
+
urlSignal.get();
|
|
58
|
+
return () => update(state, { playbackInitiated: false });
|
|
59
|
+
} }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { trackPlaybackInitiated };
|
|
65
|
+
|
|
66
|
+
//# 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 { Reactor } from '../../core/create-machine-reactor';\nimport { createMachineReactor } from '../../core/create-machine-reactor';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once play has been requested for the current presentation URL. */\n playbackInitiated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: { url?: string };\n}\n\n/**\n * Owners shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * FSM states for playback initiation tracking.\n *\n * ```\n * 'preconditions-unmet' ──── element + URL ────→ 'monitoring'\n * ↑ ↑ │\n * │ preconditions lost │ play / !paused\n * │ │ ↓\n * └────────────── 'playback-initiated' ←──────┘\n * (exit cleanup resets state.playbackInitiated → false)\n *\n * any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'\n * ```\n */\nfunction deriveState(\n state: PlaybackInitiatedState,\n owners: PlaybackInitiatedOwners\n): 'preconditions-unmet' | 'monitoring' | 'playback-initiated' {\n if (!owners.mediaElement || !state.presentation?.url) return 'preconditions-unmet';\n if (state.playbackInitiated) return 'playback-initiated';\n return 'monitoring';\n}\n\n/**\n * Track whether playback has been initiated for the current presentation URL.\n *\n * A three-state Reactor FSM driven by `state.playbackInitiated` and the\n * `deriveState` pattern:\n * - `'preconditions-unmet'` — no element or URL yet; no effects.\n * - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.\n * - `'playback-initiated'` — tracks element and URL; exit cleanup resets\n * `state.playbackInitiated` to `false` on any change or lost preconditions.\n *\n * @example\n * const reactor = trackPlaybackInitiated({ state, owners });\n * // later:\n * reactor.destroy();\n */\nexport function trackPlaybackInitiated<S extends PlaybackInitiatedState, O extends PlaybackInitiatedOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): Reactor<'preconditions-unmet' | 'monitoring' | 'playback-initiated' | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n const urlSignal = computed(() => state.get().presentation?.url);\n\n return createMachineReactor<'preconditions-unmet' | 'monitoring' | 'playback-initiated'>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // Entry: check if already playing; otherwise listen for play.\n // The fn body is automatically untracked — el is read at entry time only.\n entry: () => {\n const el = mediaElementSignal.get()!;\n update(state, { playbackInitiated: !el.paused } as Partial<S>);\n return listen(el, 'play', () => {\n update(state, { playbackInitiated: !el.paused } as Partial<S>);\n });\n },\n },\n\n 'playback-initiated': {\n // Reaction: tracks element and URL while initiated. When either changes,\n // the effect re-runs — the exit cleanup fires first, resetting\n // state.playbackInitiated to false. deriveState then returns 'monitoring'\n // on the next microtask, and the always monitor drives the transition.\n //\n // This covers both the preconditions-lost path (element/URL → undefined,\n // which also triggers a deriveState → 'preconditions-unmet' transition)\n // and the URL-change / element-swap path (preconditions still met but\n // values changed, handled entirely by this effect's cleanup).\n effects: () => {\n mediaElementSignal.get(); // tracked — re-run on element change\n urlSignal.get(); // tracked — re-run on URL change\n return () => update(state, { playbackInitiated: false } as Partial<S>);\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoCA,SAAS,YACP,OACA,QAC6D;AAC7D,KAAI,CAAC,OAAO,gBAAgB,CAAC,MAAM,cAAc,IAAK,QAAO;AAC7D,KAAI,MAAM,kBAAmB,QAAO;AACpC,QAAO;;;;;;;;;;;;;;;;;AAkBT,SAAgB,uBAA4F,EAC1G,OACA,UAIoG;CACpG,MAAM,qBAAqB,eAAe,YAAY,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;CACjF,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CACpE,MAAM,YAAY,eAAe,MAAM,KAAK,CAAC,cAAc,IAAI;AAE/D,QAAO,qBAAkF;EACvF,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,YAAY,EAGV,aAAa;IACX,MAAM,KAAK,mBAAmB,KAAK;AACnC,WAAO,OAAO,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAe;AAC9D,WAAO,OAAO,IAAI,cAAc;AAC9B,YAAO,OAAO,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAe;MAC9D;MAEL;GAED,sBAAsB,EAUpB,eAAe;AACb,uBAAmB,KAAK;AACxB,cAAU,KAAK;AACf,iBAAa,OAAO,OAAO,EAAE,mBAAmB,OAAO,CAAe;MAEzE;GACF;EACF,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { effect } from "../../core/signals/effect.js";
|
|
2
|
+
import { computed, update } from "../../core/signals/primitives.js";
|
|
3
|
+
import { listen } from "@videojs/utils/dom";
|
|
4
|
+
//#region src/dom/features/track-playback-rate.ts
|
|
5
|
+
/**
|
|
6
|
+
* Track playback rate from the media element.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors `mediaElement.playbackRate` into reactive state on each `ratechange`
|
|
9
|
+
* event. Also syncs immediately when a media element becomes available.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const cleanup = trackPlaybackRate({ state, owners });
|
|
13
|
+
*/
|
|
14
|
+
function trackPlaybackRate({ state, owners }) {
|
|
15
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
16
|
+
const canTrackPlaybackRate = computed(() => !!mediaElementSignal.get());
|
|
17
|
+
const cleanupEffect = effect(() => {
|
|
18
|
+
if (!canTrackPlaybackRate.get()) return;
|
|
19
|
+
const mediaElement = mediaElementSignal.get();
|
|
20
|
+
update(state, { playbackRate: mediaElement.playbackRate });
|
|
21
|
+
return listen(mediaElement, "ratechange", () => {
|
|
22
|
+
update(state, { playbackRate: mediaElement.playbackRate });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
cleanupEffect();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { trackPlaybackRate };
|
|
31
|
+
|
|
32
|
+
//# 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 { effect } from '../../core/signals/effect';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\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 * 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<O extends PlaybackRateOwners>({\n state,\n owners,\n}: {\n state: Signal<PlaybackRateState>;\n owners: Signal<O>;\n}): () => void {\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n const canTrackPlaybackRate = computed(() => !!mediaElementSignal.get());\n\n const cleanupEffect = effect(() => {\n if (!canTrackPlaybackRate.get()) return;\n const mediaElement = mediaElementSignal.get() as HTMLMediaElement;\n\n update(state, { playbackRate: mediaElement.playbackRate });\n\n return listen(mediaElement, 'ratechange', () => {\n update(state, { playbackRate: mediaElement.playbackRate });\n });\n });\n\n return () => {\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,kBAAgD,EAC9D,OACA,UAIa;CACb,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CACpE,MAAM,uBAAuB,eAAe,CAAC,CAAC,mBAAmB,KAAK,CAAC;CAEvE,MAAM,gBAAgB,aAAa;AACjC,MAAI,CAAC,qBAAqB,KAAK,CAAE;EACjC,MAAM,eAAe,mBAAmB,KAAK;AAE7C,SAAO,OAAO,EAAE,cAAc,aAAa,cAAc,CAAC;AAE1D,SAAO,OAAO,cAAc,oBAAoB;AAC9C,UAAO,OAAO,EAAE,cAAc,aAAa,cAAc,CAAC;IAC1D;GACF;AAEF,cAAa;AACX,iBAAe"}
|