@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.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. 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
+ }