@zenvor/hls.js 1.0.0
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/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides methods dealing with playlist sliding and drift
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { stringify } from './safe-json-stringify';
|
|
6
|
+
import { DateRange } from '../loader/date-range';
|
|
7
|
+
import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
|
|
8
|
+
import type { ILogger } from './logger';
|
|
9
|
+
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
|
|
10
|
+
import type { LevelDetails } from '../loader/level-details';
|
|
11
|
+
import type { Level } from '../types/level';
|
|
12
|
+
|
|
13
|
+
type FragmentIntersection = (
|
|
14
|
+
oldFrag: MediaFragment,
|
|
15
|
+
newFrag: MediaFragment,
|
|
16
|
+
newFragIndex: number,
|
|
17
|
+
newFragments: MediaFragment[],
|
|
18
|
+
) => void;
|
|
19
|
+
type PartIntersection = (oldPart: Part, newPart: Part) => void;
|
|
20
|
+
|
|
21
|
+
export function updatePTS(
|
|
22
|
+
fragments: MediaFragment[],
|
|
23
|
+
fromIdx: number,
|
|
24
|
+
toIdx: number,
|
|
25
|
+
): void {
|
|
26
|
+
const fragFrom = fragments[fromIdx];
|
|
27
|
+
const fragTo = fragments[toIdx];
|
|
28
|
+
updateFromToPTS(fragFrom, fragTo);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
|
|
32
|
+
const fragToPTS = fragTo.startPTS as number;
|
|
33
|
+
// if we know startPTS[toIdx]
|
|
34
|
+
if (Number.isFinite(fragToPTS)) {
|
|
35
|
+
// update fragment duration.
|
|
36
|
+
// it helps to fix drifts between playlist reported duration and fragment real duration
|
|
37
|
+
let duration: number = 0;
|
|
38
|
+
let frag: Fragment;
|
|
39
|
+
if (fragTo.sn > fragFrom.sn) {
|
|
40
|
+
duration = fragToPTS - fragFrom.start;
|
|
41
|
+
frag = fragFrom;
|
|
42
|
+
} else {
|
|
43
|
+
duration = fragFrom.start - fragToPTS;
|
|
44
|
+
frag = fragTo;
|
|
45
|
+
}
|
|
46
|
+
if (frag.duration !== duration) {
|
|
47
|
+
frag.setDuration(duration);
|
|
48
|
+
}
|
|
49
|
+
// we dont know startPTS[toIdx]
|
|
50
|
+
} else if (fragTo.sn > fragFrom.sn) {
|
|
51
|
+
const contiguous = fragFrom.cc === fragTo.cc;
|
|
52
|
+
// TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS
|
|
53
|
+
if (contiguous && fragFrom.minEndPTS) {
|
|
54
|
+
fragTo.setStart(fragFrom.start + (fragFrom.minEndPTS - fragFrom.start));
|
|
55
|
+
} else {
|
|
56
|
+
fragTo.setStart(fragFrom.start + fragFrom.duration);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
fragTo.setStart(Math.max(fragFrom.start - fragTo.duration, 0));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function updateFragPTSDTS(
|
|
64
|
+
details: LevelDetails | undefined,
|
|
65
|
+
frag: MediaFragment,
|
|
66
|
+
startPTS: number,
|
|
67
|
+
endPTS: number,
|
|
68
|
+
startDTS: number,
|
|
69
|
+
endDTS: number,
|
|
70
|
+
logger: ILogger,
|
|
71
|
+
): number {
|
|
72
|
+
const parsedMediaDuration = endPTS - startPTS;
|
|
73
|
+
if (parsedMediaDuration <= 0) {
|
|
74
|
+
logger.warn('Fragment should have a positive duration', frag);
|
|
75
|
+
endPTS = startPTS + frag.duration;
|
|
76
|
+
endDTS = startDTS + frag.duration;
|
|
77
|
+
}
|
|
78
|
+
let maxStartPTS = startPTS;
|
|
79
|
+
let minEndPTS = endPTS;
|
|
80
|
+
const fragStartPts = frag.startPTS as number;
|
|
81
|
+
const fragEndPts = frag.endPTS as number;
|
|
82
|
+
if (Number.isFinite(fragStartPts)) {
|
|
83
|
+
// delta PTS between audio and video
|
|
84
|
+
const deltaPTS = Math.abs(fragStartPts - startPTS);
|
|
85
|
+
if (details && deltaPTS > details.totalduration) {
|
|
86
|
+
logger.warn(
|
|
87
|
+
`media timestamps and playlist times differ by ${deltaPTS}s for level ${frag.level} ${details.url}`,
|
|
88
|
+
);
|
|
89
|
+
} else if (!Number.isFinite(frag.deltaPTS as number)) {
|
|
90
|
+
frag.deltaPTS = deltaPTS;
|
|
91
|
+
} else {
|
|
92
|
+
frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS as number);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
maxStartPTS = Math.max(startPTS, fragStartPts);
|
|
96
|
+
startPTS = Math.min(startPTS, fragStartPts);
|
|
97
|
+
startDTS =
|
|
98
|
+
frag.startDTS !== undefined
|
|
99
|
+
? Math.min(startDTS, frag.startDTS)
|
|
100
|
+
: startDTS;
|
|
101
|
+
|
|
102
|
+
minEndPTS = Math.min(endPTS, fragEndPts);
|
|
103
|
+
endPTS = Math.max(endPTS, fragEndPts);
|
|
104
|
+
endDTS = frag.endDTS !== undefined ? Math.max(endDTS, frag.endDTS) : endDTS;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const drift = startPTS - frag.start;
|
|
108
|
+
if (frag.start !== 0) {
|
|
109
|
+
frag.setStart(startPTS);
|
|
110
|
+
}
|
|
111
|
+
frag.setDuration(endPTS - frag.start);
|
|
112
|
+
frag.startPTS = startPTS;
|
|
113
|
+
frag.maxStartPTS = maxStartPTS;
|
|
114
|
+
frag.startDTS = startDTS;
|
|
115
|
+
frag.endPTS = endPTS;
|
|
116
|
+
frag.minEndPTS = minEndPTS;
|
|
117
|
+
frag.endDTS = endDTS;
|
|
118
|
+
|
|
119
|
+
const sn = frag.sn;
|
|
120
|
+
// exit if sn out of range
|
|
121
|
+
if (!details || sn < details.startSN || sn > details.endSN) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
let i: number;
|
|
125
|
+
const fragIdx = sn - details.startSN;
|
|
126
|
+
const fragments = details.fragments;
|
|
127
|
+
// update frag reference in fragments array
|
|
128
|
+
// rationale is that fragments array might not contain this frag object.
|
|
129
|
+
// this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
|
|
130
|
+
// if we don't update frag, we won't be able to propagate PTS info on the playlist
|
|
131
|
+
// resulting in invalid sliding computation
|
|
132
|
+
fragments[fragIdx] = frag;
|
|
133
|
+
// adjust fragment PTS/duration from seqnum-1 to frag 0
|
|
134
|
+
for (i = fragIdx; i > 0; i--) {
|
|
135
|
+
updateFromToPTS(fragments[i], fragments[i - 1]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// adjust fragment PTS/duration from seqnum to last frag
|
|
139
|
+
for (i = fragIdx; i < fragments.length - 1; i++) {
|
|
140
|
+
updateFromToPTS(fragments[i], fragments[i + 1]);
|
|
141
|
+
}
|
|
142
|
+
if (details.fragmentHint) {
|
|
143
|
+
updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
details.PTSKnown = details.alignedSliding = true;
|
|
147
|
+
return drift;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function mergeDetails(
|
|
151
|
+
oldDetails: LevelDetails,
|
|
152
|
+
newDetails: LevelDetails,
|
|
153
|
+
logger: ILogger,
|
|
154
|
+
) {
|
|
155
|
+
if (oldDetails === newDetails) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Track the last initSegment processed. Initialize it to the last one on the timeline.
|
|
159
|
+
let currentInitSegment: Fragment | null = null;
|
|
160
|
+
const oldFragments = oldDetails.fragments;
|
|
161
|
+
for (let i = oldFragments.length - 1; i >= 0; i--) {
|
|
162
|
+
const oldInit = oldFragments[i].initSegment;
|
|
163
|
+
if (oldInit) {
|
|
164
|
+
currentInitSegment = oldInit;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (oldDetails.fragmentHint) {
|
|
170
|
+
// prevent PTS and duration from being adjusted on the next hint
|
|
171
|
+
delete oldDetails.fragmentHint.endPTS;
|
|
172
|
+
}
|
|
173
|
+
// check if old/new playlists have fragments in common
|
|
174
|
+
// loop through overlapping SN and update startPTS, cc, and duration if any found
|
|
175
|
+
let PTSFrag: MediaFragment | undefined;
|
|
176
|
+
mapFragmentIntersection(
|
|
177
|
+
oldDetails,
|
|
178
|
+
newDetails,
|
|
179
|
+
(oldFrag, newFrag, newFragIndex, newFragments) => {
|
|
180
|
+
if (
|
|
181
|
+
(!newDetails.startCC || newDetails.skippedSegments) &&
|
|
182
|
+
newFrag.cc !== oldFrag.cc
|
|
183
|
+
) {
|
|
184
|
+
const ccOffset = oldFrag.cc - newFrag.cc;
|
|
185
|
+
for (let i = newFragIndex; i < newFragments.length; i++) {
|
|
186
|
+
newFragments[i].cc += ccOffset;
|
|
187
|
+
}
|
|
188
|
+
newDetails.endCC = newFragments[newFragments.length - 1].cc;
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
Number.isFinite(oldFrag.startPTS) &&
|
|
192
|
+
Number.isFinite(oldFrag.endPTS)
|
|
193
|
+
) {
|
|
194
|
+
newFrag.setStart((newFrag.startPTS = oldFrag.startPTS!));
|
|
195
|
+
newFrag.startDTS = oldFrag.startDTS;
|
|
196
|
+
newFrag.maxStartPTS = oldFrag.maxStartPTS;
|
|
197
|
+
|
|
198
|
+
newFrag.endPTS = oldFrag.endPTS;
|
|
199
|
+
newFrag.endDTS = oldFrag.endDTS;
|
|
200
|
+
newFrag.minEndPTS = oldFrag.minEndPTS;
|
|
201
|
+
newFrag.setDuration(oldFrag.endPTS! - oldFrag.startPTS!);
|
|
202
|
+
|
|
203
|
+
if (newFrag.duration) {
|
|
204
|
+
PTSFrag = newFrag;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// PTS is known when any segment has startPTS and endPTS
|
|
208
|
+
newDetails.PTSKnown = newDetails.alignedSliding = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (oldFrag.hasStreams) {
|
|
212
|
+
newFrag.elementaryStreams = oldFrag.elementaryStreams;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
newFrag.loader = oldFrag.loader;
|
|
216
|
+
|
|
217
|
+
if (oldFrag.hasStats) {
|
|
218
|
+
newFrag.stats = oldFrag.stats;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (oldFrag.initSegment) {
|
|
222
|
+
newFrag.initSegment = oldFrag.initSegment;
|
|
223
|
+
currentInitSegment = oldFrag.initSegment;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const newFragments = newDetails.fragments;
|
|
229
|
+
const fragmentsToCheck = newDetails.fragmentHint
|
|
230
|
+
? newFragments.concat(newDetails.fragmentHint)
|
|
231
|
+
: newFragments;
|
|
232
|
+
if (currentInitSegment) {
|
|
233
|
+
fragmentsToCheck.forEach((frag) => {
|
|
234
|
+
if (
|
|
235
|
+
(frag as any) &&
|
|
236
|
+
(!frag.initSegment ||
|
|
237
|
+
frag.initSegment.relurl === currentInitSegment?.relurl)
|
|
238
|
+
) {
|
|
239
|
+
frag.initSegment = currentInitSegment;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (newDetails.skippedSegments) {
|
|
245
|
+
newDetails.deltaUpdateFailed = newFragments.some((frag) => !frag as any);
|
|
246
|
+
if (newDetails.deltaUpdateFailed) {
|
|
247
|
+
logger.warn(
|
|
248
|
+
'[level-helper] Previous playlist missing segments skipped in delta playlist',
|
|
249
|
+
);
|
|
250
|
+
for (let i = newDetails.skippedSegments; i--; ) {
|
|
251
|
+
newFragments.shift();
|
|
252
|
+
}
|
|
253
|
+
newDetails.startSN = newFragments[0].sn;
|
|
254
|
+
} else {
|
|
255
|
+
if (newDetails.canSkipDateRanges) {
|
|
256
|
+
newDetails.dateRanges = mergeDateRanges(
|
|
257
|
+
oldDetails.dateRanges,
|
|
258
|
+
newDetails,
|
|
259
|
+
logger,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const programDateTimes = oldDetails.fragments.filter(
|
|
263
|
+
(frag) => frag.rawProgramDateTime,
|
|
264
|
+
);
|
|
265
|
+
if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
|
|
266
|
+
for (let i = 1; i < fragmentsToCheck.length; i++) {
|
|
267
|
+
if (fragmentsToCheck[i].programDateTime === null) {
|
|
268
|
+
assignProgramDateTime(
|
|
269
|
+
fragmentsToCheck[i],
|
|
270
|
+
fragmentsToCheck[i - 1],
|
|
271
|
+
programDateTimes,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
mapDateRanges(programDateTimes, newDetails);
|
|
277
|
+
}
|
|
278
|
+
newDetails.endCC = newFragments[newFragments.length - 1].cc;
|
|
279
|
+
}
|
|
280
|
+
if (!newDetails.startCC) {
|
|
281
|
+
const fragPriorToNewStart = getFragmentWithSN(
|
|
282
|
+
oldDetails,
|
|
283
|
+
newDetails.startSN - 1,
|
|
284
|
+
);
|
|
285
|
+
newDetails.startCC = fragPriorToNewStart?.cc ?? newFragments[0].cc;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Merge parts
|
|
289
|
+
mapPartIntersection(
|
|
290
|
+
oldDetails.partList,
|
|
291
|
+
newDetails.partList,
|
|
292
|
+
(oldPart: Part, newPart: Part) => {
|
|
293
|
+
newPart.elementaryStreams = oldPart.elementaryStreams;
|
|
294
|
+
newPart.stats = oldPart.stats;
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// if at least one fragment contains PTS info, recompute PTS information for all fragments
|
|
299
|
+
if (PTSFrag) {
|
|
300
|
+
updateFragPTSDTS(
|
|
301
|
+
newDetails,
|
|
302
|
+
PTSFrag,
|
|
303
|
+
PTSFrag.startPTS as number,
|
|
304
|
+
PTSFrag.endPTS as number,
|
|
305
|
+
PTSFrag.startDTS as number,
|
|
306
|
+
PTSFrag.endDTS as number,
|
|
307
|
+
logger,
|
|
308
|
+
);
|
|
309
|
+
} else {
|
|
310
|
+
// ensure that delta is within oldFragments range
|
|
311
|
+
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
|
|
312
|
+
// in that case we also need to adjust start offset of all fragments
|
|
313
|
+
adjustSliding(oldDetails, newDetails);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (newFragments.length) {
|
|
317
|
+
newDetails.totalduration = newDetails.edge - newFragments[0].start;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
newDetails.driftStartTime = oldDetails.driftStartTime;
|
|
321
|
+
newDetails.driftStart = oldDetails.driftStart;
|
|
322
|
+
const advancedDateTime = newDetails.advancedDateTime;
|
|
323
|
+
if (newDetails.advanced && advancedDateTime) {
|
|
324
|
+
const edge = newDetails.edge;
|
|
325
|
+
if (!newDetails.driftStart) {
|
|
326
|
+
newDetails.driftStartTime = advancedDateTime;
|
|
327
|
+
newDetails.driftStart = edge;
|
|
328
|
+
}
|
|
329
|
+
newDetails.driftEndTime = advancedDateTime;
|
|
330
|
+
newDetails.driftEnd = edge;
|
|
331
|
+
} else {
|
|
332
|
+
newDetails.driftEndTime = oldDetails.driftEndTime;
|
|
333
|
+
newDetails.driftEnd = oldDetails.driftEnd;
|
|
334
|
+
newDetails.advancedDateTime = oldDetails.advancedDateTime;
|
|
335
|
+
}
|
|
336
|
+
if (newDetails.requestScheduled === -1) {
|
|
337
|
+
newDetails.requestScheduled = oldDetails.requestScheduled;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function mergeDateRanges(
|
|
342
|
+
oldDateRanges: Record<string, DateRange | undefined>,
|
|
343
|
+
newDetails: LevelDetails,
|
|
344
|
+
logger: ILogger,
|
|
345
|
+
): Record<string, DateRange | undefined> {
|
|
346
|
+
const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
|
|
347
|
+
const dateRanges = Object.assign({}, oldDateRanges);
|
|
348
|
+
if (recentlyRemovedDateranges) {
|
|
349
|
+
recentlyRemovedDateranges.forEach((id) => {
|
|
350
|
+
delete dateRanges[id];
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const mergeIds = Object.keys(dateRanges);
|
|
354
|
+
const mergeCount = mergeIds.length;
|
|
355
|
+
if (!mergeCount) {
|
|
356
|
+
return deltaDateRanges;
|
|
357
|
+
}
|
|
358
|
+
Object.keys(deltaDateRanges).forEach((id) => {
|
|
359
|
+
const mergedDateRange = dateRanges[id];
|
|
360
|
+
const dateRange = new DateRange(deltaDateRanges[id]!.attr, mergedDateRange);
|
|
361
|
+
if (dateRange.isValid) {
|
|
362
|
+
dateRanges[id] = dateRange;
|
|
363
|
+
if (!mergedDateRange) {
|
|
364
|
+
dateRange.tagOrder += mergeCount;
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
logger.warn(
|
|
368
|
+
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${stringify(
|
|
369
|
+
deltaDateRanges[id]!.attr,
|
|
370
|
+
)}"`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return dateRanges;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function mapPartIntersection(
|
|
378
|
+
oldParts: Part[] | null,
|
|
379
|
+
newParts: Part[] | null,
|
|
380
|
+
intersectionFn: PartIntersection,
|
|
381
|
+
) {
|
|
382
|
+
if (oldParts && newParts) {
|
|
383
|
+
let delta = 0;
|
|
384
|
+
for (let i = 0, len = oldParts.length; i <= len; i++) {
|
|
385
|
+
const oldPart = oldParts[i];
|
|
386
|
+
const newPart = newParts[i + delta];
|
|
387
|
+
if (
|
|
388
|
+
(oldPart as any) &&
|
|
389
|
+
(newPart as any) &&
|
|
390
|
+
oldPart.index === newPart.index &&
|
|
391
|
+
oldPart.fragment.sn === newPart.fragment.sn
|
|
392
|
+
) {
|
|
393
|
+
intersectionFn(oldPart, newPart);
|
|
394
|
+
} else {
|
|
395
|
+
delta--;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function mapFragmentIntersection(
|
|
402
|
+
oldDetails: LevelDetails,
|
|
403
|
+
newDetails: LevelDetails,
|
|
404
|
+
intersectionFn: FragmentIntersection,
|
|
405
|
+
) {
|
|
406
|
+
const skippedSegments = newDetails.skippedSegments;
|
|
407
|
+
const start =
|
|
408
|
+
Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN;
|
|
409
|
+
const end =
|
|
410
|
+
(oldDetails.fragmentHint ? 1 : 0) +
|
|
411
|
+
(skippedSegments
|
|
412
|
+
? newDetails.endSN
|
|
413
|
+
: Math.min(oldDetails.endSN, newDetails.endSN)) -
|
|
414
|
+
newDetails.startSN;
|
|
415
|
+
const delta = newDetails.startSN - oldDetails.startSN;
|
|
416
|
+
const newFrags = newDetails.fragmentHint
|
|
417
|
+
? newDetails.fragments.concat(newDetails.fragmentHint)
|
|
418
|
+
: newDetails.fragments;
|
|
419
|
+
const oldFrags = oldDetails.fragmentHint
|
|
420
|
+
? oldDetails.fragments.concat(oldDetails.fragmentHint)
|
|
421
|
+
: oldDetails.fragments;
|
|
422
|
+
|
|
423
|
+
for (let i = start; i <= end; i++) {
|
|
424
|
+
const oldFrag = oldFrags[delta + i];
|
|
425
|
+
let newFrag = newFrags[i];
|
|
426
|
+
if (skippedSegments && (!newFrag as any) && (oldFrag as any)) {
|
|
427
|
+
// Fill in skipped segments in delta playlist
|
|
428
|
+
newFrag = newDetails.fragments[i] = oldFrag;
|
|
429
|
+
}
|
|
430
|
+
if ((oldFrag as any) && (newFrag as any)) {
|
|
431
|
+
intersectionFn(oldFrag, newFrag, i, newFrags);
|
|
432
|
+
const uriBefore = oldFrag.relurl;
|
|
433
|
+
const uriAfter = newFrag.relurl;
|
|
434
|
+
if (uriBefore && notEqualAfterStrippingQueries(uriBefore, uriAfter)) {
|
|
435
|
+
newDetails.playlistParsingError = getSequenceError(
|
|
436
|
+
`media sequence mismatch ${newFrag.sn}:`,
|
|
437
|
+
oldDetails,
|
|
438
|
+
newDetails,
|
|
439
|
+
oldFrag,
|
|
440
|
+
newFrag,
|
|
441
|
+
);
|
|
442
|
+
return;
|
|
443
|
+
} else if (oldFrag.cc !== newFrag.cc) {
|
|
444
|
+
newDetails.playlistParsingError = getSequenceError(
|
|
445
|
+
`discontinuity sequence mismatch (${oldFrag.cc}!=${newFrag.cc})`,
|
|
446
|
+
oldDetails,
|
|
447
|
+
newDetails,
|
|
448
|
+
oldFrag,
|
|
449
|
+
newFrag,
|
|
450
|
+
);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getSequenceError(
|
|
458
|
+
message: string,
|
|
459
|
+
oldDetails: LevelDetails,
|
|
460
|
+
newDetails: LevelDetails,
|
|
461
|
+
oldFrag: MediaFragment,
|
|
462
|
+
newFrag: MediaFragment,
|
|
463
|
+
): Error {
|
|
464
|
+
return new Error(
|
|
465
|
+
`${message} ${newFrag.url}
|
|
466
|
+
Playlist starting @${oldDetails.startSN}
|
|
467
|
+
${oldDetails.m3u8}
|
|
468
|
+
|
|
469
|
+
Playlist starting @${newDetails.startSN}
|
|
470
|
+
${newDetails.m3u8}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function adjustSliding(
|
|
475
|
+
oldDetails: LevelDetails,
|
|
476
|
+
newDetails: LevelDetails,
|
|
477
|
+
matchingStableVariantOrRendition: boolean = true,
|
|
478
|
+
logger?: ILogger,
|
|
479
|
+
): number {
|
|
480
|
+
const delta =
|
|
481
|
+
newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
|
|
482
|
+
const oldFragments = oldDetails.fragments;
|
|
483
|
+
const advancedOrStable = delta >= 0;
|
|
484
|
+
let sliding = 0;
|
|
485
|
+
if (advancedOrStable && delta < oldFragments.length) {
|
|
486
|
+
sliding = oldFragments[delta].start;
|
|
487
|
+
logger?.log(
|
|
488
|
+
`Aligning playlists based on SN ${newDetails.startSN} (diff: ${sliding})`,
|
|
489
|
+
);
|
|
490
|
+
} else if (advancedOrStable && newDetails.startSN === oldDetails.endSN + 1) {
|
|
491
|
+
sliding = oldDetails.fragmentEnd;
|
|
492
|
+
logger?.log(
|
|
493
|
+
`Aligning playlists based on first/last SN ${newDetails.startSN} (diff: ${sliding})`,
|
|
494
|
+
);
|
|
495
|
+
} else if (advancedOrStable && matchingStableVariantOrRendition) {
|
|
496
|
+
// align with expected position (updated playlist start sequence is past end sequence of last update)
|
|
497
|
+
sliding = oldDetails.fragmentStart + delta * newDetails.levelTargetDuration;
|
|
498
|
+
} else if (!newDetails.skippedSegments && newDetails.fragmentStart === 0) {
|
|
499
|
+
// align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment)
|
|
500
|
+
sliding = oldDetails.fragmentStart;
|
|
501
|
+
logger?.log(
|
|
502
|
+
`Aligning playlists based on first SN ${newDetails.startSN} (diff: ${sliding})`,
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
// new details already has a sliding offset or has skipped segments
|
|
506
|
+
return sliding; // 0
|
|
507
|
+
}
|
|
508
|
+
addSliding(newDetails, sliding);
|
|
509
|
+
return sliding;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function addSliding(details: LevelDetails, sliding: number) {
|
|
513
|
+
if (sliding) {
|
|
514
|
+
const fragments = details.fragments;
|
|
515
|
+
for (let i = details.skippedSegments; i < fragments.length; i++) {
|
|
516
|
+
fragments[i].addStart(sliding);
|
|
517
|
+
}
|
|
518
|
+
if (details.fragmentHint) {
|
|
519
|
+
details.fragmentHint.addStart(sliding);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function computeReloadInterval(
|
|
525
|
+
newDetails: LevelDetails,
|
|
526
|
+
distanceToLiveEdgeMs: number = Infinity,
|
|
527
|
+
): number {
|
|
528
|
+
let reloadInterval = 1000 * newDetails.targetduration;
|
|
529
|
+
|
|
530
|
+
if (newDetails.updated) {
|
|
531
|
+
// Use last segment duration when shorter than target duration and near live edge
|
|
532
|
+
const fragments = newDetails.fragments;
|
|
533
|
+
const liveEdgeMaxTargetDurations = 4;
|
|
534
|
+
if (
|
|
535
|
+
fragments.length &&
|
|
536
|
+
reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs
|
|
537
|
+
) {
|
|
538
|
+
const lastSegmentDuration =
|
|
539
|
+
fragments[fragments.length - 1].duration * 1000;
|
|
540
|
+
if (lastSegmentDuration < reloadInterval) {
|
|
541
|
+
reloadInterval = lastSegmentDuration;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// estimate = 'miss half average';
|
|
546
|
+
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
|
|
547
|
+
// changed then it MUST wait for a period of one-half the target
|
|
548
|
+
// duration before retrying.
|
|
549
|
+
reloadInterval /= 2;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return Math.round(reloadInterval);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function getFragmentWithSN(
|
|
556
|
+
details: LevelDetails | undefined,
|
|
557
|
+
sn: number,
|
|
558
|
+
fragCurrent?: Fragment | null,
|
|
559
|
+
): MediaFragment | null {
|
|
560
|
+
if (!details) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
let fragment = details.fragments[sn - details.startSN] as
|
|
564
|
+
| MediaFragment
|
|
565
|
+
| undefined;
|
|
566
|
+
if (fragment) {
|
|
567
|
+
return fragment;
|
|
568
|
+
}
|
|
569
|
+
fragment = details.fragmentHint;
|
|
570
|
+
if (fragment?.sn === sn) {
|
|
571
|
+
return fragment;
|
|
572
|
+
}
|
|
573
|
+
if (fragCurrent && sn < details.startSN && fragCurrent.sn === sn) {
|
|
574
|
+
return fragCurrent as MediaFragment;
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function getPartWith(
|
|
580
|
+
details: LevelDetails | undefined,
|
|
581
|
+
sn: number,
|
|
582
|
+
partIndex: number,
|
|
583
|
+
): Part | null {
|
|
584
|
+
if (!details) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
return findPart(details.partList, sn, partIndex);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function findPart(
|
|
591
|
+
partList: Part[] | null | undefined,
|
|
592
|
+
sn: number,
|
|
593
|
+
partIndex: number,
|
|
594
|
+
): Part | null {
|
|
595
|
+
if (partList) {
|
|
596
|
+
for (let i = partList.length; i--; ) {
|
|
597
|
+
const part = partList[i];
|
|
598
|
+
if (part.index === partIndex && part.fragment.sn === sn) {
|
|
599
|
+
return part;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function reassignFragmentLevelIndexes(levels: Level[]) {
|
|
607
|
+
levels.forEach((level, index) => {
|
|
608
|
+
level.details?.fragments.forEach((fragment) => {
|
|
609
|
+
fragment.level = index;
|
|
610
|
+
if (fragment.initSegment) {
|
|
611
|
+
fragment.initSegment.level = index;
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function notEqualAfterStrippingQueries(
|
|
618
|
+
uriBefore: string,
|
|
619
|
+
uriAfter: string | undefined,
|
|
620
|
+
): boolean {
|
|
621
|
+
if (uriBefore !== uriAfter && uriAfter) {
|
|
622
|
+
return stripQuery(uriBefore) !== stripQuery(uriAfter);
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function stripQuery(uri: string): string {
|
|
628
|
+
return uri.replace(/\?[^?]*$/, '');
|
|
629
|
+
}
|