dasha 3.1.5 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dasha.js ADDED
@@ -0,0 +1,1019 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
4
+ import path from "node:path";
5
+ import { Temporal } from "temporal-polyfill";
6
+ import { DOMParser } from "@xmldom/xmldom";
7
+ import crypto from "node:crypto";
8
+
9
+ //#region lib/shared/media-type.ts
10
+ const MEDIA_TYPES = {
11
+ VIDEO: "video",
12
+ AUDIO: "audio",
13
+ SUBTITLES: "subtitle",
14
+ CLOSED_CAPTIONS: "closed-captions"
15
+ };
16
+
17
+ //#endregion
18
+ //#region lib/shared/encrypt-method.ts
19
+ const ENCRYPT_METHODS = {
20
+ NONE: 0,
21
+ AES_128: 1,
22
+ AES_128_ECB: 2,
23
+ SAMPLE_AES: 3,
24
+ SAMPLE_AES_CTR: 4,
25
+ CENC: 5,
26
+ CHACHA20: 6,
27
+ UNKNOWN: 7
28
+ };
29
+
30
+ //#endregion
31
+ //#region lib/shared/extractor-type.ts
32
+ const EXTRACTOR_TYPES = {
33
+ MPEG_DASH: "MPEG_DASH",
34
+ HLS: "HLS",
35
+ HTTP_LIVE: "HTTP_LIVE",
36
+ MSS: "MSS"
37
+ };
38
+
39
+ //#endregion
40
+ //#region lib/shared/role-type.ts
41
+ const ROLE_TYPE = {
42
+ Subtitle: 0,
43
+ Main: 1,
44
+ Alternate: 2,
45
+ Supplementary: 3,
46
+ Commentary: 4,
47
+ Dub: 5,
48
+ Description: 6,
49
+ Sign: 7,
50
+ Metadata: 8,
51
+ ForcedSubtitle: 9
52
+ };
53
+
54
+ //#endregion
55
+ //#region lib/parser-config.ts
56
+ var ParserConfig = class {
57
+ url = "";
58
+ originalUrl = "";
59
+ baseUrl;
60
+ customParserArgs = {};
61
+ headers = {};
62
+ contentProcessors = [];
63
+ urlProcessors = [];
64
+ keyProcessors = [];
65
+ customMethod;
66
+ customKey;
67
+ customIv;
68
+ urlProcessorArgs;
69
+ appendUrlParams = false;
70
+ keyRetryCount = 3;
71
+ };
72
+
73
+ //#endregion
74
+ //#region lib/hls/hls-tags.ts
75
+ const HLS_TAGS = {
76
+ extM3u: "#EXTM3U",
77
+ extXTargetDuration: "#EXT-X-TARGETDURATION",
78
+ extXMediaSequence: "#EXT-X-MEDIA-SEQUENCE",
79
+ extXDiscontinuitySequence: "#EXT-X-DISCONTINUITY-SEQUENCE",
80
+ extXProgramDateTime: "#EXT-X-PROGRAM-DATE-TIME",
81
+ extXMedia: "#EXT-X-MEDIA",
82
+ extXPlaylistType: "#EXT-X-PLAYLIST-TYPE",
83
+ extXKey: "#EXT-X-KEY",
84
+ extXStreamInf: "#EXT-X-STREAM-INF",
85
+ extXVersion: "#EXT-X-VERSION",
86
+ extXAllowCache: "#EXT-X-ALLOW-CACHE",
87
+ extXEndlist: "#EXT-X-ENDLIST",
88
+ extInf: "#EXTINF",
89
+ extIframesOnly: "#EXT-X-I-FRAMES-ONLY",
90
+ extXByterange: "#EXT-X-BYTERANGE",
91
+ extXIframeStreamInf: "#EXT-X-I-FRAME-STREAM-INF",
92
+ extXDiscontinuity: "#EXT-X-DISCONTINUITY",
93
+ extXCueOutStart: "#EXT-X-CUE-OUT",
94
+ extXCueOut: "#EXT-X-CUE-OUT-CONT",
95
+ extIsIndependentSegments: "#EXT-X-INDEPENDENT-SEGMENTS",
96
+ extXScte35: "#EXT-OATCLS-SCTE35",
97
+ extXCueStart: "#EXT-X-CUE-OUT",
98
+ extXCueEnd: "#EXT-X-CUE-IN",
99
+ extXCueSpan: "#EXT-X-CUE-SPAN",
100
+ extXMap: "#EXT-X-MAP",
101
+ extXStart: "#EXT-X-START"
102
+ };
103
+
104
+ //#endregion
105
+ //#region lib/shared/stream-spec.ts
106
+ var StreamSpec = class {
107
+ mediaType;
108
+ groupId = null;
109
+ language;
110
+ name;
111
+ default;
112
+ skippedDuration;
113
+ bandwidth;
114
+ codecs = null;
115
+ resolution;
116
+ frameRate;
117
+ channels = null;
118
+ extension = null;
119
+ role;
120
+ videoRange;
121
+ characteristics;
122
+ publishTime;
123
+ audioId;
124
+ videoId;
125
+ subtitleId;
126
+ periodId = null;
127
+ url = "";
128
+ originalUrl = "";
129
+ playlist;
130
+ get segmentsCount() {
131
+ return this.playlist?.mediaParts.reduce((sum, part) => sum + part.mediaSegments.length, 0) ?? 0;
132
+ }
133
+ toShortString() {
134
+ let prefixStr = "";
135
+ let returnStr = "";
136
+ const encStr = "";
137
+ const bandwidth = this.bandwidth ? `${this.bandwidth / 1e3} Kbps` : "";
138
+ const channels = this.channels ? `${this.channels}CH` : "";
139
+ if (this.mediaType === MEDIA_TYPES.AUDIO) {
140
+ prefixStr = `Aud ${encStr}`;
141
+ returnStr = [
142
+ this.groupId,
143
+ bandwidth,
144
+ this.name,
145
+ this.codecs,
146
+ this.language,
147
+ channels,
148
+ this.role
149
+ ].filter(Boolean).join(" | ");
150
+ } else if (this.mediaType === MEDIA_TYPES.SUBTITLES) {
151
+ prefixStr = `Sub ${encStr}`;
152
+ returnStr = [
153
+ this.groupId,
154
+ this.language,
155
+ this.name,
156
+ this.codecs,
157
+ this.role
158
+ ].filter(Boolean).join(" | ");
159
+ } else {
160
+ prefixStr = `Vid ${encStr}`;
161
+ returnStr = [
162
+ this.resolution,
163
+ bandwidth,
164
+ this.groupId,
165
+ this.frameRate,
166
+ this.codecs,
167
+ this.videoRange,
168
+ this.role
169
+ ].filter(Boolean).join(" | ");
170
+ }
171
+ returnStr = `${prefixStr} | ${returnStr}`;
172
+ return returnStr.trim();
173
+ }
174
+ };
175
+
176
+ //#endregion
177
+ //#region lib/dash/dash-tags.ts
178
+ const DASH_TAGS = {
179
+ TemplateRepresentationID: "$RepresentationID$",
180
+ TemplateBandwidth: "$Bandwidth$",
181
+ TemplateNumber: "$Number$",
182
+ TemplateTime: "$Time$"
183
+ };
184
+
185
+ //#endregion
186
+ //#region lib/shared/util.ts
187
+ const combineUrl = (baseUrl, relativeUrl) => {
188
+ if (!baseUrl.trim()) return relativeUrl;
189
+ const url1 = new URL(baseUrl);
190
+ const url2 = new URL(relativeUrl, url1);
191
+ return url2.toString();
192
+ };
193
+ const replaceVars = (text, dict) => {
194
+ let result = text;
195
+ for (const [key, value] of Object.entries(dict)) result = result.replaceAll(key, String(value));
196
+ const regex = /\$Number%([0-9]+)d\$/g;
197
+ if (regex.test(result)) {
198
+ const template = dict[DASH_TAGS.TemplateNumber];
199
+ result = result.replace(regex, (_match, p1) => {
200
+ if (!template) return "";
201
+ return template.toString().padStart(parseInt(p1), "0");
202
+ });
203
+ }
204
+ return result;
205
+ };
206
+ /**
207
+ * Extracts parameters from text like:
208
+ * #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
209
+ * @param line - The line of text to be parsed
210
+ * @param key - If empty, returns all characters after the first colon
211
+ * @returns The extracted attribute value
212
+ */
213
+ const getAttribute = (line, key = "") => {
214
+ line = line.trim();
215
+ if (key === "") return line.slice(line.indexOf(":") + 1);
216
+ let index = -1;
217
+ let result = "";
218
+ if ((index = line.indexOf(key + "=\"")) > -1) {
219
+ const startIndex = index + (key + "=\"").length;
220
+ const endIndex = line.indexOf("\"", startIndex);
221
+ result = line.slice(startIndex, endIndex);
222
+ } else if ((index = line.indexOf(key + "=")) > -1) {
223
+ const startIndex = index + (key + "=").length;
224
+ const endIndex = line.indexOf(",", startIndex);
225
+ result = endIndex >= startIndex ? line.slice(startIndex, endIndex) : line.slice(startIndex);
226
+ }
227
+ return result;
228
+ };
229
+ const distinctBy = (array, callbackfn) => {
230
+ const seen = new Set();
231
+ return array.filter((item) => {
232
+ const value = callbackfn(item);
233
+ if (seen.has(value)) return false;
234
+ seen.add(value);
235
+ return true;
236
+ });
237
+ };
238
+
239
+ //#endregion
240
+ //#region lib/shared/playlist.ts
241
+ var Playlist = class {
242
+ url = "";
243
+ isLive = false;
244
+ refreshIntervalMs = 15e3;
245
+ get totalDuration() {
246
+ let result = 0;
247
+ for (const part of this.mediaParts) for (const segment of part.mediaSegments) result += segment.duration;
248
+ return result;
249
+ }
250
+ targetDuration;
251
+ mediaInit;
252
+ mediaParts = [];
253
+ };
254
+
255
+ //#endregion
256
+ //#region lib/shared/media-part.ts
257
+ var MediaPart = class {
258
+ mediaSegments = [];
259
+ constructor(segments) {
260
+ this.mediaSegments = segments || [];
261
+ }
262
+ };
263
+
264
+ //#endregion
265
+ //#region lib/shared/encrypt-info.ts
266
+ var EncryptInfo = class {
267
+ method = ENCRYPT_METHODS.NONE;
268
+ key;
269
+ iv;
270
+ constructor(method) {
271
+ this.method = this.parseMethod(method);
272
+ }
273
+ parseMethod(method) {
274
+ if (method !== void 0) return ENCRYPT_METHODS[method.replace("-", "_")];
275
+ else return ENCRYPT_METHODS.UNKNOWN;
276
+ }
277
+ };
278
+
279
+ //#endregion
280
+ //#region lib/shared/media-segment.ts
281
+ var MediaSegment = class MediaSegment {
282
+ index = NaN;
283
+ duration = NaN;
284
+ title;
285
+ dateTime;
286
+ startRange;
287
+ get stopRange() {
288
+ return this.startRange !== void 0 && this.expectLength !== void 0 ? this.startRange + this.expectLength - 1 : void 0;
289
+ }
290
+ expectLength;
291
+ encryptInfo = new EncryptInfo();
292
+ get isEncrypted() {
293
+ return this.encryptInfo.method !== ENCRYPT_METHODS.NONE;
294
+ }
295
+ url = "";
296
+ nameFromVar;
297
+ equals(segment) {
298
+ if (segment instanceof MediaSegment) return this.index == segment.index && Math.abs(this.duration - segment.duration) < .001 && this.title == segment.title && this.startRange == segment.startRange && this.stopRange == segment.stopRange && this.expectLength == segment.expectLength && this.url == segment.url;
299
+ else return false;
300
+ }
301
+ getHashCode() {
302
+ const payload = [
303
+ this.index,
304
+ this.duration,
305
+ this.title,
306
+ this.startRange,
307
+ this.stopRange,
308
+ this.expectLength,
309
+ this.url
310
+ ].join("-");
311
+ return crypto.createHash("md5").update(payload).digest("hex");
312
+ }
313
+ };
314
+
315
+ //#endregion
316
+ //#region lib/dash/dash-utils.ts
317
+ /**
318
+ * Extracts StartRange and ExpectLength information from a string like "100-300"
319
+ * @param range - The range string in the format "start-end"
320
+ * @returns A tuple containing [StartRange, ExpectLength]
321
+ */
322
+ const parseRange = (range) => {
323
+ const [startRange, end] = range.split("-").map(Number);
324
+ const expectLength = end - startRange + 1;
325
+ return [startRange, expectLength];
326
+ };
327
+
328
+ //#endregion
329
+ //#region lib/dash/dash-extractor.ts
330
+ var DashExtractor = class DashExtractor {
331
+ static #DEFAULT_METHOD = ENCRYPT_METHODS.CENC;
332
+ get extractorType() {
333
+ return EXTRACTOR_TYPES.MPEG_DASH;
334
+ }
335
+ #mpdUrl = "";
336
+ #baseUrl = "";
337
+ #mpdContent = "";
338
+ #parserConfig;
339
+ constructor(parserConfig) {
340
+ this.#parserConfig = parserConfig;
341
+ this.#setInitUrl();
342
+ }
343
+ #setInitUrl() {
344
+ this.#mpdUrl = this.#parserConfig.url ?? "";
345
+ this.#baseUrl = this.#parserConfig.baseUrl ?? this.#mpdUrl;
346
+ }
347
+ #extendBaseUrl(node, baseUrl) {
348
+ const target = node.getElementsByTagName("BaseURL")[0];
349
+ if (target?.textContent) return combineUrl(baseUrl, target.textContent);
350
+ return baseUrl;
351
+ }
352
+ #getFrameRate(node) {
353
+ const frameRate = node.getAttribute("frameRate");
354
+ if (!frameRate || !frameRate.includes("/")) return;
355
+ const d = Number(frameRate.split("/")[0]) / Number(frameRate.split("/")[1]);
356
+ return Number(d.toFixed(3));
357
+ }
358
+ async extractStreams(rawText) {
359
+ const streamList = [];
360
+ this.#mpdContent = rawText;
361
+ const document = new DOMParser().parseFromString(this.#mpdContent, "text/xml");
362
+ const mpdElement = document.getElementsByTagName("MPD")[0];
363
+ const type = mpdElement.getAttribute("type");
364
+ const isLive = type === "dynamic";
365
+ const maxSegmentDuration = mpdElement.getAttribute("maxSegmentDuration");
366
+ const availabilityStartTime = mpdElement.getAttribute("availabilityStartTime");
367
+ const timeShiftBufferDepth = mpdElement.getAttribute("timeShiftBufferDepth") || "PT1M";
368
+ const publishTime = mpdElement.getAttribute("publishTime");
369
+ const mediaPresentationDuration = mpdElement.getAttribute("mediaPresentationDuration");
370
+ const baseUrlElement = mpdElement.getElementsByTagName("BaseURL")[0];
371
+ if (baseUrlElement?.textContent) {
372
+ let baseUrl = baseUrlElement.textContent;
373
+ if (baseUrl.includes("kkbox.com.tw/")) baseUrl = baseUrl.replace("//https:%2F%2F", "//");
374
+ this.#baseUrl = combineUrl(this.#mpdUrl, baseUrl);
375
+ }
376
+ const periods = mpdElement.getElementsByTagName("Period");
377
+ for (const period of periods) {
378
+ const periodDuration = period.getAttribute("duration");
379
+ const periodId = period.getAttribute("id");
380
+ const periodDurationSeconds = Temporal.Duration.from(periodDuration || mediaPresentationDuration || "PT0S").total("seconds");
381
+ let segBaseUrl = this.#extendBaseUrl(period, this.#baseUrl);
382
+ const adaptationSetsBaseUrl = segBaseUrl;
383
+ const adaptationSets = period.getElementsByTagName("AdaptationSet");
384
+ for (const adaptationSet of adaptationSets) {
385
+ segBaseUrl = this.#extendBaseUrl(adaptationSet, segBaseUrl);
386
+ const representationsBaseUrl = segBaseUrl;
387
+ let mimeType = adaptationSet.getAttribute("contentType") || adaptationSet.getAttribute("mimeType");
388
+ const frameRate = this.#getFrameRate(adaptationSet);
389
+ const representations = adaptationSet.getElementsByTagName("Representation");
390
+ for (const representation of representations) {
391
+ segBaseUrl = this.#extendBaseUrl(representation, segBaseUrl);
392
+ if (!mimeType) mimeType = representation.getAttribute("contentType") || representation.getAttribute("mimeType") || "";
393
+ const bandwidth = representation.getAttribute("bandwidth");
394
+ const streamSpec = new StreamSpec();
395
+ streamSpec.originalUrl = this.#parserConfig.originalUrl;
396
+ streamSpec.periodId = periodId;
397
+ streamSpec.playlist = new Playlist();
398
+ streamSpec.playlist.mediaParts.push(new MediaPart());
399
+ streamSpec.groupId = representation.getAttribute("id");
400
+ streamSpec.bandwidth = Number(bandwidth || 0);
401
+ streamSpec.codecs = representation.getAttribute("codecs") || adaptationSet.getAttribute("codecs");
402
+ streamSpec.language = this.#filterLanguage(representation.getAttribute("lang") || adaptationSet.getAttribute("lang"));
403
+ streamSpec.frameRate = frameRate || this.#getFrameRate(representation);
404
+ const width = representation.getAttribute("width");
405
+ const height = representation.getAttribute("height");
406
+ streamSpec.resolution = width && height ? `${width}x${height}` : void 0;
407
+ streamSpec.url = this.#mpdUrl;
408
+ const mimeTypePart = mimeType.split("/")[0];
409
+ if (mimeTypePart === "text") streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
410
+ else if (mimeTypePart === "audio") streamSpec.mediaType = MEDIA_TYPES.AUDIO;
411
+ else if (mimeTypePart === "video" || !!streamSpec.resolution) streamSpec.mediaType = MEDIA_TYPES.VIDEO;
412
+ const volumeAdjust = representation.getAttribute("volumeAdjust");
413
+ if (volumeAdjust) streamSpec.groupId = streamSpec.groupId + "-" + volumeAdjust;
414
+ const mType = representation.getAttribute("mimeType") || adaptationSet.getAttribute("mimeType");
415
+ if (mType) {
416
+ const mTypeSplit = mType.split("/");
417
+ streamSpec.extension = mTypeSplit.length === 2 ? mTypeSplit[1] : null;
418
+ }
419
+ if (streamSpec.codecs === "stpp" || streamSpec.codecs === "wvtt") streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
420
+ const role = representation.getElementsByTagName("Role")[0] || adaptationSet.getElementsByTagName("Role")[0];
421
+ if (role) {
422
+ const roleValue = role.getAttribute("value");
423
+ const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
424
+ const roleTypeKey = roleValue.split("-").map(capitalize).join("");
425
+ const roleType = ROLE_TYPE[roleTypeKey];
426
+ streamSpec.role = roleType;
427
+ if (roleType === ROLE_TYPE.Subtitle) {
428
+ streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
429
+ if (mType?.includes("ttml")) streamSpec.extension = "ttml";
430
+ } else if (roleType === ROLE_TYPE.ForcedSubtitle) streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
431
+ }
432
+ streamSpec.playlist.isLive = isLive;
433
+ if (timeShiftBufferDepth) streamSpec.playlist.refreshIntervalMs = Temporal.Duration.from(timeShiftBufferDepth).total("milliseconds") / 2;
434
+ const audioChannelConfiguration = adaptationSet.getElementsByTagName("AudioChannelConfiguration")[0] || representation.getElementsByTagName("AudioChannelConfiguration")[0];
435
+ if (audioChannelConfiguration) streamSpec.channels = audioChannelConfiguration.getAttribute("value");
436
+ if (publishTime) streamSpec.publishTime = new Date(publishTime);
437
+ const segmentBaseElement = representation.getElementsByTagName("SegmentBase")[0];
438
+ if (segmentBaseElement) {
439
+ const initialization = segmentBaseElement.getElementsByTagName("Initialization")[0];
440
+ if (initialization) {
441
+ const sourceUrl = initialization.getAttribute("sourceURL");
442
+ if (!sourceUrl) {
443
+ const mediaSegment = new MediaSegment();
444
+ mediaSegment.index = 0;
445
+ mediaSegment.url = segBaseUrl;
446
+ mediaSegment.duration = periodDurationSeconds;
447
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
448
+ } else {
449
+ const initUrl = combineUrl(segBaseUrl, sourceUrl);
450
+ const initRange = initialization.getAttribute("range");
451
+ const initSegment = new MediaSegment();
452
+ initSegment.index = -1;
453
+ initSegment.url = initUrl;
454
+ if (initRange) {
455
+ const [start, expect] = parseRange(initRange);
456
+ initSegment.startRange = start;
457
+ initSegment.expectLength = expect;
458
+ }
459
+ streamSpec.playlist.mediaInit = initSegment;
460
+ }
461
+ }
462
+ }
463
+ const segmentList = representation.getElementsByTagName("SegmentList")[0];
464
+ if (segmentList) {
465
+ const durationStr = segmentList.getAttribute("duration");
466
+ const initialization = segmentList.getElementsByTagName("Initialization")[0];
467
+ if (initialization) {
468
+ const sourceUrl = initialization.getAttribute("sourceURL");
469
+ const initUrl = combineUrl(segBaseUrl, sourceUrl);
470
+ const initRange = initialization.getAttribute("range");
471
+ const initSegment = new MediaSegment();
472
+ initSegment.index = -1;
473
+ initSegment.url = initUrl;
474
+ if (initRange) {
475
+ const [start, expect] = parseRange(initRange);
476
+ initSegment.startRange = start;
477
+ initSegment.expectLength = expect;
478
+ }
479
+ streamSpec.playlist.mediaInit = initSegment;
480
+ }
481
+ const segmentUrls = segmentList.getElementsByTagName("SegmentURL");
482
+ const timescaleStr = segmentList.getAttribute("timescale") || "1";
483
+ for (let segmentIndex = 0; segmentIndex < segmentUrls.length; segmentIndex++) {
484
+ const segmentUrl = segmentUrls[segmentIndex];
485
+ const mediaUrl = combineUrl(segBaseUrl, segmentUrl.getAttribute("media"));
486
+ const mediaRange = segmentUrl.getAttribute("mediaRange");
487
+ const timescale = Number(timescaleStr);
488
+ const duration = Number(durationStr);
489
+ const segment = new MediaSegment();
490
+ segment.index = segmentIndex;
491
+ segment.url = mediaUrl;
492
+ segment.duration = duration / timescale;
493
+ if (mediaRange) {
494
+ const [start, expect] = parseRange(mediaRange);
495
+ segment.startRange = start;
496
+ segment.expectLength = expect;
497
+ }
498
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(segment);
499
+ }
500
+ }
501
+ const segmentTemplateElementsOuter = adaptationSet.getElementsByTagName("SegmentTemplate");
502
+ const segmentTemplateElements = representation.getElementsByTagName("SegmentTemplate");
503
+ if (segmentTemplateElementsOuter.length || segmentTemplateElements.length) {
504
+ const segmentTemplate = segmentTemplateElements[0] || segmentTemplateElementsOuter[0];
505
+ const segmentTemplateOuter = segmentTemplateElementsOuter[0] || segmentTemplateElements[0];
506
+ const varDic = {};
507
+ varDic[DASH_TAGS.TemplateRepresentationID] = streamSpec.groupId;
508
+ varDic[DASH_TAGS.TemplateBandwidth] = bandwidth;
509
+ const presentationTimeOffsetStr = segmentTemplate.getAttribute("presentationTimeOffset") || segmentTemplateOuter.getAttribute("presentationTimeOffset") || "0";
510
+ const timescaleStr = segmentTemplate.getAttribute("timescale") || segmentTemplateOuter.getAttribute("timescale") || "1";
511
+ const durationStr = segmentTemplate.getAttribute("duration") || segmentTemplateOuter.getAttribute("duration");
512
+ const startNumberStr = segmentTemplate.getAttribute("startNumber") || segmentTemplateOuter.getAttribute("startNumber") || "1";
513
+ const initialization = segmentTemplate.getAttribute("initialization") || segmentTemplateOuter.getAttribute("initialization");
514
+ if (initialization) {
515
+ const _init = replaceVars(initialization, varDic);
516
+ const initUrl = combineUrl(segBaseUrl, _init);
517
+ const mediaSegment = new MediaSegment();
518
+ mediaSegment.index = -1;
519
+ mediaSegment.url = initUrl;
520
+ streamSpec.playlist.mediaInit = mediaSegment;
521
+ }
522
+ const mediaTemplate = segmentTemplate.getAttribute("media") || segmentTemplateOuter.getAttribute("media");
523
+ const segmentTimeline = segmentTemplate.getElementsByTagName("SegmentTimeline")[0];
524
+ if (segmentTimeline) {
525
+ const Ss = segmentTimeline.getElementsByTagName("S");
526
+ let segNumber = Number(startNumberStr);
527
+ let currentTime = 0;
528
+ let segIndex = 0;
529
+ for (const s of Ss) {
530
+ const _startTimeStr = s.getAttribute("t");
531
+ const _durationStr = s.getAttribute("d");
532
+ const _repeatCountStr = s.getAttribute("r");
533
+ if (_startTimeStr) currentTime = Number(_startTimeStr);
534
+ const _duration = Number(_durationStr);
535
+ const timescale = Number(timescaleStr);
536
+ let _repeatCount = Number(_repeatCountStr);
537
+ varDic[DASH_TAGS.TemplateTime] = currentTime;
538
+ varDic[DASH_TAGS.TemplateNumber] = segNumber++;
539
+ const hasTime = mediaTemplate?.includes(DASH_TAGS.TemplateTime);
540
+ const media = replaceVars(mediaTemplate, varDic);
541
+ const mediaUrl = combineUrl(segBaseUrl, media);
542
+ const mediaSegment = new MediaSegment();
543
+ mediaSegment.url = mediaUrl;
544
+ if (hasTime) mediaSegment.nameFromVar = currentTime.toString();
545
+ mediaSegment.duration = _duration / timescale;
546
+ mediaSegment.index = segIndex++;
547
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
548
+ if (_repeatCount < 0) _repeatCount = Math.ceil(periodDurationSeconds * timescale / _duration) - 1;
549
+ for (let i = 0; i < _repeatCount; i++) {
550
+ currentTime += _duration;
551
+ const _mediaSegment = new MediaSegment();
552
+ varDic[DASH_TAGS.TemplateTime] = currentTime;
553
+ varDic[DASH_TAGS.TemplateNumber] = segNumber++;
554
+ const _hashTime = mediaTemplate?.includes(DASH_TAGS.TemplateTime);
555
+ const _media = replaceVars(mediaTemplate, varDic);
556
+ const _mediaUrl = combineUrl(segBaseUrl, _media);
557
+ _mediaSegment.url = _mediaUrl;
558
+ _mediaSegment.index = segIndex++;
559
+ _mediaSegment.duration = _duration / timescale;
560
+ if (_hashTime) _mediaSegment.nameFromVar = currentTime.toString();
561
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(_mediaSegment);
562
+ }
563
+ currentTime += _duration;
564
+ }
565
+ } else {
566
+ const timescale = Number(timescaleStr);
567
+ let startNumber = Number(startNumberStr);
568
+ const duration = Number(durationStr);
569
+ let totalNumber = Math.ceil(periodDurationSeconds * timescale / duration);
570
+ if (totalNumber === 0 && isLive) {
571
+ const now = Date.now();
572
+ const availableTime = new Date(availabilityStartTime);
573
+ const offsetMs = Number(presentationTimeOffsetStr) / 1e3;
574
+ availableTime.setUTCMilliseconds(availableTime.getUTCMilliseconds() + offsetMs);
575
+ const ts = (now - availableTime.getTime()) / 1e3;
576
+ const updateTs = Temporal.Duration.from(timeShiftBufferDepth).total("seconds");
577
+ startNumber += (ts - updateTs) * timescale / duration;
578
+ totalNumber = updateTs * timescale / duration;
579
+ }
580
+ for (let index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++) {
581
+ varDic[DASH_TAGS.TemplateNumber] = index;
582
+ const hasNumber = mediaTemplate.includes(DASH_TAGS.TemplateNumber);
583
+ const media = replaceVars(mediaTemplate, varDic);
584
+ const mediaUrl = combineUrl(segBaseUrl, media);
585
+ const mediaSegment = new MediaSegment();
586
+ mediaSegment.url = mediaUrl;
587
+ if (hasNumber) mediaSegment.nameFromVar = index.toString();
588
+ mediaSegment.index = isLive ? index : segIndex;
589
+ mediaSegment.duration = duration / timescale;
590
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
591
+ }
592
+ }
593
+ }
594
+ if (streamSpec.playlist.mediaParts[0].mediaSegments.length === 0) {
595
+ const mediaSegment = new MediaSegment();
596
+ mediaSegment.index = 0;
597
+ mediaSegment.url = segBaseUrl;
598
+ mediaSegment.duration = periodDurationSeconds;
599
+ streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
600
+ }
601
+ const contentProtection = adaptationSet.getElementsByTagName("ContentProtection")[0] || representation.getElementsByTagName("ContentProtection")[0];
602
+ if (contentProtection) {
603
+ const encryptInfo = new EncryptInfo();
604
+ encryptInfo.method = DashExtractor.#DEFAULT_METHOD;
605
+ if (streamSpec.playlist.mediaInit) streamSpec.playlist.mediaInit.encryptInfo = encryptInfo;
606
+ const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
607
+ for (const segment of segments) if (!segment.encryptInfo) segment.encryptInfo = encryptInfo;
608
+ }
609
+ const _index = streamList.findIndex((item) => item.periodId !== streamSpec.periodId && item.groupId === streamSpec.groupId && item.resolution === streamSpec.resolution && item.mediaType === streamSpec.mediaType);
610
+ if (_index > -1) if (isLive) {} else {
611
+ const url1 = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).url;
612
+ const url2 = streamSpec.playlist.mediaParts[0].mediaSegments.at(-1)?.url;
613
+ if (url1 !== url2) {
614
+ const startIndex = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).index + 1;
615
+ const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
616
+ for (const segment of segments) segment.index += startIndex;
617
+ const mediaPart = new MediaPart();
618
+ mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
619
+ streamList[_index].playlist.mediaParts.push(mediaPart);
620
+ } else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
621
+ }
622
+ else {
623
+ if (streamSpec.mediaType === MEDIA_TYPES.SUBTITLES && streamSpec.extension === "mp4") streamSpec.extension = "m4s";
624
+ if (streamSpec.mediaType !== MEDIA_TYPES.SUBTITLES && (streamSpec.extension == null || streamSpec.playlist.mediaParts.reduce((sum, part) => sum + part.mediaSegments.length, 0) > 1)) streamSpec.extension = "m4s";
625
+ streamList.push(streamSpec);
626
+ }
627
+ segBaseUrl = representationsBaseUrl;
628
+ }
629
+ segBaseUrl = adaptationSetsBaseUrl;
630
+ }
631
+ }
632
+ const audioList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.AUDIO);
633
+ const subtitleList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.SUBTITLES);
634
+ const videoList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.VIDEO);
635
+ for (const video of videoList) {
636
+ const audioGroupId = audioList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
637
+ const subtitleGroupId = subtitleList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
638
+ if (audioGroupId) video.audioId = audioGroupId;
639
+ if (subtitleGroupId) video.subtitleId = subtitleGroupId;
640
+ }
641
+ return streamList;
642
+ }
643
+ #filterLanguage(v) {
644
+ if (!v) return;
645
+ return v;
646
+ }
647
+ fetchPlayList(streamSpecs) {
648
+ throw new Error("Method not implemented.");
649
+ }
650
+ refreshPlayList(streamSpecs) {
651
+ throw new Error("Method not implemented.");
652
+ }
653
+ preProcessUrl(url) {
654
+ throw new Error("Method not implemented.");
655
+ }
656
+ preProcessContent() {
657
+ throw new Error("Method not implemented.");
658
+ }
659
+ };
660
+
661
+ //#endregion
662
+ //#region lib/hls/hls-utils.ts
663
+ /**
664
+ * Extracts length and optional start values from a string formatted as "n[@o]".
665
+ * @param input - The input string.
666
+ * @returns A tuple containing [n (length), o (start)].
667
+ */
668
+ function getRange(input) {
669
+ const parts = input.split("@");
670
+ switch (parts.length) {
671
+ case 0: return [0, null];
672
+ case 1: return [parseInt(parts[0], 10), null];
673
+ case 2: return [parseInt(parts[0], 10), parseInt(parts[1], 10)];
674
+ default: return [0, null];
675
+ }
676
+ }
677
+
678
+ //#endregion
679
+ //#region lib/hls/hls-extractor.ts
680
+ var HlsExtractor = class {
681
+ get extractorType() {
682
+ return EXTRACTOR_TYPES.HLS;
683
+ }
684
+ #m3u8Url = "";
685
+ #baseUrl = "";
686
+ #m3u8Content = "";
687
+ #masterM3u8Flag = false;
688
+ parserConfig;
689
+ constructor(parserConfig) {
690
+ this.parserConfig = parserConfig;
691
+ this.#m3u8Url = parserConfig.url || "";
692
+ this.#setBaseUrl();
693
+ }
694
+ #setBaseUrl() {
695
+ this.#baseUrl = this.parserConfig.baseUrl || this.#m3u8Url;
696
+ }
697
+ preProcessContent() {
698
+ this.#m3u8Content = this.#m3u8Content.trim();
699
+ if (!this.#m3u8Content.startsWith(HLS_TAGS.extM3u)) throw new Error("Invalid m3u8");
700
+ for (const processor of this.parserConfig.contentProcessors) if (processor.canProcess(this.extractorType, this.#m3u8Content, this.parserConfig)) this.#m3u8Content = processor.process(this.#m3u8Content, this.parserConfig);
701
+ }
702
+ preProcessUrl(url) {
703
+ let result = url;
704
+ for (const processor of this.parserConfig.urlProcessors) if (processor.canProcess(this.extractorType, url, this.parserConfig)) result = processor.process(url, this.parserConfig);
705
+ return result;
706
+ }
707
+ async #parseMasterList() {
708
+ this.#masterM3u8Flag = true;
709
+ const streams = [];
710
+ let expectPlaylist = false;
711
+ let streamSpec = new StreamSpec();
712
+ const lines = this.#m3u8Content.split("\n");
713
+ for (const line of lines) {
714
+ if (!line.trim()) continue;
715
+ if (line.startsWith(HLS_TAGS.extXStreamInf)) {
716
+ streamSpec = new StreamSpec();
717
+ streamSpec.originalUrl = this.parserConfig.originalUrl;
718
+ const bandwidth = getAttribute(line, "AVERAGE-BANDWIDTH") || getAttribute(line, "BANDWIDTH");
719
+ streamSpec.bandwidth = Number(bandwidth || 0);
720
+ streamSpec.codecs = getAttribute(line, "CODECS");
721
+ streamSpec.resolution = getAttribute(line, "RESOLUTION");
722
+ const frameRate = getAttribute(line, "FRAME-RATE");
723
+ if (frameRate) streamSpec.frameRate = Number(frameRate);
724
+ const audioId = getAttribute(line, "AUDIO");
725
+ if (audioId) streamSpec.audioId = audioId;
726
+ const videoId = getAttribute(line, "VIDEO");
727
+ if (videoId) streamSpec.videoId = videoId;
728
+ const subtitleId = getAttribute(line, "SUBTITLES");
729
+ if (subtitleId) streamSpec.subtitleId = subtitleId;
730
+ const videoRange = getAttribute(line, "VIDEO-RANGE");
731
+ if (videoRange) streamSpec.videoRange = videoRange;
732
+ if (streamSpec.codecs && streamSpec.audioId) streamSpec.codecs = streamSpec.codecs.split(",")[0];
733
+ expectPlaylist = true;
734
+ } else if (line.startsWith(HLS_TAGS.extXMedia)) {
735
+ streamSpec = new StreamSpec();
736
+ const type = getAttribute(line, "TYPE").replace("-", "_");
737
+ const mediaType = MEDIA_TYPES[type];
738
+ if (mediaType) streamSpec.mediaType = mediaType;
739
+ if (mediaType === MEDIA_TYPES.CLOSED_CAPTIONS) continue;
740
+ let url = getAttribute(line, "URI");
741
+ if (!url) continue;
742
+ url = combineUrl(this.#baseUrl, url);
743
+ streamSpec.url = this.preProcessUrl(url);
744
+ const groupId = getAttribute(line, "GROUP-ID");
745
+ if (groupId) streamSpec.groupId = groupId;
746
+ const language = getAttribute(line, "LANGUAGE");
747
+ if (language) streamSpec.language = language;
748
+ const name = getAttribute(line, "NAME");
749
+ if (name) streamSpec.name = name;
750
+ const defaultFlag = getAttribute(line, "DEFAULT");
751
+ if (defaultFlag) streamSpec.default = defaultFlag.toLowerCase() === "yes";
752
+ const channels = getAttribute(line, "CHANNELS");
753
+ if (channels) streamSpec.channels = channels;
754
+ const characteristics = getAttribute(line, "CHARACTERISTICS");
755
+ if (characteristics) streamSpec.characteristics = characteristics.split(",").at(-1)?.split(".").at(-1);
756
+ streams.push(streamSpec);
757
+ } else if (line.startsWith("#")) continue;
758
+ else if (expectPlaylist) {
759
+ const url = combineUrl(this.#baseUrl, line);
760
+ streamSpec.url = this.preProcessUrl(url);
761
+ expectPlaylist = false;
762
+ streams.push(streamSpec);
763
+ }
764
+ }
765
+ return streams;
766
+ }
767
+ async #parseList() {
768
+ let hasAd = false;
769
+ const allowHlsMultiExtMap = this.parserConfig.customParserArgs["allowHlsMultiExtMap"] === "true";
770
+ if (allowHlsMultiExtMap) console.log(`allowHlsMultiExtMap is set to true`);
771
+ let expectSegment = false;
772
+ let isEndList = false;
773
+ let segIndex = 0;
774
+ let isAd = false;
775
+ const playlist = new Playlist();
776
+ const mediaParts = [];
777
+ const currentEncryptInfo = new EncryptInfo();
778
+ if (this.parserConfig.customMethod) currentEncryptInfo.method = this.parserConfig.customMethod;
779
+ if (this.parserConfig.customKey) currentEncryptInfo.key = this.parserConfig.customKey;
780
+ if (this.parserConfig.customIv) currentEncryptInfo.iv = this.parserConfig.customIv;
781
+ let lastKeyLine = "";
782
+ let segment = new MediaSegment();
783
+ let segments = [];
784
+ const lines = this.#m3u8Content.split("\n");
785
+ for (const line of lines) {
786
+ if (!line.trim()) continue;
787
+ if (line.startsWith(HLS_TAGS.extXByterange)) {
788
+ const p = getAttribute(line);
789
+ const [n, o] = getRange(p);
790
+ segment.expectLength = n;
791
+ segment.startRange = o || (segments.at(-1)?.startRange || 0) + (segments.at(-1)?.expectLength || 0);
792
+ expectSegment = true;
793
+ } else if (line.startsWith("#UPLYNK-SEGMENT")) {
794
+ if (line.includes(",ad")) isAd = true;
795
+ else if (line.includes(",segment")) isAd = false;
796
+ } else if (isAd) continue;
797
+ else if (line.startsWith(HLS_TAGS.extXTargetDuration)) playlist.targetDuration = Number(getAttribute(line));
798
+ else if (line.startsWith(HLS_TAGS.extXMediaSequence)) segIndex = Number(getAttribute(line));
799
+ else if (line.startsWith(HLS_TAGS.extXProgramDateTime)) segment.dateTime = new Date(getAttribute(line));
800
+ else if (line.startsWith(HLS_TAGS.extXDiscontinuity)) {
801
+ if (hasAd && mediaParts.length) {
802
+ segments = mediaParts.at(-1)?.mediaSegments || [];
803
+ mediaParts.pop();
804
+ hasAd = false;
805
+ continue;
806
+ }
807
+ if (hasAd && !segments.length) continue;
808
+ mediaParts.push(new MediaPart(segments));
809
+ segments = [];
810
+ } else if (line.startsWith(HLS_TAGS.extXKey)) {
811
+ const uri = getAttribute(line, "URI");
812
+ const uriLast = getAttribute(lastKeyLine, "URI");
813
+ if (uri !== uriLast) {
814
+ const parsedInfo = this.#parseKey(line);
815
+ currentEncryptInfo.method = parsedInfo.method;
816
+ currentEncryptInfo.key = parsedInfo.key;
817
+ currentEncryptInfo.iv = parsedInfo.iv;
818
+ }
819
+ lastKeyLine = line;
820
+ } else if (line.startsWith(HLS_TAGS.extInf)) {
821
+ const tmp = getAttribute(line).split(",");
822
+ segment.duration = Number(tmp[0]);
823
+ segment.index = segIndex;
824
+ if (currentEncryptInfo.method != ENCRYPT_METHODS.NONE) {
825
+ segment.encryptInfo.method = currentEncryptInfo.method;
826
+ segment.encryptInfo.key = currentEncryptInfo.key;
827
+ segment.encryptInfo.iv = currentEncryptInfo.iv;
828
+ }
829
+ expectSegment = true;
830
+ segIndex++;
831
+ } else if (line.startsWith(HLS_TAGS.extXEndlist)) {
832
+ if (segments.length > 0) mediaParts.push(new MediaPart(segments));
833
+ segments = [];
834
+ isEndList = true;
835
+ } else if (line.startsWith(HLS_TAGS.extXMap)) if (!playlist.mediaInit || hasAd) {
836
+ const mediaSegment = new MediaSegment();
837
+ mediaSegment.url = this.preProcessUrl(combineUrl(this.#baseUrl, getAttribute(line, "URI")));
838
+ mediaSegment.index = -1;
839
+ playlist.mediaInit = mediaSegment;
840
+ if (line.includes("BYTERANGE")) {
841
+ const p = getAttribute(line, "BYTERANGE");
842
+ const [n, o] = getRange(p);
843
+ mediaSegment.expectLength = n;
844
+ mediaSegment.startRange = o || 0;
845
+ }
846
+ if (currentEncryptInfo.method === ENCRYPT_METHODS.NONE) continue;
847
+ playlist.mediaInit.encryptInfo.method = currentEncryptInfo.method;
848
+ playlist.mediaInit.encryptInfo.key = currentEncryptInfo.key;
849
+ playlist.mediaInit.encryptInfo.iv = currentEncryptInfo.iv;
850
+ } else {
851
+ if (segments.length) mediaParts.push(new MediaPart(segments));
852
+ segments = [];
853
+ if (!allowHlsMultiExtMap) {
854
+ isEndList = true;
855
+ break;
856
+ }
857
+ }
858
+ else if (line.startsWith("#")) continue;
859
+ else if (line.startsWith("\r\n")) continue;
860
+ else if (expectSegment) {
861
+ const segUrl = this.preProcessUrl(combineUrl(this.#baseUrl, line));
862
+ segment.url = segUrl;
863
+ segments.push(segment);
864
+ segment = new MediaSegment();
865
+ if (segUrl.includes("ccode=") && segUrl.includes("/ad/") && segUrl.includes("duratio=")) {
866
+ segments.pop();
867
+ segIndex--;
868
+ hasAd = true;
869
+ }
870
+ if (segUrl.includes("ccode=0902") && segUrl.includes("duration=")) {
871
+ segments.pop();
872
+ segIndex--;
873
+ hasAd = false;
874
+ }
875
+ expectSegment = false;
876
+ }
877
+ }
878
+ if (!isEndList) mediaParts.push(new MediaPart(segments));
879
+ playlist.mediaParts = mediaParts;
880
+ playlist.isLive = !isEndList;
881
+ if (playlist.isLive) playlist.refreshIntervalMs = (playlist.targetDuration || 5) * 2 * 1e3;
882
+ return playlist;
883
+ }
884
+ #parseKey(keyLine) {
885
+ for (const p of this.parserConfig.keyProcessors) if (p.canProcess(this.extractorType, keyLine, this.#m3u8Url, this.#m3u8Content, this.parserConfig)) return p.process(keyLine, this.#m3u8Url, this.#m3u8Content, this.parserConfig);
886
+ throw new Error("No key processor found");
887
+ }
888
+ async extractStreams(rawText) {
889
+ this.#m3u8Content = rawText;
890
+ this.preProcessContent();
891
+ if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) {
892
+ console.log("Master m3u8 found");
893
+ return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
894
+ }
895
+ const playlist = await this.#parseList();
896
+ const streamSpec = new StreamSpec();
897
+ streamSpec.url = this.parserConfig.url;
898
+ streamSpec.playlist = playlist;
899
+ streamSpec.extension = playlist.mediaInit ? "mp4" : "ts";
900
+ return [streamSpec];
901
+ }
902
+ async #loadM3u8FromUrl(url) {
903
+ if (url.startsWith("file:")) {
904
+ const uri = new URL(url);
905
+ const filePath = uri.pathname;
906
+ this.#m3u8Content = await readFile(filePath, "utf8");
907
+ } else if (url.startsWith("http")) try {
908
+ const response = await fetch(url, { headers: this.parserConfig.headers });
909
+ url = response.url;
910
+ this.#m3u8Content = await response.text();
911
+ } catch (e) {
912
+ if (url !== this.parserConfig.originalUrl) {
913
+ const response = await fetch(this.parserConfig.originalUrl, { headers: this.parserConfig.headers });
914
+ url = response.url;
915
+ this.#m3u8Content = await response.text();
916
+ }
917
+ }
918
+ this.#m3u8Url = url;
919
+ this.#setBaseUrl();
920
+ this.preProcessContent();
921
+ }
922
+ async #refreshUrlFromMaster(lists) {
923
+ await this.#loadM3u8FromUrl(this.parserConfig.url);
924
+ const newStreams = await this.#parseMasterList().then((lists$1) => distinctBy(lists$1, (list) => list.url));
925
+ for (const list of lists) {
926
+ const match = newStreams.filter((stream) => stream.toShortString() === list.toShortString());
927
+ if (!match.length) continue;
928
+ list.url = match.at(0).url;
929
+ }
930
+ }
931
+ async fetchPlayList(lists) {
932
+ for (const list of lists) {
933
+ try {
934
+ await this.#loadM3u8FromUrl(list.url);
935
+ } catch (e) {
936
+ if (this.#masterM3u8Flag) console.warn("Can not load m3u8. Try refreshing url from master url...");
937
+ await this.#refreshUrlFromMaster(lists);
938
+ await this.#loadM3u8FromUrl(list.url);
939
+ }
940
+ const newPlaylist = await this.#parseList();
941
+ if (list.playlist?.mediaInit) list.playlist.mediaParts = newPlaylist.mediaParts;
942
+ else list.playlist = newPlaylist;
943
+ if (list.mediaType === MEDIA_TYPES.SUBTITLES) {
944
+ const a = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".ttml")));
945
+ const b = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".vtt") || segment.url.includes(".webvtt")));
946
+ if (a) list.extension = "ttml";
947
+ if (b) list.extension = "vtt";
948
+ } else list.extension = list.playlist.mediaInit ? "m4s" : "ts";
949
+ }
950
+ }
951
+ async refreshPlayList(streamSpecs) {
952
+ await this.fetchPlayList(streamSpecs);
953
+ }
954
+ };
955
+
956
+ //#endregion
957
+ //#region lib/stream-extractor.ts
958
+ var StreamExtractor = class {
959
+ #extractor;
960
+ #rawText;
961
+ #parserConfig;
962
+ #rawFiles = {};
963
+ constructor(parserConfig) {
964
+ this.#parserConfig = parserConfig ?? new ParserConfig();
965
+ }
966
+ get extractorType() {
967
+ return this.#extractor.extractorType;
968
+ }
969
+ #setUrl(url) {
970
+ this.#parserConfig.originalUrl = url;
971
+ this.#parserConfig.url = url;
972
+ }
973
+ async loadSourceFromUrl(url) {
974
+ if (url.startsWith("file:")) {
975
+ const uri = new URL(url);
976
+ const filePath = uri.pathname;
977
+ this.#rawText = await readFile(filePath, "utf8");
978
+ this.#setUrl(url);
979
+ } else if (url.startsWith("http")) {
980
+ this.#parserConfig.originalUrl = url;
981
+ const response = await fetch(url, { headers: this.#parserConfig.headers });
982
+ this.#rawText = await response.text();
983
+ this.#parserConfig.url = response.url;
984
+ } else if (existsSync(url)) {
985
+ const filePath = path.resolve(url);
986
+ this.#rawText = await readFile(filePath, "utf8");
987
+ const absoluteUri = pathToFileURL(filePath).toString();
988
+ this.#setUrl(absoluteUri);
989
+ }
990
+ this.#rawText = this.#rawText.trim();
991
+ this.loadSourceFromText(this.#rawText);
992
+ }
993
+ loadSourceFromText(rawText, url) {
994
+ if (url) this.#setUrl(url);
995
+ let rawType = "txt";
996
+ this.#rawText = rawText.trim();
997
+ if (this.#rawText.startsWith(HLS_TAGS.extM3u)) {
998
+ this.#extractor = new HlsExtractor(this.#parserConfig);
999
+ rawType = "m3u8";
1000
+ } else if (this.#rawText.includes("</MPD>") && this.#rawText.includes("<MPD")) {
1001
+ this.#extractor = new DashExtractor(this.#parserConfig);
1002
+ rawType = "mpd";
1003
+ } else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia")) rawType = "ism";
1004
+ else if (rawText === "<RE_LIVE_TS>") {} else throw new Error("Unsupported stream type");
1005
+ this.#rawFiles[`raw.${rawType}`] = rawText;
1006
+ }
1007
+ async extractStreams() {
1008
+ return this.#extractor.extractStreams(this.#rawText);
1009
+ }
1010
+ async fetchPlayList(streamSpecs) {
1011
+ return this.#extractor.fetchPlayList(streamSpecs);
1012
+ }
1013
+ async refreshPlayList(streamSpecs) {
1014
+ return this.#extractor.refreshPlayList(streamSpecs);
1015
+ }
1016
+ };
1017
+
1018
+ //#endregion
1019
+ export { ENCRYPT_METHODS, EXTRACTOR_TYPES, MEDIA_TYPES, ParserConfig, ROLE_TYPE, StreamExtractor };