@videojs/spf 10.0.0-beta.1 → 10.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -40
- package/dist/default/all.js +16 -0
- package/dist/default/core/abr/bandwidth-estimator.js +96 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/events/create-event-stream.js +48 -0
- package/dist/default/core/events/create-event-stream.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +51 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +68 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +99 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +77 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +147 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +83 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +215 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/reactive/combine-latest.js +72 -0
- package/dist/default/core/reactive/combine-latest.js.map +1 -0
- package/dist/default/core/state/create-state.js +143 -0
- package/dist/default/core/state/create-state.js.map +1 -0
- package/dist/default/core/task.js +122 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +31 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +169 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +188 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +151 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +197 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +55 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +70 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/setup-text-tracks.js +88 -0
- package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js +63 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/default/dom/features/sync-text-track-modes.js +38 -0
- package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +46 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +55 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/{dom/index.js → default/dom/features/track-playback-rate.js} +2 -4
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +72 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +154 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +186 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +56 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +95 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +176 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +11 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +2 -2
- package/dist/default/index.js.map +1 -0
- package/dist/dev/all.js +16 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +96 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +31 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/events/create-event-stream.d.ts +48 -0
- package/dist/dev/core/events/create-event-stream.d.ts.map +1 -0
- package/dist/dev/core/events/create-event-stream.js +48 -0
- package/dist/dev/core/events/create-event-stream.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +51 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +68 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.d.ts +16 -0
- package/dist/dev/core/features/resolve-presentation.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +99 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.d.ts +13 -0
- package/dist/dev/core/features/resolve-track.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-track.js +77 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.d.ts +11 -0
- package/dist/dev/core/features/select-tracks.d.ts.map +1 -0
- package/dist/dev/core/features/select-tracks.js +147 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +83 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +215 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/reactive/combine-latest.js +72 -0
- package/dist/dev/core/reactive/combine-latest.js.map +1 -0
- package/dist/dev/core/state/create-state.d.ts +104 -0
- package/dist/dev/core/state/create-state.d.ts.map +1 -0
- package/dist/dev/core/state/create-state.js +143 -0
- package/dist/dev/core/state/create-state.js.map +1 -0
- package/dist/dev/core/task.js +122 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +173 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +31 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +169 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +188 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +151 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +197 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +55 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +70 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/setup-text-tracks.js +88 -0
- package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +63 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/dev/dom/features/sync-text-track-modes.js +38 -0
- package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +46 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +53 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +55 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +36 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +72 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +154 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +186 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +56 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +95 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +135 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +176 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +3 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +11 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +22 -22
- package/dist/adapter-CMw-rvbk.js +0 -3512
- package/dist/adapter-CMw-rvbk.js.map +0 -1
- package/dist/adapter-l8iJN_GI.d.ts +0 -689
- package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
- package/dist/dom/index.d.ts +0 -243
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
|
+
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
+
|
|
4
|
+
//#region src/core/hls/parse-media-playlist.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parse HLS media playlist and resolve track with segments.
|
|
7
|
+
*
|
|
8
|
+
* Takes an unresolved track (from multivariant playlist) and media playlist text,
|
|
9
|
+
* returns a HAM-compliant resolved track with segments.
|
|
10
|
+
*
|
|
11
|
+
* @param text - Media playlist text content
|
|
12
|
+
* @param unresolved - Unresolved track from parseMultivariantPlaylist
|
|
13
|
+
* @returns Resolved track with segments (type inferred from input)
|
|
14
|
+
*/
|
|
15
|
+
function parseMediaPlaylist(text, unresolved) {
|
|
16
|
+
const lines = text.split(/\r?\n/);
|
|
17
|
+
const baseUrl = unresolved.url;
|
|
18
|
+
const segments = [];
|
|
19
|
+
let initSegmentUrl;
|
|
20
|
+
let initSegmentByteRange;
|
|
21
|
+
let currentDuration = 0;
|
|
22
|
+
let currentByteRange;
|
|
23
|
+
let currentTime = 0;
|
|
24
|
+
let segmentIndex = 0;
|
|
25
|
+
let previousByteRangeEnd;
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
29
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
30
|
+
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
31
|
+
if (mapAttrs) {
|
|
32
|
+
const uri = mapAttrs.get("URI");
|
|
33
|
+
if (uri) {
|
|
34
|
+
initSegmentUrl = resolveUrl(uri, baseUrl);
|
|
35
|
+
const byteRangeStr = mapAttrs.get("BYTERANGE");
|
|
36
|
+
if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (trimmed.startsWith("#EXTINF:")) {
|
|
41
|
+
currentDuration = parseExtInfDuration(trimmed.slice(8));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
|
|
45
|
+
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (trimmed === "#EXT-X-ENDLIST") continue;
|
|
49
|
+
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
50
|
+
const segment = {
|
|
51
|
+
id: `segment-${segmentIndex}`,
|
|
52
|
+
url: resolveUrl(trimmed, baseUrl),
|
|
53
|
+
duration: currentDuration,
|
|
54
|
+
startTime: currentTime
|
|
55
|
+
};
|
|
56
|
+
if (currentByteRange) {
|
|
57
|
+
segment.byteRange = currentByteRange;
|
|
58
|
+
previousByteRangeEnd = currentByteRange.end + 1;
|
|
59
|
+
} else previousByteRangeEnd = void 0;
|
|
60
|
+
segments.push(segment);
|
|
61
|
+
currentTime += currentDuration;
|
|
62
|
+
segmentIndex++;
|
|
63
|
+
currentDuration = 0;
|
|
64
|
+
currentByteRange = void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const totalDuration = currentTime;
|
|
68
|
+
const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
69
|
+
url: initSegmentUrl,
|
|
70
|
+
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
71
|
+
} : { url: "" };
|
|
72
|
+
return {
|
|
73
|
+
...unresolved,
|
|
74
|
+
startTime: 0,
|
|
75
|
+
duration: totalDuration,
|
|
76
|
+
segments,
|
|
77
|
+
initialization
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
export { parseMediaPlaylist };
|
|
83
|
+
//# sourceMappingURL=parse-media-playlist.js.map
|
|
@@ -0,0 +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":";;;;;;;;;;;;;;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;;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;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;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,SACA,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"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { matchTag, parseCodecs } from "./parse-attributes.js";
|
|
2
|
+
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
+
import { generateId } from "../utils/generate-id.js";
|
|
4
|
+
|
|
5
|
+
//#region src/core/hls/parse-multivariant.ts
|
|
6
|
+
/**
|
|
7
|
+
* Parse HLS multivariant playlist into a Presentation.
|
|
8
|
+
*
|
|
9
|
+
* Returns Presentation with partially resolved tracks (no segment information).
|
|
10
|
+
* Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
|
|
11
|
+
* but segment information is added when media playlists are fetched.
|
|
12
|
+
*
|
|
13
|
+
* @param text - Raw playlist text content
|
|
14
|
+
* @param unresolved - Unresolved presentation (contains URL for base URL resolution)
|
|
15
|
+
* @returns Presentation with partially resolved tracks (duration is undefined)
|
|
16
|
+
*/
|
|
17
|
+
function parseMultivariantPlaylist(text, unresolved) {
|
|
18
|
+
const baseUrl = unresolved.url;
|
|
19
|
+
const lines = text.split(/\r?\n/);
|
|
20
|
+
const streams = [];
|
|
21
|
+
const audioRenditions = [];
|
|
22
|
+
const subtitleRenditions = [];
|
|
23
|
+
let pendingStreamInfo = null;
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
27
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
28
|
+
const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
|
|
29
|
+
if (mediaAttrs) {
|
|
30
|
+
const type = mediaAttrs.get("TYPE");
|
|
31
|
+
const groupId = mediaAttrs.get("GROUP-ID");
|
|
32
|
+
const name = mediaAttrs.get("NAME");
|
|
33
|
+
if (type === "AUDIO" && groupId && name) {
|
|
34
|
+
const uri = mediaAttrs.get("URI");
|
|
35
|
+
audioRenditions.push({
|
|
36
|
+
groupId,
|
|
37
|
+
name,
|
|
38
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
39
|
+
uri: uri ? resolveUrl(uri, baseUrl) : void 0,
|
|
40
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
41
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT")
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (type === "SUBTITLES" && groupId && name) {
|
|
45
|
+
const uri = mediaAttrs.get("URI");
|
|
46
|
+
if (uri) subtitleRenditions.push({
|
|
47
|
+
groupId,
|
|
48
|
+
name,
|
|
49
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
50
|
+
uri: resolveUrl(uri, baseUrl),
|
|
51
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
52
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT"),
|
|
53
|
+
forced: mediaAttrs.getBool("FORCED")
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
|
|
59
|
+
if (streamInfAttrs) {
|
|
60
|
+
pendingStreamInfo = {
|
|
61
|
+
bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
|
|
62
|
+
resolution: streamInfAttrs.getResolution("RESOLUTION"),
|
|
63
|
+
codecs: streamInfAttrs.get("CODECS"),
|
|
64
|
+
frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
|
|
65
|
+
audioGroupId: streamInfAttrs.get("AUDIO")
|
|
66
|
+
};
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!trimmed.startsWith("#") && pendingStreamInfo) {
|
|
70
|
+
streams.push({
|
|
71
|
+
...pendingStreamInfo,
|
|
72
|
+
uri: resolveUrl(trimmed, baseUrl)
|
|
73
|
+
});
|
|
74
|
+
pendingStreamInfo = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const videoStreams = [];
|
|
78
|
+
const audioOnlyStreams = [];
|
|
79
|
+
for (const stream of streams) {
|
|
80
|
+
if (!stream.codecs) {
|
|
81
|
+
videoStreams.push(stream);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const parsedCodecs = parseCodecs(stream.codecs);
|
|
85
|
+
if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
|
|
86
|
+
else videoStreams.push(stream);
|
|
87
|
+
else videoStreams.push(stream);
|
|
88
|
+
}
|
|
89
|
+
const videoTracks = videoStreams.map((stream) => {
|
|
90
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
91
|
+
const track = {
|
|
92
|
+
type: "video",
|
|
93
|
+
id: generateId(),
|
|
94
|
+
url: stream.uri,
|
|
95
|
+
bandwidth: stream.bandwidth,
|
|
96
|
+
mimeType: "video/mp4",
|
|
97
|
+
codecs: []
|
|
98
|
+
};
|
|
99
|
+
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
100
|
+
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
101
|
+
if (codecs?.video) track.codecs = [codecs.video];
|
|
102
|
+
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
103
|
+
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
104
|
+
return track;
|
|
105
|
+
});
|
|
106
|
+
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
107
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
108
|
+
return {
|
|
109
|
+
type: "audio",
|
|
110
|
+
id: generateId(),
|
|
111
|
+
url: stream.uri,
|
|
112
|
+
bandwidth: stream.bandwidth,
|
|
113
|
+
mimeType: "audio/mp4",
|
|
114
|
+
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
115
|
+
groupId: stream.audioGroupId || "default",
|
|
116
|
+
name: "Default",
|
|
117
|
+
sampleRate: 48e3,
|
|
118
|
+
channels: 2
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
122
|
+
let audioCodecs;
|
|
123
|
+
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
124
|
+
const codecs = parseCodecs(stream.codecs);
|
|
125
|
+
if (codecs.audio) {
|
|
126
|
+
audioCodecs = [codecs.audio];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const track = {
|
|
131
|
+
type: "audio",
|
|
132
|
+
id: generateId(),
|
|
133
|
+
url: rendition.uri ?? "",
|
|
134
|
+
groupId: rendition.groupId,
|
|
135
|
+
name: rendition.name,
|
|
136
|
+
mimeType: "audio/mp4",
|
|
137
|
+
bandwidth: 0,
|
|
138
|
+
sampleRate: 48e3,
|
|
139
|
+
channels: 2,
|
|
140
|
+
codecs: []
|
|
141
|
+
};
|
|
142
|
+
if (rendition.language) track.language = rendition.language;
|
|
143
|
+
if (audioCodecs) track.codecs = audioCodecs;
|
|
144
|
+
if (rendition.default) track.default = rendition.default;
|
|
145
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
146
|
+
return track;
|
|
147
|
+
}), ...audioOnlyTracks];
|
|
148
|
+
const textTracks = subtitleRenditions.map((rendition) => {
|
|
149
|
+
const track = {
|
|
150
|
+
type: "text",
|
|
151
|
+
id: generateId(),
|
|
152
|
+
url: rendition.uri,
|
|
153
|
+
groupId: rendition.groupId,
|
|
154
|
+
label: rendition.name,
|
|
155
|
+
kind: "subtitles",
|
|
156
|
+
mimeType: "text/vtt",
|
|
157
|
+
bandwidth: 0
|
|
158
|
+
};
|
|
159
|
+
if (rendition.language) track.language = rendition.language;
|
|
160
|
+
if (rendition.default && rendition.autoselect) track.default = true;
|
|
161
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
162
|
+
if (rendition.forced) track.forced = rendition.forced;
|
|
163
|
+
return track;
|
|
164
|
+
});
|
|
165
|
+
const selectionSets = [];
|
|
166
|
+
if (videoTracks.length > 0) {
|
|
167
|
+
const videoSwitchingSet = {
|
|
168
|
+
id: generateId(),
|
|
169
|
+
type: "video",
|
|
170
|
+
tracks: videoTracks
|
|
171
|
+
};
|
|
172
|
+
const videoSelectionSet = {
|
|
173
|
+
id: generateId(),
|
|
174
|
+
type: "video",
|
|
175
|
+
switchingSets: [videoSwitchingSet]
|
|
176
|
+
};
|
|
177
|
+
selectionSets.push(videoSelectionSet);
|
|
178
|
+
}
|
|
179
|
+
if (audioTracks.length > 0) {
|
|
180
|
+
const audioSwitchingSet = {
|
|
181
|
+
id: generateId(),
|
|
182
|
+
type: "audio",
|
|
183
|
+
tracks: audioTracks
|
|
184
|
+
};
|
|
185
|
+
const audioSelectionSet = {
|
|
186
|
+
id: generateId(),
|
|
187
|
+
type: "audio",
|
|
188
|
+
switchingSets: [audioSwitchingSet]
|
|
189
|
+
};
|
|
190
|
+
selectionSets.push(audioSelectionSet);
|
|
191
|
+
}
|
|
192
|
+
if (textTracks.length > 0) {
|
|
193
|
+
const textSwitchingSet = {
|
|
194
|
+
id: generateId(),
|
|
195
|
+
type: "text",
|
|
196
|
+
tracks: textTracks
|
|
197
|
+
};
|
|
198
|
+
const textSelectionSet = {
|
|
199
|
+
id: generateId(),
|
|
200
|
+
type: "text",
|
|
201
|
+
switchingSets: [textSwitchingSet]
|
|
202
|
+
};
|
|
203
|
+
selectionSets.push(textSelectionSet);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
id: generateId(),
|
|
207
|
+
url: unresolved.url,
|
|
208
|
+
startTime: 0,
|
|
209
|
+
selectionSets
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
export { parseMultivariantPlaylist };
|
|
215
|
+
//# sourceMappingURL=parse-multivariant.js.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/core/hls/resolve-url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a potentially relative URL against a base URL using native URL API.
|
|
4
|
+
*/
|
|
5
|
+
function resolveUrl(url, baseUrl) {
|
|
6
|
+
return new URL(url, baseUrl).href;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { resolveUrl };
|
|
11
|
+
//# sourceMappingURL=resolve-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-url.js","names":[],"sources":["../../../../src/core/hls/resolve-url.ts"],"sourcesContent":["/**\n * Resolve a potentially relative URL against a base URL using native URL API.\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n return new URL(url, baseUrl).href;\n}\n"],"mappings":";;;;AAGA,SAAgB,WAAW,KAAa,SAAyB;AAC/D,QAAO,IAAI,IAAI,KAAK,QAAQ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/core/reactive/combine-latest.ts
|
|
2
|
+
/**
|
|
3
|
+
* Combines multiple Observable sources into a single Observable.
|
|
4
|
+
*
|
|
5
|
+
* Emits an array of latest values whenever any source emits.
|
|
6
|
+
* Only emits after all sources have emitted at least once.
|
|
7
|
+
*
|
|
8
|
+
* Supports selector-based subscriptions (fires only when the selected value
|
|
9
|
+
* changes, per the optional equalityFn) mirroring the createState API.
|
|
10
|
+
*
|
|
11
|
+
* @param sources - Array of Observable sources
|
|
12
|
+
* @returns Combined Observable
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const state = createState({ count: 0 });
|
|
17
|
+
* const events = createEventStream<Action>();
|
|
18
|
+
*
|
|
19
|
+
* combineLatest([state, events]).subscribe(([state, event]) => {
|
|
20
|
+
* if (event.type === 'PLAY' && state.count > 0) {
|
|
21
|
+
* // React to event + state condition
|
|
22
|
+
* }
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Selector subscription
|
|
27
|
+
* ```ts
|
|
28
|
+
* combineLatest([state, owners]).subscribe(
|
|
29
|
+
* ([s, o]) => deriveKey(s, o),
|
|
30
|
+
* (key) => { ... },
|
|
31
|
+
* { equalityFn: keyEq }
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function combineLatest(sources) {
|
|
36
|
+
const subscribeToSources = (listener) => {
|
|
37
|
+
const latest = new Array(sources.length);
|
|
38
|
+
const hasValue = new Array(sources.length).fill(false);
|
|
39
|
+
const unsubscribers = [];
|
|
40
|
+
for (let i = 0; i < sources.length; i++) {
|
|
41
|
+
const unsubscribe = sources[i].subscribe((value) => {
|
|
42
|
+
latest[i] = value;
|
|
43
|
+
hasValue[i] = true;
|
|
44
|
+
if (hasValue.every((has) => has)) listener([...latest]);
|
|
45
|
+
});
|
|
46
|
+
unsubscribers.push(unsubscribe);
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
return { subscribe(listenerOrSelector, maybeListener, options) {
|
|
53
|
+
if (maybeListener === void 0) return subscribeToSources(listenerOrSelector);
|
|
54
|
+
const selector = listenerOrSelector;
|
|
55
|
+
const listener = maybeListener;
|
|
56
|
+
const equalityFn = options?.equalityFn ?? Object.is;
|
|
57
|
+
let prevSelected;
|
|
58
|
+
let initialized = false;
|
|
59
|
+
return subscribeToSources((values) => {
|
|
60
|
+
const nextSelected = selector(values);
|
|
61
|
+
if (!initialized || !equalityFn(prevSelected, nextSelected)) {
|
|
62
|
+
prevSelected = nextSelected;
|
|
63
|
+
initialized = true;
|
|
64
|
+
listener(nextSelected);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { combineLatest };
|
|
72
|
+
//# sourceMappingURL=combine-latest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combine-latest.js","names":[],"sources":["../../../../src/core/reactive/combine-latest.ts"],"sourcesContent":["/**\n * Minimal combineLatest operator for reactive composition.\n *\n * Combines multiple Observable-like sources and emits an array of their latest values\n * whenever any source emits.\n */\n\n/**\n * Observable-like interface.\n * Both State and EventStream conform to this shape.\n */\nexport interface Observable<T> {\n subscribe(listener: (value: T) => void): () => void;\n}\n\n/**\n * Infer value types from array of Observables.\n */\nexport type InferObservableValues<T extends readonly Observable<unknown>[]> = {\n [K in keyof T]: T[K] extends Observable<infer V> ? V : never;\n};\n\n/**\n * Options for selector-based subscriptions on a combined observable.\n */\nexport interface SelectorOptions<R> {\n equalityFn?: (a: R, b: R) => boolean;\n}\n\n/**\n * Observable returned by combineLatest — supports both plain and\n * selector-based subscriptions.\n */\nexport interface CombinedObservable<T> extends Observable<T> {\n subscribe(listener: (value: T) => void): () => void;\n subscribe<R>(selector: (value: T) => R, listener: (value: R) => void, options?: SelectorOptions<R>): () => void;\n}\n\n/**\n * Combines multiple Observable sources into a single Observable.\n *\n * Emits an array of latest values whenever any source emits.\n * Only emits after all sources have emitted at least once.\n *\n * Supports selector-based subscriptions (fires only when the selected value\n * changes, per the optional equalityFn) mirroring the createState API.\n *\n * @param sources - Array of Observable sources\n * @returns Combined Observable\n *\n * @example\n * ```ts\n * const state = createState({ count: 0 });\n * const events = createEventStream<Action>();\n *\n * combineLatest([state, events]).subscribe(([state, event]) => {\n * if (event.type === 'PLAY' && state.count > 0) {\n * // React to event + state condition\n * }\n * });\n * ```\n *\n * @example Selector subscription\n * ```ts\n * combineLatest([state, owners]).subscribe(\n * ([s, o]) => deriveKey(s, o),\n * (key) => { ... },\n * { equalityFn: keyEq }\n * );\n * ```\n */\nexport function combineLatest<T extends readonly Observable<unknown>[]>(\n sources: [...T]\n): CombinedObservable<InferObservableValues<T>> {\n type Values = InferObservableValues<T>;\n\n const subscribeToSources = (listener: (values: Values) => void): (() => void) => {\n const latest: unknown[] = new Array(sources.length);\n const hasValue: boolean[] = new Array(sources.length).fill(false);\n const unsubscribers: Array<() => void> = [];\n\n for (let i = 0; i < sources.length; i++) {\n const unsubscribe = sources[i]!.subscribe((value) => {\n latest[i] = value;\n hasValue[i] = true;\n if (hasValue.every((has) => has)) {\n listener([...latest] as Values);\n }\n });\n unsubscribers.push(unsubscribe);\n }\n\n return () => {\n for (const unsubscribe of unsubscribers) {\n unsubscribe();\n }\n };\n };\n\n return {\n subscribe<R>(\n listenerOrSelector: ((values: Values) => void) | ((values: Values) => R),\n maybeListener?: (value: R) => void,\n options?: SelectorOptions<R>\n ): () => void {\n if (maybeListener === undefined) {\n return subscribeToSources(listenerOrSelector as (values: Values) => void);\n }\n\n const selector = listenerOrSelector as (values: Values) => R;\n const listener = maybeListener;\n const equalityFn = options?.equalityFn ?? Object.is;\n\n let prevSelected: R | undefined;\n let initialized = false;\n\n return subscribeToSources((values) => {\n const nextSelected = selector(values);\n if (!initialized || !equalityFn(prevSelected as R, nextSelected)) {\n prevSelected = nextSelected;\n initialized = true;\n listener(nextSelected);\n }\n });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,cACd,SAC8C;CAG9C,MAAM,sBAAsB,aAAqD;EAC/E,MAAM,SAAoB,IAAI,MAAM,QAAQ,OAAO;EACnD,MAAM,WAAsB,IAAI,MAAM,QAAQ,OAAO,CAAC,KAAK,MAAM;EACjE,MAAM,gBAAmC,EAAE;AAE3C,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,cAAc,QAAQ,GAAI,WAAW,UAAU;AACnD,WAAO,KAAK;AACZ,aAAS,KAAK;AACd,QAAI,SAAS,OAAO,QAAQ,IAAI,CAC9B,UAAS,CAAC,GAAG,OAAO,CAAW;KAEjC;AACF,iBAAc,KAAK,YAAY;;AAGjC,eAAa;AACX,QAAK,MAAM,eAAe,cACxB,cAAa;;;AAKnB,QAAO,EACL,UACE,oBACA,eACA,SACY;AACZ,MAAI,kBAAkB,OACpB,QAAO,mBAAmB,mBAA+C;EAG3E,MAAM,WAAW;EACjB,MAAM,WAAW;EACjB,MAAM,aAAa,SAAS,cAAc,OAAO;EAEjD,IAAI;EACJ,IAAI,cAAc;AAElB,SAAO,oBAAoB,WAAW;GACpC,MAAM,eAAe,SAAS,OAAO;AACrC,OAAI,CAAC,eAAe,CAAC,WAAW,cAAmB,aAAa,EAAE;AAChE,mBAAe;AACf,kBAAc;AACd,aAAS,aAAa;;IAExB;IAEL"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/core/state/create-state.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reactive state container with selectors, custom equality, and batched updates.
|
|
4
|
+
*
|
|
5
|
+
* Manages both immutable state values and mutable object references (e.g., HTMLMediaElement).
|
|
6
|
+
*/
|
|
7
|
+
const STATE_SYMBOL = Symbol("@videojs/spf/state");
|
|
8
|
+
/**
|
|
9
|
+
* Default equality function using Object.is.
|
|
10
|
+
*/
|
|
11
|
+
function defaultEquality(a, b) {
|
|
12
|
+
return Object.is(a, b);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* State container implementation.
|
|
16
|
+
*/
|
|
17
|
+
var StateContainer = class {
|
|
18
|
+
[STATE_SYMBOL] = true;
|
|
19
|
+
#current;
|
|
20
|
+
#pending = null;
|
|
21
|
+
#pendingFlush = false;
|
|
22
|
+
#equalityFn;
|
|
23
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
24
|
+
#selectorListeners = /* @__PURE__ */ new Set();
|
|
25
|
+
constructor(initial, config) {
|
|
26
|
+
this.#current = typeof initial === "object" && initial !== null ? { ...initial } : initial;
|
|
27
|
+
this.#equalityFn = config?.equalityFn ?? defaultEquality;
|
|
28
|
+
}
|
|
29
|
+
get current() {
|
|
30
|
+
return this.#pending ?? this.#current;
|
|
31
|
+
}
|
|
32
|
+
patch(partial) {
|
|
33
|
+
const base = this.#pending ?? this.#current;
|
|
34
|
+
if (typeof base !== "object" || base === null) {
|
|
35
|
+
const value = partial;
|
|
36
|
+
if (!Object.is(base, value)) {
|
|
37
|
+
this.#pending = value;
|
|
38
|
+
this.#scheduleFlush();
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const next = { ...base };
|
|
43
|
+
let changed = false;
|
|
44
|
+
for (const key in partial) {
|
|
45
|
+
if (!Object.hasOwn(partial, key)) continue;
|
|
46
|
+
const value = partial[key];
|
|
47
|
+
if (!Object.is(base[key], value)) {
|
|
48
|
+
next[key] = value;
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (changed) {
|
|
53
|
+
this.#pending = next;
|
|
54
|
+
this.#scheduleFlush();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
subscribe(selectorOrListener, maybeListener, options) {
|
|
58
|
+
if (maybeListener === void 0) {
|
|
59
|
+
const listener = selectorOrListener;
|
|
60
|
+
this.#listeners.add(listener);
|
|
61
|
+
listener(this.current);
|
|
62
|
+
return () => {
|
|
63
|
+
this.#listeners.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const selector = selectorOrListener;
|
|
67
|
+
const listener = maybeListener;
|
|
68
|
+
const entry = {
|
|
69
|
+
selector,
|
|
70
|
+
listener,
|
|
71
|
+
options: options ?? {}
|
|
72
|
+
};
|
|
73
|
+
this.#selectorListeners.add(entry);
|
|
74
|
+
listener(selector(this.current));
|
|
75
|
+
return () => {
|
|
76
|
+
this.#selectorListeners.delete(entry);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
flush() {
|
|
80
|
+
if (this.#pending === null) return;
|
|
81
|
+
const prev = this.#current;
|
|
82
|
+
const next = this.#pending;
|
|
83
|
+
this.#pending = null;
|
|
84
|
+
this.#pendingFlush = false;
|
|
85
|
+
if (this.#equalityFn(prev, next)) return;
|
|
86
|
+
this.#current = next;
|
|
87
|
+
for (const listener of this.#listeners) listener(this.#current);
|
|
88
|
+
for (const entry of this.#selectorListeners) {
|
|
89
|
+
const prevSelected = entry.selector(prev);
|
|
90
|
+
const nextSelected = entry.selector(this.#current);
|
|
91
|
+
if (!(entry.options.equalityFn ?? Object.is)(prevSelected, nextSelected)) entry.listener(nextSelected);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
#scheduleFlush() {
|
|
95
|
+
if (this.#pendingFlush) return;
|
|
96
|
+
this.#pendingFlush = true;
|
|
97
|
+
queueMicrotask(() => this.flush());
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Create a reactive state container.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const state = createState({ count: 0 });
|
|
106
|
+
*
|
|
107
|
+
* // Subscribe to changes
|
|
108
|
+
* state.subscribe((current, prev) => {
|
|
109
|
+
* console.log('Changed:', prev, '->', current);
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* // Updates are batched
|
|
113
|
+
* state.patch({ count: 1 });
|
|
114
|
+
* state.patch({ count: 2 });
|
|
115
|
+
* // Only one notification fires (with count: 2)
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @example Selector subscriptions
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const state = createState({ count: 0, name: 'test' });
|
|
121
|
+
*
|
|
122
|
+
* // Only notified when count changes
|
|
123
|
+
* state.subscribe(
|
|
124
|
+
* s => s.count,
|
|
125
|
+
* (current, prev) => console.log(current, prev)
|
|
126
|
+
* );
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example Custom equality
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const state = createState(
|
|
132
|
+
* { count: 0, name: 'test' },
|
|
133
|
+
* { equalityFn: (a, b) => a.count === b.count }
|
|
134
|
+
* );
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
function createState(initial, config) {
|
|
138
|
+
return new StateContainer(initial, config);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { createState };
|
|
143
|
+
//# sourceMappingURL=create-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-state.js","names":["#current","#equalityFn","#pending","#scheduleFlush","#listeners","#selectorListeners","#pendingFlush"],"sources":["../../../../src/core/state/create-state.ts"],"sourcesContent":["/**\n * Reactive state container with selectors, custom equality, and batched updates.\n *\n * Manages both immutable state values and mutable object references (e.g., HTMLMediaElement).\n */\n\n// =============================================================================\n// Symbol for type identification\n// =============================================================================\n\nconst STATE_SYMBOL = Symbol('@videojs/spf/state');\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * State change listener for full-state subscriptions.\n */\nexport type StateListener<T> = (current: T) => void;\n\n/**\n * Selector function to derive a value from state.\n */\nexport type StateSelector<T, R> = (state: T) => R;\n\n/**\n * Listener for selector-based subscriptions.\n */\nexport type SelectorListener<R> = (current: R) => void;\n\n/**\n * Options for selector subscriptions.\n */\nexport interface SelectorOptions<R> {\n /**\n * Custom equality function for selected value.\n * Defaults to Object.is\n */\n equalityFn?: (a: R, b: R) => boolean;\n}\n\n/**\n * Configuration for creating a state container.\n */\nexport interface StateConfig<T> {\n /**\n * Custom equality function for change detection.\n * Defaults to Object.is for top-level comparison.\n */\n equalityFn?: (a: T, b: T) => boolean;\n}\n\n/**\n * Read-only state interface.\n */\nexport interface State<T> {\n /** Symbol for type identification */\n readonly [STATE_SYMBOL]: true;\n\n /** Current state snapshot */\n readonly current: T;\n\n /** Subscribe to state changes */\n subscribe(listener: StateListener<T>): () => void;\n}\n\n/**\n * Writable state interface with patch capability.\n */\nexport interface WritableState<T> extends State<T> {\n /** Update state with partial object (for object state) or full value (for primitive state) */\n patch(partial: T extends object ? Partial<T> : T): void;\n\n /** Subscribe to full-state changes */\n subscribe(listener: StateListener<T>): () => void;\n\n /** Subscribe to selected value changes */\n subscribe<R>(selector: StateSelector<T, R>, listener: SelectorListener<R>, options?: SelectorOptions<R>): () => void;\n\n /** Manually flush pending updates */\n flush(): void;\n}\n\n/**\n * Entry for selector subscriptions.\n */\ninterface SelectorEntry<T, R> {\n selector: StateSelector<T, R>;\n listener: SelectorListener<R>;\n options: SelectorOptions<R>;\n}\n\n// =============================================================================\n// Default equality function\n// =============================================================================\n\n/**\n * Default equality function using Object.is.\n */\nfunction defaultEquality<T>(a: T, b: T): boolean {\n return Object.is(a, b);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * State container implementation.\n */\nclass StateContainer<T> implements WritableState<T> {\n [STATE_SYMBOL] = true as const;\n\n #current: T;\n #pending: T | null = null;\n #pendingFlush = false;\n #equalityFn: (a: T, b: T) => boolean;\n\n // Full-state listeners\n #listeners = new Set<StateListener<T>>();\n\n // Selector-based listeners\n #selectorListeners = new Set<SelectorEntry<T, unknown>>();\n\n constructor(initial: T, config?: StateConfig<T>) {\n this.#current = typeof initial === 'object' && initial !== null ? ({ ...initial } as T) : initial;\n this.#equalityFn = config?.equalityFn ?? defaultEquality;\n }\n\n get current(): T {\n // Return pending state if available (for chained patches)\n return this.#pending ?? this.#current;\n }\n\n patch(partial: T extends object ? Partial<T> : T): void {\n const base = this.#pending ?? this.#current;\n\n if (typeof base !== 'object' || base === null) {\n // Primitive state: replace the whole value\n const value = partial as T;\n if (!Object.is(base, value)) {\n this.#pending = value;\n this.#scheduleFlush();\n }\n return;\n }\n\n const next = { ...base } as T;\n let changed = false;\n\n // Apply partial updates with change detection\n for (const key in partial) {\n if (!Object.hasOwn(partial as any, key)) continue;\n\n const value = (partial as Partial<T>)[key as keyof T];\n\n if (!Object.is((base as Partial<T>)[key as keyof T], value)) {\n (next as Partial<T>)[key as keyof T] = value;\n changed = true;\n }\n }\n\n if (changed) {\n this.#pending = next;\n this.#scheduleFlush();\n }\n }\n\n subscribe(listener: StateListener<T>): () => void;\n subscribe<R>(selector: StateSelector<T, R>, listener: SelectorListener<R>, options?: SelectorOptions<R>): () => void;\n subscribe<R>(\n selectorOrListener: StateListener<T> | StateSelector<T, R>,\n maybeListener?: SelectorListener<R>,\n options?: SelectorOptions<R>\n ): () => void {\n // Overload 1: Full-state subscription\n if (maybeListener === undefined) {\n const listener = selectorOrListener as StateListener<T>;\n this.#listeners.add(listener);\n\n // Fire immediately with current state\n listener(this.current);\n\n return () => {\n this.#listeners.delete(listener);\n };\n }\n\n // Overload 2: Selector subscription\n const selector = selectorOrListener as StateSelector<T, R>;\n const listener = maybeListener;\n const opts = options ?? {};\n\n const entry: SelectorEntry<T, unknown> = {\n selector: selector as StateSelector<T, unknown>,\n listener: listener as SelectorListener<unknown>,\n options: opts as SelectorOptions<unknown>,\n };\n\n this.#selectorListeners.add(entry);\n\n // Fire immediately with current selected value\n const selected = selector(this.current);\n listener(selected);\n\n return () => {\n this.#selectorListeners.delete(entry);\n };\n }\n\n flush(): void {\n if (this.#pending === null) return;\n\n const prev = this.#current;\n const next = this.#pending;\n this.#pending = null;\n this.#pendingFlush = false;\n\n // Check if state actually changed using custom equality\n if (this.#equalityFn(prev, next)) return;\n\n this.#current = next;\n\n // Notify full-state listeners\n for (const listener of this.#listeners) {\n listener(this.#current);\n }\n\n // Notify selector listeners\n for (const entry of this.#selectorListeners) {\n const prevSelected = entry.selector(prev);\n const nextSelected = entry.selector(this.#current);\n\n const equalityFn = entry.options.equalityFn ?? Object.is;\n\n if (!equalityFn(prevSelected, nextSelected)) {\n entry.listener(nextSelected);\n }\n }\n }\n\n #scheduleFlush(): void {\n if (this.#pendingFlush) return;\n this.#pendingFlush = true;\n queueMicrotask(() => this.flush());\n }\n}\n\n// =============================================================================\n// Factory function\n// =============================================================================\n\n/**\n * Create a reactive state container.\n *\n * @example\n * ```typescript\n * const state = createState({ count: 0 });\n *\n * // Subscribe to changes\n * state.subscribe((current, prev) => {\n * console.log('Changed:', prev, '->', current);\n * });\n *\n * // Updates are batched\n * state.patch({ count: 1 });\n * state.patch({ count: 2 });\n * // Only one notification fires (with count: 2)\n * ```\n *\n * @example Selector subscriptions\n * ```typescript\n * const state = createState({ count: 0, name: 'test' });\n *\n * // Only notified when count changes\n * state.subscribe(\n * s => s.count,\n * (current, prev) => console.log(current, prev)\n * );\n * ```\n *\n * @example Custom equality\n * ```typescript\n * const state = createState(\n * { count: 0, name: 'test' },\n * { equalityFn: (a, b) => a.count === b.count }\n * );\n * ```\n */\nexport function createState<T>(initial: T, config?: StateConfig<T>): WritableState<T> {\n return new StateContainer(initial, config);\n}\n\n// =============================================================================\n// Type guard\n// =============================================================================\n\n/**\n * Check if a value is a State container.\n */\nexport function isState(value: unknown): value is State<object> {\n return typeof value === 'object' && value !== null && STATE_SYMBOL in value;\n}\n"],"mappings":";;;;;;AAUA,MAAM,eAAe,OAAO,qBAAqB;;;;AA0FjD,SAAS,gBAAmB,GAAM,GAAe;AAC/C,QAAO,OAAO,GAAG,GAAG,EAAE;;;;;AAUxB,IAAM,iBAAN,MAAoD;CAClD,CAAC,gBAAgB;CAEjB;CACA,WAAqB;CACrB,gBAAgB;CAChB;CAGA,6BAAa,IAAI,KAAuB;CAGxC,qCAAqB,IAAI,KAAgC;CAEzD,YAAY,SAAY,QAAyB;AAC/C,QAAKA,UAAW,OAAO,YAAY,YAAY,YAAY,OAAQ,EAAE,GAAG,SAAS,GAAS;AAC1F,QAAKC,aAAc,QAAQ,cAAc;;CAG3C,IAAI,UAAa;AAEf,SAAO,MAAKC,WAAY,MAAKF;;CAG/B,MAAM,SAAkD;EACtD,MAAM,OAAO,MAAKE,WAAY,MAAKF;AAEnC,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAE7C,MAAM,QAAQ;AACd,OAAI,CAAC,OAAO,GAAG,MAAM,MAAM,EAAE;AAC3B,UAAKE,UAAW;AAChB,UAAKC,eAAgB;;AAEvB;;EAGF,MAAM,OAAO,EAAE,GAAG,MAAM;EACxB,IAAI,UAAU;AAGd,OAAK,MAAM,OAAO,SAAS;AACzB,OAAI,CAAC,OAAO,OAAO,SAAgB,IAAI,CAAE;GAEzC,MAAM,QAAS,QAAuB;AAEtC,OAAI,CAAC,OAAO,GAAI,KAAoB,MAAiB,MAAM,EAAE;AAC3D,IAAC,KAAoB,OAAkB;AACvC,cAAU;;;AAId,MAAI,SAAS;AACX,SAAKD,UAAW;AAChB,SAAKC,eAAgB;;;CAMzB,UACE,oBACA,eACA,SACY;AAEZ,MAAI,kBAAkB,QAAW;GAC/B,MAAM,WAAW;AACjB,SAAKC,UAAW,IAAI,SAAS;AAG7B,YAAS,KAAK,QAAQ;AAEtB,gBAAa;AACX,UAAKA,UAAW,OAAO,SAAS;;;EAKpC,MAAM,WAAW;EACjB,MAAM,WAAW;EAGjB,MAAM,QAAmC;GAC7B;GACA;GACV,SALW,WAAW,EAAE;GAMzB;AAED,QAAKC,kBAAmB,IAAI,MAAM;AAIlC,WADiB,SAAS,KAAK,QAAQ,CACrB;AAElB,eAAa;AACX,SAAKA,kBAAmB,OAAO,MAAM;;;CAIzC,QAAc;AACZ,MAAI,MAAKH,YAAa,KAAM;EAE5B,MAAM,OAAO,MAAKF;EAClB,MAAM,OAAO,MAAKE;AAClB,QAAKA,UAAW;AAChB,QAAKI,eAAgB;AAGrB,MAAI,MAAKL,WAAY,MAAM,KAAK,CAAE;AAElC,QAAKD,UAAW;AAGhB,OAAK,MAAM,YAAY,MAAKI,UAC1B,UAAS,MAAKJ,QAAS;AAIzB,OAAK,MAAM,SAAS,MAAKK,mBAAoB;GAC3C,MAAM,eAAe,MAAM,SAAS,KAAK;GACzC,MAAM,eAAe,MAAM,SAAS,MAAKL,QAAS;AAIlD,OAAI,EAFe,MAAM,QAAQ,cAAc,OAAO,IAEtC,cAAc,aAAa,CACzC,OAAM,SAAS,aAAa;;;CAKlC,iBAAuB;AACrB,MAAI,MAAKM,aAAe;AACxB,QAAKA,eAAgB;AACrB,uBAAqB,KAAK,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CtC,SAAgB,YAAe,SAAY,QAA2C;AACpF,QAAO,IAAI,eAAe,SAAS,OAAO"}
|