@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,1072 @@
1
+ import { buildAbsoluteURL } from 'url-toolkit';
2
+ import { DateRange } from './date-range';
3
+ import { Fragment, Part } from './fragment';
4
+ import { LevelDetails } from './level-details';
5
+ import { LevelKey } from './level-key';
6
+ import { PlaylistLevelType } from '../types/loader';
7
+ import { AttrList } from '../utils/attr-list';
8
+ import { isCodecType } from '../utils/codecs';
9
+ import { logger } from '../utils/logger';
10
+ import {
11
+ addVariableDefinition,
12
+ hasVariableReferences,
13
+ importVariableDefinition,
14
+ substituteVariables,
15
+ } from '../utils/variable-substitution';
16
+ import type { MediaFragment } from './fragment';
17
+ import type { ContentSteeringOptions } from '../types/events';
18
+ import type {
19
+ CodecsParsed,
20
+ LevelAttributes,
21
+ LevelParsed,
22
+ VariableMap,
23
+ } from '../types/level';
24
+ import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
25
+ import type { CodecType } from '../utils/codecs';
26
+
27
+ type M3U8ParserFragments = Array<Fragment | null>;
28
+
29
+ export type ParsedMultivariantPlaylist = {
30
+ contentSteering: ContentSteeringOptions | null;
31
+ levels: LevelParsed[];
32
+ playlistParsingError: Error | null;
33
+ sessionData: Record<string, AttrList> | null;
34
+ sessionKeys: LevelKey[] | null;
35
+ startTimeOffset: number | null;
36
+ variableList: VariableMap | null;
37
+ hasVariableRefs: boolean;
38
+ };
39
+
40
+ type ParsedMultivariantMediaOptions = {
41
+ AUDIO?: MediaPlaylist[];
42
+ SUBTITLES?: MediaPlaylist[];
43
+ 'CLOSED-CAPTIONS'?: MediaPlaylist[];
44
+ };
45
+
46
+ type LevelKeys = { [key: string]: LevelKey | undefined };
47
+
48
+ const MASTER_PLAYLIST_REGEX =
49
+ /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g;
50
+ const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
51
+
52
+ const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present)
53
+
54
+ const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
55
+ [
56
+ /#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
57
+ /(?!#) *(\S[^\r\n]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
58
+ /#.*/.source, // All other non-segment oriented tags will match with all groups empty
59
+ ].join('|'),
60
+ 'g',
61
+ );
62
+
63
+ const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
64
+ [
65
+ /#EXT-X-(PROGRAM-DATE-TIME|BYTERANGE|DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/
66
+ .source,
67
+ /#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/
68
+ .source,
69
+ /#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source,
70
+ /(#)([^:]*):(.*)/.source,
71
+ /(#)(.*)(?:.*)\r?\n?/.source,
72
+ ].join('|'),
73
+ );
74
+
75
+ export default class M3U8Parser {
76
+ static findGroup(
77
+ groups: (
78
+ | { id?: string; audioCodec?: string }
79
+ | { id?: string; textCodec?: string }
80
+ )[],
81
+ mediaGroupId: string,
82
+ ):
83
+ | { id?: string; audioCodec?: string }
84
+ | { id?: string; textCodec?: string }
85
+ | undefined {
86
+ for (let i = 0; i < groups.length; i++) {
87
+ const group = groups[i];
88
+ if (group.id === mediaGroupId) {
89
+ return group;
90
+ }
91
+ }
92
+ }
93
+
94
+ static resolve(url, baseUrl) {
95
+ return buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
96
+ }
97
+
98
+ static isMediaPlaylist(str: string): boolean {
99
+ return IS_MEDIA_PLAYLIST.test(str);
100
+ }
101
+
102
+ static parseMasterPlaylist(
103
+ string: string,
104
+ baseurl: string,
105
+ ): ParsedMultivariantPlaylist {
106
+ const hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
107
+ ? hasVariableReferences(string)
108
+ : false;
109
+ const parsed: ParsedMultivariantPlaylist = {
110
+ contentSteering: null,
111
+ levels: [],
112
+ playlistParsingError: null,
113
+ sessionData: null,
114
+ sessionKeys: null,
115
+ startTimeOffset: null,
116
+ variableList: null,
117
+ hasVariableRefs,
118
+ };
119
+ const levelsWithKnownCodecs: LevelParsed[] = [];
120
+
121
+ MASTER_PLAYLIST_REGEX.lastIndex = 0;
122
+ if (!string.startsWith('#EXTM3U')) {
123
+ parsed.playlistParsingError = new Error('no EXTM3U delimiter');
124
+ return parsed;
125
+ }
126
+ let result: RegExpExecArray | null;
127
+ while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
128
+ if (result[1]) {
129
+ // '#EXT-X-STREAM-INF' is found, parse level tag in group 1
130
+ const attrs = new AttrList(result[1], parsed) as LevelAttributes;
131
+ const uri = __USE_VARIABLE_SUBSTITUTION__
132
+ ? substituteVariables(parsed, result[2])
133
+ : result[2];
134
+ const level: LevelParsed = {
135
+ attrs,
136
+ bitrate:
137
+ attrs.decimalInteger('BANDWIDTH') ||
138
+ attrs.decimalInteger('AVERAGE-BANDWIDTH'),
139
+ name: attrs.NAME,
140
+ url: M3U8Parser.resolve(uri, baseurl),
141
+ };
142
+
143
+ const resolution = attrs.decimalResolution('RESOLUTION');
144
+ if (resolution) {
145
+ level.width = resolution.width;
146
+ level.height = resolution.height;
147
+ }
148
+
149
+ setCodecs(attrs.CODECS, level);
150
+ const supplementalCodecs = attrs['SUPPLEMENTAL-CODECS'];
151
+ if (supplementalCodecs) {
152
+ level.supplemental = {};
153
+ setCodecs(supplementalCodecs, level.supplemental);
154
+ }
155
+
156
+ if (!level.unknownCodecs?.length) {
157
+ levelsWithKnownCodecs.push(level);
158
+ }
159
+
160
+ parsed.levels.push(level);
161
+ } else if (result[3]) {
162
+ const tag = result[3];
163
+ const attributes = result[4];
164
+ switch (tag) {
165
+ case 'SESSION-DATA': {
166
+ // #EXT-X-SESSION-DATA
167
+ const sessionAttrs = new AttrList(attributes, parsed);
168
+ const dataId = sessionAttrs['DATA-ID'];
169
+ if (dataId) {
170
+ if (parsed.sessionData === null) {
171
+ parsed.sessionData = {};
172
+ }
173
+ parsed.sessionData[dataId] = sessionAttrs;
174
+ }
175
+ break;
176
+ }
177
+ case 'SESSION-KEY': {
178
+ // #EXT-X-SESSION-KEY
179
+ const sessionKey = parseKey(attributes, baseurl, parsed);
180
+ if (sessionKey.encrypted && sessionKey.isSupported()) {
181
+ if (parsed.sessionKeys === null) {
182
+ parsed.sessionKeys = [];
183
+ }
184
+ parsed.sessionKeys.push(sessionKey);
185
+ } else {
186
+ logger.warn(
187
+ `[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`,
188
+ );
189
+ }
190
+ break;
191
+ }
192
+ case 'DEFINE': {
193
+ // #EXT-X-DEFINE
194
+ if (__USE_VARIABLE_SUBSTITUTION__) {
195
+ const variableAttributes = new AttrList(attributes, parsed);
196
+ addVariableDefinition(parsed, variableAttributes, baseurl);
197
+ }
198
+ break;
199
+ }
200
+ case 'CONTENT-STEERING': {
201
+ // #EXT-X-CONTENT-STEERING
202
+ const contentSteeringAttributes = new AttrList(attributes, parsed);
203
+ parsed.contentSteering = {
204
+ uri: M3U8Parser.resolve(
205
+ contentSteeringAttributes['SERVER-URI'],
206
+ baseurl,
207
+ ),
208
+ pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.',
209
+ };
210
+ break;
211
+ }
212
+ case 'START': {
213
+ // #EXT-X-START
214
+ parsed.startTimeOffset = parseStartTimeOffset(attributes);
215
+ break;
216
+ }
217
+ default:
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ // Filter out levels with unknown codecs if it does not remove all levels
223
+ const stripUnknownCodecLevels =
224
+ levelsWithKnownCodecs.length > 0 &&
225
+ levelsWithKnownCodecs.length < parsed.levels.length;
226
+
227
+ parsed.levels = stripUnknownCodecLevels
228
+ ? levelsWithKnownCodecs
229
+ : parsed.levels;
230
+ if (parsed.levels.length === 0) {
231
+ parsed.playlistParsingError = new Error('no levels found in manifest');
232
+ }
233
+
234
+ return parsed;
235
+ }
236
+
237
+ static parseMasterPlaylistMedia(
238
+ string: string,
239
+ baseurl: string,
240
+ parsed: ParsedMultivariantPlaylist,
241
+ ): ParsedMultivariantMediaOptions {
242
+ let result: RegExpExecArray | null;
243
+ const results: ParsedMultivariantMediaOptions = {};
244
+ const levels = parsed.levels;
245
+ const groupsByType = {
246
+ AUDIO: levels.map((level: LevelParsed) => ({
247
+ id: level.attrs.AUDIO,
248
+ audioCodec: level.audioCodec,
249
+ })),
250
+ SUBTITLES: levels.map((level: LevelParsed) => ({
251
+ id: level.attrs.SUBTITLES,
252
+ textCodec: level.textCodec,
253
+ })),
254
+ 'CLOSED-CAPTIONS': [],
255
+ };
256
+ let id = 0;
257
+ MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
258
+ while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
259
+ const attrs = new AttrList(result[1], parsed) as MediaAttributes;
260
+ const type = attrs.TYPE;
261
+ if (type) {
262
+ const groups:
263
+ | (typeof groupsByType)[keyof typeof groupsByType]
264
+ | undefined = groupsByType[type];
265
+ const medias: MediaPlaylist[] = results[type] || [];
266
+ results[type] = medias;
267
+ const lang = attrs.LANGUAGE;
268
+ const assocLang = attrs['ASSOC-LANGUAGE'];
269
+ const channels = attrs.CHANNELS;
270
+ const characteristics = attrs.CHARACTERISTICS;
271
+ const instreamId = attrs['INSTREAM-ID'];
272
+ const media: MediaPlaylist = {
273
+ attrs,
274
+ bitrate: 0,
275
+ id: id++,
276
+ groupId: attrs['GROUP-ID'] || '',
277
+ name: attrs.NAME || lang || '',
278
+ type,
279
+ default: attrs.bool('DEFAULT'),
280
+ autoselect: attrs.bool('AUTOSELECT'),
281
+ forced: attrs.bool('FORCED'),
282
+ lang,
283
+ url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
284
+ };
285
+ if (assocLang) {
286
+ media.assocLang = assocLang;
287
+ }
288
+ if (channels) {
289
+ media.channels = channels;
290
+ }
291
+ if (characteristics) {
292
+ media.characteristics = characteristics;
293
+ }
294
+ if (instreamId) {
295
+ media.instreamId = instreamId;
296
+ }
297
+
298
+ if (groups?.length) {
299
+ // If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track
300
+ // If we don't find the track signalled, lets use the first audio groups codec we have
301
+ // Acting as a best guess
302
+ const groupCodec =
303
+ M3U8Parser.findGroup(groups, media.groupId as string) || groups[0];
304
+ assignCodec(media, groupCodec, 'audioCodec');
305
+ assignCodec(media, groupCodec, 'textCodec');
306
+ }
307
+
308
+ medias.push(media);
309
+ }
310
+ }
311
+ return results;
312
+ }
313
+
314
+ static parseLevelPlaylist(
315
+ string: string,
316
+ baseurl: string,
317
+ id: number,
318
+ type: PlaylistLevelType,
319
+ levelUrlId: number,
320
+ multivariantVariableList: VariableMap | null,
321
+ algoSegmentPattern?: RegExp | string | null,
322
+ ): LevelDetails {
323
+ const base = { url: baseurl };
324
+ const level = new LevelDetails(baseurl);
325
+ const fragments: M3U8ParserFragments = level.fragments;
326
+ const programDateTimes: MediaFragment[] = [];
327
+ // The most recent init segment seen (applies to all subsequent segments)
328
+ let currentInitSegment: Fragment | null = null;
329
+ let currentSN = 0;
330
+ let currentPart = 0;
331
+ let totalduration = 0;
332
+ let discontinuityCounter = 0;
333
+ let currentBitrate = 0;
334
+ let prevFrag: Fragment | null = null;
335
+ let frag: Fragment = new Fragment(type, base);
336
+ let result: RegExpExecArray | RegExpMatchArray | null;
337
+ let i: number;
338
+ let levelkeys: LevelKeys | undefined;
339
+ let firstPdtIndex = -1;
340
+ let createNextFrag = false;
341
+ let nextByteRange: string | null = null;
342
+ let serverControlAttrs: AttrList | undefined;
343
+
344
+ LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
345
+ level.m3u8 = string;
346
+ level.hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
347
+ ? hasVariableReferences(string)
348
+ : false;
349
+ if (LEVEL_PLAYLIST_REGEX_FAST.exec(string)?.[0] !== '#EXTM3U') {
350
+ level.playlistParsingError = new Error(
351
+ 'Missing format identifier #EXTM3U',
352
+ );
353
+ return level;
354
+ }
355
+ while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
356
+ if (createNextFrag) {
357
+ createNextFrag = false;
358
+ frag = new Fragment(type, base);
359
+ // setup the next fragment for part loading
360
+ frag.playlistOffset = totalduration;
361
+ frag.setStart(totalduration);
362
+ frag.sn = currentSN;
363
+ frag.cc = discontinuityCounter;
364
+ if (currentBitrate) {
365
+ frag.bitrate = currentBitrate;
366
+ }
367
+ frag.level = id;
368
+ if (currentInitSegment) {
369
+ frag.initSegment = currentInitSegment;
370
+ if (currentInitSegment.rawProgramDateTime) {
371
+ frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
372
+ currentInitSegment.rawProgramDateTime = null;
373
+ }
374
+ if (nextByteRange) {
375
+ frag.setByteRange(nextByteRange);
376
+ nextByteRange = null;
377
+ }
378
+ }
379
+ }
380
+
381
+ const duration = result[1];
382
+ if (duration) {
383
+ // INF
384
+ frag.duration = parseFloat(duration);
385
+ // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
386
+ const title = (' ' + result[2]).slice(1);
387
+ frag.title = title || null;
388
+ frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
389
+ } else if (result[3]) {
390
+ // url
391
+ // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
392
+ const uri = (' ' + result[3]).slice(1);
393
+ const relurl = __USE_VARIABLE_SUBSTITUTION__
394
+ ? substituteVariables(level, uri)
395
+ : uri;
396
+ const shouldTreatAsAlgo =
397
+ type === PlaylistLevelType.MAIN &&
398
+ !!algoSegmentPattern &&
399
+ (isAlgoSegment(relurl, algoSegmentPattern) ||
400
+ isLikelyAlgoSegment(relurl, frag.duration));
401
+ if (shouldTreatAsAlgo) {
402
+ if (prevFrag) {
403
+ prevFrag.algoRelurl = relurl;
404
+ } else {
405
+ logger.warn(
406
+ `[m3u8-parser] 发现算法分片但缺少前序视频分片:${relurl}`,
407
+ );
408
+ }
409
+ continue;
410
+ }
411
+ if (Number.isFinite(frag.duration)) {
412
+ frag.playlistOffset = totalduration;
413
+ frag.setStart(totalduration);
414
+ if (levelkeys) {
415
+ setFragLevelKeys(frag, levelkeys, level);
416
+ }
417
+ frag.sn = currentSN;
418
+ frag.level = id;
419
+ frag.cc = discontinuityCounter;
420
+ fragments.push(frag);
421
+ frag.relurl = relurl;
422
+ assignProgramDateTime(
423
+ frag as MediaFragment,
424
+ prevFrag as MediaFragment,
425
+ programDateTimes,
426
+ );
427
+ prevFrag = frag;
428
+ totalduration += frag.duration;
429
+ currentSN++;
430
+ currentPart = 0;
431
+ createNextFrag = true;
432
+ }
433
+ } else {
434
+ result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
435
+ if (!result) {
436
+ logger.warn('No matches on slow regex match for level playlist!');
437
+ continue;
438
+ }
439
+ for (i = 1; i < result.length; i++) {
440
+ if ((result[i] as any) !== undefined) {
441
+ break;
442
+ }
443
+ }
444
+
445
+ // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
446
+ const tag = (' ' + result[i]).slice(1);
447
+ const value1 = (' ' + result[i + 1]).slice(1);
448
+ const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : null;
449
+
450
+ switch (tag) {
451
+ case 'BYTERANGE':
452
+ if (prevFrag) {
453
+ frag.setByteRange(value1, prevFrag);
454
+ } else {
455
+ frag.setByteRange(value1);
456
+ }
457
+ break;
458
+ case 'PROGRAM-DATE-TIME':
459
+ // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
460
+ frag.rawProgramDateTime = value1;
461
+ frag.tagList.push(['PROGRAM-DATE-TIME', value1]);
462
+ if (firstPdtIndex === -1) {
463
+ firstPdtIndex = fragments.length;
464
+ }
465
+ break;
466
+ case 'PLAYLIST-TYPE':
467
+ if (level.type) {
468
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
469
+ }
470
+ level.type = value1.toUpperCase();
471
+ break;
472
+ case 'MEDIA-SEQUENCE':
473
+ if (level.startSN !== 0) {
474
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
475
+ } else if (fragments.length > 0) {
476
+ assignMustAppearBeforeSegmentsError(level, tag, result);
477
+ }
478
+ currentSN = level.startSN = parseInt(value1);
479
+ break;
480
+ case 'SKIP': {
481
+ if (level.skippedSegments) {
482
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
483
+ }
484
+ const skipAttrs = new AttrList(value1, level);
485
+ const skippedSegments =
486
+ skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
487
+ if (Number.isFinite(skippedSegments)) {
488
+ level.skippedSegments += skippedSegments;
489
+ // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
490
+ for (let i = skippedSegments; i--; ) {
491
+ fragments.push(null);
492
+ }
493
+ currentSN += skippedSegments;
494
+ }
495
+ const recentlyRemovedDateranges = skipAttrs.enumeratedString(
496
+ 'RECENTLY-REMOVED-DATERANGES',
497
+ );
498
+ if (recentlyRemovedDateranges) {
499
+ level.recentlyRemovedDateranges = (
500
+ level.recentlyRemovedDateranges || []
501
+ ).concat(recentlyRemovedDateranges.split('\t'));
502
+ }
503
+ break;
504
+ }
505
+ case 'TARGETDURATION':
506
+ if (level.targetduration !== 0) {
507
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
508
+ }
509
+ level.targetduration = Math.max(parseInt(value1), 1);
510
+ break;
511
+ case 'VERSION':
512
+ if (level.version !== null) {
513
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
514
+ }
515
+ level.version = parseInt(value1);
516
+ break;
517
+ case 'INDEPENDENT-SEGMENTS':
518
+ break;
519
+ case 'ENDLIST':
520
+ if (!level.live) {
521
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
522
+ }
523
+ level.live = false;
524
+ break;
525
+ case '#':
526
+ if (value1 || value2) {
527
+ frag.tagList.push(value2 ? [value1, value2] : [value1]);
528
+ }
529
+ break;
530
+ case 'DISCONTINUITY':
531
+ discontinuityCounter++;
532
+ frag.tagList.push(['DIS']);
533
+ break;
534
+ case 'GAP':
535
+ frag.gap = true;
536
+ frag.tagList.push([tag]);
537
+ break;
538
+ case 'BITRATE':
539
+ frag.tagList.push([tag, value1]);
540
+ currentBitrate = parseInt(value1) * 1000;
541
+ if (Number.isFinite(currentBitrate)) {
542
+ frag.bitrate = currentBitrate;
543
+ } else {
544
+ currentBitrate = 0;
545
+ }
546
+ break;
547
+ case 'DATERANGE': {
548
+ const dateRangeAttr = new AttrList(value1, level);
549
+ const dateRange = new DateRange(
550
+ dateRangeAttr,
551
+ level.dateRanges[dateRangeAttr.ID],
552
+ level.dateRangeTagCount,
553
+ );
554
+ level.dateRangeTagCount++;
555
+ if (dateRange.isValid || level.skippedSegments) {
556
+ level.dateRanges[dateRange.id] = dateRange;
557
+ } else {
558
+ logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`);
559
+ }
560
+ // Add to fragment tag list for backwards compatibility (< v1.2.0)
561
+ frag.tagList.push(['EXT-X-DATERANGE', value1]);
562
+ break;
563
+ }
564
+ case 'DEFINE': {
565
+ if (__USE_VARIABLE_SUBSTITUTION__) {
566
+ const variableAttributes = new AttrList(value1, level);
567
+ if ('IMPORT' in variableAttributes) {
568
+ importVariableDefinition(
569
+ level,
570
+ variableAttributes,
571
+ multivariantVariableList,
572
+ );
573
+ } else {
574
+ addVariableDefinition(level, variableAttributes, baseurl);
575
+ }
576
+ }
577
+ break;
578
+ }
579
+
580
+ case 'DISCONTINUITY-SEQUENCE':
581
+ if (level.startCC !== 0) {
582
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
583
+ } else if (fragments.length > 0) {
584
+ assignMustAppearBeforeSegmentsError(level, tag, result);
585
+ }
586
+ level.startCC = discontinuityCounter = parseInt(value1);
587
+ break;
588
+ case 'KEY': {
589
+ const levelKey = parseKey(value1, baseurl, level);
590
+ if (levelKey.isSupported()) {
591
+ if (levelKey.method === 'NONE') {
592
+ levelkeys = undefined;
593
+ break;
594
+ }
595
+ if (!levelkeys) {
596
+ levelkeys = {};
597
+ }
598
+ const currentKey = levelkeys[levelKey.keyFormat];
599
+ // Ignore duplicate playlist KEY tags
600
+ if (!currentKey?.matches(levelKey)) {
601
+ if (currentKey) {
602
+ levelkeys = Object.assign({}, levelkeys);
603
+ }
604
+ levelkeys[levelKey.keyFormat] = levelKey;
605
+ }
606
+ } else {
607
+ logger.warn(
608
+ `[Keys] Ignoring unsupported EXT-X-KEY tag: "${value1}"${__USE_EME_DRM__ ? '' : ' (light build)'}`,
609
+ );
610
+ }
611
+ break;
612
+ }
613
+ case 'START':
614
+ level.startTimeOffset = parseStartTimeOffset(value1);
615
+ break;
616
+ case 'MAP': {
617
+ const mapAttrs = new AttrList(value1, level);
618
+ if (frag.duration) {
619
+ // Initial segment tag is after segment duration tag.
620
+ // #EXTINF: 6.0
621
+ // #EXT-X-MAP:URI="init.mp4
622
+ const init = new Fragment(type, base);
623
+ setInitSegment(init, mapAttrs, id, levelkeys);
624
+ currentInitSegment = init;
625
+ frag.initSegment = currentInitSegment;
626
+ if (
627
+ currentInitSegment.rawProgramDateTime &&
628
+ !frag.rawProgramDateTime
629
+ ) {
630
+ frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
631
+ }
632
+ } else {
633
+ // Initial segment tag is before segment duration tag
634
+ // Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE
635
+ const end = frag.byteRangeEndOffset;
636
+ if (end) {
637
+ const start = frag.byteRangeStartOffset as number;
638
+ nextByteRange = `${end - start}@${start}`;
639
+ } else {
640
+ nextByteRange = null;
641
+ }
642
+ setInitSegment(frag, mapAttrs, id, levelkeys);
643
+ currentInitSegment = frag;
644
+ createNextFrag = true;
645
+ }
646
+ currentInitSegment.cc = discontinuityCounter;
647
+ break;
648
+ }
649
+ case 'SERVER-CONTROL': {
650
+ if (serverControlAttrs) {
651
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
652
+ }
653
+ serverControlAttrs = new AttrList(value1);
654
+ level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
655
+ level.canSkipUntil = serverControlAttrs.optionalFloat(
656
+ 'CAN-SKIP-UNTIL',
657
+ 0,
658
+ );
659
+ level.canSkipDateRanges =
660
+ level.canSkipUntil > 0 &&
661
+ serverControlAttrs.bool('CAN-SKIP-DATERANGES');
662
+ level.partHoldBack = serverControlAttrs.optionalFloat(
663
+ 'PART-HOLD-BACK',
664
+ 0,
665
+ );
666
+ level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
667
+ break;
668
+ }
669
+ case 'PART-INF': {
670
+ if (level.partTarget) {
671
+ assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
672
+ }
673
+ const partInfAttrs = new AttrList(value1);
674
+ level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
675
+ break;
676
+ }
677
+ case 'PART': {
678
+ let partList = level.partList;
679
+ if (!partList) {
680
+ partList = level.partList = [];
681
+ }
682
+ const previousPart =
683
+ currentPart > 0 ? partList[partList.length - 1] : undefined;
684
+ const index = currentPart++;
685
+ const partAttrs = new AttrList(value1, level);
686
+ const part = new Part(
687
+ partAttrs,
688
+ frag as MediaFragment,
689
+ base,
690
+ index,
691
+ previousPart,
692
+ );
693
+ partList.push(part);
694
+ frag.duration += part.duration;
695
+ break;
696
+ }
697
+ case 'PRELOAD-HINT': {
698
+ const preloadHintAttrs = new AttrList(value1, level);
699
+ level.preloadHint = preloadHintAttrs;
700
+ break;
701
+ }
702
+ case 'RENDITION-REPORT': {
703
+ const renditionReportAttrs = new AttrList(value1, level);
704
+ level.renditionReports = level.renditionReports || [];
705
+ level.renditionReports.push(renditionReportAttrs);
706
+ break;
707
+ }
708
+ default:
709
+ logger.warn(`line parsed but not handled: ${result}`);
710
+ break;
711
+ }
712
+ }
713
+ }
714
+ if (prevFrag && !prevFrag.relurl) {
715
+ fragments.pop();
716
+ totalduration -= prevFrag.duration;
717
+ if (level.partList) {
718
+ level.fragmentHint = prevFrag as MediaFragment;
719
+ }
720
+ } else if (level.partList) {
721
+ assignProgramDateTime(
722
+ frag as MediaFragment,
723
+ prevFrag as MediaFragment,
724
+ programDateTimes,
725
+ );
726
+ frag.cc = discontinuityCounter;
727
+ level.fragmentHint = frag as MediaFragment;
728
+ if (levelkeys) {
729
+ setFragLevelKeys(frag, levelkeys, level);
730
+ }
731
+ }
732
+ if (!level.targetduration) {
733
+ level.playlistParsingError = new Error(`Missing Target Duration`);
734
+ }
735
+ const fragmentLength = fragments.length;
736
+ const firstFragment = fragments[0];
737
+ const lastFragment = fragments[fragmentLength - 1];
738
+ totalduration += level.skippedSegments * level.targetduration;
739
+ if (totalduration > 0 && fragmentLength && lastFragment) {
740
+ level.averagetargetduration = totalduration / fragmentLength;
741
+ const lastSn = lastFragment.sn;
742
+ level.endSN = lastSn !== 'initSegment' ? lastSn : 0;
743
+ if (!level.live) {
744
+ lastFragment.endList = true;
745
+ }
746
+ /**
747
+ * Backfill any missing PDT values
748
+ * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
749
+ * one or more Media Segment URIs, the client SHOULD extrapolate
750
+ * backward from that tag (using EXTINF durations and/or media
751
+ * timestamps) to associate dates with those segments."
752
+ * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
753
+ * computed.
754
+ */
755
+ if (firstPdtIndex > 0) {
756
+ backfillProgramDateTimes(fragments, firstPdtIndex);
757
+ if (firstFragment) {
758
+ programDateTimes.unshift(firstFragment as MediaFragment);
759
+ }
760
+ }
761
+ }
762
+ if (level.fragmentHint) {
763
+ totalduration += level.fragmentHint.duration;
764
+ }
765
+ level.totalduration = totalduration;
766
+ if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
767
+ mapDateRanges(programDateTimes, level);
768
+ }
769
+
770
+ level.endCC = discontinuityCounter;
771
+
772
+ return level;
773
+ }
774
+ }
775
+
776
+ export function mapDateRanges(
777
+ programDateTimes: MediaFragment[],
778
+ details: LevelDetails,
779
+ ) {
780
+ // Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
781
+ let programDateTimeCount = programDateTimes.length;
782
+ if (!programDateTimeCount) {
783
+ if (details.hasProgramDateTime) {
784
+ const lastFragment = details.fragments[details.fragments.length - 1];
785
+ programDateTimes.push(lastFragment);
786
+ programDateTimeCount++;
787
+ } else {
788
+ // no segments with EXT-X-PROGRAM-DATE-TIME references in playlist history
789
+ return;
790
+ }
791
+ }
792
+ const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
793
+ const playlistEnd = details.live ? Infinity : details.totalduration;
794
+ const dateRangeIds = Object.keys(details.dateRanges);
795
+ for (let i = dateRangeIds.length; i--; ) {
796
+ const dateRange = details.dateRanges[dateRangeIds[i]]!;
797
+ const startDateTime = dateRange.startDate.getTime();
798
+ dateRange.tagAnchor = lastProgramDateTime.ref;
799
+ for (let j = programDateTimeCount; j--; ) {
800
+ if (programDateTimes[j]?.sn < details.startSN) {
801
+ break;
802
+ }
803
+ const fragIndex = findFragmentWithStartDate(
804
+ details,
805
+ startDateTime,
806
+ programDateTimes,
807
+ j,
808
+ playlistEnd,
809
+ );
810
+ if (fragIndex !== -1) {
811
+ dateRange.tagAnchor = details.fragments[fragIndex].ref;
812
+ break;
813
+ }
814
+ }
815
+ }
816
+ }
817
+
818
+ const algoSegmentPatternWarnedMessages = new Set<string>();
819
+
820
+ function warnAlgoSegmentPattern(message: string): void {
821
+ if (algoSegmentPatternWarnedMessages.has(message)) return;
822
+ algoSegmentPatternWarnedMessages.add(message);
823
+ logger.warn(`[m3u8-parser] ${message}`);
824
+ }
825
+
826
+ function isAlgoSegment(
827
+ uri: string,
828
+ algoSegmentPattern?: RegExp | string | null,
829
+ ): boolean {
830
+ if (!algoSegmentPattern) {
831
+ return false;
832
+ }
833
+ // 先去掉查询参数和 hash,避免正则因结尾匹配失败
834
+ const cleanUri = uri.split(/[?#]/)[0];
835
+ if (algoSegmentPattern instanceof RegExp) {
836
+ algoSegmentPattern.lastIndex = 0;
837
+ return algoSegmentPattern.test(cleanUri);
838
+ }
839
+ if (typeof algoSegmentPattern !== 'string') {
840
+ warnAlgoSegmentPattern(
841
+ `algoSegmentPattern 类型非法,已忽略:${String(algoSegmentPattern)}`,
842
+ );
843
+ return false;
844
+ }
845
+ const trimmedPattern = algoSegmentPattern.trim();
846
+ if (!trimmedPattern) {
847
+ warnAlgoSegmentPattern('algoSegmentPattern 为空,已忽略');
848
+ return false;
849
+ }
850
+ try {
851
+ return new RegExp(trimmedPattern).test(cleanUri);
852
+ } catch (error) {
853
+ warnAlgoSegmentPattern(
854
+ `algoSegmentPattern 无效,已忽略:${algoSegmentPattern}`,
855
+ );
856
+ return false;
857
+ }
858
+ }
859
+
860
+ function isLikelyAlgoSegment(uri: string, duration: number | null): boolean {
861
+ if (duration === null || !Number.isFinite(duration)) {
862
+ return false;
863
+ }
864
+ // 超短分片且后缀为 _dat.ts 时,视为算法分片兜底处理
865
+ if (duration > 0.02) {
866
+ return false;
867
+ }
868
+ const cleanUri = uri.split(/[?#]/)[0];
869
+ return /_dat\.ts$/i.test(cleanUri);
870
+ }
871
+
872
+ function findFragmentWithStartDate(
873
+ details: LevelDetails,
874
+ startDateTime: number,
875
+ programDateTimes: MediaFragment[],
876
+ index: number,
877
+ endTime: number,
878
+ ): number {
879
+ const pdtFragment = programDateTimes[index] as MediaFragment | undefined;
880
+ if (pdtFragment) {
881
+ // find matching range between PDT tags
882
+ const pdtStart = pdtFragment.programDateTime as number;
883
+ if (startDateTime >= pdtStart || index === 0) {
884
+ const durationBetweenPdt =
885
+ (programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
886
+ if (startDateTime <= pdtStart + durationBetweenPdt * 1000) {
887
+ // map to fragment with date-time range
888
+ const startIndex = programDateTimes[index].sn - details.startSN;
889
+ if (startIndex < 0) {
890
+ return -1;
891
+ }
892
+ const fragments = details.fragments;
893
+ if (fragments.length > programDateTimes.length) {
894
+ const endSegment =
895
+ programDateTimes[index + 1] || fragments[fragments.length - 1];
896
+ const endIndex = endSegment.sn - details.startSN;
897
+ for (let i = endIndex; i > startIndex; i--) {
898
+ const fragStartDateTime = fragments[i].programDateTime as number;
899
+ if (
900
+ startDateTime >= fragStartDateTime &&
901
+ startDateTime < fragStartDateTime + fragments[i].duration * 1000
902
+ ) {
903
+ return i;
904
+ }
905
+ }
906
+ }
907
+ return startIndex;
908
+ }
909
+ }
910
+ }
911
+ return -1;
912
+ }
913
+
914
+ function parseKey(
915
+ keyTagAttributes: string,
916
+ baseurl: string,
917
+ parsed: ParsedMultivariantPlaylist | LevelDetails,
918
+ ): LevelKey {
919
+ // https://tools.ietf.org/html/rfc8216#section-4.3.2.4
920
+ const keyAttrs = new AttrList(keyTagAttributes, parsed);
921
+ const decryptmethod = keyAttrs.METHOD ?? '';
922
+ const decrypturi = keyAttrs.URI;
923
+ const decryptiv = keyAttrs.hexadecimalInteger('IV');
924
+ const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS;
925
+ // From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
926
+ const decryptkeyformat = keyAttrs.KEYFORMAT ?? 'identity';
927
+
928
+ if (decrypturi && keyAttrs.IV && !decryptiv) {
929
+ logger.error(`Invalid IV: ${keyAttrs.IV}`);
930
+ }
931
+ // If decrypturi is a URI with a scheme, then baseurl will be ignored
932
+ // No uri is allowed when METHOD is NONE
933
+ const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : '';
934
+ const keyFormatVersions = (
935
+ decryptkeyformatversions ? decryptkeyformatversions : '1'
936
+ )
937
+ .split('/')
938
+ .map(Number)
939
+ .filter(Number.isFinite);
940
+
941
+ return new LevelKey(
942
+ decryptmethod,
943
+ resolvedUri,
944
+ decryptkeyformat,
945
+ keyFormatVersions,
946
+ decryptiv,
947
+ keyAttrs.KEYID,
948
+ );
949
+ }
950
+
951
+ function parseStartTimeOffset(startAttributes: string): number | null {
952
+ const startAttrs = new AttrList(startAttributes);
953
+ const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
954
+ if (Number.isFinite(startTimeOffset)) {
955
+ return startTimeOffset;
956
+ }
957
+ return null;
958
+ }
959
+
960
+ function setCodecs(
961
+ codecsAttributeValue: string | undefined,
962
+ level: CodecsParsed,
963
+ ) {
964
+ let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter((c) => c);
965
+ ['video', 'audio', 'text'].forEach((type: CodecType) => {
966
+ const filtered = codecs.filter((codec) => isCodecType(codec, type));
967
+ if (filtered.length) {
968
+ // Comma separated list of all codecs for type
969
+ level[`${type}Codec`] = filtered.map((c) => c.split('/')[0]).join(',');
970
+ // Remove known codecs so that only unknownCodecs are left after iterating through each type
971
+ codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
972
+ }
973
+ });
974
+ level.unknownCodecs = codecs;
975
+ }
976
+
977
+ function assignCodec(
978
+ media: MediaPlaylist,
979
+ groupItem: { audioCodec?: string; textCodec?: string },
980
+ codecProperty: 'audioCodec' | 'textCodec',
981
+ ) {
982
+ const codecValue = groupItem[codecProperty];
983
+ if (codecValue) {
984
+ media[codecProperty] = codecValue;
985
+ }
986
+ }
987
+
988
+ function backfillProgramDateTimes(
989
+ fragments: M3U8ParserFragments,
990
+ firstPdtIndex: number,
991
+ ) {
992
+ let fragPrev = fragments[firstPdtIndex] as Fragment;
993
+ for (let i = firstPdtIndex; i--; ) {
994
+ const frag = fragments[i];
995
+ // Exit on delta-playlist skipped segments
996
+ if (!frag) {
997
+ return;
998
+ }
999
+ frag.programDateTime =
1000
+ (fragPrev.programDateTime as number) - frag.duration * 1000;
1001
+ fragPrev = frag;
1002
+ }
1003
+ }
1004
+
1005
+ export function assignProgramDateTime(
1006
+ frag: MediaFragment,
1007
+ prevFrag: MediaFragment | null,
1008
+ programDateTimes: MediaFragment[],
1009
+ ) {
1010
+ if (frag.rawProgramDateTime) {
1011
+ programDateTimes.push(frag);
1012
+ } else if (prevFrag?.programDateTime) {
1013
+ frag.programDateTime = prevFrag.endProgramDateTime;
1014
+ }
1015
+ }
1016
+
1017
+ function setInitSegment(
1018
+ frag: Fragment,
1019
+ mapAttrs: AttrList,
1020
+ id: number,
1021
+ levelkeys: LevelKeys | undefined,
1022
+ ) {
1023
+ frag.relurl = mapAttrs.URI;
1024
+ if (mapAttrs.BYTERANGE) {
1025
+ frag.setByteRange(mapAttrs.BYTERANGE);
1026
+ }
1027
+ frag.level = id;
1028
+ frag.sn = 'initSegment';
1029
+ if (levelkeys) {
1030
+ frag.levelkeys = levelkeys;
1031
+ }
1032
+ frag.initSegment = null;
1033
+ }
1034
+
1035
+ function setFragLevelKeys(
1036
+ frag: Fragment,
1037
+ levelkeys: LevelKeys,
1038
+ level: LevelDetails,
1039
+ ) {
1040
+ frag.levelkeys = levelkeys;
1041
+ const { encryptedFragments } = level;
1042
+ if (
1043
+ (!encryptedFragments.length ||
1044
+ encryptedFragments[encryptedFragments.length - 1].levelkeys !==
1045
+ levelkeys) &&
1046
+ Object.keys(levelkeys).some(
1047
+ (format) => levelkeys[format]!.isCommonEncryption,
1048
+ )
1049
+ ) {
1050
+ encryptedFragments.push(frag);
1051
+ }
1052
+ }
1053
+
1054
+ function assignMultipleMediaPlaylistTagOccuranceError(
1055
+ level: LevelDetails,
1056
+ tag: string,
1057
+ result: string[],
1058
+ ) {
1059
+ level.playlistParsingError = new Error(
1060
+ `#EXT-X-${tag} must not appear more than once (${result[0]})`,
1061
+ );
1062
+ }
1063
+
1064
+ function assignMustAppearBeforeSegmentsError(
1065
+ level: LevelDetails,
1066
+ tag: string,
1067
+ result: string[],
1068
+ ) {
1069
+ level.playlistParsingError = new Error(
1070
+ `#EXT-X-${tag} must appear before the first Media Segment (${result[0]})`,
1071
+ );
1072
+ }