@videojs/spf 10.0.0-beta.11 → 10.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/default/core/abr/bandwidth-estimator.js +2 -3
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -1
- package/dist/default/core/abr/ewma.js +1 -1
- package/dist/default/core/abr/quality-selection.js +2 -2
- package/dist/default/core/abr/quality-selection.js.map +1 -1
- package/dist/default/core/buffer/back-buffer.js +2 -2
- package/dist/default/core/buffer/forward-buffer.js +1 -1
- package/dist/default/core/features/calculate-presentation-duration.js +11 -9
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -1
- package/dist/default/core/features/quality-switching.js +7 -6
- package/dist/default/core/features/quality-switching.js.map +1 -1
- package/dist/default/core/features/resolve-presentation.js +30 -52
- package/dist/default/core/features/resolve-presentation.js.map +1 -1
- package/dist/default/core/features/resolve-track.js +17 -28
- package/dist/default/core/features/resolve-track.js.map +1 -1
- package/dist/default/core/features/select-tracks.js +18 -35
- package/dist/default/core/features/select-tracks.js.map +1 -1
- 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 +1 -1
- package/dist/default/core/hls/parse-attributes.js.map +1 -1
- package/dist/default/core/hls/parse-media-playlist.js +1 -2
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/core/hls/parse-multivariant.js +2 -3
- package/dist/default/core/hls/parse-multivariant.js.map +1 -1
- package/dist/default/core/hls/resolve-url.js +1 -1
- 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 +3 -3
- package/dist/default/core/task.js.map +1 -1
- package/dist/default/core/types/index.js +1 -1
- package/dist/default/core/types/index.js.map +1 -1
- package/dist/default/core/utils/generate-id.js +1 -1
- package/dist/default/core/utils/track-selection.js +10 -2
- package/dist/default/core/utils/track-selection.js.map +1 -1
- package/dist/default/dom/features/end-of-stream.js +24 -48
- package/dist/default/dom/features/end-of-stream.js.map +1 -1
- package/dist/default/dom/features/load-segments.js +40 -36
- package/dist/default/dom/features/load-segments.js.map +1 -1
- package/dist/default/dom/features/load-text-track-cues.js +36 -26
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -1
- package/dist/default/dom/features/segment-loader-actor.js +4 -5
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -1
- package/dist/default/dom/features/setup-mediasource.js +29 -36
- package/dist/default/dom/features/setup-mediasource.js.map +1 -1
- package/dist/default/dom/features/setup-sourcebuffer.js +43 -35
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -1
- package/dist/default/dom/features/setup-text-tracks.js +25 -46
- package/dist/default/dom/features/setup-text-tracks.js.map +1 -1
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js +14 -23
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
- package/dist/default/dom/features/sync-text-track-modes.js +12 -17
- package/dist/default/dom/features/sync-text-track-modes.js.map +1 -1
- package/dist/default/dom/features/track-current-time.js +10 -7
- package/dist/default/dom/features/track-current-time.js.map +1 -1
- package/dist/default/dom/features/track-playback-initiated.js +34 -36
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -1
- package/dist/default/dom/features/track-playback-rate.js +12 -16
- package/dist/default/dom/features/track-playback-rate.js.map +1 -1
- package/dist/default/dom/features/update-duration.js +19 -13
- package/dist/default/dom/features/update-duration.js.map +1 -1
- package/dist/default/dom/media/append-segment.js +1 -1
- package/dist/default/dom/media/buffer-flusher.js +1 -1
- package/dist/default/dom/media/mediasource-setup.js +32 -40
- package/dist/default/dom/media/mediasource-setup.js.map +1 -1
- package/dist/default/dom/media/source-buffer-actor.js +16 -26
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -1
- package/dist/default/dom/network/chunked-stream-iterable.js +1 -1
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -1
- package/dist/default/dom/network/fetch.js +1 -2
- package/dist/default/dom/network/fetch.js.map +1 -1
- package/dist/default/dom/playback-engine/adapter.js +13 -13
- package/dist/default/dom/playback-engine/adapter.js.map +1 -1
- package/dist/default/dom/playback-engine/engine.js +24 -49
- package/dist/default/dom/playback-engine/engine.js.map +1 -1
- package/dist/default/dom/playback-engine.js +2 -2
- package/dist/default/dom/text/parse-vtt-segment.js +1 -1
- package/dist/default/dom.js +1 -2
- package/dist/default/index.js +1 -1
- package/dist/dev/core/abr/bandwidth-estimator.js +2 -3
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -1
- package/dist/dev/core/abr/ewma.js +1 -1
- package/dist/dev/core/abr/quality-selection.js +2 -2
- package/dist/dev/core/abr/quality-selection.js.map +1 -1
- package/dist/dev/core/actor.d.ts +6 -19
- package/dist/dev/core/actor.d.ts.map +1 -1
- package/dist/dev/core/buffer/back-buffer.js +2 -2
- package/dist/dev/core/buffer/forward-buffer.js +1 -1
- package/dist/dev/core/features/calculate-presentation-duration.js +11 -9
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/core/features/quality-switching.js +7 -6
- package/dist/dev/core/features/quality-switching.js.map +1 -1
- package/dist/dev/core/features/resolve-presentation.js +30 -52
- package/dist/dev/core/features/resolve-presentation.js.map +1 -1
- package/dist/dev/core/features/resolve-track.js +17 -28
- package/dist/dev/core/features/resolve-track.js.map +1 -1
- package/dist/dev/core/features/select-tracks.js +18 -35
- package/dist/dev/core/features/select-tracks.js.map +1 -1
- 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 +1 -1
- package/dist/dev/core/hls/parse-attributes.js.map +1 -1
- package/dist/dev/core/hls/parse-media-playlist.js +1 -2
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/core/hls/parse-multivariant.js +2 -3
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/core/hls/resolve-url.js +1 -1
- 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 +3 -3
- package/dist/dev/core/task.js.map +1 -1
- package/dist/dev/core/types/index.js +1 -1
- package/dist/dev/core/types/index.js.map +1 -1
- package/dist/dev/core/utils/generate-id.js +1 -1
- package/dist/dev/core/utils/track-selection.js +10 -2
- package/dist/dev/core/utils/track-selection.js.map +1 -1
- package/dist/dev/dom/features/end-of-stream.js +24 -48
- package/dist/dev/dom/features/end-of-stream.js.map +1 -1
- package/dist/dev/dom/features/load-segments.d.ts +4 -4
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -1
- package/dist/dev/dom/features/load-segments.js +40 -36
- package/dist/dev/dom/features/load-segments.js.map +1 -1
- package/dist/dev/dom/features/load-text-track-cues.d.ts +4 -4
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -1
- package/dist/dev/dom/features/load-text-track-cues.js +36 -26
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -1
- package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +4 -5
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -1
- package/dist/dev/dom/features/setup-mediasource.js +29 -36
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -1
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -1
- package/dist/dev/dom/features/setup-sourcebuffer.js +43 -35
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -1
- package/dist/dev/dom/features/setup-text-tracks.js +25 -46
- package/dist/dev/dom/features/setup-text-tracks.js.map +1 -1
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +14 -23
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
- package/dist/dev/dom/features/sync-text-track-modes.js +12 -17
- package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -1
- package/dist/dev/dom/features/track-current-time.d.ts +4 -4
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -1
- package/dist/dev/dom/features/track-current-time.js +10 -7
- package/dist/dev/dom/features/track-current-time.js.map +1 -1
- package/dist/dev/dom/features/track-playback-initiated.d.ts +14 -23
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -1
- package/dist/dev/dom/features/track-playback-initiated.js +34 -36
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -1
- package/dist/dev/dom/features/track-playback-rate.d.ts +4 -4
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -1
- package/dist/dev/dom/features/track-playback-rate.js +12 -16
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -1
- package/dist/dev/dom/features/update-duration.js +19 -13
- package/dist/dev/dom/features/update-duration.js.map +1 -1
- package/dist/dev/dom/media/append-segment.js +1 -1
- package/dist/dev/dom/media/buffer-flusher.js +1 -1
- package/dist/dev/dom/media/mediasource-setup.js +32 -40
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -1
- package/dist/dev/dom/media/source-buffer-actor.d.ts +2 -2
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -1
- package/dist/dev/dom/media/source-buffer-actor.js +16 -26
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -1
- package/dist/dev/dom/network/chunked-stream-iterable.js +1 -1
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -1
- package/dist/dev/dom/network/fetch.js +1 -2
- package/dist/dev/dom/network/fetch.js.map +1 -1
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -1
- package/dist/dev/dom/playback-engine/adapter.js +13 -13
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -1
- package/dist/dev/dom/playback-engine/engine.d.ts +14 -26
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -1
- package/dist/dev/dom/playback-engine/engine.js +24 -49
- package/dist/dev/dom/playback-engine/engine.js.map +1 -1
- package/dist/dev/dom/playback-engine.d.ts +3 -2
- package/dist/dev/dom/playback-engine.js +2 -2
- package/dist/dev/dom/text/parse-vtt-segment.js +1 -1
- package/dist/dev/dom.js +1 -2
- package/dist/dev/index.js +1 -1
- package/package.json +5 -4
- package/dist/default/all.js +0 -16
- package/dist/default/core/events/create-event-stream.js +0 -48
- package/dist/default/core/events/create-event-stream.js.map +0 -1
- package/dist/default/core/reactive/combine-latest.js +0 -72
- package/dist/default/core/reactive/combine-latest.js.map +0 -1
- package/dist/default/core/state/create-state.js +0 -143
- package/dist/default/core/state/create-state.js.map +0 -1
- package/dist/dev/all.js +0 -16
- package/dist/dev/core/events/create-event-stream.d.ts +0 -48
- package/dist/dev/core/events/create-event-stream.d.ts.map +0 -1
- package/dist/dev/core/events/create-event-stream.js +0 -48
- package/dist/dev/core/events/create-event-stream.js.map +0 -1
- package/dist/dev/core/features/resolve-presentation.d.ts +0 -16
- package/dist/dev/core/features/resolve-presentation.d.ts.map +0 -1
- package/dist/dev/core/features/resolve-track.d.ts +0 -13
- package/dist/dev/core/features/resolve-track.d.ts.map +0 -1
- package/dist/dev/core/features/select-tracks.d.ts +0 -11
- package/dist/dev/core/features/select-tracks.d.ts.map +0 -1
- package/dist/dev/core/reactive/combine-latest.js +0 -72
- package/dist/dev/core/reactive/combine-latest.js.map +0 -1
- package/dist/dev/core/state/create-state.d.ts +0 -104
- package/dist/dev/core/state/create-state.d.ts.map +0 -1
- package/dist/dev/core/state/create-state.js +0 -143
- package/dist/dev/core/state/create-state.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { update } from "../signals/primitives.js";
|
|
1
3
|
import { SelectedTrackIdKeyByType } from "../utils/track-selection.js";
|
|
2
|
-
|
|
3
4
|
//#region src/core/features/select-tracks.ts
|
|
4
5
|
/**
|
|
5
6
|
* Pick text track to activate.
|
|
@@ -73,19 +74,11 @@ function shouldSelectTrack(state, config) {
|
|
|
73
74
|
* );
|
|
74
75
|
*/
|
|
75
76
|
function selectVideoTrack({ state }, config = { type: "video" }) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
|
|
82
|
-
if (selectedTrackId) {
|
|
83
|
-
const selectedTrackKey = SelectedTrackIdKeyByType[config.type];
|
|
84
|
-
state.patch({ [selectedTrackKey]: selectedTrackId });
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
selecting = false;
|
|
88
|
-
}
|
|
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 });
|
|
89
82
|
});
|
|
90
83
|
}
|
|
91
84
|
/**
|
|
@@ -104,16 +97,11 @@ function selectVideoTrack({ state }, config = { type: "video" }) {
|
|
|
104
97
|
* );
|
|
105
98
|
*/
|
|
106
99
|
function selectAudioTrack({ state }, config = { type: "audio" }) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
|
|
113
|
-
if (selectedTrackId) state.patch({ selectedAudioTrackId: selectedTrackId });
|
|
114
|
-
} finally {
|
|
115
|
-
selecting = false;
|
|
116
|
-
}
|
|
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 });
|
|
117
105
|
});
|
|
118
106
|
}
|
|
119
107
|
/**
|
|
@@ -129,19 +117,14 @@ function selectAudioTrack({ state }, config = { type: "audio" }) {
|
|
|
129
117
|
* const cleanup = selectTextTrack({ state, owners, events }, {});
|
|
130
118
|
*/
|
|
131
119
|
function selectTextTrack({ state }, config = { type: "text" }) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
|
|
138
|
-
if (selectedTextTrackId) state.patch({ selectedTextTrackId });
|
|
139
|
-
} finally {
|
|
140
|
-
selecting = false;
|
|
141
|
-
}
|
|
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 });
|
|
142
125
|
});
|
|
143
126
|
}
|
|
144
|
-
|
|
145
127
|
//#endregion
|
|
146
128
|
export { selectAudioTrack, selectTextTrack, selectVideoTrack };
|
|
129
|
+
|
|
147
130
|
//# sourceMappingURL=select-tracks.js.map
|
|
@@ -1 +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 type { EventStream } from '../events/create-event-stream';\nimport type { WritableState } from '../state/create-state';\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: VideoSelectionConfig = { type: 'video' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\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 state.patch({ [selectedTrackKey]: selectedTrackId });\n }\n } finally {\n selecting = false;\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: AudioSelectionConfig = { type: 'audio' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\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 state.patch({ selectedAudioTrackId: selectedTrackId });\n }\n } finally {\n selecting = false;\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: TextSelectionConfig = { type: 'text' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\n\n // Text tracks are user opt-in - don't auto-select\n const selectedTextTrackId = pickTextTrack(currentState.presentation!, config);\n\n if (selectedTextTrackId) {\n state.patch({ selectedTextTrackId });\n }\n } finally {\n selecting = false;\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;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,OAAO,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAErG,KAAI,gBAAgB,WAAW,EAAG,QAAO;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,EACE,SAMF,SAA+B,EAAE,MAAM,SAAS,EACpC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EACrG,cAAc,IAAI,OAAO,IAAI;AAEjC,OAAI,iBAAiB;IACnB,MAAM,mBAAmB,yBAAyB,OAAO;AACzD,UAAM,MAAM,GAAG,mBAAmB,iBAAiB,CAAC;;YAE9C;AACR,eAAY;;GAEd;;;;;;;;;;;;;;;;;AAkBJ,SAAgB,iBACd,EACE,SAMF,SAA+B,EAAE,MAAM,SAAS,EACpC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,QAAQ,EACjG,cAAc,IAAI,OAAO,IAAI;AAEjC,OAAI,gBACF,OAAM,MAAM,EAAE,sBAAsB,iBAAiB,CAAC;YAEhD;AACR,eAAY;;GAEd;;;;;;;;;;;;;;AAeJ,SAAgB,gBACd,EACE,SAMF,SAA8B,EAAE,MAAM,QAAQ,EAClC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,sBAAsB,cAAc,aAAa,cAAe,OAAO;AAE7E,OAAI,oBACF,OAAM,MAAM,EAAE,qBAAqB,CAAC;YAE9B;AACR,eAAY;;GAEd"}
|
|
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"}
|
|
@@ -128,7 +128,7 @@ function matchTag(line, tag) {
|
|
|
128
128
|
if (!line.startsWith(prefix)) return null;
|
|
129
129
|
return createAttributeList(line.slice(prefix.length));
|
|
130
130
|
}
|
|
131
|
-
|
|
132
131
|
//#endregion
|
|
133
132
|
export { matchTag, parseByteRange, parseCodecs, parseExtInfDuration };
|
|
133
|
+
|
|
134
134
|
//# sourceMappingURL=parse-attributes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/core/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (codec.startsWith('mp4a.')) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":";;;;;AAMA,SAAgB,mBAAmB,MAAmC;CACpE,MAAM,6BAAa,IAAI,KAAqB;AAG5C,MAAK,MAAM,SAAS,KAAK,SAFX,sCAE0B,EAAE;EACxC,MAAM,MAAM,MAAM;EAClB,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM;AACtC,MAAI,IACF,YAAW,IAAI,KAAK,MAAM;;AAI9B,QAAO;;;;;AAMT,SAAgB,gBAAgB,OAAyD;CACvF,MAAM,QAAQ,gBAAgB,KAAK,MAAM;AACzC,KAAI,CAAC,MAAO,QAAO;AAKnB,QAAO;EAAE,OAHK,OAAO,SAAS,MAAM,IAAK,GAAG;EAG5B,QAFD,OAAO,SAAS,MAAM,IAAK,GAAG;EAErB;;;;;AAM1B,SAAgB,eAAe,OAAsC;CACnE,MAAM,MAAM,OAAO,WAAW,MAAM;AACpC,KAAI,OAAO,MAAM,IAAI,IAAI,OAAO,EAAG,QAAO;AAG1C,KAAI,KAAK,IAAI,MAAM,OAAO,GAAG,IAC3B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAElE,KAAI,KAAK,IAAI,MAAM,MAAM,GAAG,IAC1B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAElE,KAAI,KAAK,IAAI,MAAM,MAAM,GAAG,IAC1B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAIlE,KAAI,MAAM,MAAM,EACd,QAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,EAAE;AAIhD,QAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,EAAE;;;;;AAMhD,SAAgB,YAAY,QAAoD;CAC9E,MAAM,QAAQ,OAAO,MAAM,IAAI,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;CACpD,MAAM,SAA6C,EAAE;AAErD,MAAK,MAAM,SAAS,MAClB,KAAI,MAAM,WAAW,QAAQ,IAAI,MAAM,WAAW,QAAQ,IAAI,MAAM,WAAW,QAAQ,CACrF,QAAO,QAAQ;UACN,MAAM,WAAW,QAAQ,CAClC,QAAO,QAAQ;AAInB,QAAO;;;;;AAMT,SAAgB,oBAAoB,OAAuB;CACzD,MAAM,eAAe,MAAM,MAAM,IAAI,CAAC,MAAM;CAC5C,MAAM,WAAW,OAAO,WAAW,aAAa;AAChD,QAAO,OAAO,MAAM,SAAS,GAAG,IAAI;;;;;;;AAQtC,SAAgB,eAAe,OAAe,aAA6D;CACzG,MAAM,QAAQ,qBAAqB,KAAK,MAAM;AAC9C,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,SAAS,OAAO,SAAS,MAAM,IAAK,GAAG;AAC7C,KAAI,OAAO,MAAM,OAAO,CAAE,QAAO;CAEjC,IAAI;AACJ,KAAI,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/core/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (codec.startsWith('mp4a.')) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
2
|
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
-
|
|
4
3
|
//#region src/core/hls/parse-media-playlist.ts
|
|
5
4
|
/**
|
|
6
5
|
* Parse HLS media playlist and resolve track with segments.
|
|
@@ -77,7 +76,7 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
77
76
|
initialization
|
|
78
77
|
};
|
|
79
78
|
}
|
|
80
|
-
|
|
81
79
|
//#endregion
|
|
82
80
|
export { parseMediaPlaylist };
|
|
81
|
+
|
|
83
82
|
//# sourceMappingURL=parse-media-playlist.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/core/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/core/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":";;;;;;;;;;;;;AAkCA,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAGjC,MAAM,UAAU,WAAW;CAG3B,MAAM,WAAsB,EAAE;CAC9B,IAAI;CACJ,IAAI;CAEJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI;AAEJ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAE3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAGF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,yBAAyB,IAC5C,QAAQ,WAAW,wBAAwB,IAC3C,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,WAAW,SAAS,SAAS,YAAY;AAC/C,MAAI,UAAU;GACZ,MAAM,MAAM,SAAS,IAAI,MAAM;AAC/B,OAAI,KAAK;AACP,qBAAiB,WAAW,KAAK,QAAQ;IACzC,MAAM,eAAe,SAAS,IAAI,YAAY;AAC9C,QAAI,aACF,wBAAuB,eAAe,cAAc,EAAE,IAAI,KAAA;;AAG9D;;AAIF,MAAI,QAAQ,WAAW,WAAW,EAAE;AAClC,qBAAkB,oBAAoB,QAAQ,MAAM,EAAE,CAAC;AACvD;;AAIF,MAAI,QAAQ,WAAW,oBAAoB,EAAE;AAC3C,sBAAmB,eAAe,QAAQ,MAAM,GAAG,EAAE,qBAAqB,IAAI,KAAA;AAC9E;;AAGF,MAAI,YAAY,iBACd;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,kBAAkB,GAAG;GACnD,MAAM,UAAmB;IACvB,IAAI,WAAW;IACf,KAAK,WAAW,SAAS,QAAQ;IACjC,UAAU;IACV,WAAW;IACZ;AAED,OAAI,kBAAkB;AACpB,YAAQ,YAAY;AACpB,2BAAuB,iBAAiB,MAAM;SAE9C,wBAAuB,KAAA;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB,KAAA;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,KAAA,IACA,iBACE;EAAE,KAAK;EAAgB,GAAI,uBAAuB,EAAE,WAAW,sBAAsB,GAAG,EAAE;EAAG,GAC7F,EAAE,KAAK,IAAI;AAInB,QAAO;EACL,GAAG;EACH,WAAW;EACX,UAAU;EACV;EACA;EACD"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { generateId } from "../utils/generate-id.js";
|
|
1
2
|
import { matchTag, parseCodecs } from "./parse-attributes.js";
|
|
2
3
|
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
-
import { generateId } from "../utils/generate-id.js";
|
|
4
|
-
|
|
5
4
|
//#region src/core/hls/parse-multivariant.ts
|
|
6
5
|
/**
|
|
7
6
|
* Parse HLS multivariant playlist into a Presentation.
|
|
@@ -209,7 +208,7 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
209
208
|
selectionSets
|
|
210
209
|
};
|
|
211
210
|
}
|
|
212
|
-
|
|
213
211
|
//#endregion
|
|
214
212
|
export { parseMultivariantPlaylist };
|
|
213
|
+
|
|
215
214
|
//# sourceMappingURL=parse-multivariant.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/core/hls/parse-multivariant.ts"],"sourcesContent":["import type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { generateId } from '../utils/generate-id';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams\n const videoTracks: PartiallyResolvedVideoTrack[] = videoStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupId = stream.audioGroupId;\n }\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: 2, // Stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA8BA,SAAgB,0BAA0B,MAAc,YAA6C;CACnG,MAAM,UAAU,WAAW;CAC3B,MAAM,QAAQ,KAAK,MAAM,QAAQ;CA+BjC,MAAM,UAAwB,EAAE;CAChC,MAAM,kBAAwC,EAAE;CAChD,MAAM,qBAA8C,EAAE;CAGtD,IAAI,oBAAoD;AAExD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAG3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAIF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,aAAa,SAAS,SAAS,cAAc;AACnD,MAAI,YAAY;GACd,MAAM,OAAO,WAAW,IAAI,OAAO;GACnC,MAAM,UAAU,WAAW,IAAI,WAAW;GAC1C,MAAM,OAAO,WAAW,IAAI,OAAO;AAEnC,OAAI,SAAS,WAAW,WAAW,MAAM;IACvC,MAAM,MAAM,WAAW,IAAI,MAAM;AACjC,oBAAgB,KAAK;KACnB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,MAAM,WAAW,KAAK,QAAQ,GAAG;KACtC,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAC7C,CAAC;;AAGJ,OAAI,SAAS,eAAe,WAAW,MAAM;IAC3C,MAAM,MAAM,WAAW,IAAI,MAAM;AAEjC,QAAI,IACF,oBAAmB,KAAK;KACtB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,WAAW,KAAK,QAAQ;KAC7B,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAC5C,QAAQ,WAAW,QAAQ,SAAS;KACrC,CAAC;;AAGN;;EAIF,MAAM,iBAAiB,SAAS,SAAS,mBAAmB;AAC5D,MAAI,gBAAgB;AAClB,uBAAoB;IAClB,WAAW,eAAe,OAAO,aAAa,EAAE;IAChD,YAAY,eAAe,cAAc,aAAa;IACtD,QAAQ,eAAe,IAAI,SAAS;IACpC,WAAW,eAAe,aAAa,aAAa;IACpD,cAAc,eAAe,IAAI,QAAQ;IAC1C;AACD;;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,mBAAmB;AACjD,WAAQ,KAAK;IACX,GAAG;IACH,KAAK,WAAW,SAAS,QAAQ;IAClC,CAAC;AACF,uBAAoB;;;CAMxB,MAAM,eAA+B,EAAE;CACvC,MAAM,mBAAmC,EAAE;AAE3C,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,CAAC,OAAO,QAAQ;AAElB,gBAAa,KAAK,OAAO;AACzB;;EAGF,MAAM,eAAe,YAAY,OAAO,OAAO;AAI/C,MAHmB,OAAO,OAAO,MAAM,IAAI,CAAC,WAGzB,EACjB,KAAI,aAAa,SAAS,CAAC,aAAa,MAEtC,kBAAiB,KAAK,OAAO;MAG7B,cAAa,KAAK,OAAO;MAI3B,cAAa,KAAK,OAAO;;CAK7B,MAAM,cAA6C,aAAa,KAAK,WAAW;EAC9E,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG;EAE5D,MAAM,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAElB,UAAU;GACV,QAAQ,EAAE;GACX;AAED,MAAI,OAAO,YAAY,UAAU,OAC/B,OAAM,QAAQ,OAAO,WAAW;AAElC,MAAI,OAAO,YAAY,WAAW,OAChC,OAAM,SAAS,OAAO,WAAW;AAEnC,MAAI,QAAQ,MACV,OAAM,SAAS,CAAC,OAAO,MAAM;AAE/B,MAAI,OAAO,UACT,OAAM,YAAY,OAAO;AAE3B,MAAI,OAAO,aACT,OAAM,eAAe,OAAO;AAG9B,SAAO;GACP;CAGF,MAAM,kBAAiD,iBAAiB,KAAK,WAAW;EACtF,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG;AAe5D,SAb2C;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAClB,UAAU;GACV,QAAQ,QAAQ,QAAQ,CAAC,OAAO,MAAM,GAAG,EAAE;GAC3C,SAAS,OAAO,gBAAgB;GAChC,MAAM;GACN,YAAY;GACZ,UAAU;GACX;GAGD;CA+CF,MAAM,cAAc,CAAC,GA3CuC,gBAAgB,KAAK,cAAc;EAC7F,IAAI;AACJ,OAAK,MAAM,UAAU,QACnB,KAAI,OAAO,iBAAiB,UAAU,WAAW,OAAO,QAAQ;GAC9D,MAAM,SAAS,YAAY,OAAO,OAAO;AACzC,OAAI,OAAO,OAAO;AAChB,kBAAc,CAAC,OAAO,MAAM;AAC5B;;;EAKN,MAAM,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU,OAAO;GACtB,SAAS,UAAU;GACnB,MAAM,UAAU;GAEhB,UAAU;GACV,WAAW;GACX,YAAY;GACZ,UAAU;GACV,QAAQ,EAAE;GACX;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAE7B,MAAI,YACF,OAAM,SAAS;AAEjB,MAAI,UAAU,QACZ,OAAM,UAAU,UAAU;AAE5B,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAG/B,SAAO;GACP,EAG4C,GAAG,gBAAgB;CAGjE,MAAM,aAA2C,mBAAmB,KAAK,cAAc;EACrF,MAAM,QAAoC;GACxC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU;GACf,SAAS,UAAU;GACnB,OAAO,UAAU;GACjB,MAAM;GAEN,UAAU;GACV,WAAW;GACZ;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAG7B,MAAI,UAAU,WAAW,UAAU,WACjC,OAAM,UAAU;AAElB,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAE/B,MAAI,UAAU,OACZ,OAAM,SAAS,UAAU;AAG3B,SAAO;GACP;CAGF,MAAM,gBAAgC,EAAE;AAExC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,WAAW,SAAS,GAAG;EACzB,MAAM,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,iBAAiB;GAClC;AAED,gBAAc,KAAK,iBAAiB;;AAItC,QAAO;EACL,IAAI,YAAY;EAChB,KAAK,WAAW;EAChB,WAAW;EAEX;EACD"}
|
|
1
|
+
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/core/hls/parse-multivariant.ts"],"sourcesContent":["import type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { generateId } from '../utils/generate-id';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams\n const videoTracks: PartiallyResolvedVideoTrack[] = videoStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupId = stream.audioGroupId;\n }\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: 2, // Stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Signal } from "signal-polyfill";
|
|
2
|
+
//#region src/core/signals/effect.ts
|
|
3
|
+
const pending = /* @__PURE__ */ new Set();
|
|
4
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
5
|
+
queueMicrotask(runPending);
|
|
6
|
+
});
|
|
7
|
+
function runPending() {
|
|
8
|
+
for (const c of watcher.getPending()) pending.add(c);
|
|
9
|
+
watcher.watch();
|
|
10
|
+
for (const c of pending) {
|
|
11
|
+
pending.delete(c);
|
|
12
|
+
c.get();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run a side effect whenever its signal dependencies change.
|
|
17
|
+
*
|
|
18
|
+
* Executes immediately (synchronous initial run), then re-runs on the next
|
|
19
|
+
* microtask after any dependency changes. If the callback returns a function,
|
|
20
|
+
* it is called before each re-run and when the effect is stopped — the same
|
|
21
|
+
* cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
|
|
22
|
+
*
|
|
23
|
+
* Returns a cleanup function that stops the effect.
|
|
24
|
+
*/
|
|
25
|
+
function effect(fn) {
|
|
26
|
+
let cleanup;
|
|
27
|
+
const c = new Signal.Computed(() => {
|
|
28
|
+
if (typeof cleanup === "function") cleanup();
|
|
29
|
+
cleanup = fn();
|
|
30
|
+
});
|
|
31
|
+
watcher.watch(c);
|
|
32
|
+
c.get();
|
|
33
|
+
return () => {
|
|
34
|
+
watcher.unwatch(c);
|
|
35
|
+
if (typeof cleanup === "function") cleanup();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { effect };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=effect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect.js","names":[],"sources":["../../../../src/core/signals/effect.ts"],"sourcesContent":["import { Signal } from 'signal-polyfill';\n\n// Computeds waiting to re-run after their dependencies changed.\nconst pending = new Set<Signal.Computed<void>>();\n\nconst watcher = new Signal.subtle.Watcher(() => {\n queueMicrotask(runPending);\n});\n\nfunction runPending() {\n for (const c of watcher.getPending()) {\n pending.add(c as Signal.Computed<void>);\n }\n watcher.watch(); // re-arm before running effects, in case they write signals\n for (const c of pending) {\n pending.delete(c);\n c.get(); // re-run the effect body\n }\n}\n\n/**\n * Run a side effect whenever its signal dependencies change.\n *\n * Executes immediately (synchronous initial run), then re-runs on the next\n * microtask after any dependency changes. If the callback returns a function,\n * it is called before each re-run and when the effect is stopped — the same\n * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.\n *\n * Returns a cleanup function that stops the effect.\n */\nexport function effect(fn: () => (() => void) | void): () => void {\n let cleanup: (() => void) | void;\n const c = new Signal.Computed(() => {\n if (typeof cleanup === 'function') cleanup();\n cleanup = fn();\n });\n watcher.watch(c);\n c.get(); // initial run\n return () => {\n watcher.unwatch(c);\n if (typeof cleanup === 'function') cleanup();\n };\n}\n"],"mappings":";;AAGA,MAAM,0BAAU,IAAI,KAA4B;AAEhD,MAAM,UAAU,IAAI,OAAO,OAAO,cAAc;AAC9C,gBAAe,WAAW;EAC1B;AAEF,SAAS,aAAa;AACpB,MAAK,MAAM,KAAK,QAAQ,YAAY,CAClC,SAAQ,IAAI,EAA2B;AAEzC,SAAQ,OAAO;AACf,MAAK,MAAM,KAAK,SAAS;AACvB,UAAQ,OAAO,EAAE;AACjB,IAAE,KAAK;;;;;;;;;;;;;AAcX,SAAgB,OAAO,IAA2C;CAChE,IAAI;CACJ,MAAM,IAAI,IAAI,OAAO,eAAe;AAClC,MAAI,OAAO,YAAY,WAAY,UAAS;AAC5C,YAAU,IAAI;GACd;AACF,SAAQ,MAAM,EAAE;AAChB,GAAE,KAAK;AACP,cAAa;AACX,UAAQ,QAAQ,EAAE;AAClB,MAAI,OAAO,YAAY,WAAY,UAAS"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Signal } from "signal-polyfill";
|
|
2
|
+
//#region src/core/signals/primitives.ts
|
|
3
|
+
/** Read a signal value without tracking it as a dependency. */
|
|
4
|
+
const untrack = Signal.subtle.untrack;
|
|
5
|
+
/** Create a writable reactive value. */
|
|
6
|
+
function signal(initialValue, options) {
|
|
7
|
+
return new Signal.State(initialValue, options);
|
|
8
|
+
}
|
|
9
|
+
/** Create a computed reactive value. */
|
|
10
|
+
function computed(fn, options) {
|
|
11
|
+
return new Signal.Computed(fn, options);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Update a writable signal. Accepts either a partial object to merge into the
|
|
15
|
+
* current state, or an updater function that receives the current state and
|
|
16
|
+
* returns the next state.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* update(state, { playbackRate: 2 });
|
|
20
|
+
* update(state, (s) => ({ ...s, count: s.count + 1 }));
|
|
21
|
+
*/
|
|
22
|
+
function update(signal, updater) {
|
|
23
|
+
const current = untrack(() => signal.get());
|
|
24
|
+
signal.set(typeof updater === "function" ? updater(current) : {
|
|
25
|
+
...current,
|
|
26
|
+
...updater
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { computed, signal, update };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=primitives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.js","names":["SignalNS"],"sources":["../../../../src/core/signals/primitives.ts"],"sourcesContent":["import { Signal as SignalNS } from 'signal-polyfill';\n\n/** Read a signal value without tracking it as a dependency. */\nexport const untrack: <T>(fn: () => T) => T = SignalNS.subtle.untrack;\n\n/** A writable reactive value (read + write). */\nexport type Signal<T> = SignalNS.State<T>;\n\n/** A derived reactive value that re-evaluates when its dependencies change (read-only). */\nexport type Computed<T> = SignalNS.Computed<T>;\n\n/** A read-only view of a reactive value. */\nexport type ReadonlySignal<T> = Omit<SignalNS.State<T>, 'set'>;\n\nexport interface SignalOptions<T> {\n equals?: (t: T, t2: T) => boolean;\n}\n\n/** Create a writable reactive value. */\nexport function signal<T>(initialValue: T, options?: SignalOptions<T>): Signal<T> {\n return new SignalNS.State(initialValue, options as SignalNS.Options<T>);\n}\n\n/** Create a computed reactive value. */\nexport function computed<T>(fn: () => T, options?: SignalOptions<T>): Computed<T> {\n return new SignalNS.Computed(fn, options as SignalNS.Options<T>);\n}\n\n/**\n * Update a writable signal. Accepts either a partial object to merge into the\n * current state, or an updater function that receives the current state and\n * returns the next state.\n *\n * @example\n * update(state, { playbackRate: 2 });\n * update(state, (s) => ({ ...s, count: s.count + 1 }));\n */\nexport function update<T extends object>(signal: Signal<T>, updater: Partial<T> | ((current: T) => T)): void {\n const current = untrack(() => signal.get());\n signal.set(typeof updater === 'function' ? updater(current) : { ...current, ...updater });\n}\n"],"mappings":";;;AAGA,MAAa,UAAiCA,OAAS,OAAO;;AAgB9D,SAAgB,OAAU,cAAiB,SAAuC;AAChF,QAAO,IAAIA,OAAS,MAAM,cAAc,QAA+B;;;AAIzE,SAAgB,SAAY,IAAa,SAAyC;AAChF,QAAO,IAAIA,OAAS,SAAS,IAAI,QAA+B;;;;;;;;;;;AAYlE,SAAgB,OAAyB,QAAmB,SAAiD;CAC3G,MAAM,UAAU,cAAc,OAAO,KAAK,CAAC;AAC3C,QAAO,IAAI,OAAO,YAAY,aAAa,QAAQ,QAAQ,GAAG;EAAE,GAAG;EAAS,GAAG;EAAS,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateId } from "./utils/generate-id.js";
|
|
2
|
-
|
|
2
|
+
import { anyAbortSignal } from "@videojs/utils/events";
|
|
3
3
|
//#region src/core/task.ts
|
|
4
4
|
/**
|
|
5
5
|
* Generic reusable task that wraps an async run function.
|
|
@@ -25,7 +25,7 @@ var Task = class {
|
|
|
25
25
|
this.#runFn = runFn;
|
|
26
26
|
const rawId = config?.id;
|
|
27
27
|
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
|
|
28
|
-
this.#signal = config?.signal ?
|
|
28
|
+
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
|
|
29
29
|
}
|
|
30
30
|
get status() {
|
|
31
31
|
return this.#status;
|
|
@@ -116,7 +116,7 @@ var SerialRunner = class {
|
|
|
116
116
|
this.abortAll();
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
|
-
|
|
120
119
|
//#endregion
|
|
121
120
|
export { ConcurrentRunner, SerialRunner, Task };
|
|
121
|
+
|
|
122
122
|
//# sourceMappingURL=task.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.js","names":["#runFn","#abortController","#signal","#status","#value","#error","#pending","#chain","#current"],"sources":["../../../src/core/task.ts"],"sourcesContent":["import { generateId } from './utils/generate-id';\n\n// =============================================================================\n// DeepReadonly\n// =============================================================================\n\n/** Recursively marks all properties as readonly. */\nexport type DeepReadonly<T> = T extends (infer U)[]\n ? ReadonlyArray<DeepReadonly<U>>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n// =============================================================================\n// Task\n// =============================================================================\n\nexport type TaskStatus = 'pending' | 'running' | 'done' | 'error';\n\n/**\n * Configuration for a Task.\n */\nexport interface TaskConfig {\n /**\n * Identifier for this task.\n * - string: used as-is\n * - () => string: called once at construction time\n * - undefined: a unique ID is generated via generateId()\n */\n id?: string | (() => string);\n\n /**\n * Optional external AbortSignal to compose with the task's internal one.\n * The task's work is aborted when either the internal controller (via abort())\n * or this external signal fires — whichever comes first.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal contract for a schedulable unit of async work.\n */\nexport interface TaskLike<TValue = void, TError = unknown> {\n readonly id: string;\n readonly status: TaskStatus;\n readonly value: DeepReadonly<TValue> | undefined;\n readonly error: DeepReadonly<TError> | undefined;\n run(): Promise<TValue>;\n abort(): void;\n}\n\n/**\n * Generic reusable task that wraps an async run function.\n *\n * Owns its own AbortController so it can always be aborted independently.\n * Optionally composes an external AbortSignal so that a parent's cancellation\n * propagates into the task's work without requiring the caller to track the\n * task separately.\n *\n * Ordering guarantee: `value` is written before `status` transitions to `'done'`;\n * `error` is written before `status` transitions to `'error'`. Any reader\n * observing `status === 'done'` is guaranteed `value` is already present.\n */\nexport class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {\n readonly id: string;\n readonly #runFn: (signal: AbortSignal) => Promise<TValue>;\n readonly #abortController = new AbortController();\n readonly #signal: AbortSignal;\n\n #status: TaskStatus = 'pending';\n #value: TValue | undefined = undefined;\n #error: TError | undefined = undefined;\n\n constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig) {\n this.#runFn = runFn;\n const rawId = config?.id;\n this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());\n this.#signal = config?.signal\n ?
|
|
1
|
+
{"version":3,"file":"task.js","names":["#runFn","#abortController","#signal","#status","#value","#error","#pending","#chain","#current"],"sources":["../../../src/core/task.ts"],"sourcesContent":["import { anyAbortSignal } from '@videojs/utils/events';\nimport { generateId } from './utils/generate-id';\n\n// =============================================================================\n// DeepReadonly\n// =============================================================================\n\n/** Recursively marks all properties as readonly. */\nexport type DeepReadonly<T> = T extends (infer U)[]\n ? ReadonlyArray<DeepReadonly<U>>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n// =============================================================================\n// Task\n// =============================================================================\n\nexport type TaskStatus = 'pending' | 'running' | 'done' | 'error';\n\n/**\n * Configuration for a Task.\n */\nexport interface TaskConfig {\n /**\n * Identifier for this task.\n * - string: used as-is\n * - () => string: called once at construction time\n * - undefined: a unique ID is generated via generateId()\n */\n id?: string | (() => string);\n\n /**\n * Optional external AbortSignal to compose with the task's internal one.\n * The task's work is aborted when either the internal controller (via abort())\n * or this external signal fires — whichever comes first.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal contract for a schedulable unit of async work.\n */\nexport interface TaskLike<TValue = void, TError = unknown> {\n readonly id: string;\n readonly status: TaskStatus;\n readonly value: DeepReadonly<TValue> | undefined;\n readonly error: DeepReadonly<TError> | undefined;\n run(): Promise<TValue>;\n abort(): void;\n}\n\n/**\n * Generic reusable task that wraps an async run function.\n *\n * Owns its own AbortController so it can always be aborted independently.\n * Optionally composes an external AbortSignal so that a parent's cancellation\n * propagates into the task's work without requiring the caller to track the\n * task separately.\n *\n * Ordering guarantee: `value` is written before `status` transitions to `'done'`;\n * `error` is written before `status` transitions to `'error'`. Any reader\n * observing `status === 'done'` is guaranteed `value` is already present.\n */\nexport class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {\n readonly id: string;\n readonly #runFn: (signal: AbortSignal) => Promise<TValue>;\n readonly #abortController = new AbortController();\n readonly #signal: AbortSignal;\n\n #status: TaskStatus = 'pending';\n #value: TValue | undefined = undefined;\n #error: TError | undefined = undefined;\n\n constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig) {\n this.#runFn = runFn;\n const rawId = config?.id;\n this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());\n this.#signal = config?.signal\n ? anyAbortSignal([this.#abortController.signal, config.signal])\n : this.#abortController.signal;\n }\n\n get status(): TaskStatus {\n return this.#status;\n }\n\n get value(): DeepReadonly<TValue> | undefined {\n return this.#value as DeepReadonly<TValue> | undefined;\n }\n\n get error(): DeepReadonly<TError> | undefined {\n return this.#error as DeepReadonly<TError> | undefined;\n }\n\n async run(): Promise<TValue> {\n this.#status = 'running';\n try {\n const result = await this.#runFn(this.#signal);\n this.#value = result; // value before status — ordering guarantee\n this.#status = 'done';\n return result;\n } catch (e) {\n this.#error = e as TError; // error before status — ordering guarantee\n this.#status = 'error';\n throw e;\n }\n }\n\n abort(): void {\n this.#abortController.abort();\n }\n}\n\n// =============================================================================\n// ConcurrentRunner\n// =============================================================================\n\n/**\n * Runs tasks concurrently, deduplicated by task id.\n *\n * If a task with a given id is already in flight, subsequent schedule() calls\n * for that id are silently ignored until the first completes. Tasks are stored\n * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).\n */\nexport class ConcurrentRunner {\n readonly #pending = new Map<string, TaskLike<unknown, unknown>>();\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): void {\n if (this.#pending.has(task.id)) return;\n\n this.#pending.set(task.id, task as TaskLike<unknown, unknown>);\n task\n .run()\n .catch((error) => {\n if (!(error instanceof Error && error.name === 'AbortError')) throw error;\n })\n .finally(() => {\n this.#pending.delete(task.id);\n });\n }\n\n abortAll(): void {\n for (const task of this.#pending.values()) task.abort();\n this.#pending.clear();\n }\n}\n\n// =============================================================================\n// SerialRunner\n// =============================================================================\n\n/**\n * Runs tasks one at a time in submission order.\n *\n * Each schedule() call returns a Promise that resolves or rejects with the\n * task's result when it is eventually executed. Tasks wait in queue until the\n * prior task completes.\n *\n * Serialization is achieved by chaining each task's run() onto the tail of a\n * shared promise chain — no explicit queue or drain loop needed.\n *\n * abortAll() aborts all pending (not yet started) tasks and the currently\n * in-flight task. Pending tasks still run briefly but receive an aborted\n * signal and are expected to exit early.\n */\nexport class SerialRunner {\n #chain: Promise<unknown> = Promise.resolve();\n readonly #pending = new Set<TaskLike<unknown, unknown>>();\n #current: TaskLike<unknown, unknown> | null = null;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n const t = task as TaskLike<unknown, unknown>;\n this.#pending.add(t);\n\n const result = this.#chain\n .then(() => {\n this.#pending.delete(t);\n this.#current = t;\n return task.run();\n })\n .finally(() => {\n this.#current = null;\n });\n\n // Advance the chain regardless of whether this task succeeds or fails.\n this.#chain = result.then(\n () => {},\n () => {}\n );\n\n return result as Promise<TValue>;\n }\n\n abortAll(): void {\n for (const task of this.#pending) task.abort();\n this.#pending.clear();\n this.#current?.abort();\n }\n\n destroy(): void {\n this.abortAll();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgEA,IAAa,OAAb,MAAuF;CACrF;CACA;CACA,mBAA4B,IAAI,iBAAiB;CACjD;CAEA,UAAsB;CACtB,SAA6B,KAAA;CAC7B,SAA6B,KAAA;CAE7B,YAAY,OAAiD,QAAqB;AAChF,QAAA,QAAc;EACd,MAAM,QAAQ,QAAQ;AACtB,OAAK,KAAK,OAAO,UAAU,aAAa,OAAO,GAAI,SAAS,YAAY;AACxE,QAAA,SAAe,QAAQ,SACnB,eAAe,CAAC,MAAA,gBAAsB,QAAQ,OAAO,OAAO,CAAC,GAC7D,MAAA,gBAAsB;;CAG5B,IAAI,SAAqB;AACvB,SAAO,MAAA;;CAGT,IAAI,QAA0C;AAC5C,SAAO,MAAA;;CAGT,IAAI,QAA0C;AAC5C,SAAO,MAAA;;CAGT,MAAM,MAAuB;AAC3B,QAAA,SAAe;AACf,MAAI;GACF,MAAM,SAAS,MAAM,MAAA,MAAY,MAAA,OAAa;AAC9C,SAAA,QAAc;AACd,SAAA,SAAe;AACf,UAAO;WACA,GAAG;AACV,SAAA,QAAc;AACd,SAAA,SAAe;AACf,SAAM;;;CAIV,QAAc;AACZ,QAAA,gBAAsB,OAAO;;;;;;;;;;AAejC,IAAa,mBAAb,MAA8B;CAC5B,2BAAoB,IAAI,KAAyC;CAEjE,SAA0C,MAAsC;AAC9E,MAAI,MAAA,QAAc,IAAI,KAAK,GAAG,CAAE;AAEhC,QAAA,QAAc,IAAI,KAAK,IAAI,KAAmC;AAC9D,OACG,KAAK,CACL,OAAO,UAAU;AAChB,OAAI,EAAE,iBAAiB,SAAS,MAAM,SAAS,cAAe,OAAM;IACpE,CACD,cAAc;AACb,SAAA,QAAc,OAAO,KAAK,GAAG;IAC7B;;CAGN,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAA,QAAc,QAAQ,CAAE,MAAK,OAAO;AACvD,QAAA,QAAc,OAAO;;;;;;;;;;;;;;;;;AAsBzB,IAAa,eAAb,MAA0B;CACxB,SAA2B,QAAQ,SAAS;CAC5C,2BAAoB,IAAI,KAAiC;CACzD,WAA8C;CAE9C,SAA0C,MAAiD;EACzF,MAAM,IAAI;AACV,QAAA,QAAc,IAAI,EAAE;EAEpB,MAAM,SAAS,MAAA,MACZ,WAAW;AACV,SAAA,QAAc,OAAO,EAAE;AACvB,SAAA,UAAgB;AAChB,UAAO,KAAK,KAAK;IACjB,CACD,cAAc;AACb,SAAA,UAAgB;IAChB;AAGJ,QAAA,QAAc,OAAO,WACb,UACA,GACP;AAED,SAAO;;CAGT,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAA,QAAe,MAAK,OAAO;AAC9C,QAAA,QAAc,OAAO;AACrB,QAAA,SAAe,OAAO;;CAGxB,UAAgB;AACd,OAAK,UAAU"}
|