@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 @@
|
|
|
1
|
+
{"version":3,"file":"create-transition-actor.js","names":[],"sources":["../../../src/core/create-transition-actor.ts"],"sourcesContent":["import type { ActorSnapshot } from './actor';\nimport type { Machine } from './machine';\nimport { createMachineCore } from './machine';\nimport { untrack, update } from './signals/primitives';\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * A reducer-shaped actor: `(context, message) => context`.\n *\n * No finite states — the snapshot carries `value: 'active' | 'destroyed'`\n * as a universal lifecycle marker rather than domain state. The interesting\n * state is entirely in the context, which is reactive via `snapshot`.\n *\n * Use this when the actor has context that needs to be reactive but no\n * meaningful state machine (e.g., a message-driven model with DOM side\n * effects). For actors that need per-state behavior, use `createMachineActor`.\n */\nexport interface TransitionActor<Context extends object, Message extends { type: string }>\n extends Machine<ActorSnapshot<'active' | 'destroyed', Context>> {\n send(message: Message): void;\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a reducer-shaped actor from an initial context and a reducer function.\n *\n * The reducer receives the current context and a message and returns the next\n * context. Returning the same reference (by identity) skips the signal update —\n * so early-returning `context` unchanged is both the no-op and the optimization.\n *\n * Side effects (e.g. DOM mutations) may be performed inside the reducer.\n * They run synchronously before the signal is updated.\n *\n * @example\n * const actor = createTransitionActor(\n * { count: 0 },\n * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })\n * );\n */\nexport function createTransitionActor<Context extends object, Message extends { type: string }>(\n initialContext: Context,\n reducer: (context: Context, message: Message) => Context\n): TransitionActor<Context, Message> {\n const { snapshotSignal, getState, transition } = createMachineCore<\n 'active' | 'destroyed',\n ActorSnapshot<'active' | 'destroyed', Context>\n >({ value: 'active', context: initialContext });\n\n const getContext = (): Context => untrack(() => snapshotSignal.get().context);\n const setContext = (context: Context): void => update(snapshotSignal, { context });\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n send(message: Message): void {\n if (getState() === 'destroyed') return;\n const context = getContext();\n const newContext = reducer(context, message);\n if (newContext !== context) setContext(newContext);\n },\n\n destroy(): void {\n if (getState() === 'destroyed') return;\n transition('destroyed');\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,sBACd,gBACA,SACmC;CACnC,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAG/C;EAAE,OAAO;EAAU,SAAS;EAAgB,CAAC;CAE/C,MAAM,mBAA4B,cAAc,eAAe,KAAK,CAAC,QAAQ;CAC7E,MAAM,cAAc,YAA2B,OAAO,gBAAgB,EAAE,SAAS,CAAC;AAElF,QAAO;EACL,IAAI,WAAW;AACb,UAAO;;EAGT,KAAK,SAAwB;AAC3B,OAAI,UAAU,KAAK,YAAa;GAChC,MAAM,UAAU,YAAY;GAC5B,MAAM,aAAa,QAAQ,SAAS,QAAQ;AAC5C,OAAI,eAAe,QAAS,YAAW,WAAW;;EAGpD,UAAgB;AACd,OAAI,UAAU,KAAK,YAAa;AAChC,cAAW,YAAY;;EAE1B"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { isResolvedTrack } from "../types/index.js";
|
|
3
|
+
import { getSelectedTrack } from "../utils/track-selection.js";
|
|
4
|
+
//#region src/core/features/calculate-presentation-duration.ts
|
|
5
|
+
/**
|
|
6
|
+
* Check if we can calculate presentation duration (have required data).
|
|
7
|
+
*/
|
|
8
|
+
function canCalculateDuration(state) {
|
|
9
|
+
if (!state.presentation) return false;
|
|
10
|
+
return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if we should calculate presentation duration (conditions met).
|
|
14
|
+
*/
|
|
15
|
+
function shouldCalculateDuration(state) {
|
|
16
|
+
if (!canCalculateDuration(state)) return false;
|
|
17
|
+
const { presentation } = state;
|
|
18
|
+
if (presentation.duration !== void 0) return false;
|
|
19
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
20
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
21
|
+
return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get duration from the first resolved track (prefer video, fallback to audio).
|
|
25
|
+
*/
|
|
26
|
+
function getDurationFromResolvedTracks(state) {
|
|
27
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
28
|
+
if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
|
|
29
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
30
|
+
if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calculate and set presentation duration from resolved tracks.
|
|
34
|
+
*/
|
|
35
|
+
function calculatePresentationDuration({ state }) {
|
|
36
|
+
return effect(() => {
|
|
37
|
+
const currentState = state.get();
|
|
38
|
+
if (!shouldCalculateDuration(currentState)) return;
|
|
39
|
+
const duration = getDurationFromResolvedTracks(currentState);
|
|
40
|
+
if (duration === void 0 || !Number.isFinite(duration)) return;
|
|
41
|
+
state.set({
|
|
42
|
+
...currentState,
|
|
43
|
+
presentation: {
|
|
44
|
+
...currentState.presentation,
|
|
45
|
+
duration
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { calculatePresentationDuration };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=calculate-presentation-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculate-presentation-duration.js","names":[],"sources":["../../../../src/core/features/calculate-presentation-duration.ts"],"sourcesContent":["import { effect } from '../signals/effect';\nimport type { Signal } from '../signals/primitives';\nimport type { AudioTrack, Presentation, VideoTrack } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack } from '../utils/track-selection';\n\nexport interface PresentationDurationState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Check if we can calculate presentation duration (have required data).\n */\nexport function canCalculateDuration(state: PresentationDurationState): boolean {\n if (!state.presentation) return false;\n // Need at least one selected track\n return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);\n}\n\n/**\n * Check if we should calculate presentation duration (conditions met).\n */\nexport function shouldCalculateDuration(state: PresentationDurationState): boolean {\n if (!canCalculateDuration(state)) return false;\n\n const { presentation } = state;\n\n // Don't recalculate if already set\n if (presentation!.duration !== undefined) return false;\n\n // Check if any selected track is resolved\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // At least one track must be resolved (has segments and duration)\n return !!((videoTrack && isResolvedTrack(videoTrack)) || (audioTrack && isResolvedTrack(audioTrack)));\n}\n\n/**\n * Get duration from the first resolved track (prefer video, fallback to audio).\n */\nexport function getDurationFromResolvedTracks(state: PresentationDurationState): number | undefined {\n // Try video track first\n const videoTrack = state.selectedVideoTrackId\n ? (getSelectedTrack(state, 'video') as VideoTrack | undefined)\n : undefined;\n if (videoTrack && isResolvedTrack(videoTrack)) {\n return videoTrack.duration;\n }\n\n // Fallback to audio track\n const audioTrack = state.selectedAudioTrackId\n ? (getSelectedTrack(state, 'audio') as AudioTrack | undefined)\n : undefined;\n if (audioTrack && isResolvedTrack(audioTrack)) {\n return audioTrack.duration;\n }\n\n return undefined;\n}\n\n/**\n * Calculate and set presentation duration from resolved tracks.\n */\nexport function calculatePresentationDuration<S extends PresentationDurationState>({\n state,\n}: {\n state: Signal<S>;\n}): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!shouldCalculateDuration(currentState)) return;\n\n const duration = getDurationFromResolvedTracks(currentState);\n if (duration === undefined || !Number.isFinite(duration)) return;\n\n state.set({\n ...currentState,\n presentation: {\n ...currentState.presentation!,\n duration,\n },\n } as S);\n });\n}\n"],"mappings":";;;;;;;AAeA,SAAgB,qBAAqB,OAA2C;AAC9E,KAAI,CAAC,MAAM,aAAc,QAAO;AAEhC,QAAO,CAAC,EAAE,MAAM,wBAAwB,MAAM;;;;;AAMhD,SAAgB,wBAAwB,OAA2C;AACjF,KAAI,CAAC,qBAAqB,MAAM,CAAE,QAAO;CAEzC,MAAM,EAAE,iBAAiB;AAGzB,KAAI,aAAc,aAAa,KAAA,EAAW,QAAO;CAGjD,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;AAGnF,QAAO,CAAC,EAAG,cAAc,gBAAgB,WAAW,IAAM,cAAc,gBAAgB,WAAW;;;;;AAMrG,SAAgB,8BAA8B,OAAsD;CAElG,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC,KAAA;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;CAIpB,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC,KAAA;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;;;;;AAStB,SAAgB,8BAAmE,EACjF,SAGa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,wBAAwB,aAAa,CAAE;EAE5C,MAAM,WAAW,8BAA8B,aAAa;AAC5D,MAAI,aAAa,KAAA,KAAa,CAAC,OAAO,SAAS,SAAS,CAAE;AAE1D,QAAM,IAAI;GACR,GAAG;GACH,cAAc;IACZ,GAAG,aAAa;IAChB;IACD;GACF,CAAM;GACP"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getBandwidthEstimate } from "../abr/bandwidth-estimator.js";
|
|
2
|
+
import { effect } from "../signals/effect.js";
|
|
3
|
+
import { update } from "../signals/primitives.js";
|
|
4
|
+
import { selectQuality } from "../abr/quality-selection.js";
|
|
5
|
+
//#region src/core/features/quality-switching.ts
|
|
6
|
+
/**
|
|
7
|
+
* Default quality switching configuration.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_SWITCHING_CONFIG = {
|
|
10
|
+
safetyMargin: .85,
|
|
11
|
+
minUpgradeInterval: 8e3,
|
|
12
|
+
defaultBandwidth: 5e6
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Get all video tracks from a presentation's first switching set.
|
|
16
|
+
* Returns [] when the presentation is still unresolved (no selectionSets yet).
|
|
17
|
+
*/
|
|
18
|
+
function getVideoTracks(presentation) {
|
|
19
|
+
return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Quality switching orchestration (F9).
|
|
23
|
+
*
|
|
24
|
+
* Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
|
|
25
|
+
* when a different quality is optimal:
|
|
26
|
+
*
|
|
27
|
+
* - **Downgrades** happen immediately to avoid buffering stalls.
|
|
28
|
+
* - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.
|
|
29
|
+
* - The first switch (from any track, or no track) is always immediate.
|
|
30
|
+
*
|
|
31
|
+
* Smooth switching is handled downstream: when `selectedVideoTrackId` changes,
|
|
32
|
+
* `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
|
|
33
|
+
* segment, then appends media segments from the current position in the new
|
|
34
|
+
* quality. The browser's SourceBuffer replaces the overlapping buffered range.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const cleanup = switchQuality({ state });
|
|
38
|
+
* // Later, when done:
|
|
39
|
+
* cleanup();
|
|
40
|
+
*/
|
|
41
|
+
function switchQuality({ state }, config = {}) {
|
|
42
|
+
const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;
|
|
43
|
+
const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;
|
|
44
|
+
const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;
|
|
45
|
+
let lastUpgradeTime = Date.now();
|
|
46
|
+
let firstMeaningfulFire = true;
|
|
47
|
+
return effect(() => {
|
|
48
|
+
const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = state.get();
|
|
49
|
+
if (abrDisabled === true) return;
|
|
50
|
+
if (!presentation || !bandwidthState) return;
|
|
51
|
+
const videoTracks = getVideoTracks(presentation);
|
|
52
|
+
if (videoTracks.length === 0) return;
|
|
53
|
+
const isFirst = firstMeaningfulFire;
|
|
54
|
+
firstMeaningfulFire = false;
|
|
55
|
+
const optimal = selectQuality(videoTracks, getBandwidthEstimate(bandwidthState, defaultBandwidth), { safetyMargin });
|
|
56
|
+
if (!optimal || optimal.id === selectedVideoTrackId) return;
|
|
57
|
+
const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);
|
|
58
|
+
if (!currentTrack || optimal.bandwidth > currentTrack.bandwidth) {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;
|
|
61
|
+
lastUpgradeTime = now;
|
|
62
|
+
}
|
|
63
|
+
update(state, { selectedVideoTrackId: optimal.id });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { switchQuality };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=quality-switching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality-switching.js","names":[],"sources":["../../../../src/core/features/quality-switching.ts"],"sourcesContent":["import type { BandwidthState } from '../abr/bandwidth-estimator';\nimport { getBandwidthEstimate } from '../abr/bandwidth-estimator';\nimport { selectQuality } from '../abr/quality-selection';\nimport { effect } from '../signals/effect';\nimport { type Signal, update } from '../signals/primitives';\nimport type { Presentation, VideoSelectionSet } from '../types';\n\n/**\n * State shape for quality switching.\n */\nexport interface QualitySwitchingState {\n presentation?: Presentation;\n bandwidthState?: BandwidthState;\n selectedVideoTrackId?: string;\n // TODO: abrDisabled is a blunt instrument — it requires callers to know they're\n // competing with ABR and explicitly opt out. A better long-term design would\n // separate ABR selection (abrVideoTrackId) from manual selection (manualVideoTrackId)\n // and derive the effective selectedVideoTrackId as manualVideoTrackId ?? abrVideoTrackId.\n // That way the two concerns never write to the same field and consumers are unchanged.\n /** When true, ABR quality switching is suppressed. Use for manual quality selection. */\n abrDisabled?: boolean;\n}\n\n/**\n * Configuration for quality switching behavior.\n */\nexport interface QualitySwitchingConfig {\n /**\n * Safety margin for quality selection (0–1).\n * Track is selected only when bandwidth >= track.bandwidth / safetyMargin.\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n\n /**\n * Minimum milliseconds between upgrades.\n * Prevents oscillation when bandwidth fluctuates around a quality threshold.\n * Downgrades are always immediate regardless of this setting.\n * Default: 8000 (8 seconds).\n */\n minUpgradeInterval?: number;\n\n /**\n * Bandwidth estimate in bps to use before enough samples have been collected.\n * Default: 5_000_000 (5 Mbps).\n */\n defaultBandwidth?: number;\n}\n\n/**\n * Default quality switching configuration.\n */\nexport const DEFAULT_SWITCHING_CONFIG: Required<QualitySwitchingConfig> = {\n safetyMargin: 0.85,\n minUpgradeInterval: 8000,\n defaultBandwidth: 5_000_000,\n};\n\n/**\n * Get all video tracks from a presentation's first switching set.\n * Returns [] when the presentation is still unresolved (no selectionSets yet).\n */\nfunction getVideoTracks(presentation: Presentation) {\n const videoSet = presentation.selectionSets?.find((s) => s.type === 'video') as VideoSelectionSet | undefined;\n return videoSet?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Quality switching orchestration (F9).\n *\n * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`\n * when a different quality is optimal:\n *\n * - **Downgrades** happen immediately to avoid buffering stalls.\n * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.\n * - The first switch (from any track, or no track) is always immediate.\n *\n * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,\n * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init\n * segment, then appends media segments from the current position in the new\n * quality. The browser's SourceBuffer replaces the overlapping buffered range.\n *\n * @example\n * const cleanup = switchQuality({ state });\n * // Later, when done:\n * cleanup();\n */\nexport function switchQuality<S extends QualitySwitchingState>(\n { state }: { state: Signal<S> },\n config: QualitySwitchingConfig = {}\n): () => void {\n const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;\n const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;\n const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;\n\n // Initialize to creation time so the interval starts counting immediately.\n // The first time we have enough data to make a meaningful quality decision\n // (presentation resolved + bandwidth available), the upgrade gate is skipped\n // so the initial ABR correction does not wait for minUpgradeInterval.\n let lastUpgradeTime = Date.now();\n let firstMeaningfulFire = true;\n\n return effect(() => {\n const currentState = state.get();\n const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = currentState;\n\n if (abrDisabled === true) return;\n if (!presentation || !bandwidthState) return;\n\n const videoTracks = getVideoTracks(presentation);\n if (videoTracks.length === 0) return;\n\n // Consume the first-meaningful-fire flag now that we have all required data.\n const isFirst = firstMeaningfulFire;\n firstMeaningfulFire = false;\n\n const bandwidth = getBandwidthEstimate(bandwidthState, defaultBandwidth);\n const optimal = selectQuality(videoTracks as any, bandwidth, { safetyMargin });\n if (!optimal || optimal.id === selectedVideoTrackId) return;\n\n // Determine whether this is an upgrade or downgrade.\n const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);\n const isUpgrade = !currentTrack || optimal.bandwidth > currentTrack.bandwidth;\n\n if (isUpgrade) {\n const now = Date.now();\n // Gate upgrades with minUpgradeInterval to prevent oscillation.\n // Downgrades are always immediate; the first meaningful evaluation is always allowed.\n if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;\n lastUpgradeTime = now;\n }\n\n const patch: Partial<QualitySwitchingState> = { selectedVideoTrackId: optimal.id };\n update(state, patch);\n });\n}\n"],"mappings":";;;;;;;;AAoDA,MAAa,2BAA6D;CACxE,cAAc;CACd,oBAAoB;CACpB,kBAAkB;CACnB;;;;;AAMD,SAAS,eAAe,cAA4B;AAElD,SADiB,aAAa,eAAe,MAAM,MAAM,EAAE,SAAS,QAAQ,GAC3D,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;;;;;;;;;;;;;AAuBjD,SAAgB,cACd,EAAE,SACF,SAAiC,EAAE,EACvB;CACZ,MAAM,eAAe,OAAO,gBAAgB,yBAAyB;CACrE,MAAM,qBAAqB,OAAO,sBAAsB,yBAAyB;CACjF,MAAM,mBAAmB,OAAO,oBAAoB,yBAAyB;CAM7E,IAAI,kBAAkB,KAAK,KAAK;CAChC,IAAI,sBAAsB;AAE1B,QAAO,aAAa;EAElB,MAAM,EAAE,cAAc,gBAAgB,sBAAsB,gBADvC,MAAM,KAAK;AAGhC,MAAI,gBAAgB,KAAM;AAC1B,MAAI,CAAC,gBAAgB,CAAC,eAAgB;EAEtC,MAAM,cAAc,eAAe,aAAa;AAChD,MAAI,YAAY,WAAW,EAAG;EAG9B,MAAM,UAAU;AAChB,wBAAsB;EAGtB,MAAM,UAAU,cAAc,aADZ,qBAAqB,gBAAgB,iBAAiB,EACX,EAAE,cAAc,CAAC;AAC9E,MAAI,CAAC,WAAW,QAAQ,OAAO,qBAAsB;EAGrD,MAAM,eAAe,YAAY,MAAM,MAAM,EAAE,OAAO,qBAAqB;AAG3E,MAFkB,CAAC,gBAAgB,QAAQ,YAAY,aAAa,WAErD;GACb,MAAM,MAAM,KAAK,KAAK;AAGtB,OAAI,CAAC,WAAW,MAAM,kBAAkB,mBAAoB;AAC5D,qBAAkB;;AAIpB,SAAO,OADuC,EAAE,sBAAsB,QAAQ,IAAI,CAC9D;GACpB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { computed, update } from "../signals/primitives.js";
|
|
2
|
+
import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
|
|
3
|
+
import { createMachineReactor } from "../create-machine-reactor.js";
|
|
4
|
+
import { parseMultivariantPlaylist } from "../hls/parse-multivariant.js";
|
|
5
|
+
//#region src/core/features/resolve-presentation.ts
|
|
6
|
+
/**
|
|
7
|
+
* Determines if resolution conditions are met based on preload policy and playback state.
|
|
8
|
+
*
|
|
9
|
+
* Resolution conditions:
|
|
10
|
+
* - State-driven: preload is 'auto' or 'metadata'
|
|
11
|
+
* - Playback-driven: playbackInitiated is true
|
|
12
|
+
*
|
|
13
|
+
* @param state - Current presentation state
|
|
14
|
+
* @returns true if resolution conditions are met
|
|
15
|
+
*/
|
|
16
|
+
function shouldResolve(state) {
|
|
17
|
+
const { preload, playbackInitiated } = state;
|
|
18
|
+
return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derives the correct state from current state conditions.
|
|
22
|
+
*
|
|
23
|
+
* States are mutually exclusive and exhaustive:
|
|
24
|
+
* - `'preconditions-unmet'`: no presentation, or presentation has no URL
|
|
25
|
+
* - `'idle'`: URL present, unresolved (no id), shouldResolve not met
|
|
26
|
+
* - `'resolving'`: URL present, unresolved (no id), shouldResolve met
|
|
27
|
+
* - `'resolved'`: URL present, resolved (has id)
|
|
28
|
+
*/
|
|
29
|
+
function deriveState(state) {
|
|
30
|
+
const { presentation } = state;
|
|
31
|
+
if (!presentation || !("url" in presentation)) return "preconditions-unmet";
|
|
32
|
+
if ("id" in presentation) return "resolved";
|
|
33
|
+
return shouldResolve(state) ? "resolving" : "idle";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolves unresolved presentations using reactive composition.
|
|
37
|
+
*
|
|
38
|
+
* FSM driven by `deriveState` — a single `always` monitor keeps the state in
|
|
39
|
+
* sync with conditions at all times. `'resolving'` additionally runs the fetch
|
|
40
|
+
* task and returns an AbortController so the framework aborts it on state exit.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const reactor = resolvePresentation({ state });
|
|
44
|
+
* // later:
|
|
45
|
+
* reactor.destroy();
|
|
46
|
+
*/
|
|
47
|
+
function resolvePresentation({ state }) {
|
|
48
|
+
const derivedStateSignal = computed(() => deriveState(state.get()));
|
|
49
|
+
return createMachineReactor({
|
|
50
|
+
initial: "preconditions-unmet",
|
|
51
|
+
monitor: () => derivedStateSignal.get(),
|
|
52
|
+
states: {
|
|
53
|
+
"preconditions-unmet": {},
|
|
54
|
+
idle: {},
|
|
55
|
+
resolving: { entry: () => {
|
|
56
|
+
const presentation = state.get().presentation;
|
|
57
|
+
const ac = new AbortController();
|
|
58
|
+
fetchResolvable(presentation, { signal: ac.signal }).then((response) => getResponseText(response)).then((text) => {
|
|
59
|
+
update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
|
|
60
|
+
}).catch((error) => {
|
|
61
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
64
|
+
return ac;
|
|
65
|
+
} },
|
|
66
|
+
resolved: {}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { resolvePresentation };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=resolve-presentation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-presentation.js","names":[],"sources":["../../../../src/core/features/resolve-presentation.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport type { Reactor } from '../create-machine-reactor';\nimport { createMachineReactor } from '../create-machine-reactor';\nimport { parseMultivariantPlaylist } from '../hls/parse-multivariant';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { AddressableObject, Presentation } from '../types';\n\n/**\n * Unresolved presentation - has a URL but no data yet.\n * Identical to AddressableObject per user requirement.\n */\nexport type UnresolvedPresentation = AddressableObject;\n\n/**\n * State shape for presentation resolution.\n */\nexport interface PresentationState {\n presentation?: UnresolvedPresentation | Presentation | undefined;\n preload?: 'auto' | 'metadata' | 'none' | undefined;\n /** True once the user has initiated playback — enables resolution regardless of preload. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Type guard to check if presentation is unresolved.\n */\nexport function isUnresolved(\n presentation: UnresolvedPresentation | Presentation | undefined\n): presentation is UnresolvedPresentation {\n return presentation !== undefined && 'url' in presentation && !('id' in presentation);\n}\n\nexport function canResolve(\n state: PresentationState\n): state is PresentationState & { presentation: UnresolvedPresentation } {\n return isUnresolved(state.presentation);\n}\n\n/**\n * Determines if resolution conditions are met based on preload policy and playback state.\n *\n * Resolution conditions:\n * - State-driven: preload is 'auto' or 'metadata'\n * - Playback-driven: playbackInitiated is true\n *\n * @param state - Current presentation state\n * @returns true if resolution conditions are met\n */\nexport function shouldResolve(state: PresentationState): boolean {\n const { preload, playbackInitiated } = state;\n return (\n // State-driven: preload allows (auto/metadata)\n ['auto', 'metadata'].includes(preload as any) ||\n // Playback-driven: user has initiated playback\n !!playbackInitiated\n );\n}\n\nexport type ResolvePresentationState = 'preconditions-unmet' | 'idle' | 'resolving' | 'resolved';\n\n/**\n * Derives the correct state from current state conditions.\n *\n * States are mutually exclusive and exhaustive:\n * - `'preconditions-unmet'`: no presentation, or presentation has no URL\n * - `'idle'`: URL present, unresolved (no id), shouldResolve not met\n * - `'resolving'`: URL present, unresolved (no id), shouldResolve met\n * - `'resolved'`: URL present, resolved (has id)\n */\nfunction deriveState(state: PresentationState): ResolvePresentationState {\n const { presentation } = state;\n if (!presentation || !('url' in presentation)) return 'preconditions-unmet';\n if ('id' in presentation) return 'resolved';\n return shouldResolve(state) ? 'resolving' : 'idle';\n}\n\n/**\n * Resolves unresolved presentations using reactive composition.\n *\n * FSM driven by `deriveState` — a single `always` monitor keeps the state in\n * sync with conditions at all times. `'resolving'` additionally runs the fetch\n * task and returns an AbortController so the framework aborts it on state exit.\n *\n * @example\n * const reactor = resolvePresentation({ state });\n * // later:\n * reactor.destroy();\n */\nexport function resolvePresentation<S extends PresentationState>({\n state,\n}: {\n state: Signal<S>;\n}): Reactor<ResolvePresentationState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.get()));\n\n return createMachineReactor<ResolvePresentationState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n idle: {},\n resolving: {\n // Entry: start fetch on state entry; return AbortController so the\n // framework aborts the in-flight request on state exit.\n entry: () => {\n const presentation = state.get().presentation as UnresolvedPresentation;\n const ac = new AbortController();\n\n fetchResolvable(presentation, { signal: ac.signal })\n .then((response) => getResponseText(response))\n .then((text) => {\n const parsed = parseMultivariantPlaylist(text, presentation);\n update(state, { presentation: parsed } as Partial<S>);\n })\n .catch((error) => {\n if (error instanceof Error && error.name === 'AbortError') return;\n throw error;\n });\n\n return ac;\n },\n },\n resolved: {},\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgDA,SAAgB,cAAc,OAAmC;CAC/D,MAAM,EAAE,SAAS,sBAAsB;AACvC,QAEE,CAAC,QAAQ,WAAW,CAAC,SAAS,QAAe,IAE7C,CAAC,CAAC;;;;;;;;;;;AAeN,SAAS,YAAY,OAAoD;CACvE,MAAM,EAAE,iBAAiB;AACzB,KAAI,CAAC,gBAAgB,EAAE,SAAS,cAAe,QAAO;AACtD,KAAI,QAAQ,aAAc,QAAO;AACjC,QAAO,cAAc,MAAM,GAAG,cAAc;;;;;;;;;;;;;;AAe9C,SAAgB,oBAAiD,EAC/D,SAGiE;CACjE,MAAM,qBAAqB,eAAe,YAAY,MAAM,KAAK,CAAC,CAAC;AAEnE,QAAO,qBAA+C;EACpD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,MAAM,EAAE;GACR,WAAW,EAGT,aAAa;IACX,MAAM,eAAe,MAAM,KAAK,CAAC;IACjC,MAAM,KAAK,IAAI,iBAAiB;AAEhC,oBAAgB,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,CACjD,MAAM,aAAa,gBAAgB,SAAS,CAAC,CAC7C,MAAM,SAAS;AAEd,YAAO,OAAO,EAAE,cADD,0BAA0B,MAAM,aAAa,EACtB,CAAe;MACrD,CACD,OAAO,UAAU;AAChB,SAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAC3D,WAAM;MACN;AAEJ,WAAO;MAEV;GACD,UAAU,EAAE;GACb;EACF,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { isResolvedTrack } from "../types/index.js";
|
|
3
|
+
import { getSelectedTrack } from "../utils/track-selection.js";
|
|
4
|
+
import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
|
|
5
|
+
import { ConcurrentRunner, Task } from "../task.js";
|
|
6
|
+
import { parseMediaPlaylist } from "../hls/parse-media-playlist.js";
|
|
7
|
+
//#region src/core/features/resolve-track.ts
|
|
8
|
+
function canResolve(state, config) {
|
|
9
|
+
const track = getSelectedTrack(state, config.type);
|
|
10
|
+
if (!track) return false;
|
|
11
|
+
return !isResolvedTrack(track);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Updates a track within a presentation (immutably).
|
|
15
|
+
* Generic - works for video, audio, or text tracks.
|
|
16
|
+
*/
|
|
17
|
+
function updateTrackInPresentation(presentation, resolvedTrack) {
|
|
18
|
+
const trackId = resolvedTrack.id;
|
|
19
|
+
return {
|
|
20
|
+
...presentation,
|
|
21
|
+
selectionSets: presentation.selectionSets.map((selectionSet) => ({
|
|
22
|
+
...selectionSet,
|
|
23
|
+
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
|
|
24
|
+
...switchingSet,
|
|
25
|
+
tracks: switchingSet.tracks.map((track) => track.id === trackId ? resolvedTrack : track)
|
|
26
|
+
}))
|
|
27
|
+
}))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolves unresolved tracks using reactive composition.
|
|
32
|
+
*
|
|
33
|
+
* Reacts to state changes and schedules fetch tasks via ConcurrentRunner when
|
|
34
|
+
* a selected track is unresolved. The ConcurrentRunner handles deduplication,
|
|
35
|
+
* parallel execution, and cleanup.
|
|
36
|
+
*
|
|
37
|
+
* Generic version that works for video, audio, or text tracks based on config.
|
|
38
|
+
* Type parameter T is inferred from config.type (use 'as const' for inference).
|
|
39
|
+
*/
|
|
40
|
+
function resolveTrack({ state }, config) {
|
|
41
|
+
const runner = new ConcurrentRunner();
|
|
42
|
+
const cleanup = effect(() => {
|
|
43
|
+
const currentState = state.get();
|
|
44
|
+
if (!canResolve(currentState, config)) return;
|
|
45
|
+
const track = getSelectedTrack(currentState, config.type);
|
|
46
|
+
if (!track) return;
|
|
47
|
+
const resolvedTrack = track;
|
|
48
|
+
runner.schedule(new Task(async (signal) => {
|
|
49
|
+
const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(resolvedTrack, { signal })), resolvedTrack);
|
|
50
|
+
const latest = state.get();
|
|
51
|
+
const updatedPresentation = updateTrackInPresentation(latest.presentation, mediaTrack);
|
|
52
|
+
state.set({
|
|
53
|
+
...latest,
|
|
54
|
+
presentation: updatedPresentation
|
|
55
|
+
});
|
|
56
|
+
}, { id: track.id }));
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
runner.abortAll();
|
|
60
|
+
cleanup();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { resolveTrack };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=resolve-track.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-track.js","names":[],"sources":["../../../../src/core/features/resolve-track.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport { parseMediaPlaylist } from '../hls/parse-media-playlist';\nimport { effect } from '../signals/effect';\nimport type { Signal } from '../signals/primitives';\nimport { ConcurrentRunner, Task } from '../task';\nimport type { Presentation, ResolvedTrack, TrackType } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack, type TrackSelectionState } from '../utils/track-selection';\n\n/**\n * State shape for track resolution.\n */\nexport interface TrackResolutionState extends TrackSelectionState {\n presentation?: Presentation | undefined;\n selectedVideoTrackId?: string | undefined;\n selectedAudioTrackId?: string | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\nexport function canResolve<T extends TrackType>(\n state: TrackResolutionState,\n config: TrackResolutionConfig<T>\n): boolean {\n const track = getSelectedTrack(state, config.type);\n if (!track) return false;\n\n return !isResolvedTrack(track);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Updates a track within a presentation (immutably).\n * Generic - works for video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n\n/**\n * Configuration for track resolution.\n */\nexport interface TrackResolutionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Resolves unresolved tracks using reactive composition.\n *\n * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when\n * a selected track is unresolved. The ConcurrentRunner handles deduplication,\n * parallel execution, and cleanup.\n *\n * Generic version that works for video, audio, or text tracks based on config.\n * Type parameter T is inferred from config.type (use 'as const' for inference).\n */\nexport function resolveTrack<S extends TrackResolutionState, T extends TrackType>(\n { state }: { state: Signal<S> },\n config: TrackResolutionConfig<T>\n): () => void {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n const cleanup = effect(() => {\n const currentState = state.get();\n if (!canResolve(currentState, config)) return;\n\n const track = getSelectedTrack(currentState, config.type);\n if (!track) return;\n\n const resolvedTrack = track;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n const response = await fetchResolvable(resolvedTrack, { signal });\n const text = await getResponseText(response);\n const mediaTrack = parseMediaPlaylist(text, resolvedTrack);\n\n // IMPORTANT: Read state.get().presentation at write time, not from the\n // captured currentState snapshot. Multiple Tasks may be running concurrently\n // (one per track being resolved), so the snapshot is likely already stale by\n // the time a task completes — a sibling task may have already written the\n // presentation with its own resolved track. Reading live state ensures each\n // task builds on top of whatever has been committed so far.\n const latest = state.get();\n const updatedPresentation = updateTrackInPresentation(latest.presentation!, mediaTrack);\n state.set({ ...latest, presentation: updatedPresentation } as S);\n },\n { id: track.id }\n )\n );\n });\n\n return () => {\n runner.abortAll();\n cleanup();\n };\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,WACd,OACA,QACS;CACT,MAAM,QAAQ,iBAAiB,OAAO,OAAO,KAAK;AAClD,KAAI,CAAC,MAAO,QAAO;AAEnB,QAAO,CAAC,gBAAgB,MAAM;;;;;;AAWhC,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;AAC9B,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,MAAO;IAC3F,EAAE;GACJ,EAAE;EACJ;;;;;;;;;;;;AAoBH,SAAgB,aACd,EAAE,SACF,QACY;CAIZ,MAAM,SAAS,IAAI,kBAAkB;CAErC,MAAM,UAAU,aAAa;EAC3B,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,WAAW,cAAc,OAAO,CAAE;EAEvC,MAAM,QAAQ,iBAAiB,cAAc,OAAO,KAAK;AACzD,MAAI,CAAC,MAAO;EAEZ,MAAM,gBAAgB;AAEtB,SAAO,SAGL,IAAI,KACF,OAAO,WAAW;GAGhB,MAAM,aAAa,mBADN,MAAM,gBADF,MAAM,gBAAgB,eAAe,EAAE,QAAQ,CAAC,CACrB,EACA,cAAc;GAQ1D,MAAM,SAAS,MAAM,KAAK;GAC1B,MAAM,sBAAsB,0BAA0B,OAAO,cAAe,WAAW;AACvF,SAAM,IAAI;IAAE,GAAG;IAAQ,cAAc;IAAqB,CAAM;KAElE,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;GACD;AAEF,cAAa;AACX,SAAO,UAAU;AACjB,WAAS"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { update } from "../signals/primitives.js";
|
|
3
|
+
import { SelectedTrackIdKeyByType } from "../utils/track-selection.js";
|
|
4
|
+
//#region src/core/features/select-tracks.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pick text track to activate.
|
|
7
|
+
*
|
|
8
|
+
* Selection priority (if enabled):
|
|
9
|
+
* 1. User preference (preferredSubtitleLanguage)
|
|
10
|
+
* 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)
|
|
11
|
+
* 3. No auto-selection (user opt-in)
|
|
12
|
+
*
|
|
13
|
+
* By default, FORCED tracks are excluded per Apple's HLS spec.
|
|
14
|
+
*
|
|
15
|
+
* @param presentation - Presentation with text tracks
|
|
16
|
+
* @param config - Selection configuration
|
|
17
|
+
* @returns Track ID or undefined (no auto-selection)
|
|
18
|
+
*/
|
|
19
|
+
function pickTextTrack(presentation, config) {
|
|
20
|
+
const textSet = presentation.selectionSets.find((set) => set.type === "text");
|
|
21
|
+
if (!textSet?.switchingSets?.[0]?.tracks.length) return void 0;
|
|
22
|
+
const tracks = textSet.switchingSets[0].tracks;
|
|
23
|
+
const availableTracks = config.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);
|
|
24
|
+
if (availableTracks.length === 0) return void 0;
|
|
25
|
+
const { preferredSubtitleLanguage, enableDefaultTrack = false } = config;
|
|
26
|
+
if (preferredSubtitleLanguage) {
|
|
27
|
+
const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);
|
|
28
|
+
if (languageMatch) return languageMatch.id;
|
|
29
|
+
}
|
|
30
|
+
if (enableDefaultTrack) {
|
|
31
|
+
const defaultTrack = availableTracks.find((track) => track.default === true);
|
|
32
|
+
if (defaultTrack) return defaultTrack.id;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if we can select a track of the given type.
|
|
37
|
+
*
|
|
38
|
+
* Returns true when:
|
|
39
|
+
* - Presentation exists
|
|
40
|
+
* - Has tracks of the specified type
|
|
41
|
+
*
|
|
42
|
+
* Generic over track type - works for video, audio, or text.
|
|
43
|
+
*/
|
|
44
|
+
function canSelectTrack(state, config) {
|
|
45
|
+
return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks.length;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if we should select a track of the given type.
|
|
49
|
+
*
|
|
50
|
+
* Returns true when:
|
|
51
|
+
* - Track of this type is not already selected
|
|
52
|
+
*
|
|
53
|
+
* Generic over track type - works for video, audio, or text.
|
|
54
|
+
*
|
|
55
|
+
* @TODO figure out reactive model for ABR cases - right now we're only selecting
|
|
56
|
+
* if we have nothing selected (CJP)
|
|
57
|
+
*/
|
|
58
|
+
function shouldSelectTrack(state, config) {
|
|
59
|
+
return !state[SelectedTrackIdKeyByType[config.type]];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Select video track orchestration.
|
|
63
|
+
*
|
|
64
|
+
* Selects video track when:
|
|
65
|
+
* - Presentation exists
|
|
66
|
+
* - No video track is selected yet
|
|
67
|
+
*
|
|
68
|
+
* Uses bandwidth-based quality selection algorithm.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const cleanup = selectVideoTrack(
|
|
72
|
+
* { state, owners, events },
|
|
73
|
+
* { initialBandwidth: 2_000_000 }
|
|
74
|
+
* );
|
|
75
|
+
*/
|
|
76
|
+
function selectVideoTrack({ state }, config = { type: "video" }) {
|
|
77
|
+
return effect(() => {
|
|
78
|
+
const currentState = state.get();
|
|
79
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
80
|
+
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
|
|
81
|
+
if (selectedTrackId) update(state, { [SelectedTrackIdKeyByType[config.type]]: selectedTrackId });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Select audio track orchestration.
|
|
86
|
+
*
|
|
87
|
+
* Selects audio track when:
|
|
88
|
+
* - Presentation exists
|
|
89
|
+
* - No audio track is selected yet
|
|
90
|
+
*
|
|
91
|
+
* Uses language and preference-based selection.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const cleanup = selectAudioTrack(
|
|
95
|
+
* { state, owners, events },
|
|
96
|
+
* { preferredAudioLanguage: 'en' }
|
|
97
|
+
* );
|
|
98
|
+
*/
|
|
99
|
+
function selectAudioTrack({ state }, config = { type: "audio" }) {
|
|
100
|
+
return effect(() => {
|
|
101
|
+
const currentState = state.get();
|
|
102
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
103
|
+
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
|
|
104
|
+
if (selectedTrackId) update(state, { selectedAudioTrackId: selectedTrackId });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Select text track orchestration.
|
|
109
|
+
*
|
|
110
|
+
* Selects text track when:
|
|
111
|
+
* - Presentation exists
|
|
112
|
+
* - No text track is selected yet
|
|
113
|
+
*
|
|
114
|
+
* Note: Currently does not auto-select (user opt-in).
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const cleanup = selectTextTrack({ state, owners, events }, {});
|
|
118
|
+
*/
|
|
119
|
+
function selectTextTrack({ state }, config = { type: "text" }) {
|
|
120
|
+
return effect(() => {
|
|
121
|
+
const currentState = state.get();
|
|
122
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
123
|
+
const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
|
|
124
|
+
if (selectedTextTrackId) update(state, { selectedTextTrackId });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { selectAudioTrack, selectTextTrack, selectVideoTrack };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=select-tracks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/core/features/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport { effect } from '../signals/effect';\nimport { type Signal, update } from '../signals/primitives';\nimport type { AudioSelectionSet, Presentation, TrackType, VideoSelectionSet } from '../types';\nimport { SelectedTrackIdKeyByType } from '../utils/track-selection';\n\n/**\n * Default initial bandwidth estimate for cold start (bits per second).\n * Conservative 1 Mbps to avoid over-selecting on slow connections.\n */\nexport const DEFAULT_INITIAL_BANDWIDTH = 1_000_000;\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Owners shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionOwners = Record<string, never>;\n\n/**\n * Action types for track selection.\n * Reserved for future event-driven selection triggers.\n */\nexport type TrackSelectionAction = { type: 'presentation-loaded' };\n\n/**\n * Base configuration for track selection.\n * Generic over track type with discriminant `type` field.\n */\nexport interface TrackSelectionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Configuration for video track selection.\n * Generic with default to 'video' for convenience.\n */\nexport interface VideoSelectionConfig<T extends TrackType = 'video'> extends TrackSelectionConfig<T> {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Used to select video quality before we have real measurements.\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Safety margin for quality selection (0-1).\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n}\n\n/**\n * Configuration for audio track selection.\n * Generic with default to 'audio' for convenience.\n */\nexport interface AudioSelectionConfig<T extends TrackType = 'audio'> extends TrackSelectionConfig<T> {\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n}\n\n/**\n * Configuration for text track selection.\n * Generic with default to 'text' for convenience.\n */\nexport interface TextSelectionConfig<T extends TrackType = 'text'> extends TrackSelectionConfig<T> {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n *\n * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n *\n * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n// =============================================================================\n\n/**\n * Pick video track using quality selection algorithm.\n *\n * Uses bandwidth-based selection with safety margin to pick\n * the highest quality track that fits available bandwidth.\n *\n * @param presentation - Presentation with video tracks\n * @param config - Selection configuration (bandwidth, safety margin)\n * @returns Selected video track ID, or undefined if no video tracks\n */\nexport function pickVideoTrack(presentation: Presentation, config: VideoSelectionConfig): string | undefined {\n const videoSet = presentation.selectionSets.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n\n if (!videoSet || videoSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks (HLS typically has one switching set per type)\n const switchingSet = videoSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const safetyMargin = config.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n\n // selectQuality works with both partially resolved and resolved tracks\n const selected = selectQuality(switchingSet.tracks as any, initialBandwidth, { safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Pick audio track.\n *\n * Selection priority:\n * 1. First track matching preferred language (if specified)\n * 2. First default track\n * 3. First audio track\n *\n * @param presentation - Presentation with audio tracks\n * @param config - Selection configuration (preferred language)\n * @returns Selected audio track ID, or undefined if no audio tracks\n */\nexport function pickAudioTrack(presentation: Presentation, config: AudioSelectionConfig): string | undefined {\n const audioSet = presentation.selectionSets.find((set) => set.type === 'audio') as AudioSelectionSet | undefined;\n\n if (!audioSet || audioSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks\n const switchingSet = audioSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const tracks = switchingSet.tracks;\n\n // Try preferred language first\n if (config.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Pick text track to activate.\n *\n * Selection priority (if enabled):\n * 1. User preference (preferredSubtitleLanguage)\n * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)\n * 3. No auto-selection (user opt-in)\n *\n * By default, FORCED tracks are excluded per Apple's HLS spec.\n *\n * @param presentation - Presentation with text tracks\n * @param config - Selection configuration\n * @returns Track ID or undefined (no auto-selection)\n */\nexport function pickTextTrack(presentation: Presentation, config: TextSelectionConfig): string | undefined {\n const textSet = presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks.length) return undefined;\n\n const tracks = textSet.switchingSets[0].tracks;\n\n // Filter out FORCED tracks by default (following hls.js/http-streaming pattern)\n // Per Apple spec: regular tracks MUST contain forced content when both exist\n const availableTracks = config.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config;\n\n // 1. Preferred language match (if specified)\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n // 2. DEFAULT track (if enabled AND track has both DEFAULT=YES + AUTOSELECT=YES)\n // Note: Parser only sets default=true when BOTH attributes present\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n // 3. User opt-in (no auto-selection)\n return undefined;\n}\n\n/**\n * Check if we can select a track of the given type.\n *\n * Returns true when:\n * - Presentation exists\n * - Has tracks of the specified type\n *\n * Generic over track type - works for video, audio, or text.\n */\nexport function canSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks\n .length;\n}\n\n/**\n * Check if we should select a track of the given type.\n *\n * Returns true when:\n * - Track of this type is not already selected\n *\n * Generic over track type - works for video, audio, or text.\n *\n * @TODO figure out reactive model for ABR cases - right now we're only selecting\n * if we have nothing selected (CJP)\n */\nexport function shouldSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !state[SelectedTrackIdKeyByType[config.type]];\n}\n\n// =============================================================================\n// Orchestrations\n// =============================================================================\n\n/**\n * Select video track orchestration.\n *\n * Selects video track when:\n * - Presentation exists\n * - No video track is selected yet\n *\n * Uses bandwidth-based quality selection algorithm.\n *\n * @example\n * const cleanup = selectVideoTrack(\n * { state, owners, events },\n * { initialBandwidth: 2_000_000 }\n * );\n */\nexport function selectVideoTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: VideoSelectionConfig = { type: 'video' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const selectedTrackKey = SelectedTrackIdKeyByType[config.type];\n const patch: Partial<TrackSelectionState> = { [selectedTrackKey]: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select audio track orchestration.\n *\n * Selects audio track when:\n * - Presentation exists\n * - No audio track is selected yet\n *\n * Uses language and preference-based selection.\n *\n * @example\n * const cleanup = selectAudioTrack(\n * { state, owners, events },\n * { preferredAudioLanguage: 'en' }\n * );\n */\nexport function selectAudioTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: AudioSelectionConfig = { type: 'audio' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === 'audio')\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedAudioTrackId: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select text track orchestration.\n *\n * Selects text track when:\n * - Presentation exists\n * - No text track is selected yet\n *\n * Note: Currently does not auto-select (user opt-in).\n *\n * @example\n * const cleanup = selectTextTrack({ state, owners, events }, {});\n */\nexport function selectTextTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: TextSelectionConfig = { type: 'text' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Text tracks are user opt-in - don't auto-select\n const selectedTextTrackId = pickTextTrack(currentState.presentation!, config);\n\n if (selectedTextTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedTextTrackId };\n update(state, patch);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwMA,SAAgB,cAAc,cAA4B,QAAiD;CACzG,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO,KAAA;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,OAAO,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAErG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU;AAGlE,KAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,0BAA0B;AACnG,MAAI,cAAe,QAAO,cAAc;;AAK1C,KAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,KAAK;AAC5E,MAAI,aAAc,QAAO,aAAa;;;;;;;;;;;;AAgB1C,SAAgB,eACd,OACA,QACS;AACT,QAAO,CAAC,CAAC,OAAO,cAAc,eAAe,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EAAE,gBAAgB,IAAI,OACxG;;;;;;;;;;;;;AAcL,SAAgB,kBACd,OACA,QACS;AACT,QAAO,CAAC,MAAM,yBAAyB,OAAO;;;;;;;;;;;;;;;;;AAsBhD,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EACrG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAGF,QAAO,OADqC,GADnB,yBAAyB,OAAO,QACS,iBAAiB,CAC/D;GAEtB;;;;;;;;;;;;;;;;;AAkBJ,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,QAAQ,EACjG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAEF,QAAO,OADqC,EAAE,sBAAsB,iBAAiB,CACjE;GAEtB;;;;;;;;;;;;;;AAeJ,SAAgB,gBACd,EAAE,SACF,SAA8B,EAAE,MAAM,QAAQ,EAClC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,sBAAsB,cAAc,aAAa,cAAe,OAAO;AAE7E,MAAI,oBAEF,QAAO,OADqC,EAAE,qBAAqB,CAC/C;GAEtB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { computed, update } from "../signals/primitives.js";
|
|
3
|
+
//#region src/core/features/sync-preload-attribute.ts
|
|
4
|
+
/**
|
|
5
|
+
* Syncs preload attribute from mediaElement to state.
|
|
6
|
+
*
|
|
7
|
+
* Watches the owners signal for mediaElement changes and copies the
|
|
8
|
+
* preload attribute to state when no explicit value has been set.
|
|
9
|
+
* An explicit value (set via SpfMedia.preload) always wins.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const cleanup = syncPreloadAttribute({ state, owners });
|
|
13
|
+
*/
|
|
14
|
+
function syncPreloadAttribute({ state, owners }) {
|
|
15
|
+
const mediaElement = computed(() => owners.get().mediaElement);
|
|
16
|
+
return effect(() => {
|
|
17
|
+
if (state.get().preload !== void 0) return;
|
|
18
|
+
const preload = mediaElement.get()?.preload || void 0;
|
|
19
|
+
if (preload === void 0) return;
|
|
20
|
+
update(state, { preload });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { syncPreloadAttribute };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=sync-preload-attribute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-preload-attribute.js","names":[],"sources":["../../../../src/core/features/sync-preload-attribute.ts"],"sourcesContent":["import { effect } from '../signals/effect';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { MediaElementLike } from '../types';\nimport type { PresentationState } from './resolve-presentation';\n\n/**\n * Owners shape for preload attribute sync.\n */\nexport interface PlatformOwners {\n mediaElement?: MediaElementLike | undefined;\n}\n\n/**\n * Syncs preload attribute from mediaElement to state.\n *\n * Watches the owners signal for mediaElement changes and copies the\n * preload attribute to state when no explicit value has been set.\n * An explicit value (set via SpfMedia.preload) always wins.\n *\n * @example\n * const cleanup = syncPreloadAttribute({ state, owners });\n */\nexport function syncPreloadAttribute<S extends PresentationState, O extends PlatformOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const mediaElement = computed(() => owners.get().mediaElement);\n return effect(() => {\n if (state.get().preload !== undefined) return;\n const preload = mediaElement.get()?.preload || undefined;\n if (preload === undefined) return;\n const patch: Partial<PresentationState> = { preload: preload as 'auto' | 'metadata' | 'none' };\n update(state, patch);\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,qBAA4E,EAC1F,OACA,UAIa;CACb,MAAM,eAAe,eAAe,OAAO,KAAK,CAAC,aAAa;AAC9D,QAAO,aAAa;AAClB,MAAI,MAAM,KAAK,CAAC,YAAY,KAAA,EAAW;EACvC,MAAM,UAAU,aAAa,KAAK,EAAE,WAAW,KAAA;AAC/C,MAAI,YAAY,KAAA,EAAW;AAE3B,SAAO,OADmC,EAAW,SAAyC,CAC1E;GACpB"}
|