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