dasha 4.0.0-alpha.1 → 4.0.0-alpha.3
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 +15 -8
- package/dist/dasha.cjs +272 -112
- package/dist/dasha.d.cts +46 -34
- package/dist/dasha.d.ts +46 -34
- package/dist/dasha.js +252 -100
- package/package.json +17 -20
package/README.md
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
Library for parsing MPEG-DASH (.mpd) and HLS (.m3u8) manifests. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments.
|
|
8
8
|
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> This README is for the alpha version. Info about latest stable version is available on [NPM](https://www.npmjs.com/package/dasha/v/3.1.5) or [another GitHub branch](https://github.com/azot-labs/dasha/tree/v3).
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
## Install
|
|
10
14
|
|
|
11
15
|
```shell
|
|
12
|
-
npm i dasha
|
|
16
|
+
npm i dasha@alpha
|
|
13
17
|
```
|
|
14
18
|
|
|
15
19
|
## Usage
|
|
@@ -19,13 +23,16 @@ import fs from 'node:fs/promises';
|
|
|
19
23
|
import { parse } from 'dasha';
|
|
20
24
|
|
|
21
25
|
const url = 'https://dash.akamaized.net/dash264/TestCases/1a/sony/SNE_DASH_SD_CASE1A_REVISED.mpd';
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const streamExtractor = new StreamExtractor();
|
|
27
|
+
await streamExtractor.loadSourceFromUrl(url);
|
|
28
|
+
const streams = await streamExtractor.extractStreams();
|
|
29
|
+
|
|
30
|
+
for (const stream of streams) {
|
|
31
|
+
const segments = stream.playlist?.mediaParts[0].mediaSegments || [];
|
|
32
|
+
const filename = `${stream.name}_${stream.groupId}`;
|
|
33
|
+
for (const segment of segments) {
|
|
34
|
+
const content = await fetch(segment.url).then((res) => res.arrayBuffer());
|
|
35
|
+
await fs.appendFile(`${filename}.${stream.extension}`, content);
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
```
|
package/dist/dasha.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
//#region rolldown:runtime
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -22,13 +21,22 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
21
|
}) : target, mod));
|
|
23
22
|
|
|
24
23
|
//#endregion
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
let node_fs = require("node:fs");
|
|
25
|
+
node_fs = __toESM(node_fs);
|
|
26
|
+
let barsic = require("barsic");
|
|
27
|
+
barsic = __toESM(barsic);
|
|
28
|
+
let node_fs_promises = require("node:fs/promises");
|
|
29
|
+
node_fs_promises = __toESM(node_fs_promises);
|
|
30
|
+
let node_url = require("node:url");
|
|
31
|
+
node_url = __toESM(node_url);
|
|
32
|
+
let node_path = require("node:path");
|
|
33
|
+
node_path = __toESM(node_path);
|
|
34
|
+
let temporal_polyfill = require("temporal-polyfill");
|
|
35
|
+
temporal_polyfill = __toESM(temporal_polyfill);
|
|
36
|
+
let __xmldom_xmldom = require("@xmldom/xmldom");
|
|
37
|
+
__xmldom_xmldom = __toESM(__xmldom_xmldom);
|
|
38
|
+
let node_crypto = require("node:crypto");
|
|
39
|
+
node_crypto = __toESM(node_crypto);
|
|
32
40
|
|
|
33
41
|
//#region lib/shared/media-type.ts
|
|
34
42
|
const MEDIA_TYPES = {
|
|
@@ -41,14 +49,14 @@ const MEDIA_TYPES = {
|
|
|
41
49
|
//#endregion
|
|
42
50
|
//#region lib/shared/encrypt-method.ts
|
|
43
51
|
const ENCRYPT_METHODS = {
|
|
44
|
-
NONE:
|
|
45
|
-
AES_128:
|
|
46
|
-
AES_128_ECB:
|
|
47
|
-
SAMPLE_AES:
|
|
48
|
-
SAMPLE_AES_CTR:
|
|
49
|
-
CENC:
|
|
50
|
-
CHACHA20:
|
|
51
|
-
UNKNOWN:
|
|
52
|
+
NONE: "none",
|
|
53
|
+
AES_128: "aes-128",
|
|
54
|
+
AES_128_ECB: "aes-128-ecb",
|
|
55
|
+
SAMPLE_AES: "sample-aes",
|
|
56
|
+
SAMPLE_AES_CTR: "sample-aes-ctr",
|
|
57
|
+
CENC: "cenc",
|
|
58
|
+
CHACHA20: "chacha20",
|
|
59
|
+
UNKNOWN: "unknown"
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
//#endregion
|
|
@@ -76,22 +84,38 @@ const ROLE_TYPE = {
|
|
|
76
84
|
};
|
|
77
85
|
|
|
78
86
|
//#endregion
|
|
79
|
-
//#region lib/
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
//#region lib/processor.ts
|
|
88
|
+
var DefaultUrlProcessor = class {
|
|
89
|
+
canProcess(_extractorType, _originalUrl, parserConfig) {
|
|
90
|
+
return parserConfig.appendUrlParams;
|
|
91
|
+
}
|
|
92
|
+
process(url, parserConfig) {
|
|
93
|
+
if (!url.startsWith("http")) return url;
|
|
94
|
+
const urlFromConfigQuery = new URL(parserConfig.url).searchParams;
|
|
95
|
+
const oldUrl = new URL(url);
|
|
96
|
+
const newQuery = oldUrl.searchParams;
|
|
97
|
+
for (const [key, value] of urlFromConfigQuery) if (newQuery.has(key)) newQuery.set(key, value);
|
|
98
|
+
else newQuery.append(key, value);
|
|
99
|
+
if (!newQuery.toString()) return url;
|
|
100
|
+
console.debug(`Before: ${url}`);
|
|
101
|
+
url = `${oldUrl.pathname}?${newQuery.toString()}`;
|
|
102
|
+
console.debug(`After: ${url}`);
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region lib/dash/dash-content-processor.ts
|
|
109
|
+
var DefaultDashContentProcessor = class {
|
|
110
|
+
canProcess(extractorType, mpdContent) {
|
|
111
|
+
if (extractorType !== EXTRACTOR_TYPES.MPEG_DASH) return false;
|
|
112
|
+
return mpdContent.includes("<mas:") && !mpdContent.includes("xmlns:mas");
|
|
113
|
+
}
|
|
114
|
+
process(mpdContent) {
|
|
115
|
+
console.debug("Fix xigua mpd...");
|
|
116
|
+
mpdContent = mpdContent.replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
|
|
117
|
+
return mpdContent;
|
|
118
|
+
}
|
|
95
119
|
};
|
|
96
120
|
|
|
97
121
|
//#endregion
|
|
@@ -125,6 +149,138 @@ const HLS_TAGS = {
|
|
|
125
149
|
extXStart: "#EXT-X-START"
|
|
126
150
|
};
|
|
127
151
|
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region lib/hls/hls-content-processor.ts
|
|
154
|
+
var DefaultHlsContentProcessor = class DefaultHlsContentProcessor {
|
|
155
|
+
static YkDVRegex = /#EXT-X-DISCONTINUITY\s+#EXT-X-MAP:URI="(.*?)",BYTERANGE="(.*?)"/g;
|
|
156
|
+
static DNSPRegex = /#EXT-X-MAP:URI=".*?BUMPER\/[\s\S]+?#EXT-X-DISCONTINUITY/;
|
|
157
|
+
static DNSPSubRegex = /#EXTINF:.*?,\s+.*BUMPER.*\s+#EXT-X-DISCONTINUITY/;
|
|
158
|
+
static OrderFixRegex = /(#EXTINF.*)(\s+)(#EXT-X-KEY.*)/g;
|
|
159
|
+
static ATVRegex = /#EXT-X-MAP.*\.apple\.com\//;
|
|
160
|
+
static ATVRegex2 = /(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)/;
|
|
161
|
+
canProcess(extractorType) {
|
|
162
|
+
return extractorType === EXTRACTOR_TYPES.HLS;
|
|
163
|
+
}
|
|
164
|
+
process(m3u8Content, parserConfig) {
|
|
165
|
+
if (m3u8Content.includes("\r") && !m3u8Content.includes("\n")) m3u8Content = m3u8Content.replace(/\r/g, "\n");
|
|
166
|
+
const m3u8Url = parserConfig.url;
|
|
167
|
+
if (m3u8Url.includes("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.includes("endtime=")) m3u8Content += "\n" + HLS_TAGS.extXEndlist;
|
|
168
|
+
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}`);
|
|
169
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("#EXT-X-MAP") && m3u8Url.includes("media.dssott.com/")) this.applyRegexReplacement(m3u8Content, DefaultHlsContentProcessor.DNSPRegex);
|
|
170
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("seg_00000.vtt") && m3u8Url.includes("media.dssott.com/")) this.applyRegexReplacement(m3u8Content, DefaultHlsContentProcessor.DNSPSubRegex);
|
|
171
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("#EXT-X-MAP") && (m3u8Url.includes(".apple.com/") || DefaultHlsContentProcessor.ATVRegex.test(m3u8Content))) {
|
|
172
|
+
const match = DefaultHlsContentProcessor.ATVRegex2.exec(m3u8Content);
|
|
173
|
+
if (match) m3u8Content = `#EXTM3U\n${match[1]}\n#EXT-X-ENDLIST`;
|
|
174
|
+
}
|
|
175
|
+
if (DefaultHlsContentProcessor.OrderFixRegex.test(m3u8Content)) m3u8Content = m3u8Content.replace(DefaultHlsContentProcessor.OrderFixRegex, "$3$2$1");
|
|
176
|
+
return m3u8Content;
|
|
177
|
+
}
|
|
178
|
+
applyRegexReplacement(content, regex) {
|
|
179
|
+
if (regex.test(content)) {
|
|
180
|
+
const match = regex.exec(content);
|
|
181
|
+
if (match) return content.split(match[0]).join("#XXX");
|
|
182
|
+
}
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region lib/shared/encrypt-info.ts
|
|
189
|
+
var EncryptInfo = class {
|
|
190
|
+
method = ENCRYPT_METHODS.NONE;
|
|
191
|
+
key;
|
|
192
|
+
iv;
|
|
193
|
+
drm;
|
|
194
|
+
constructor(method) {
|
|
195
|
+
this.method = this.parseMethod(method);
|
|
196
|
+
this.drm = {};
|
|
197
|
+
}
|
|
198
|
+
parseMethod(method) {
|
|
199
|
+
if (method) return ENCRYPT_METHODS[method.replace("-", "_")];
|
|
200
|
+
else return ENCRYPT_METHODS.UNKNOWN;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region lib/hls/hls-key-processor.ts
|
|
206
|
+
var DefaultHlsKeyProcessor = class {
|
|
207
|
+
canProcess(extractorType) {
|
|
208
|
+
return extractorType === EXTRACTOR_TYPES.HLS;
|
|
209
|
+
}
|
|
210
|
+
async process(keyLine, m3u8Url, _m3u8Content, parserConfig) {
|
|
211
|
+
const iv = this.getAttribute(keyLine, "IV");
|
|
212
|
+
const method = this.getAttribute(keyLine, "METHOD");
|
|
213
|
+
const uri = this.getAttribute(keyLine, "URI");
|
|
214
|
+
const encryptInfo = new EncryptInfo(method);
|
|
215
|
+
if (iv) encryptInfo.iv = barsic.b.hex().encode(iv);
|
|
216
|
+
if (parserConfig.customIv && parserConfig.customIv.length > 0) encryptInfo.iv = parserConfig.customIv;
|
|
217
|
+
try {
|
|
218
|
+
if (parserConfig.customKey && parserConfig.customKey.length > 0) encryptInfo.key = parserConfig.customKey;
|
|
219
|
+
else if (uri) {
|
|
220
|
+
const lowerUri = uri.toLowerCase();
|
|
221
|
+
if (lowerUri.startsWith("base64:")) encryptInfo.key = barsic.b.base64().encode(uri.slice(7));
|
|
222
|
+
else if (lowerUri.startsWith("data:;base64,")) encryptInfo.key = barsic.b.base64().encode(uri.slice(13));
|
|
223
|
+
else if (lowerUri.startsWith("data:text/plain;base64,")) encryptInfo.key = barsic.b.base64().encode(uri.slice(23));
|
|
224
|
+
else if ((0, node_fs.existsSync)(uri)) encryptInfo.key = (0, node_fs.readFileSync)(uri);
|
|
225
|
+
else {
|
|
226
|
+
const processedUrl = this.preProcessUrl(new URL(uri, m3u8Url).toString(), parserConfig);
|
|
227
|
+
encryptInfo.key = await this.fetchKeyWithRetry(processedUrl, parserConfig);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(`Failed to load key: ${error.message}`);
|
|
232
|
+
encryptInfo.method = ENCRYPT_METHODS.UNKNOWN;
|
|
233
|
+
}
|
|
234
|
+
if (parserConfig.customMethod) {
|
|
235
|
+
console.warn(`METHOD changed from ${encryptInfo.method} to ${parserConfig.customMethod}`);
|
|
236
|
+
encryptInfo.method = parserConfig.customMethod;
|
|
237
|
+
}
|
|
238
|
+
return encryptInfo;
|
|
239
|
+
}
|
|
240
|
+
getAttribute(line, attrName) {
|
|
241
|
+
const regex = new RegExp(`${attrName}=(?:"([^"]+)"|([^,]+))`, "i");
|
|
242
|
+
const match = line.match(regex);
|
|
243
|
+
return match?.[1] ?? match?.[2] ?? null;
|
|
244
|
+
}
|
|
245
|
+
async fetchKeyWithRetry(url, parserConfig) {
|
|
246
|
+
let retryCount = parserConfig.keyRetryCount ?? 3;
|
|
247
|
+
while (retryCount >= 0) try {
|
|
248
|
+
const response = await fetch(url, { headers: parserConfig.headers });
|
|
249
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error.message.includes("scheme is not supported")) throw error;
|
|
252
|
+
console.warn(`Error fetching key: ${error.message}. Retries left: ${retryCount}`);
|
|
253
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
254
|
+
retryCount--;
|
|
255
|
+
}
|
|
256
|
+
throw new Error("Maximum retry attempts reached");
|
|
257
|
+
}
|
|
258
|
+
preProcessUrl(url, parserConfig) {
|
|
259
|
+
let processedUrl = url;
|
|
260
|
+
for (const processor of parserConfig.urlProcessors ?? []) if (processor.canProcess(EXTRACTOR_TYPES.HLS, processedUrl, parserConfig)) processedUrl = processor.process(processedUrl, parserConfig);
|
|
261
|
+
return processedUrl;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region lib/parser-config.ts
|
|
267
|
+
var ParserConfig = class {
|
|
268
|
+
url = "";
|
|
269
|
+
originalUrl = "";
|
|
270
|
+
baseUrl;
|
|
271
|
+
customParserArgs = {};
|
|
272
|
+
headers = {};
|
|
273
|
+
contentProcessors = [new DefaultDashContentProcessor(), new DefaultHlsContentProcessor()];
|
|
274
|
+
urlProcessors = [new DefaultUrlProcessor()];
|
|
275
|
+
keyProcessors = [new DefaultHlsKeyProcessor()];
|
|
276
|
+
customMethod;
|
|
277
|
+
customKey;
|
|
278
|
+
customIv;
|
|
279
|
+
urlProcessorArgs;
|
|
280
|
+
appendUrlParams = false;
|
|
281
|
+
keyRetryCount = 3;
|
|
282
|
+
};
|
|
283
|
+
|
|
128
284
|
//#endregion
|
|
129
285
|
//#region lib/shared/stream-spec.ts
|
|
130
286
|
var StreamSpec = class {
|
|
@@ -211,8 +367,7 @@ const DASH_TAGS = {
|
|
|
211
367
|
const combineUrl = (baseUrl, relativeUrl) => {
|
|
212
368
|
if (!baseUrl.trim()) return relativeUrl;
|
|
213
369
|
const url1 = new URL(baseUrl);
|
|
214
|
-
|
|
215
|
-
return url2.toString();
|
|
370
|
+
return new URL(relativeUrl, url1).toString();
|
|
216
371
|
};
|
|
217
372
|
const replaceVars = (text, dict) => {
|
|
218
373
|
let result = text;
|
|
@@ -251,7 +406,7 @@ const getAttribute = (line, key = "") => {
|
|
|
251
406
|
return result;
|
|
252
407
|
};
|
|
253
408
|
const distinctBy = (array, callbackfn) => {
|
|
254
|
-
const seen = new Set();
|
|
409
|
+
const seen = /* @__PURE__ */ new Set();
|
|
255
410
|
return array.filter((item) => {
|
|
256
411
|
const value = callbackfn(item);
|
|
257
412
|
if (seen.has(value)) return false;
|
|
@@ -285,21 +440,6 @@ var MediaPart = class {
|
|
|
285
440
|
}
|
|
286
441
|
};
|
|
287
442
|
|
|
288
|
-
//#endregion
|
|
289
|
-
//#region lib/shared/encrypt-info.ts
|
|
290
|
-
var EncryptInfo = class {
|
|
291
|
-
method = ENCRYPT_METHODS.NONE;
|
|
292
|
-
key;
|
|
293
|
-
iv;
|
|
294
|
-
constructor(method) {
|
|
295
|
-
this.method = this.parseMethod(method);
|
|
296
|
-
}
|
|
297
|
-
parseMethod(method) {
|
|
298
|
-
if (method !== void 0) return ENCRYPT_METHODS[method.replace("-", "_")];
|
|
299
|
-
else return ENCRYPT_METHODS.UNKNOWN;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
443
|
//#endregion
|
|
304
444
|
//#region lib/shared/media-segment.ts
|
|
305
445
|
var MediaSegment = class MediaSegment {
|
|
@@ -345,8 +485,7 @@ var MediaSegment = class MediaSegment {
|
|
|
345
485
|
*/
|
|
346
486
|
const parseRange = (range) => {
|
|
347
487
|
const [startRange, end] = range.split("-").map(Number);
|
|
348
|
-
|
|
349
|
-
return [startRange, expectLength];
|
|
488
|
+
return [startRange, end - startRange + 1];
|
|
350
489
|
};
|
|
351
490
|
|
|
352
491
|
//#endregion
|
|
@@ -382,11 +521,9 @@ var DashExtractor = class DashExtractor {
|
|
|
382
521
|
async extractStreams(rawText) {
|
|
383
522
|
const streamList = [];
|
|
384
523
|
this.#mpdContent = rawText;
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const isLive = type === "dynamic";
|
|
389
|
-
const maxSegmentDuration = mpdElement.getAttribute("maxSegmentDuration");
|
|
524
|
+
const mpdElement = new __xmldom_xmldom.DOMParser().parseFromString(this.#mpdContent, "text/xml").getElementsByTagName("MPD")[0];
|
|
525
|
+
const isLive = mpdElement.getAttribute("type") === "dynamic";
|
|
526
|
+
mpdElement.getAttribute("maxSegmentDuration");
|
|
390
527
|
const availabilityStartTime = mpdElement.getAttribute("availabilityStartTime");
|
|
391
528
|
const timeShiftBufferDepth = mpdElement.getAttribute("timeShiftBufferDepth") || "PT1M";
|
|
392
529
|
const publishTime = mpdElement.getAttribute("publishTime");
|
|
@@ -445,8 +582,7 @@ var DashExtractor = class DashExtractor {
|
|
|
445
582
|
if (role) {
|
|
446
583
|
const roleValue = role.getAttribute("value");
|
|
447
584
|
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
448
|
-
const
|
|
449
|
-
const roleType = ROLE_TYPE[roleTypeKey];
|
|
585
|
+
const roleType = ROLE_TYPE[roleValue.split("-").map(capitalize).join("")];
|
|
450
586
|
streamSpec.role = roleType;
|
|
451
587
|
if (roleType === ROLE_TYPE.Subtitle) {
|
|
452
588
|
streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
|
|
@@ -577,8 +713,7 @@ var DashExtractor = class DashExtractor {
|
|
|
577
713
|
varDic[DASH_TAGS.TemplateNumber] = segNumber++;
|
|
578
714
|
const _hashTime = mediaTemplate?.includes(DASH_TAGS.TemplateTime);
|
|
579
715
|
const _media = replaceVars(mediaTemplate, varDic);
|
|
580
|
-
|
|
581
|
-
_mediaSegment.url = _mediaUrl;
|
|
716
|
+
_mediaSegment.url = combineUrl(segBaseUrl, _media);
|
|
582
717
|
_mediaSegment.index = segIndex++;
|
|
583
718
|
_mediaSegment.duration = _duration / timescale;
|
|
584
719
|
if (_hashTime) _mediaSegment.nameFromVar = currentTime.toString();
|
|
@@ -622,27 +757,37 @@ var DashExtractor = class DashExtractor {
|
|
|
622
757
|
mediaSegment.duration = periodDurationSeconds;
|
|
623
758
|
streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
|
|
624
759
|
}
|
|
625
|
-
const
|
|
626
|
-
|
|
760
|
+
const adaptationSetProtections = adaptationSet.getElementsByTagName("ContentProtection");
|
|
761
|
+
const representationProtections = representation.getElementsByTagName("ContentProtection");
|
|
762
|
+
const contentProtections = representationProtections[0] ? representationProtections : adaptationSetProtections;
|
|
763
|
+
if (contentProtections.length) {
|
|
627
764
|
const encryptInfo = new EncryptInfo();
|
|
628
765
|
encryptInfo.method = DashExtractor.#DEFAULT_METHOD;
|
|
766
|
+
const widevineSystemId = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
|
|
767
|
+
const playreadySystemId = "9a04f079-9840-4286-ab92-e65be0885f95";
|
|
768
|
+
for (const contentProtection of contentProtections) {
|
|
769
|
+
const schemeIdUri = contentProtection.getAttribute("schemeIdUri");
|
|
770
|
+
const drmData = {
|
|
771
|
+
keyId: contentProtection.getAttribute("cenc:default_KID") || void 0,
|
|
772
|
+
pssh: contentProtection.getElementsByTagName("cenc:pssh")[0]?.textContent || void 0
|
|
773
|
+
};
|
|
774
|
+
if (schemeIdUri?.includes(widevineSystemId)) encryptInfo.drm.widevine = drmData;
|
|
775
|
+
else if (schemeIdUri?.includes(playreadySystemId)) encryptInfo.drm.playready = drmData;
|
|
776
|
+
else continue;
|
|
777
|
+
}
|
|
629
778
|
if (streamSpec.playlist.mediaInit) streamSpec.playlist.mediaInit.encryptInfo = encryptInfo;
|
|
630
779
|
const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
|
|
631
780
|
for (const segment of segments) if (!segment.encryptInfo) segment.encryptInfo = encryptInfo;
|
|
632
781
|
}
|
|
633
782
|
const _index = streamList.findIndex((item) => item.periodId !== streamSpec.periodId && item.groupId === streamSpec.groupId && item.resolution === streamSpec.resolution && item.mediaType === streamSpec.mediaType);
|
|
634
|
-
if (_index > -1) if (isLive) {} else {
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
|
|
643
|
-
streamList[_index].playlist.mediaParts.push(mediaPart);
|
|
644
|
-
} else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
|
|
645
|
-
}
|
|
783
|
+
if (_index > -1) if (isLive) {} else if (streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).url !== streamSpec.playlist.mediaParts[0].mediaSegments.at(-1)?.url) {
|
|
784
|
+
const startIndex = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).index + 1;
|
|
785
|
+
const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
|
|
786
|
+
for (const segment of segments) segment.index += startIndex;
|
|
787
|
+
const mediaPart = new MediaPart();
|
|
788
|
+
mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
|
|
789
|
+
streamList[_index].playlist.mediaParts.push(mediaPart);
|
|
790
|
+
} else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
|
|
646
791
|
else {
|
|
647
792
|
if (streamSpec.mediaType === MEDIA_TYPES.SUBTITLES && streamSpec.extension === "mp4") streamSpec.extension = "m4s";
|
|
648
793
|
if (streamSpec.mediaType !== MEDIA_TYPES.SUBTITLES && (streamSpec.extension == null || streamSpec.playlist.mediaParts.reduce((sum, part) => sum + part.mediaSegments.length, 0) > 1)) streamSpec.extension = "m4s";
|
|
@@ -657,8 +802,8 @@ var DashExtractor = class DashExtractor {
|
|
|
657
802
|
const subtitleList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.SUBTITLES);
|
|
658
803
|
const videoList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.VIDEO);
|
|
659
804
|
for (const video of videoList) {
|
|
660
|
-
const audioGroupId = audioList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
|
|
661
|
-
const subtitleGroupId = subtitleList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
|
|
805
|
+
const audioGroupId = audioList.toSorted((a, b$1) => (b$1.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
|
|
806
|
+
const subtitleGroupId = subtitleList.toSorted((a, b$1) => (b$1.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
|
|
662
807
|
if (audioGroupId) video.audioId = audioGroupId;
|
|
663
808
|
if (subtitleGroupId) video.subtitleId = subtitleGroupId;
|
|
664
809
|
}
|
|
@@ -668,17 +813,38 @@ var DashExtractor = class DashExtractor {
|
|
|
668
813
|
if (!v) return;
|
|
669
814
|
return v;
|
|
670
815
|
}
|
|
671
|
-
|
|
672
|
-
|
|
816
|
+
async refreshPlayList(streamSpecs) {
|
|
817
|
+
if (!streamSpecs.length) return;
|
|
818
|
+
const response = await fetch(this.#parserConfig.url, this.#parserConfig.headers).catch(() => fetch(this.#parserConfig.originalUrl, this.#parserConfig.headers));
|
|
819
|
+
const rawText = await response.text();
|
|
820
|
+
const url = response.url;
|
|
821
|
+
this.#parserConfig.url = url;
|
|
822
|
+
this.#setInitUrl();
|
|
823
|
+
const newStreams = await this.extractStreams(rawText);
|
|
824
|
+
for (const streamSpec of streamSpecs) {
|
|
825
|
+
let results = newStreams.filter((n) => n.toShortString() === streamSpec.toShortString());
|
|
826
|
+
if (!results.length) results = newStreams.filter((n) => n.playlist?.mediaInit?.url === streamSpec.playlist?.mediaInit?.url);
|
|
827
|
+
if (results.length) streamSpec.playlist.mediaParts = results.at(0).playlist.mediaParts;
|
|
828
|
+
}
|
|
829
|
+
await this.#processUrl(streamSpecs);
|
|
830
|
+
}
|
|
831
|
+
async #processUrl(streamSpecs) {
|
|
832
|
+
for (const spec of streamSpecs) {
|
|
833
|
+
const playlist = spec.playlist;
|
|
834
|
+
if (!playlist) continue;
|
|
835
|
+
if (playlist.mediaInit) playlist.mediaInit.url = this.preProcessUrl(playlist.mediaInit.url);
|
|
836
|
+
for (const part of playlist.mediaParts) for (const segment of part.mediaSegments) segment.url = this.preProcessUrl(segment.url);
|
|
837
|
+
}
|
|
673
838
|
}
|
|
674
|
-
|
|
675
|
-
|
|
839
|
+
async fetchPlayList(streamSpecs) {
|
|
840
|
+
this.#processUrl(streamSpecs);
|
|
676
841
|
}
|
|
677
842
|
preProcessUrl(url) {
|
|
678
|
-
|
|
843
|
+
for (const processor of this.#parserConfig.urlProcessors) if (processor.canProcess(this.extractorType, url, this.#parserConfig)) url = processor.process(url, this.#parserConfig);
|
|
844
|
+
return url;
|
|
679
845
|
}
|
|
680
846
|
preProcessContent() {
|
|
681
|
-
|
|
847
|
+
for (const processor of this.#parserConfig.contentProcessors) if (processor.canProcess(this.extractorType, this.#mpdContent, this.#parserConfig)) this.#mpdContent = processor.process(this.#mpdContent, this.#parserConfig);
|
|
682
848
|
}
|
|
683
849
|
};
|
|
684
850
|
|
|
@@ -757,8 +923,7 @@ var HlsExtractor = class {
|
|
|
757
923
|
expectPlaylist = true;
|
|
758
924
|
} else if (line.startsWith(HLS_TAGS.extXMedia)) {
|
|
759
925
|
streamSpec = new StreamSpec();
|
|
760
|
-
const
|
|
761
|
-
const mediaType = MEDIA_TYPES[type];
|
|
926
|
+
const mediaType = MEDIA_TYPES[getAttribute(line, "TYPE").replace("-", "_")];
|
|
762
927
|
if (mediaType) streamSpec.mediaType = mediaType;
|
|
763
928
|
if (mediaType === MEDIA_TYPES.CLOSED_CAPTIONS) continue;
|
|
764
929
|
let url = getAttribute(line, "URI");
|
|
@@ -809,8 +974,7 @@ var HlsExtractor = class {
|
|
|
809
974
|
for (const line of lines) {
|
|
810
975
|
if (!line.trim()) continue;
|
|
811
976
|
if (line.startsWith(HLS_TAGS.extXByterange)) {
|
|
812
|
-
const
|
|
813
|
-
const [n, o] = getRange(p);
|
|
977
|
+
const [n, o] = getRange(getAttribute(line));
|
|
814
978
|
segment.expectLength = n;
|
|
815
979
|
segment.startRange = o || (segments.at(-1)?.startRange || 0) + (segments.at(-1)?.expectLength || 0);
|
|
816
980
|
expectSegment = true;
|
|
@@ -832,10 +996,8 @@ var HlsExtractor = class {
|
|
|
832
996
|
mediaParts.push(new MediaPart(segments));
|
|
833
997
|
segments = [];
|
|
834
998
|
} else if (line.startsWith(HLS_TAGS.extXKey)) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (uri !== uriLast) {
|
|
838
|
-
const parsedInfo = this.#parseKey(line);
|
|
999
|
+
if (getAttribute(line, "URI") !== getAttribute(lastKeyLine, "URI")) {
|
|
1000
|
+
const parsedInfo = await this.#parseKey(line);
|
|
839
1001
|
currentEncryptInfo.method = parsedInfo.method;
|
|
840
1002
|
currentEncryptInfo.key = parsedInfo.key;
|
|
841
1003
|
currentEncryptInfo.iv = parsedInfo.iv;
|
|
@@ -862,8 +1024,7 @@ var HlsExtractor = class {
|
|
|
862
1024
|
mediaSegment.index = -1;
|
|
863
1025
|
playlist.mediaInit = mediaSegment;
|
|
864
1026
|
if (line.includes("BYTERANGE")) {
|
|
865
|
-
const
|
|
866
|
-
const [n, o] = getRange(p);
|
|
1027
|
+
const [n, o] = getRange(getAttribute(line, "BYTERANGE"));
|
|
867
1028
|
mediaSegment.expectLength = n;
|
|
868
1029
|
mediaSegment.startRange = o || 0;
|
|
869
1030
|
}
|
|
@@ -905,17 +1066,14 @@ var HlsExtractor = class {
|
|
|
905
1066
|
if (playlist.isLive) playlist.refreshIntervalMs = (playlist.targetDuration || 5) * 2 * 1e3;
|
|
906
1067
|
return playlist;
|
|
907
1068
|
}
|
|
908
|
-
#parseKey(keyLine) {
|
|
1069
|
+
async #parseKey(keyLine) {
|
|
909
1070
|
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);
|
|
910
1071
|
throw new Error("No key processor found");
|
|
911
1072
|
}
|
|
912
1073
|
async extractStreams(rawText) {
|
|
913
1074
|
this.#m3u8Content = rawText;
|
|
914
1075
|
this.preProcessContent();
|
|
915
|
-
if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf))
|
|
916
|
-
console.log("Master m3u8 found");
|
|
917
|
-
return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
|
|
918
|
-
}
|
|
1076
|
+
if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
|
|
919
1077
|
const playlist = await this.#parseList();
|
|
920
1078
|
const streamSpec = new StreamSpec();
|
|
921
1079
|
streamSpec.url = this.parserConfig.url;
|
|
@@ -925,8 +1083,7 @@ var HlsExtractor = class {
|
|
|
925
1083
|
}
|
|
926
1084
|
async #loadM3u8FromUrl(url) {
|
|
927
1085
|
if (url.startsWith("file:")) {
|
|
928
|
-
const
|
|
929
|
-
const filePath = uri.pathname;
|
|
1086
|
+
const filePath = new URL(url).pathname;
|
|
930
1087
|
this.#m3u8Content = await (0, node_fs_promises.readFile)(filePath, "utf8");
|
|
931
1088
|
} else if (url.startsWith("http")) try {
|
|
932
1089
|
const response = await fetch(url, { headers: this.parserConfig.headers });
|
|
@@ -966,9 +1123,9 @@ var HlsExtractor = class {
|
|
|
966
1123
|
else list.playlist = newPlaylist;
|
|
967
1124
|
if (list.mediaType === MEDIA_TYPES.SUBTITLES) {
|
|
968
1125
|
const a = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".ttml")));
|
|
969
|
-
const b = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".vtt") || segment.url.includes(".webvtt")));
|
|
1126
|
+
const b$1 = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".vtt") || segment.url.includes(".webvtt")));
|
|
970
1127
|
if (a) list.extension = "ttml";
|
|
971
|
-
if (b) list.extension = "vtt";
|
|
1128
|
+
if (b$1) list.extension = "vtt";
|
|
972
1129
|
} else list.extension = list.playlist.mediaInit ? "m4s" : "ts";
|
|
973
1130
|
}
|
|
974
1131
|
}
|
|
@@ -996,8 +1153,7 @@ var StreamExtractor = class {
|
|
|
996
1153
|
}
|
|
997
1154
|
async loadSourceFromUrl(url) {
|
|
998
1155
|
if (url.startsWith("file:")) {
|
|
999
|
-
const
|
|
1000
|
-
const filePath = uri.pathname;
|
|
1156
|
+
const filePath = new URL(url).pathname;
|
|
1001
1157
|
this.#rawText = await (0, node_fs_promises.readFile)(filePath, "utf8");
|
|
1002
1158
|
this.#setUrl(url);
|
|
1003
1159
|
} else if (url.startsWith("http")) {
|
|
@@ -1024,8 +1180,11 @@ var StreamExtractor = class {
|
|
|
1024
1180
|
} else if (this.#rawText.includes("</MPD>") && this.#rawText.includes("<MPD")) {
|
|
1025
1181
|
this.#extractor = new DashExtractor(this.#parserConfig);
|
|
1026
1182
|
rawType = "mpd";
|
|
1027
|
-
} else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia"))
|
|
1028
|
-
|
|
1183
|
+
} else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia")) {
|
|
1184
|
+
rawType = "ism";
|
|
1185
|
+
throw new Error("Smooth Streaming is not supported yet");
|
|
1186
|
+
} else if (rawText === "<RE_LIVE_TS>") throw new Error("Live TS is not supported yet");
|
|
1187
|
+
else throw new Error("Unsupported stream type");
|
|
1029
1188
|
this.#rawFiles[`raw.${rawType}`] = rawText;
|
|
1030
1189
|
}
|
|
1031
1190
|
async extractStreams() {
|
|
@@ -1040,9 +1199,10 @@ var StreamExtractor = class {
|
|
|
1040
1199
|
};
|
|
1041
1200
|
|
|
1042
1201
|
//#endregion
|
|
1043
|
-
exports.
|
|
1044
|
-
exports.
|
|
1045
|
-
exports.
|
|
1046
|
-
exports.
|
|
1047
|
-
exports.
|
|
1048
|
-
exports.
|
|
1202
|
+
exports.DefaultUrlProcessor = DefaultUrlProcessor;
|
|
1203
|
+
exports.ENCRYPT_METHODS = ENCRYPT_METHODS;
|
|
1204
|
+
exports.EXTRACTOR_TYPES = EXTRACTOR_TYPES;
|
|
1205
|
+
exports.MEDIA_TYPES = MEDIA_TYPES;
|
|
1206
|
+
exports.ParserConfig = ParserConfig;
|
|
1207
|
+
exports.ROLE_TYPE = ROLE_TYPE;
|
|
1208
|
+
exports.StreamExtractor = StreamExtractor;
|