@videojs/spf 10.0.0-beta.26 → 10.0.0-beta.27
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/signals/when.js +38 -0
- package/dist/default/core/signals/when.js.map +1 -0
- package/dist/default/core/tasks/delayed-reschedule.js +31 -0
- package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/default/core/tasks/task.js +133 -3
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls-audio.js +3 -0
- package/dist/default/hls-background-video.js +3 -0
- package/dist/default/hls-video.js +4 -0
- package/dist/default/hls.js +6 -5
- package/dist/default/media/dom/capabilities.js +2 -0
- package/dist/default/media/dom/capabilities.js.map +1 -1
- package/dist/default/media/errors.js +62 -0
- package/dist/default/media/errors.js.map +1 -0
- package/dist/default/media/hls/parse-media-playlist.js +167 -16
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +14 -3
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/reload-policy.js +71 -0
- package/dist/default/media/hls/reload-policy.js.map +1 -0
- package/dist/default/media/live-window.js +36 -0
- package/dist/default/media/live-window.js.map +1 -0
- package/dist/default/media/primitives/select-tracks.js +1 -24
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/types/index.js +15 -1
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/tracks.js +14 -0
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/mux-audio.js +3 -0
- package/dist/default/mux-background-video.js +3 -0
- package/dist/default/mux-video.js +3 -0
- package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-audio/media.js +9 -0
- package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media.js +11 -0
- package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-audio/media.js +26 -0
- package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/media.js +23 -0
- package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/default/playback/behaviors/collect-errors.js +77 -0
- package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +29 -7
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +78 -4
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +29 -8
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +18 -9
- package/dist/default/playback/behaviors/track-switching.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +9 -5
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/default/playback/engines/hls/engine.js +19 -6
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/error-messages.js +39 -0
- package/dist/default/playback/primitives/error-messages.js.map +1 -0
- package/dist/default/playback/primitives/live-window.js +66 -0
- package/dist/default/playback/primitives/live-window.js.map +1 -0
- package/dist/default/playback/primitives/report-track-conditions.js +86 -0
- package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
- package/dist/dev/core/signals/when.js +38 -0
- package/dist/dev/core/signals/when.js.map +1 -0
- package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
- package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/dev/core/tasks/task.d.ts +52 -1
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js +133 -3
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls-audio.d.ts +3 -0
- package/dist/dev/hls-audio.js +3 -0
- package/dist/dev/hls-background-video.d.ts +3 -0
- package/dist/dev/hls-background-video.js +3 -0
- package/dist/dev/hls-video.d.ts +5 -0
- package/dist/dev/hls-video.js +4 -0
- package/dist/dev/hls.d.ts +6 -5
- package/dist/dev/hls.js +6 -5
- package/dist/dev/media/dom/capabilities.js +2 -0
- package/dist/dev/media/dom/capabilities.js.map +1 -1
- package/dist/dev/media/errors.d.ts +82 -0
- package/dist/dev/media/errors.d.ts.map +1 -0
- package/dist/dev/media/errors.js +62 -0
- package/dist/dev/media/errors.js.map +1 -0
- package/dist/dev/media/hls/parse-media-playlist.js +167 -16
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +14 -3
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/reload-policy.js +71 -0
- package/dist/dev/media/hls/reload-policy.js.map +1 -0
- package/dist/dev/media/live-window.js +36 -0
- package/dist/dev/media/live-window.js.map +1 -0
- package/dist/dev/media/primitives/select-tracks.js +1 -24
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/types/index.d.ts +125 -9
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js +15 -1
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +14 -0
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/mux-audio.d.ts +4 -0
- package/dist/dev/mux-audio.js +3 -0
- package/dist/dev/mux-background-video.d.ts +3 -0
- package/dist/dev/mux-background-video.js +3 -0
- package/dist/dev/mux-video.d.ts +4 -0
- package/dist/dev/mux-video.js +3 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
- package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.js +11 -0
- package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
- package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.js +23 -0
- package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/dev/playback/behaviors/collect-errors.js +77 -0
- package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +29 -7
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +5 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.js +78 -4
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +29 -8
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.js +18 -9
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +34 -17
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.js +9 -5
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +7 -7
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
- package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/dev/playback/engines/hls/engine.d.ts +43 -15
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +19 -6
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/error-messages.js +39 -0
- package/dist/dev/playback/primitives/error-messages.js.map +1 -0
- package/dist/dev/playback/primitives/live-window.js +66 -0
- package/dist/dev/playback/primitives/live-window.js.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
- package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
- package/package.json +32 -6
- package/dist/default/background-video.js +0 -3
- package/dist/default/playback/engines/background-video/adapter.js +0 -158
- package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/default/playback/engines/background-video/engine.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -124
- package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter.js +0 -120
- package/dist/default/playback/engines/hls/adapter.js.map +0 -1
- package/dist/dev/background-video.d.ts +0 -3
- package/dist/dev/background-video.js +0 -3
- package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -59
- package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/adapter.js +0 -158
- package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -46
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -124
- package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.d.ts +0 -42
- package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.js +0 -120
- package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { MEDIA_PLAYLIST_METADATA_KEY, getMediaPlaylistMetadata, isResolvedTrack } from "../types/index.js";
|
|
1
2
|
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
3
|
import { resolveUrl } from "./resolve-url.js";
|
|
4
|
+
import { isUndefined } from "@videojs/utils/predicate";
|
|
3
5
|
//#region src/media/hls/parse-media-playlist.ts
|
|
4
6
|
/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */
|
|
5
7
|
const MPEG_TS_MIME = "video/mp2t";
|
|
@@ -27,18 +29,93 @@ function containerMimeFromSegment(url) {
|
|
|
27
29
|
return dot === -1 ? void 0 : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
32
|
+
* Position a freshly-parsed window (whose segment `startTime`s are snapshot-
|
|
33
|
+
* local, i.e. from 0) onto the timeline established by the previous resolved
|
|
34
|
+
* snapshot. Carries the timeline forward using the media-sequence overlap and
|
|
35
|
+
* the previous window's *actual* segment durations — see
|
|
36
|
+
* `internal/design/spf/live-presentation-timeline-model.md`.
|
|
37
|
+
*
|
|
38
|
+
* - **Overlap** (`0 <= offset < previous.segments.length`): the new window's
|
|
39
|
+
* first segment is the same segment as `previous.segments[offset]`; anchor to
|
|
40
|
+
* its start (URLs checked — a mismatch warns).
|
|
41
|
+
* - **Sequence went backwards** (non-conformant): reset to the local base.
|
|
42
|
+
* - **Full turnover** (no overlap): estimate forward from the previous window's
|
|
43
|
+
* end across the unseen gap. This is the *only* place EXT-X-TARGETDURATION is
|
|
44
|
+
* used for timing — an upper-bound estimate, since the actual rolled-off
|
|
45
|
+
* durations are gone (exact recovery is the deferred PDT decision).
|
|
46
|
+
*
|
|
47
|
+
* Returns the rebased segments (the window edge is `segments[0].startTime`,
|
|
48
|
+
* derived — never stored on the track).
|
|
49
|
+
*/
|
|
50
|
+
function placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration) {
|
|
51
|
+
const prevSegments = previous.segments;
|
|
52
|
+
const localBase = segments[0]?.startTime ?? 0;
|
|
53
|
+
if (prevSegments.length === 0 || segments.length === 0) return segments;
|
|
54
|
+
const offset = mediaSequence - (getMediaPlaylistMetadata(previous)?.mediaSequence ?? 0);
|
|
55
|
+
let anchor;
|
|
56
|
+
if (offset >= 0 && offset < prevSegments.length) {
|
|
57
|
+
const overlap = prevSegments[offset];
|
|
58
|
+
if (overlap.url !== segments[0].url) console.warn(`[parseMediaPlaylist] media-sequence aligns previous[${offset}] with the new window's first segment, but URLs differ (${overlap.url} vs ${segments[0].url}); sequence numbers may be unreliable.`);
|
|
59
|
+
anchor = overlap.startTime;
|
|
60
|
+
} else if (offset < 0) {
|
|
61
|
+
console.warn(`[parseMediaPlaylist] media-sequence went backwards (offset ${offset}); resetting timeline.`);
|
|
62
|
+
anchor = localBase;
|
|
63
|
+
} else {
|
|
64
|
+
const last = prevSegments[prevSegments.length - 1];
|
|
65
|
+
const newFirst = segments[0];
|
|
66
|
+
if (last.startDate !== void 0 && newFirst.startDate !== void 0) anchor = last.startTime + (newFirst.startDate - last.startDate);
|
|
67
|
+
else {
|
|
68
|
+
anchor = last.startTime + last.duration + (offset - prevSegments.length) * targetDuration;
|
|
69
|
+
console.warn(`[parseMediaPlaylist] full window turnover (offset ${offset} >= ${prevSegments.length}); no PDT to bridge — estimating from previous end.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const shift = anchor - localBase;
|
|
73
|
+
return shift === 0 ? segments : segments.map((segment) => ({
|
|
74
|
+
...segment,
|
|
75
|
+
startTime: segment.startTime + shift
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Place a window on the frozen wall-clock anchor — the track's `startDate`
|
|
80
|
+
* (wall clock at media-time 0). The anchoring PDT-bearing segment lands at
|
|
81
|
+
* `segment.startDate − anchor`, so the window sits on the shared presentation
|
|
82
|
+
* timeline rather than a local-from-zero one (the `startDate` recomputed
|
|
83
|
+
* afterwards then reads back as the anchor — idempotent). This is the **main
|
|
84
|
+
* live path on every parse**: first resolves place against the pre-stamped
|
|
85
|
+
* shell anchor, and reloads re-place from PDT so drift can't accumulate and a
|
|
86
|
+
* long-stall turnover re-places with no overlap bridging (the HLS authoring
|
|
87
|
+
* spec's §8.1 EXTINF-accuracy rule bounds the per-reload correction to ~one
|
|
88
|
+
* video frame). Falls back to the local base when no segment carries PDT —
|
|
89
|
+
* there's nothing to anchor against (callers guard this for reloads, where the
|
|
90
|
+
* local base would reset the timeline).
|
|
91
|
+
*/
|
|
92
|
+
function placeOnAnchor(segments, anchor) {
|
|
93
|
+
const anchorSegment = segments.find((segment) => !isUndefined(segment.startDate));
|
|
94
|
+
if (!anchorSegment || isUndefined(anchorSegment.startDate)) return segments;
|
|
95
|
+
const shift = anchorSegment.startDate - anchor - anchorSegment.startTime;
|
|
96
|
+
if (shift === 0) return segments;
|
|
97
|
+
return segments.map((segment) => ({
|
|
98
|
+
...segment,
|
|
99
|
+
startTime: segment.startTime + shift
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
30
103
|
* Parse HLS media playlist and resolve track with segments.
|
|
31
104
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
105
|
+
* `previous` is what was known about this track before this parse: the
|
|
106
|
+
* partially-resolved track from the multivariant playlist on the first resolve,
|
|
107
|
+
* or the previously-resolved snapshot on a live reload. Its metadata is carried
|
|
108
|
+
* onto the result either way; when it's already resolved (has segments), its
|
|
109
|
+
* timeline is carried forward so the new window lands on a stable, advancing
|
|
110
|
+
* timeline (see {@link placeOnPreviousTimeline}).
|
|
34
111
|
*
|
|
35
112
|
* @param text - Media playlist text content
|
|
36
|
-
* @param
|
|
113
|
+
* @param previous - Prior track state (unresolved shell, or previous resolved snapshot)
|
|
37
114
|
* @returns Resolved track with segments (type inferred from input)
|
|
38
115
|
*/
|
|
39
|
-
function parseMediaPlaylist(text,
|
|
116
|
+
function parseMediaPlaylist(text, previous) {
|
|
40
117
|
const lines = text.split(/\r?\n/);
|
|
41
|
-
const baseUrl =
|
|
118
|
+
const baseUrl = previous.url;
|
|
42
119
|
const segments = [];
|
|
43
120
|
let initSegmentUrl;
|
|
44
121
|
let initSegmentByteRange;
|
|
@@ -47,10 +124,56 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
47
124
|
let currentTime = 0;
|
|
48
125
|
let segmentIndex = 0;
|
|
49
126
|
let previousByteRangeEnd;
|
|
127
|
+
let currentStartDate;
|
|
128
|
+
let targetDuration = 0;
|
|
129
|
+
let mediaSequence = 0;
|
|
130
|
+
let playlistType;
|
|
131
|
+
let endList = false;
|
|
132
|
+
let holdBack;
|
|
133
|
+
let lowLatency = false;
|
|
134
|
+
let encrypted = false;
|
|
50
135
|
for (const line of lines) {
|
|
51
136
|
const trimmed = line.trim();
|
|
52
137
|
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
53
|
-
if (trimmed
|
|
138
|
+
if (trimmed.startsWith("#EXT-X-TARGETDURATION:")) {
|
|
139
|
+
targetDuration = Number.parseInt(trimmed.slice(22), 10) || 0;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (trimmed.startsWith("#EXT-X-MEDIA-SEQUENCE:")) {
|
|
143
|
+
mediaSequence = Number.parseInt(trimmed.slice(22), 10) || 0;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.startsWith("#EXT-X-PROGRAM-DATE-TIME:")) {
|
|
147
|
+
const parsed = Date.parse(trimmed.slice(25).trim());
|
|
148
|
+
currentStartDate = Number.isNaN(parsed) ? currentStartDate : parsed / 1e3;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:")) {
|
|
152
|
+
const value = trimmed.slice(21).trim();
|
|
153
|
+
playlistType = value === "VOD" || value === "EVENT" ? value : void 0;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const key = matchTag(trimmed, "EXT-X-KEY");
|
|
157
|
+
if (key) {
|
|
158
|
+
const method = key.get("METHOD");
|
|
159
|
+
if (method !== void 0 && method !== "NONE") encrypted = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const serverControl = matchTag(trimmed, "EXT-X-SERVER-CONTROL");
|
|
163
|
+
if (serverControl) {
|
|
164
|
+
const value = serverControl.get("HOLD-BACK");
|
|
165
|
+
if (value !== void 0) {
|
|
166
|
+
const parsed = Number.parseFloat(value);
|
|
167
|
+
if (Number.isFinite(parsed) && parsed > 0) holdBack = parsed;
|
|
168
|
+
}
|
|
169
|
+
if (serverControl.get("PART-HOLD-BACK") !== void 0) lowLatency = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (trimmed.startsWith("#EXT-X-PART-INF") || trimmed.startsWith("#EXT-X-PART:")) {
|
|
173
|
+
lowLatency = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
54
177
|
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
55
178
|
if (mapAttrs) {
|
|
56
179
|
const uri = mapAttrs.get("URI");
|
|
@@ -69,14 +192,21 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
69
192
|
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
70
193
|
continue;
|
|
71
194
|
}
|
|
72
|
-
if (trimmed === "#EXT-X-ENDLIST")
|
|
195
|
+
if (trimmed === "#EXT-X-ENDLIST") {
|
|
196
|
+
endList = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
73
199
|
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
74
200
|
const segment = {
|
|
75
|
-
id: `segment-${segmentIndex}`,
|
|
201
|
+
id: `segment-${mediaSequence + segmentIndex}`,
|
|
76
202
|
url: resolveUrl(trimmed, baseUrl),
|
|
77
203
|
duration: currentDuration,
|
|
78
204
|
startTime: currentTime
|
|
79
205
|
};
|
|
206
|
+
if (!isUndefined(currentStartDate)) {
|
|
207
|
+
segment.startDate = currentStartDate;
|
|
208
|
+
currentStartDate += currentDuration;
|
|
209
|
+
}
|
|
80
210
|
if (currentByteRange) {
|
|
81
211
|
segment.byteRange = currentByteRange;
|
|
82
212
|
previousByteRangeEnd = currentByteRange.end + 1;
|
|
@@ -88,20 +218,41 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
88
218
|
currentByteRange = void 0;
|
|
89
219
|
}
|
|
90
220
|
}
|
|
91
|
-
const
|
|
92
|
-
const
|
|
221
|
+
const complete = endList || playlistType === "VOD";
|
|
222
|
+
const anchor = previous.startDate;
|
|
223
|
+
const hasPdt = segments.some((segment) => !isUndefined(segment.startDate));
|
|
224
|
+
const placed = !isUndefined(anchor) && hasPdt ? placeOnAnchor(segments, anchor) : isResolvedTrack(previous) ? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration) : segments;
|
|
225
|
+
const lastPlaced = placed[placed.length - 1];
|
|
226
|
+
const placedEnd = lastPlaced ? lastPlaced.startTime + lastPlaced.duration : 0;
|
|
227
|
+
const trackDuration = complete ? placedEnd : Number.POSITIVE_INFINITY;
|
|
228
|
+
const anchorSegment = placed.find((segment) => !isUndefined(segment.startDate));
|
|
229
|
+
const startDate = anchorSegment && !isUndefined(anchorSegment.startDate) ? anchorSegment.startDate - anchorSegment.startTime : void 0;
|
|
230
|
+
const initialization = previous.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
93
231
|
url: initSegmentUrl,
|
|
94
232
|
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
95
233
|
} : { url: "" };
|
|
96
|
-
const detectedContainer = initSegmentUrl ? void 0 : containerMimeFromSegment(
|
|
97
|
-
const mimeType =
|
|
234
|
+
const detectedContainer = initSegmentUrl ? void 0 : containerMimeFromSegment(placed[0]?.url);
|
|
235
|
+
const mimeType = previous.type !== "text" && detectedContainer ? detectedContainer : previous.mimeType;
|
|
98
236
|
return {
|
|
99
|
-
...
|
|
237
|
+
...previous,
|
|
100
238
|
mimeType,
|
|
101
239
|
startTime: 0,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
240
|
+
startDate,
|
|
241
|
+
duration: trackDuration,
|
|
242
|
+
segments: placed,
|
|
243
|
+
initialization,
|
|
244
|
+
metadata: {
|
|
245
|
+
...previous.metadata,
|
|
246
|
+
[MEDIA_PLAYLIST_METADATA_KEY]: {
|
|
247
|
+
targetDuration,
|
|
248
|
+
mediaSequence,
|
|
249
|
+
playlistType,
|
|
250
|
+
lowLatency,
|
|
251
|
+
endList,
|
|
252
|
+
holdBack,
|
|
253
|
+
encrypted
|
|
254
|
+
}
|
|
255
|
+
}
|
|
105
256
|
};
|
|
106
257
|
}
|
|
107
258
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/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/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */\nexport const MPEG_TS_MIME = 'video/mp2t';\n/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */\nexport const RAW_AAC_MIME = 'audio/aac';\n\n// Non-fMP4 container MIMEs keyed by segment file extension. fMP4 (the MSE\n// default) always carries an EXT-X-MAP init segment, so a media playlist with\n// no init segment and one of these extensions is a non-fMP4 rendition,\n// relabeled from the fMP4 default. Extend with `.mp3` → 'audio/mpeg' etc.\nconst CONTAINER_MIME_BY_EXTENSION: Record<string, string> = {\n '.ts': MPEG_TS_MIME,\n '.aac': RAW_AAC_MIME,\n};\n\n/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */\nexport const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));\n\n/**\n * Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file\n * extension, ignoring the query string. `undefined` for fMP4 / unrecognized.\n */\nfunction containerMimeFromSegment(url: string | undefined): string | undefined {\n if (!url) return undefined;\n let path: string;\n try {\n path = new URL(url).pathname.toLowerCase();\n } catch {\n path = url.toLowerCase().split('?')[0] ?? '';\n }\n const dot = path.lastIndexOf('.');\n return dot === -1 ? undefined : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];\n}\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 // Container detection: fMP4 always carries an EXT-X-MAP init segment, so its\n // absence plus a recognized non-fMP4 segment extension (`.ts` → MPEG-TS,\n // `.aac` → raw ADTS AAC) marks a non-fMP4 rendition (high-precision — never\n // trips on fMP4, which mandates the map). Relabel from the fMP4 default\n // `video/mp4` / `audio/mp4` to the container MIME so capability probing prunes\n // it (these containers are currently treated as unplayable; see `canPlayTrack`).\n const detectedContainer = initSegmentUrl ? undefined : containerMimeFromSegment(segments[0]?.url);\n const mimeType = unresolved.type !== 'text' && detectedContainer ? detectedContainer : unresolved.mimeType;\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 mimeType,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":";;;;AAcA,MAAa,eAAe;;AAE5B,MAAa,eAAe;AAM5B,MAAM,8BAAsD;CAC1D,OAAO;CACP,QAAQ;AACV;;AAGA,MAAa,2BAA2B,IAAI,IAAI,OAAO,OAAO,2BAA2B,CAAC;;;;;AAM1F,SAAS,yBAAyB,KAA6C;CAC7E,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,IAAI;CACJ,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,YAAY;CAC3C,QAAQ;EACN,OAAO,IAAI,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM;CAC5C;CACA,MAAM,MAAM,KAAK,YAAY,GAAG;CAChC,OAAO,QAAQ,KAAK,KAAA,IAAY,4BAA4B,KAAK,MAAM,GAAG;AAC5E;;;;;;;;;;;AAuBA,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,OAAO;CAGhC,MAAM,UAAU,WAAW;CAG3B,MAAM,WAAsB,CAAC;CAC7B,IAAI;CACJ,IAAI;CAEJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI;CAEJ,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,KAAK;EAE1B,IAAI,CAAC,WAAY,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,WAAW,MAAM,GACpE;EAGF,IACE,YAAY,aACZ,QAAQ,WAAW,iBAAiB,KACpC,QAAQ,WAAW,wBAAwB,KAC3C,QAAQ,WAAW,uBAAuB,KAC1C,QAAQ,WAAW,6BAA6B,GAEhD;EAIF,MAAM,WAAW,SAAS,SAAS,WAAW;EAC9C,IAAI,UAAU;GACZ,MAAM,MAAM,SAAS,IAAI,KAAK;GAC9B,IAAI,KAAK;IACP,iBAAiB,WAAW,KAAK,OAAO;IACxC,MAAM,eAAe,SAAS,IAAI,WAAW;IAC7C,IAAI,cACF,uBAAuB,eAAe,cAAc,CAAC,KAAK,KAAA;GAE9D;GACA;EACF;EAGA,IAAI,QAAQ,WAAW,UAAU,GAAG;GAClC,kBAAkB,oBAAoB,QAAQ,MAAM,CAAC,CAAC;GACtD;EACF;EAGA,IAAI,QAAQ,WAAW,mBAAmB,GAAG;GAC3C,mBAAmB,eAAe,QAAQ,MAAM,EAAE,GAAG,oBAAoB,KAAK,KAAA;GAC9E;EACF;EAEA,IAAI,YAAY,kBACd;EAIF,IAAI,CAAC,QAAQ,WAAW,GAAG,KAAK,kBAAkB,GAAG;GACnD,MAAM,UAAmB;IACvB,IAAI,WAAW;IACf,KAAK,WAAW,SAAS,OAAO;IAChC,UAAU;IACV,WAAW;GACb;GAEA,IAAI,kBAAkB;IACpB,QAAQ,YAAY;IACpB,uBAAuB,iBAAiB,MAAM;GAChD,OACE,uBAAuB,KAAA;GAGzB,SAAS,KAAK,OAAO;GACrB,eAAe;GACf;GAEA,kBAAkB;GAClB,mBAAmB,KAAA;EACrB;CACF;CAEA,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,KAAA,IACA,iBACE;EAAE,KAAK;EAAgB,GAAI,uBAAuB,EAAE,WAAW,qBAAqB,IAAI,CAAC;CAAG,IAC5F,EAAE,KAAK,GAAG;CAQlB,MAAM,oBAAoB,iBAAiB,KAAA,IAAY,yBAAyB,SAAS,EAAE,EAAE,GAAG;CAChG,MAAM,WAAW,WAAW,SAAS,UAAU,oBAAoB,oBAAoB,WAAW;CAIlG,OAAO;EACL,GAAG;EACH;EACA,WAAW;EACX,UAAU;EACV;EACA;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/hls/parse-media-playlist.ts"],"sourcesContent":["import { isUndefined } from '@videojs/utils/predicate';\nimport {\n type AudioTrack,\n getMediaPlaylistMetadata,\n isResolvedTrack,\n MEDIA_PLAYLIST_METADATA_KEY,\n type MediaPlaylistMetadata,\n type PartiallyResolvedAudioTrack,\n type PartiallyResolvedTextTrack,\n type PartiallyResolvedTrack,\n type PartiallyResolvedVideoTrack,\n type ResolvedTrack,\n type Segment,\n type TextTrack,\n type VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */\nexport const MPEG_TS_MIME = 'video/mp2t';\n/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */\nexport const RAW_AAC_MIME = 'audio/aac';\n\n// Non-fMP4 container MIMEs keyed by segment file extension. fMP4 (the MSE\n// default) always carries an EXT-X-MAP init segment, so a media playlist with\n// no init segment and one of these extensions is a non-fMP4 rendition,\n// relabeled from the fMP4 default. Extend with `.mp3` → 'audio/mpeg' etc.\nconst CONTAINER_MIME_BY_EXTENSION: Record<string, string> = {\n '.ts': MPEG_TS_MIME,\n '.aac': RAW_AAC_MIME,\n};\n\n/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */\nexport const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));\n\n/**\n * Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file\n * extension, ignoring the query string. `undefined` for fMP4 / unrecognized.\n */\nfunction containerMimeFromSegment(url: string | undefined): string | undefined {\n if (!url) return undefined;\n let path: string;\n try {\n path = new URL(url).pathname.toLowerCase();\n } catch {\n path = url.toLowerCase().split('?')[0] ?? '';\n }\n const dot = path.lastIndexOf('.');\n return dot === -1 ? undefined : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];\n}\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 * Position a freshly-parsed window (whose segment `startTime`s are snapshot-\n * local, i.e. from 0) onto the timeline established by the previous resolved\n * snapshot. Carries the timeline forward using the media-sequence overlap and\n * the previous window's *actual* segment durations — see\n * `internal/design/spf/live-presentation-timeline-model.md`.\n *\n * - **Overlap** (`0 <= offset < previous.segments.length`): the new window's\n * first segment is the same segment as `previous.segments[offset]`; anchor to\n * its start (URLs checked — a mismatch warns).\n * - **Sequence went backwards** (non-conformant): reset to the local base.\n * - **Full turnover** (no overlap): estimate forward from the previous window's\n * end across the unseen gap. This is the *only* place EXT-X-TARGETDURATION is\n * used for timing — an upper-bound estimate, since the actual rolled-off\n * durations are gone (exact recovery is the deferred PDT decision).\n *\n * Returns the rebased segments (the window edge is `segments[0].startTime`,\n * derived — never stored on the track).\n */\nfunction placeOnPreviousTimeline(\n previous: ResolvedTrack,\n segments: Segment[],\n mediaSequence: number,\n targetDuration: number\n): Segment[] {\n const prevSegments = previous.segments;\n const localBase = segments[0]?.startTime ?? 0;\n\n if (prevSegments.length === 0 || segments.length === 0) {\n return segments;\n }\n\n const prevMediaSequence = getMediaPlaylistMetadata(previous)?.mediaSequence ?? 0;\n const offset = mediaSequence - prevMediaSequence;\n\n let anchor: number;\n if (offset >= 0 && offset < prevSegments.length) {\n const overlap = prevSegments[offset]!;\n if (overlap.url !== segments[0]!.url) {\n console.warn(\n `[parseMediaPlaylist] media-sequence aligns previous[${offset}] with the new window's first segment, ` +\n `but URLs differ (${overlap.url} vs ${segments[0]!.url}); sequence numbers may be unreliable.`\n );\n }\n anchor = overlap.startTime;\n } else if (offset < 0) {\n console.warn(`[parseMediaPlaylist] media-sequence went backwards (offset ${offset}); resetting timeline.`);\n anchor = localBase;\n } else {\n const last = prevSegments[prevSegments.length - 1]!;\n const newFirst = segments[0]!;\n if (last.startDate !== undefined && newFirst.startDate !== undefined) {\n // PDT bridges the gap exactly — the spec-consistent cross-reload reference,\n // immune to the target-duration over-estimate when actual segment durations\n // differ from the declared ceiling. (`startDate` is intrinsic PDT, unaffected\n // by the new window's local positioning.)\n anchor = last.startTime + (newFirst.startDate - last.startDate);\n } else {\n anchor = last.startTime + last.duration + (offset - prevSegments.length) * targetDuration;\n console.warn(\n `[parseMediaPlaylist] full window turnover (offset ${offset} >= ${prevSegments.length}); ` +\n 'no PDT to bridge — estimating from previous end.'\n );\n }\n }\n\n const shift = anchor - localBase;\n return shift === 0 ? segments : segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift }));\n}\n\n/**\n * Place a window on the frozen wall-clock anchor — the track's `startDate`\n * (wall clock at media-time 0). The anchoring PDT-bearing segment lands at\n * `segment.startDate − anchor`, so the window sits on the shared presentation\n * timeline rather than a local-from-zero one (the `startDate` recomputed\n * afterwards then reads back as the anchor — idempotent). This is the **main\n * live path on every parse**: first resolves place against the pre-stamped\n * shell anchor, and reloads re-place from PDT so drift can't accumulate and a\n * long-stall turnover re-places with no overlap bridging (the HLS authoring\n * spec's §8.1 EXTINF-accuracy rule bounds the per-reload correction to ~one\n * video frame). Falls back to the local base when no segment carries PDT —\n * there's nothing to anchor against (callers guard this for reloads, where the\n * local base would reset the timeline).\n */\nfunction placeOnAnchor(segments: Segment[], anchor: number): Segment[] {\n const anchorSegment = segments.find((segment) => !isUndefined(segment.startDate));\n if (!anchorSegment || isUndefined(anchorSegment.startDate)) {\n return segments;\n }\n\n const shift = anchorSegment.startDate - anchor - anchorSegment.startTime;\n if (shift === 0) {\n return segments;\n }\n\n return segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift }));\n}\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * `previous` is what was known about this track before this parse: the\n * partially-resolved track from the multivariant playlist on the first resolve,\n * or the previously-resolved snapshot on a live reload. Its metadata is carried\n * onto the result either way; when it's already resolved (has segments), its\n * timeline is carried forward so the new window lands on a stable, advancing\n * timeline (see {@link placeOnPreviousTimeline}).\n *\n * @param text - Media playlist text content\n * @param previous - Prior track state (unresolved shell, or previous resolved snapshot)\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n previous: 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 = previous.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 // Absolute wall-clock of the next segment's first sample, in epoch seconds.\n // Seeded by an explicit `#EXT-X-PROGRAM-DATE-TIME` (which re-anchors, e.g.\n // across a discontinuity) and advanced by each segment's duration so segments\n // without their own tag are interpolated forward (per RFC 8216).\n let currentStartDate: number | undefined;\n\n // Playlist-level metadata (surfaced for live reload pacing / merge / termination).\n let targetDuration = 0;\n let mediaSequence = 0;\n let playlistType: 'VOD' | 'EVENT' | undefined;\n let endList = false;\n let holdBack: number | undefined;\n // Low-Latency HLS delivery. Any of the partial-segment tags is enough: the\n // parser ignores parts and the loader fetches whole segments, so this records\n // that the publisher configured LL-HLS, not that we honour it.\n let lowLatency = false;\n // Any EXT-X-KEY with a real METHOD makes the rendition encrypted. Sticky: a\n // clear lead (METHOD=NONE first, a real key later) still counts, since we can\n // only report whether decryption is needed at all.\n let encrypted = false;\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 (trimmed.startsWith('#EXT-X-TARGETDURATION:')) {\n targetDuration = Number.parseInt(trimmed.slice('#EXT-X-TARGETDURATION:'.length), 10) || 0;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {\n mediaSequence = Number.parseInt(trimmed.slice('#EXT-X-MEDIA-SEQUENCE:'.length), 10) || 0;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) {\n const parsed = Date.parse(trimmed.slice('#EXT-X-PROGRAM-DATE-TIME:'.length).trim());\n currentStartDate = Number.isNaN(parsed) ? currentStartDate : parsed / 1000;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:')) {\n const value = trimmed.slice('#EXT-X-PLAYLIST-TYPE:'.length).trim();\n playlistType = value === 'VOD' || value === 'EVENT' ? value : undefined;\n continue;\n }\n\n const key = matchTag(trimmed, 'EXT-X-KEY');\n if (key) {\n const method = key.get('METHOD');\n if (method !== undefined && method !== 'NONE') encrypted = true;\n continue;\n }\n\n // #EXT-X-SERVER-CONTROL — HOLD-BACK supplies the latency target. PART-HOLD-BACK\n // is read for its presence only, as an LL-HLS signal; see MediaPlaylistMetadata\n // on why its value stays out of `holdBack` until LL-HLS support lands.\n const serverControl = matchTag(trimmed, 'EXT-X-SERVER-CONTROL');\n if (serverControl) {\n const value = serverControl.get('HOLD-BACK');\n if (value !== undefined) {\n const parsed = Number.parseFloat(value);\n if (Number.isFinite(parsed) && parsed > 0) holdBack = parsed;\n }\n // Advertised for clients playing partial segments — its presence is an\n // LL-HLS signal even though the value stays deliberately unread.\n if (serverControl.get('PART-HOLD-BACK') !== undefined) lowLatency = true;\n continue;\n }\n\n // Partial segments: noted, then skipped. `#EXT-X-PART` lines would otherwise\n // fall through as unrecognized `#EXT` tags, so LL-HLS delivery went unnoticed.\n if (trimmed.startsWith('#EXT-X-PART-INF') || trimmed.startsWith('#EXT-X-PART:')) {\n lowLatency = true;\n continue;\n }\n\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-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 endList = true;\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${mediaSequence + segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (!isUndefined(currentStartDate)) {\n segment.startDate = currentStartDate;\n // Interpolate forward: the next segment without an explicit tag inherits\n // this anchor plus this segment's duration.\n currentStartDate += currentDuration;\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 complete = endList || playlistType === 'VOD';\n\n // Position this window on the timeline. PDT is primary: whenever the track\n // carries a wall-clock anchor (`startDate` — pre-stamped on the shell for a\n // first resolve, recomputed-and-stable for a reload) and the new window has\n // PDT to place with, place from PDT against that frozen anchor\n // (`placeOnAnchor`) — self-correcting across reloads, history-free across\n // turnovers. Media-sequence/EXTINF carry-forward is the fallback for\n // PDT-less windows; a first resolve with neither anchors at the local base 0.\n const anchor = previous.startDate;\n const hasPdt = segments.some((segment) => !isUndefined(segment.startDate));\n const placed =\n !isUndefined(anchor) && hasPdt\n ? placeOnAnchor(segments, anchor)\n : isResolvedTrack(previous)\n ? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration)\n : segments;\n\n // `duration` is the track's duration: Infinity while the playlist can still\n // grow (unended live), finite once complete (VOD / ENDLIST). Paired with\n // `startTime: 0` below, it spans the presentation origin to the last placed\n // segment's end — so it's measured off `placed`, not the local EXTINF sum:\n // for a window that has slid, the sum is the window's length, not its span.\n // Never the target duration.\n const lastPlaced = placed[placed.length - 1];\n const placedEnd = lastPlaced ? lastPlaced.startTime + lastPlaced.duration : 0;\n const trackDuration = complete ? placedEnd : Number.POSITIVE_INFINITY;\n\n // Wall-clock anchor: `startDate − startTime` for the first PDT-bearing\n // segment (constant along a linear timeline). Maps this track's origin to\n // wall clock; recomputed each parse, so it stays stable as the window slides\n // and is comparable across tracks for A/V alignment.\n const anchorSegment = placed.find((segment) => !isUndefined(segment.startDate));\n const startDate =\n anchorSegment && !isUndefined(anchorSegment.startDate)\n ? anchorSegment.startDate - anchorSegment.startTime\n : undefined;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n previous.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Container detection: fMP4 always carries an EXT-X-MAP init segment, so its\n // absence plus a recognized non-fMP4 segment extension (`.ts` → MPEG-TS,\n // `.aac` → raw ADTS AAC) marks a non-fMP4 rendition (high-precision — never\n // trips on fMP4, which mandates the map). Relabel from the fMP4 default\n // `video/mp4` / `audio/mp4` to the container MIME so capability probing prunes\n // it (these containers are currently treated as unplayable; see `canPlayTrack`).\n const detectedContainer = initSegmentUrl ? undefined : containerMimeFromSegment(placed[0]?.url);\n const mimeType = previous.type !== 'text' && detectedContainer ? detectedContainer : previous.mimeType;\n\n // Generic resolution: All type-specific fields already on `previous` track\n // Just add parsed properties (startTime, duration, segments, initialization, metadata)\n return {\n ...previous,\n mimeType,\n startTime: 0,\n startDate,\n duration: trackDuration,\n segments: placed,\n initialization,\n metadata: {\n ...previous.metadata,\n [MEDIA_PLAYLIST_METADATA_KEY]: {\n targetDuration,\n mediaSequence,\n playlistType,\n lowLatency,\n endList,\n holdBack,\n encrypted,\n } satisfies MediaPlaylistMetadata,\n },\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":"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"}
|
|
@@ -105,7 +105,7 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
105
105
|
};
|
|
106
106
|
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
107
107
|
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
108
|
-
if (codecs?.video) track.codecs = [codecs.video];
|
|
108
|
+
if (codecs?.video) track.codecs = codecs.audio && !stream.audioGroupId ? [codecs.video, codecs.audio] : [codecs.video];
|
|
109
109
|
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
110
110
|
if (stream.audioGroupId) track.audioGroupIds = [stream.audioGroupId];
|
|
111
111
|
videoTracksByUrl.set(stream.uri, track);
|
|
@@ -126,7 +126,7 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
126
126
|
channels: 2
|
|
127
127
|
};
|
|
128
128
|
});
|
|
129
|
-
const audioTracks = [...audioRenditions.
|
|
129
|
+
const audioTracks = [...audioRenditions.flatMap((rendition) => {
|
|
130
130
|
let audioCodecs;
|
|
131
131
|
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
132
132
|
const codecs = parseCodecs(stream.codecs);
|
|
@@ -135,6 +135,17 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
135
135
|
break;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
if (!rendition.uri) {
|
|
139
|
+
const carrier = audioOnlyTracks.find((track) => track.groupId === rendition.groupId);
|
|
140
|
+
if (carrier) {
|
|
141
|
+
carrier.name = rendition.name;
|
|
142
|
+
if (rendition.language) carrier.language = rendition.language;
|
|
143
|
+
if (rendition.channels) carrier.channels = rendition.channels;
|
|
144
|
+
if (rendition.default) carrier.default = rendition.default;
|
|
145
|
+
if (rendition.autoselect) carrier.autoselect = rendition.autoselect;
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
138
149
|
const track = {
|
|
139
150
|
type: "audio",
|
|
140
151
|
id: generateId(),
|
|
@@ -151,7 +162,7 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
151
162
|
if (audioCodecs) track.codecs = audioCodecs;
|
|
152
163
|
if (rendition.default) track.default = rendition.default;
|
|
153
164
|
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
154
|
-
return track;
|
|
165
|
+
return [track];
|
|
155
166
|
}), ...audioOnlyTracks];
|
|
156
167
|
const textTracks = subtitleRenditions.map((rendition) => {
|
|
157
168
|
const track = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport 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 { 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 channels?: number | 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 // CHANNELS is a quoted string whose first parameter is the channel\n // count (\"6\", or \"16/JOC\" for spatial audio); getInt reads the\n // leading integer.\n channels: mediaAttrs.getInt('CHANNELS'),\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, de-duplicating the\n // HLS cross-product: one video rendition is listed across several\n // `EXT-X-STREAM-INF` entries — one per audio group it can pair with, all\n // sharing the same media-playlist URI. Collapse them to one track per URI,\n // accumulating every advertised audio group. (Redundant-stream renditions\n // live at *distinct* per-CDN URIs, so they stay separate — only the same-URI\n // cross-product merges.)\n const videoTracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();\n for (const stream of videoStreams) {\n const existing = videoTracksByUrl.get(stream.uri);\n if (existing) {\n if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) {\n existing.audioGroupIds = [...(existing.audioGroupIds ?? []), stream.audioGroupId];\n }\n // BANDWIDTH is video + audio combined; the duplicates differ only in the\n // paired audio. Keep the lowest as the closest proxy to video-only, which\n // is what ABR should rank on.\n if (stream.bandwidth < existing.bandwidth) {\n existing.bandwidth = stream.bandwidth;\n }\n continue;\n }\n\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.audioGroupIds = [stream.audioGroupId];\n }\n\n videoTracksByUrl.set(stream.uri, track);\n }\n const videoTracks: PartiallyResolvedVideoTrack[] = [...videoTracksByUrl.values()];\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: rendition.channels ?? 2, // From EXT-X-MEDIA CHANNELS; 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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"}
|
|
1
|
+
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport 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 { 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 channels?: number | 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 // CHANNELS is a quoted string whose first parameter is the channel\n // count (\"6\", or \"16/JOC\" for spatial audio); getInt reads the\n // leading integer.\n channels: mediaAttrs.getInt('CHANNELS'),\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, de-duplicating the\n // HLS cross-product: one video rendition is listed across several\n // `EXT-X-STREAM-INF` entries — one per audio group it can pair with, all\n // sharing the same media-playlist URI. Collapse them to one track per URI,\n // accumulating every advertised audio group. (Redundant-stream renditions\n // live at *distinct* per-CDN URIs, so they stay separate — only the same-URI\n // cross-product merges.)\n const videoTracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();\n for (const stream of videoStreams) {\n const existing = videoTracksByUrl.get(stream.uri);\n if (existing) {\n if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) {\n existing.audioGroupIds = [...(existing.audioGroupIds ?? []), stream.audioGroupId];\n }\n // BANDWIDTH is video + audio combined; the duplicates differ only in the\n // paired audio. Keep the lowest as the closest proxy to video-only, which\n // is what ABR should rank on.\n if (stream.bandwidth < existing.bandwidth) {\n existing.bandwidth = stream.bandwidth;\n }\n continue;\n }\n\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 // When the STREAM-INF lists an audio codec but declares no AUDIO group,\n // the audio is muxed into this rendition's segments — keep both codecs so\n // the SourceBuffer mimetype matches the muxed media (otherwise the muxed\n // audio fails to append: \"audio object type does not match the mimetype\").\n // With an AUDIO group the audio is a separate rendition, so only the video\n // codec belongs here.\n track.codecs = codecs.audio && !stream.audioGroupId ? [codecs.video, codecs.audio] : [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupIds = [stream.audioGroupId];\n }\n\n videoTracksByUrl.set(stream.uri, track);\n }\n const videoTracks: PartiallyResolvedVideoTrack[] = [...videoTracksByUrl.values()];\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.flatMap((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 // A rendition with no URI names media carried in the streams referencing its\n // group. When such a stream is audio-only it is already a track of its own, so\n // the two describe one rendition: merge rather than add a second entry that has\n // nothing to fetch. The rendition holds the naming and selection metadata, the\n // stream the URL and bandwidth.\n if (!rendition.uri) {\n const carrier = audioOnlyTracks.find((track) => track.groupId === rendition.groupId);\n if (carrier) {\n carrier.name = rendition.name;\n if (rendition.language) carrier.language = rendition.language;\n if (rendition.channels) carrier.channels = rendition.channels;\n if (rendition.default) carrier.default = rendition.default;\n if (rendition.autoselect) carrier.autoselect = rendition.autoselect;\n return [];\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: rendition.channels ?? 2, // From EXT-X-MEDIA CHANNELS; 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,71 @@
|
|
|
1
|
+
import { getMediaPlaylistMetadata, isResolvedPresentation, isResolvedTrack } from "../types/index.js";
|
|
2
|
+
import { findTrackById } from "../utils/tracks.js";
|
|
3
|
+
//#region src/media/hls/reload-policy.ts
|
|
4
|
+
/** Reload cadence when a playlist carries no usable target duration. */
|
|
5
|
+
const FALLBACK_TARGET_DURATION = 6;
|
|
6
|
+
/**
|
|
7
|
+
* Default `HOLD-BACK` as a multiple of the target duration when the playlist
|
|
8
|
+
* declares none — the HLS spec default (RFC 8216bis `EXT-X-SERVER-CONTROL`).
|
|
9
|
+
*/
|
|
10
|
+
const HOLD_BACK_TARGET_MULTIPLIER = 3;
|
|
11
|
+
/** Identity of a reload snapshot — window position + length. Changes when the window slid or grew. */
|
|
12
|
+
function snapshotSignature(track) {
|
|
13
|
+
return `${getMediaPlaylistMetadata(track)?.mediaSequence ?? 0}:${track.segments.length}`;
|
|
14
|
+
}
|
|
15
|
+
function targetDurationOf(track) {
|
|
16
|
+
return getMediaPlaylistMetadata(track)?.targetDuration || FALLBACK_TARGET_DURATION;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Live media-playlist reload cadence, per RFC 8216bis §6.3.4 — a
|
|
20
|
+
* {@link RecurrencePolicy} for a `RecurringRunner` re-resolving the selected
|
|
21
|
+
* track. Structurally matches `RecurrencePolicy<ResolvedTrack>` without
|
|
22
|
+
* importing it (media stays core-free): `current` is the freshly resolved track,
|
|
23
|
+
* `previous` the prior resolved snapshot.
|
|
24
|
+
*
|
|
25
|
+
* - Complete playlist (VoD, or live that hit `#EXT-X-ENDLIST`) → `null`: stop.
|
|
26
|
+
* Keys off `Track.duration` (finite once complete), the single completeness
|
|
27
|
+
* source of truth.
|
|
28
|
+
* - Unchanged window (same media sequence + segment count as `previous`) → poll
|
|
29
|
+
* at half the target duration; a moved/grown window (or the first reload) →
|
|
30
|
+
* full target duration.
|
|
31
|
+
*
|
|
32
|
+
* A failed reload doesn't reach here — the rejection propagates through the
|
|
33
|
+
* `RecurringRunner`; transient-failure recovery belongs at the fetch layer.
|
|
34
|
+
*
|
|
35
|
+
* Returned delays are milliseconds.
|
|
36
|
+
*/
|
|
37
|
+
function mediaPlaylistReloadDelay(current, previous) {
|
|
38
|
+
if (Number.isFinite(current.duration)) return null;
|
|
39
|
+
const target = targetDurationOf(current);
|
|
40
|
+
return (!previous || snapshotSignature(current) !== snapshotSignature(previous) ? target : target / 2) * 1e3;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Target live latency (seconds) for a resolved track — how far behind the live
|
|
44
|
+
* edge the playhead should sit. Prefers the server's declared
|
|
45
|
+
* `EXT-X-SERVER-CONTROL` `HOLD-BACK`, falling back to
|
|
46
|
+
* {@link HOLD_BACK_TARGET_MULTIPLIER}× the target duration when absent (the
|
|
47
|
+
* spec default). This is the HLS side of the format-neutral
|
|
48
|
+
* `resolveLiveLatency` seam consumed by `seek-to-live-edge`; a DASH engine
|
|
49
|
+
* supplies its own (`suggestedPresentationDelay`).
|
|
50
|
+
*
|
|
51
|
+
* Only `HOLD-BACK` — never `PART-HOLD-BACK`, which assumes partial-segment
|
|
52
|
+
* playback. See {@link MediaPlaylistMetadata.holdBack}.
|
|
53
|
+
*/
|
|
54
|
+
function liveLatencyFor(track) {
|
|
55
|
+
return getMediaPlaylistMetadata(track)?.holdBack ?? HOLD_BACK_TARGET_MULTIPLIER * targetDurationOf(track);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the target live latency for a presentation's timeline-bearing track —
|
|
59
|
+
* the HLS engine injects this as `seekToLiveEdge`'s format-neutral
|
|
60
|
+
* `resolveLiveLatency` seam. `0` when there is no resolved track to read (the
|
|
61
|
+
* behavior then seeks straight to the edge).
|
|
62
|
+
*/
|
|
63
|
+
function resolveLiveLatency(presentation, trackId) {
|
|
64
|
+
if (!isResolvedPresentation(presentation) || !trackId) return 0;
|
|
65
|
+
const track = findTrackById(presentation, trackId);
|
|
66
|
+
return track && isResolvedTrack(track) ? liveLatencyFor(track) : 0;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { liveLatencyFor, mediaPlaylistReloadDelay, resolveLiveLatency };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=reload-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reload-policy.js","names":[],"sources":["../../../../src/media/hls/reload-policy.ts"],"sourcesContent":["import {\n getMediaPlaylistMetadata,\n isResolvedPresentation,\n isResolvedTrack,\n type MaybeResolvedPresentation,\n type ResolvedTrack,\n} from '../types';\nimport { findTrackById } from '../utils/tracks';\n\n/** Reload cadence when a playlist carries no usable target duration. */\nconst FALLBACK_TARGET_DURATION = 6;\n\n/**\n * Default `HOLD-BACK` as a multiple of the target duration when the playlist\n * declares none — the HLS spec default (RFC 8216bis `EXT-X-SERVER-CONTROL`).\n */\nconst HOLD_BACK_TARGET_MULTIPLIER = 3;\n\n/** Identity of a reload snapshot — window position + length. Changes when the window slid or grew. */\nfunction snapshotSignature(track: ResolvedTrack): string {\n return `${getMediaPlaylistMetadata(track)?.mediaSequence ?? 0}:${track.segments.length}`;\n}\n\nfunction targetDurationOf(track: ResolvedTrack): number {\n return getMediaPlaylistMetadata(track)?.targetDuration || FALLBACK_TARGET_DURATION;\n}\n\n/**\n * Live media-playlist reload cadence, per RFC 8216bis §6.3.4 — a\n * {@link RecurrencePolicy} for a `RecurringRunner` re-resolving the selected\n * track. Structurally matches `RecurrencePolicy<ResolvedTrack>` without\n * importing it (media stays core-free): `current` is the freshly resolved track,\n * `previous` the prior resolved snapshot.\n *\n * - Complete playlist (VoD, or live that hit `#EXT-X-ENDLIST`) → `null`: stop.\n * Keys off `Track.duration` (finite once complete), the single completeness\n * source of truth.\n * - Unchanged window (same media sequence + segment count as `previous`) → poll\n * at half the target duration; a moved/grown window (or the first reload) →\n * full target duration.\n *\n * A failed reload doesn't reach here — the rejection propagates through the\n * `RecurringRunner`; transient-failure recovery belongs at the fetch layer.\n *\n * Returned delays are milliseconds.\n */\nexport function mediaPlaylistReloadDelay(current: ResolvedTrack, previous: ResolvedTrack | undefined): number | null {\n if (Number.isFinite(current.duration)) return null;\n\n const target = targetDurationOf(current);\n const changed = !previous || snapshotSignature(current) !== snapshotSignature(previous);\n return (changed ? target : target / 2) * 1000;\n}\n\n/**\n * Target live latency (seconds) for a resolved track — how far behind the live\n * edge the playhead should sit. Prefers the server's declared\n * `EXT-X-SERVER-CONTROL` `HOLD-BACK`, falling back to\n * {@link HOLD_BACK_TARGET_MULTIPLIER}× the target duration when absent (the\n * spec default). This is the HLS side of the format-neutral\n * `resolveLiveLatency` seam consumed by `seek-to-live-edge`; a DASH engine\n * supplies its own (`suggestedPresentationDelay`).\n *\n * Only `HOLD-BACK` — never `PART-HOLD-BACK`, which assumes partial-segment\n * playback. See {@link MediaPlaylistMetadata.holdBack}.\n */\nexport function liveLatencyFor(track: ResolvedTrack): number {\n return getMediaPlaylistMetadata(track)?.holdBack ?? HOLD_BACK_TARGET_MULTIPLIER * targetDurationOf(track);\n}\n\n/**\n * Resolve the target live latency for a presentation's timeline-bearing track —\n * the HLS engine injects this as `seekToLiveEdge`'s format-neutral\n * `resolveLiveLatency` seam. `0` when there is no resolved track to read (the\n * behavior then seeks straight to the edge).\n */\nexport function resolveLiveLatency(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): number {\n if (!isResolvedPresentation(presentation) || !trackId) return 0;\n const track = findTrackById(presentation, trackId);\n return track && isResolvedTrack(track) ? liveLatencyFor(track) : 0;\n}\n"],"mappings":";;;;AAUA,MAAM,2BAA2B;;;;;AAMjC,MAAM,8BAA8B;;AAGpC,SAAS,kBAAkB,OAA8B;CACvD,OAAO,GAAG,yBAAyB,KAAK,CAAC,EAAE,iBAAiB,EAAE,GAAG,MAAM,SAAS;AAClF;AAEA,SAAS,iBAAiB,OAA8B;CACtD,OAAO,yBAAyB,KAAK,CAAC,EAAE,kBAAkB;AAC5D;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,yBAAyB,SAAwB,UAAoD;CACnH,IAAI,OAAO,SAAS,QAAQ,QAAQ,GAAG,OAAO;CAE9C,MAAM,SAAS,iBAAiB,OAAO;CAEvC,QADgB,CAAC,YAAY,kBAAkB,OAAO,MAAM,kBAAkB,QAAQ,IACpE,SAAS,SAAS,KAAK;AAC3C;;;;;;;;;;;;;AAcA,SAAgB,eAAe,OAA8B;CAC3D,OAAO,yBAAyB,KAAK,CAAC,EAAE,YAAY,8BAA8B,iBAAiB,KAAK;AAC1G;;;;;;;AAQA,SAAgB,mBACd,cACA,SACQ;CACR,IAAI,CAAC,uBAAuB,YAAY,KAAK,CAAC,SAAS,OAAO;CAC9D,MAAM,QAAQ,cAAc,cAAc,OAAO;CACjD,OAAO,SAAS,gBAAgB,KAAK,IAAI,eAAe,KAAK,IAAI;AACnE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isResolvedPresentation, isResolvedTrack } from "./types/index.js";
|
|
2
|
+
import { findTrackById } from "./utils/tracks.js";
|
|
3
|
+
//#region src/media/live-window.ts
|
|
4
|
+
/**
|
|
5
|
+
* Derive the live window of the track with the given id — the single source of
|
|
6
|
+
* truth for "where is live," consumed (via `liveWindowFromState`) by the
|
|
7
|
+
* seek-to-live-edge and live-seekable-range behaviors so neither re-derives (or
|
|
8
|
+
* re-presumes) the window shape. Type-agnostic: the caller decides which track
|
|
9
|
+
* bears the timeline (video when present, else audio).
|
|
10
|
+
*
|
|
11
|
+
* The window is a **live read** over the current `segments` array — never
|
|
12
|
+
* stored on the track. The anchor triple is frozen per source; per-segment
|
|
13
|
+
* `startTime` values are re-derived against it on every reload, so the edge
|
|
14
|
+
* here slides while the origin stays fixed (see
|
|
15
|
+
* `internal/design/spf/live-presentation-timeline-model.md`).
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when there is no live edge to track: an unresolved
|
|
18
|
+
* presentation or track, a track with no segments, or a **complete** playlist
|
|
19
|
+
* (VoD, or live that has ended — a finite `Track.duration`).
|
|
20
|
+
*/
|
|
21
|
+
function liveWindowFor(presentation, trackId) {
|
|
22
|
+
if (!isResolvedPresentation(presentation) || !trackId) return null;
|
|
23
|
+
const track = findTrackById(presentation, trackId);
|
|
24
|
+
if (!track || !isResolvedTrack(track) || track.segments.length === 0) return null;
|
|
25
|
+
if (Number.isFinite(track.duration)) return null;
|
|
26
|
+
const { segments } = track;
|
|
27
|
+
const last = segments[segments.length - 1];
|
|
28
|
+
return {
|
|
29
|
+
start: segments[0].startTime,
|
|
30
|
+
end: last.startTime + last.duration
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { liveWindowFor };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=live-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-window.js","names":[],"sources":["../../../src/media/live-window.ts"],"sourcesContent":["/**\n * Derive the live window of the track with the given id — the single source of\n * truth for \"where is live,\" consumed (via `liveWindowFromState`) by the\n * seek-to-live-edge and live-seekable-range behaviors so neither re-derives (or\n * re-presumes) the window shape. Type-agnostic: the caller decides which track\n * bears the timeline (video when present, else audio).\n *\n * The window is a **live read** over the current `segments` array — never\n * stored on the track. The anchor triple is frozen per source; per-segment\n * `startTime` values are re-derived against it on every reload, so the edge\n * here slides while the origin stays fixed (see\n * `internal/design/spf/live-presentation-timeline-model.md`).\n *\n * Returns `null` when there is no live edge to track: an unresolved\n * presentation or track, a track with no segments, or a **complete** playlist\n * (VoD, or live that has ended — a finite `Track.duration`).\n */\nimport { isResolvedPresentation, isResolvedTrack, type MaybeResolvedPresentation } from './types';\nimport { findTrackById } from './utils/tracks';\n\nexport interface LiveWindow {\n /** Earliest time still in the window (seconds, presentation timeline). */\n start: number;\n /** The live edge — latest time in the window (seconds). */\n end: number;\n}\n\nexport function liveWindowFor(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): LiveWindow | null {\n if (!isResolvedPresentation(presentation) || !trackId) return null;\n\n const track = findTrackById(presentation, trackId);\n if (!track || !isResolvedTrack(track) || track.segments.length === 0) return null;\n\n // Complete playlist (VoD, or live that has ended) → no live edge. `Track.duration`\n // is the parser's completeness signal (finite = complete, Infinity = still growing).\n if (Number.isFinite(track.duration)) return null;\n\n const { segments } = track;\n const last = segments[segments.length - 1]!;\n return {\n start: segments[0]!.startTime,\n end: last.startTime + last.duration,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,cACd,cACA,SACmB;CACnB,IAAI,CAAC,uBAAuB,YAAY,KAAK,CAAC,SAAS,OAAO;CAE9D,MAAM,QAAQ,cAAc,cAAc,OAAO;CACjD,IAAI,CAAC,SAAS,CAAC,gBAAgB,KAAK,KAAK,MAAM,SAAS,WAAW,GAAG,OAAO;CAI7E,IAAI,OAAO,SAAS,MAAM,QAAQ,GAAG,OAAO;CAE5C,MAAM,EAAE,aAAa;CACrB,MAAM,OAAO,SAAS,SAAS,SAAS;CACxC,OAAO;EACL,OAAO,SAAS,EAAE,CAAE;EACpB,KAAK,KAAK,YAAY,KAAK;CAC7B;AACF"}
|
|
@@ -29,29 +29,6 @@ function pickFirstTrackId(presentation, type) {
|
|
|
29
29
|
return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* Translates a "max resolution" into a total total pixel area
|
|
33
|
-
* for comparisons with video track resolutions with an assumed
|
|
34
|
-
* 16:9 ratio.
|
|
35
|
-
*
|
|
36
|
-
* Example: "720p" translates to a 921600 pixel area.
|
|
37
|
-
*
|
|
38
|
-
* Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600
|
|
39
|
-
*
|
|
40
|
-
* Accepts:
|
|
41
|
-
* - string with the format '{height}p'. ('720p')
|
|
42
|
-
* - bare number, interpreted as pixel area. (921_600)
|
|
43
|
-
* - anything else will translate to `+Infinity`, meaning no cap specified
|
|
44
|
-
*/
|
|
45
|
-
function maxResolutionToPixelArea(value) {
|
|
46
|
-
if (value === void 0 || value === null) return Number.POSITIVE_INFINITY;
|
|
47
|
-
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;
|
|
48
|
-
const match = value.trim().match(/^(\d+)p?$/i);
|
|
49
|
-
if (!match) return Number.POSITIVE_INFINITY;
|
|
50
|
-
const height = Number(match[1]);
|
|
51
|
-
if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;
|
|
52
|
-
return height * height * 16 / 9;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
32
|
* Pick the track with the highest pixel area at or below `maxPixelArea`.
|
|
56
33
|
* Falls back to the lowest track when nothing satisfies the cap (the
|
|
57
34
|
* lowest of the above-cap set is the closest to the cap from above).
|
|
@@ -125,6 +102,6 @@ function pickTextTrackFromTracks(tracks, config) {
|
|
|
125
102
|
}
|
|
126
103
|
}
|
|
127
104
|
//#endregion
|
|
128
|
-
export { matchesPartialTrack,
|
|
105
|
+
export { matchesPartialTrack, pickAudioTrack, pickFirstTrackId, pickHighestResolutionVideoTrack, pickTextTrackFromTracks, pickTrackUnderPixelArea };
|
|
129
106
|
|
|
130
107
|
//# sourceMappingURL=select-tracks.js.map
|