dasha 4.0.0-alpha.1 → 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 +15 -8
- package/dist/dasha.cjs +174 -34
- package/dist/dasha.d.cts +33 -14
- package/dist/dasha.d.ts +33 -14
- package/dist/dasha.js +171 -35
- package/package.json +1 -1
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/streamyx-labs/dasha/tree/v3).
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
## Install
|
|
10
14
|
|
|
11
15
|
```shell
|
|
12
|
-
npm i dasha
|
|
16
|
+
npm i dasha@4.0.0-alpha.1
|
|
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
|
@@ -76,22 +76,42 @@ const ROLE_TYPE = {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
//#endregion
|
|
79
|
-
//#region lib/
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
//#region lib/processor.ts
|
|
80
|
+
var ContentProcessor = class {};
|
|
81
|
+
var KeyProcessor = class {};
|
|
82
|
+
var UrlProcessor = class {};
|
|
83
|
+
var DefaultUrlProcessor = class extends UrlProcessor {
|
|
84
|
+
canProcess(_extractorType, _originalUrl, parserConfig) {
|
|
85
|
+
return parserConfig.appendUrlParams;
|
|
86
|
+
}
|
|
87
|
+
process(url, parserConfig) {
|
|
88
|
+
if (!url.startsWith("http")) return url;
|
|
89
|
+
const urlFromConfig = new URL(parserConfig.url);
|
|
90
|
+
const urlFromConfigQuery = urlFromConfig.searchParams;
|
|
91
|
+
const oldUrl = new URL(url);
|
|
92
|
+
const newQuery = oldUrl.searchParams;
|
|
93
|
+
for (const [key, value] of urlFromConfigQuery) if (newQuery.has(key)) newQuery.set(key, value);
|
|
94
|
+
else newQuery.append(key, value);
|
|
95
|
+
if (!newQuery.toString()) return url;
|
|
96
|
+
console.debug(`Before: ${url}`);
|
|
97
|
+
url = `${oldUrl.pathname}?${newQuery.toString()}`;
|
|
98
|
+
console.debug(`After: ${url}`);
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region lib/dash/dash-content-processor.ts
|
|
105
|
+
var DefaultDashContentProcessor = class extends ContentProcessor {
|
|
106
|
+
canProcess(extractorType, mpdContent) {
|
|
107
|
+
if (extractorType !== EXTRACTOR_TYPES.MPEG_DASH) return false;
|
|
108
|
+
return mpdContent.includes("<mas:") && !mpdContent.includes("xmlns:mas");
|
|
109
|
+
}
|
|
110
|
+
process(mpdContent) {
|
|
111
|
+
console.debug("Fix xigua mpd...");
|
|
112
|
+
mpdContent = mpdContent.replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
|
|
113
|
+
return mpdContent;
|
|
114
|
+
}
|
|
95
115
|
};
|
|
96
116
|
|
|
97
117
|
//#endregion
|
|
@@ -125,6 +145,137 @@ const HLS_TAGS = {
|
|
|
125
145
|
extXStart: "#EXT-X-START"
|
|
126
146
|
};
|
|
127
147
|
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region lib/hls/hls-content-processor.ts
|
|
150
|
+
var DefaultHlsContentProcessor = class DefaultHlsContentProcessor extends ContentProcessor {
|
|
151
|
+
static YkDVRegex = /#EXT-X-DISCONTINUITY\s+#EXT-X-MAP:URI="(.*?)",BYTERANGE="(.*?)"/g;
|
|
152
|
+
static DNSPRegex = /#EXT-X-MAP:URI=".*?BUMPER\/[\s\S]+?#EXT-X-DISCONTINUITY/;
|
|
153
|
+
static DNSPSubRegex = /#EXTINF:.*?,\s+.*BUMPER.*\s+#EXT-X-DISCONTINUITY/;
|
|
154
|
+
static OrderFixRegex = /(#EXTINF.*)(\s+)(#EXT-X-KEY.*)/g;
|
|
155
|
+
static ATVRegex = /#EXT-X-MAP.*\.apple\.com\//;
|
|
156
|
+
static ATVRegex2 = /(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)/;
|
|
157
|
+
canProcess(extractorType) {
|
|
158
|
+
return extractorType === EXTRACTOR_TYPES.HLS;
|
|
159
|
+
}
|
|
160
|
+
process(m3u8Content, parserConfig) {
|
|
161
|
+
if (m3u8Content.includes("\r") && !m3u8Content.includes("\n")) m3u8Content = m3u8Content.replace(/\r/g, "\n");
|
|
162
|
+
const m3u8Url = parserConfig.url;
|
|
163
|
+
if (m3u8Url.includes("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.includes("endtime=")) m3u8Content += "\n" + HLS_TAGS.extXEndlist;
|
|
164
|
+
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}`);
|
|
165
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("#EXT-X-MAP") && m3u8Url.includes("media.dssott.com/")) this.applyRegexReplacement(m3u8Content, DefaultHlsContentProcessor.DNSPRegex);
|
|
166
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("seg_00000.vtt") && m3u8Url.includes("media.dssott.com/")) this.applyRegexReplacement(m3u8Content, DefaultHlsContentProcessor.DNSPSubRegex);
|
|
167
|
+
if (m3u8Content.includes("#EXT-X-DISCONTINUITY") && m3u8Content.includes("#EXT-X-MAP") && (m3u8Url.includes(".apple.com/") || DefaultHlsContentProcessor.ATVRegex.test(m3u8Content))) {
|
|
168
|
+
const match = DefaultHlsContentProcessor.ATVRegex2.exec(m3u8Content);
|
|
169
|
+
if (match) m3u8Content = `#EXTM3U\n${match[1]}\n#EXT-X-ENDLIST`;
|
|
170
|
+
}
|
|
171
|
+
if (DefaultHlsContentProcessor.OrderFixRegex.test(m3u8Content)) m3u8Content = m3u8Content.replace(DefaultHlsContentProcessor.OrderFixRegex, "$3$2$1");
|
|
172
|
+
return m3u8Content;
|
|
173
|
+
}
|
|
174
|
+
applyRegexReplacement(content, regex) {
|
|
175
|
+
if (regex.test(content)) {
|
|
176
|
+
const match = regex.exec(content);
|
|
177
|
+
if (match) return content.split(match[0]).join("#XXX");
|
|
178
|
+
}
|
|
179
|
+
return content;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region lib/shared/encrypt-info.ts
|
|
185
|
+
var EncryptInfo = class {
|
|
186
|
+
method = ENCRYPT_METHODS.NONE;
|
|
187
|
+
key;
|
|
188
|
+
iv;
|
|
189
|
+
constructor(method) {
|
|
190
|
+
this.method = this.parseMethod(method);
|
|
191
|
+
}
|
|
192
|
+
parseMethod(method) {
|
|
193
|
+
if (method) return ENCRYPT_METHODS[method.replace("-", "_")];
|
|
194
|
+
else return ENCRYPT_METHODS.UNKNOWN;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region lib/hls/hls-key-processor.ts
|
|
200
|
+
var DefaultHlsKeyProcessor = class extends KeyProcessor {
|
|
201
|
+
canProcess(extractorType) {
|
|
202
|
+
return extractorType === EXTRACTOR_TYPES.HLS;
|
|
203
|
+
}
|
|
204
|
+
async process(keyLine, m3u8Url, _m3u8Content, parserConfig) {
|
|
205
|
+
const iv = this.getAttribute(keyLine, "IV");
|
|
206
|
+
const method = this.getAttribute(keyLine, "METHOD");
|
|
207
|
+
const uri = this.getAttribute(keyLine, "URI");
|
|
208
|
+
console.debug(`METHOD:${method}, URI:${uri}, IV:${iv}`);
|
|
209
|
+
const encryptInfo = new EncryptInfo(method);
|
|
210
|
+
if (iv) encryptInfo.iv = Buffer.from(iv, "hex");
|
|
211
|
+
if (parserConfig.customIv && parserConfig.customIv.length > 0) encryptInfo.iv = parserConfig.customIv;
|
|
212
|
+
try {
|
|
213
|
+
if (parserConfig.customKey && parserConfig.customKey.length > 0) encryptInfo.key = parserConfig.customKey;
|
|
214
|
+
else if (uri) {
|
|
215
|
+
const lowerUri = uri.toLowerCase();
|
|
216
|
+
if (lowerUri.startsWith("base64:")) encryptInfo.key = Buffer.from(uri.slice(7), "base64");
|
|
217
|
+
else if (lowerUri.startsWith("data:;base64,")) encryptInfo.key = Buffer.from(uri.slice(13), "base64");
|
|
218
|
+
else if (lowerUri.startsWith("data:text/plain;base64,")) encryptInfo.key = Buffer.from(uri.slice(23), "base64");
|
|
219
|
+
else if ((0, node_fs.existsSync)(uri)) encryptInfo.key = (0, node_fs.readFileSync)(uri);
|
|
220
|
+
else {
|
|
221
|
+
const processedUrl = this.preProcessUrl(new URL(uri, m3u8Url).toString(), parserConfig);
|
|
222
|
+
encryptInfo.key = await this.fetchKeyWithRetry(processedUrl, parserConfig);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(`Failed to load key: ${error.message}`);
|
|
227
|
+
encryptInfo.method = ENCRYPT_METHODS.UNKNOWN;
|
|
228
|
+
}
|
|
229
|
+
if (parserConfig.customMethod) {
|
|
230
|
+
console.warn(`METHOD changed from ${encryptInfo.method} to ${parserConfig.customMethod}`);
|
|
231
|
+
encryptInfo.method = parserConfig.customMethod;
|
|
232
|
+
}
|
|
233
|
+
return encryptInfo;
|
|
234
|
+
}
|
|
235
|
+
getAttribute(line, attrName) {
|
|
236
|
+
const regex = new RegExp(`${attrName}="([^"]+)"`, "i");
|
|
237
|
+
const match = line.match(regex);
|
|
238
|
+
return match?.[1] ?? null;
|
|
239
|
+
}
|
|
240
|
+
async fetchKeyWithRetry(url, parserConfig) {
|
|
241
|
+
let retryCount = parserConfig.keyRetryCount ?? 3;
|
|
242
|
+
while (retryCount >= 0) try {
|
|
243
|
+
const response = await fetch(url, { headers: parserConfig.headers });
|
|
244
|
+
return Buffer.from(await response.arrayBuffer());
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error.message.includes("scheme is not supported")) throw error;
|
|
247
|
+
console.warn(`Error fetching key: ${error.message}. Retries left: ${retryCount}`);
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
249
|
+
retryCount--;
|
|
250
|
+
}
|
|
251
|
+
throw new Error("Maximum retry attempts reached");
|
|
252
|
+
}
|
|
253
|
+
preProcessUrl(url, parserConfig) {
|
|
254
|
+
let processedUrl = url;
|
|
255
|
+
for (const processor of parserConfig.urlProcessors ?? []) if (processor.canProcess(EXTRACTOR_TYPES.HLS, processedUrl, parserConfig)) processedUrl = processor.process(processedUrl, parserConfig);
|
|
256
|
+
return processedUrl;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region lib/parser-config.ts
|
|
262
|
+
var ParserConfig = class {
|
|
263
|
+
url = "";
|
|
264
|
+
originalUrl = "";
|
|
265
|
+
baseUrl;
|
|
266
|
+
customParserArgs = {};
|
|
267
|
+
headers = {};
|
|
268
|
+
contentProcessors = [new DefaultDashContentProcessor(), new DefaultHlsContentProcessor()];
|
|
269
|
+
urlProcessors = [new DefaultUrlProcessor()];
|
|
270
|
+
keyProcessors = [new DefaultHlsKeyProcessor()];
|
|
271
|
+
customMethod;
|
|
272
|
+
customKey;
|
|
273
|
+
customIv;
|
|
274
|
+
urlProcessorArgs;
|
|
275
|
+
appendUrlParams = false;
|
|
276
|
+
keyRetryCount = 3;
|
|
277
|
+
};
|
|
278
|
+
|
|
128
279
|
//#endregion
|
|
129
280
|
//#region lib/shared/stream-spec.ts
|
|
130
281
|
var StreamSpec = class {
|
|
@@ -285,21 +436,6 @@ var MediaPart = class {
|
|
|
285
436
|
}
|
|
286
437
|
};
|
|
287
438
|
|
|
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
439
|
//#endregion
|
|
304
440
|
//#region lib/shared/media-segment.ts
|
|
305
441
|
var MediaSegment = class MediaSegment {
|
|
@@ -835,7 +971,7 @@ var HlsExtractor = class {
|
|
|
835
971
|
const uri = getAttribute(line, "URI");
|
|
836
972
|
const uriLast = getAttribute(lastKeyLine, "URI");
|
|
837
973
|
if (uri !== uriLast) {
|
|
838
|
-
const parsedInfo = this.#parseKey(line);
|
|
974
|
+
const parsedInfo = await this.#parseKey(line);
|
|
839
975
|
currentEncryptInfo.method = parsedInfo.method;
|
|
840
976
|
currentEncryptInfo.key = parsedInfo.key;
|
|
841
977
|
currentEncryptInfo.iv = parsedInfo.iv;
|
|
@@ -905,7 +1041,7 @@ var HlsExtractor = class {
|
|
|
905
1041
|
if (playlist.isLive) playlist.refreshIntervalMs = (playlist.targetDuration || 5) * 2 * 1e3;
|
|
906
1042
|
return playlist;
|
|
907
1043
|
}
|
|
908
|
-
#parseKey(keyLine) {
|
|
1044
|
+
async #parseKey(keyLine) {
|
|
909
1045
|
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
1046
|
throw new Error("No key processor found");
|
|
911
1047
|
}
|
|
@@ -1040,9 +1176,13 @@ var StreamExtractor = class {
|
|
|
1040
1176
|
};
|
|
1041
1177
|
|
|
1042
1178
|
//#endregion
|
|
1179
|
+
exports.ContentProcessor = ContentProcessor
|
|
1180
|
+
exports.DefaultUrlProcessor = DefaultUrlProcessor
|
|
1043
1181
|
exports.ENCRYPT_METHODS = ENCRYPT_METHODS
|
|
1044
1182
|
exports.EXTRACTOR_TYPES = EXTRACTOR_TYPES
|
|
1183
|
+
exports.KeyProcessor = KeyProcessor
|
|
1045
1184
|
exports.MEDIA_TYPES = MEDIA_TYPES
|
|
1046
1185
|
exports.ParserConfig = ParserConfig
|
|
1047
1186
|
exports.ROLE_TYPE = ROLE_TYPE
|
|
1048
|
-
exports.StreamExtractor = StreamExtractor
|
|
1187
|
+
exports.StreamExtractor = StreamExtractor
|
|
1188
|
+
exports.UrlProcessor = UrlProcessor
|
package/dist/dasha.d.cts
CHANGED
|
@@ -47,6 +47,35 @@ declare const ROLE_TYPE: {
|
|
|
47
47
|
};
|
|
48
48
|
type RoleType = (typeof ROLE_TYPE)[keyof typeof ROLE_TYPE];
|
|
49
49
|
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/shared/encrypt-info.d.ts
|
|
52
|
+
declare class EncryptInfo {
|
|
53
|
+
method: EncryptMethod;
|
|
54
|
+
key?: Buffer;
|
|
55
|
+
iv?: Buffer;
|
|
56
|
+
constructor(method?: string | null);
|
|
57
|
+
parseMethod(method?: string | null): EncryptMethod;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region lib/processor.d.ts
|
|
62
|
+
declare abstract class ContentProcessor {
|
|
63
|
+
abstract canProcess(extractorType: ExtractorType, rawText: string, parserConfig: ParserConfig): boolean;
|
|
64
|
+
abstract process(rawText: string, parserConfig: ParserConfig): string;
|
|
65
|
+
}
|
|
66
|
+
declare abstract class KeyProcessor {
|
|
67
|
+
abstract canProcess(extractorType: ExtractorType, keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): boolean;
|
|
68
|
+
abstract process(keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): Promise<EncryptInfo>;
|
|
69
|
+
}
|
|
70
|
+
declare abstract class UrlProcessor {
|
|
71
|
+
abstract canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
|
|
72
|
+
abstract process(originalUrl: string, parserConfig: ParserConfig): string;
|
|
73
|
+
}
|
|
74
|
+
declare class DefaultUrlProcessor extends UrlProcessor {
|
|
75
|
+
canProcess(_extractorType: ExtractorType, _originalUrl: string, parserConfig: ParserConfig): boolean;
|
|
76
|
+
process(url: string, parserConfig: ParserConfig): string;
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
//#endregion
|
|
51
80
|
//#region lib/parser-config.d.ts
|
|
52
81
|
declare class ParserConfig {
|
|
@@ -55,9 +84,9 @@ declare class ParserConfig {
|
|
|
55
84
|
baseUrl?: string;
|
|
56
85
|
customParserArgs: Record<string, string>;
|
|
57
86
|
headers: Record<string, string>;
|
|
58
|
-
contentProcessors:
|
|
59
|
-
urlProcessors:
|
|
60
|
-
keyProcessors:
|
|
87
|
+
contentProcessors: ContentProcessor[];
|
|
88
|
+
urlProcessors: UrlProcessor[];
|
|
89
|
+
keyProcessors: KeyProcessor[];
|
|
61
90
|
customMethod?: EncryptMethod;
|
|
62
91
|
customKey?: Buffer;
|
|
63
92
|
customIv?: Buffer;
|
|
@@ -66,16 +95,6 @@ declare class ParserConfig {
|
|
|
66
95
|
keyRetryCount: number;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region lib/shared/encrypt-info.d.ts
|
|
71
|
-
declare class EncryptInfo {
|
|
72
|
-
method: EncryptMethod;
|
|
73
|
-
key?: Buffer;
|
|
74
|
-
iv?: Buffer;
|
|
75
|
-
constructor(method?: string);
|
|
76
|
-
parseMethod(method?: string): EncryptMethod;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
98
|
//#endregion
|
|
80
99
|
//#region lib/shared/media-segment.d.ts
|
|
81
100
|
declare class MediaSegment {
|
|
@@ -157,4 +176,4 @@ declare class StreamExtractor {
|
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
//#endregion
|
|
160
|
-
export { ENCRYPT_METHODS, EXTRACTOR_TYPES, EncryptMethod, ExtractorType, MEDIA_TYPES, MediaType, ParserConfig, ROLE_TYPE, RoleType, StreamExtractor };
|
|
179
|
+
export { ContentProcessor, DefaultUrlProcessor, ENCRYPT_METHODS, EXTRACTOR_TYPES, EncryptMethod, ExtractorType, KeyProcessor, MEDIA_TYPES, MediaType, ParserConfig, ROLE_TYPE, RoleType, StreamExtractor, UrlProcessor };
|
package/dist/dasha.d.ts
CHANGED
|
@@ -47,6 +47,35 @@ declare const ROLE_TYPE: {
|
|
|
47
47
|
};
|
|
48
48
|
type RoleType = (typeof ROLE_TYPE)[keyof typeof ROLE_TYPE];
|
|
49
49
|
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/shared/encrypt-info.d.ts
|
|
52
|
+
declare class EncryptInfo {
|
|
53
|
+
method: EncryptMethod;
|
|
54
|
+
key?: Buffer;
|
|
55
|
+
iv?: Buffer;
|
|
56
|
+
constructor(method?: string | null);
|
|
57
|
+
parseMethod(method?: string | null): EncryptMethod;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region lib/processor.d.ts
|
|
62
|
+
declare abstract class ContentProcessor {
|
|
63
|
+
abstract canProcess(extractorType: ExtractorType, rawText: string, parserConfig: ParserConfig): boolean;
|
|
64
|
+
abstract process(rawText: string, parserConfig: ParserConfig): string;
|
|
65
|
+
}
|
|
66
|
+
declare abstract class KeyProcessor {
|
|
67
|
+
abstract canProcess(extractorType: ExtractorType, keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): boolean;
|
|
68
|
+
abstract process(keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): Promise<EncryptInfo>;
|
|
69
|
+
}
|
|
70
|
+
declare abstract class UrlProcessor {
|
|
71
|
+
abstract canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
|
|
72
|
+
abstract process(originalUrl: string, parserConfig: ParserConfig): string;
|
|
73
|
+
}
|
|
74
|
+
declare class DefaultUrlProcessor extends UrlProcessor {
|
|
75
|
+
canProcess(_extractorType: ExtractorType, _originalUrl: string, parserConfig: ParserConfig): boolean;
|
|
76
|
+
process(url: string, parserConfig: ParserConfig): string;
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
//#endregion
|
|
51
80
|
//#region lib/parser-config.d.ts
|
|
52
81
|
declare class ParserConfig {
|
|
@@ -55,9 +84,9 @@ declare class ParserConfig {
|
|
|
55
84
|
baseUrl?: string;
|
|
56
85
|
customParserArgs: Record<string, string>;
|
|
57
86
|
headers: Record<string, string>;
|
|
58
|
-
contentProcessors:
|
|
59
|
-
urlProcessors:
|
|
60
|
-
keyProcessors:
|
|
87
|
+
contentProcessors: ContentProcessor[];
|
|
88
|
+
urlProcessors: UrlProcessor[];
|
|
89
|
+
keyProcessors: KeyProcessor[];
|
|
61
90
|
customMethod?: EncryptMethod;
|
|
62
91
|
customKey?: Buffer;
|
|
63
92
|
customIv?: Buffer;
|
|
@@ -66,16 +95,6 @@ declare class ParserConfig {
|
|
|
66
95
|
keyRetryCount: number;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region lib/shared/encrypt-info.d.ts
|
|
71
|
-
declare class EncryptInfo {
|
|
72
|
-
method: EncryptMethod;
|
|
73
|
-
key?: Buffer;
|
|
74
|
-
iv?: Buffer;
|
|
75
|
-
constructor(method?: string);
|
|
76
|
-
parseMethod(method?: string): EncryptMethod;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
98
|
//#endregion
|
|
80
99
|
//#region lib/shared/media-segment.d.ts
|
|
81
100
|
declare class MediaSegment {
|
|
@@ -157,4 +176,4 @@ declare class StreamExtractor {
|
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
//#endregion
|
|
160
|
-
export { ENCRYPT_METHODS, EXTRACTOR_TYPES, EncryptMethod, ExtractorType, MEDIA_TYPES, MediaType, ParserConfig, ROLE_TYPE, RoleType, StreamExtractor };
|
|
179
|
+
export { ContentProcessor, DefaultUrlProcessor, ENCRYPT_METHODS, EXTRACTOR_TYPES, EncryptMethod, ExtractorType, KeyProcessor, MEDIA_TYPES, MediaType, ParserConfig, ROLE_TYPE, RoleType, StreamExtractor, UrlProcessor };
|
package/dist/dasha.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import path from "node:path";
|
|
@@ -52,22 +52,42 @@ const ROLE_TYPE = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
//#endregion
|
|
55
|
-
//#region lib/
|
|
56
|
-
var
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
91
|
};
|
|
72
92
|
|
|
73
93
|
//#endregion
|
|
@@ -101,6 +121,137 @@ const HLS_TAGS = {
|
|
|
101
121
|
extXStart: "#EXT-X-START"
|
|
102
122
|
};
|
|
103
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
|
+
|
|
104
255
|
//#endregion
|
|
105
256
|
//#region lib/shared/stream-spec.ts
|
|
106
257
|
var StreamSpec = class {
|
|
@@ -261,21 +412,6 @@ var MediaPart = class {
|
|
|
261
412
|
}
|
|
262
413
|
};
|
|
263
414
|
|
|
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
415
|
//#endregion
|
|
280
416
|
//#region lib/shared/media-segment.ts
|
|
281
417
|
var MediaSegment = class MediaSegment {
|
|
@@ -811,7 +947,7 @@ var HlsExtractor = class {
|
|
|
811
947
|
const uri = getAttribute(line, "URI");
|
|
812
948
|
const uriLast = getAttribute(lastKeyLine, "URI");
|
|
813
949
|
if (uri !== uriLast) {
|
|
814
|
-
const parsedInfo = this.#parseKey(line);
|
|
950
|
+
const parsedInfo = await this.#parseKey(line);
|
|
815
951
|
currentEncryptInfo.method = parsedInfo.method;
|
|
816
952
|
currentEncryptInfo.key = parsedInfo.key;
|
|
817
953
|
currentEncryptInfo.iv = parsedInfo.iv;
|
|
@@ -881,7 +1017,7 @@ var HlsExtractor = class {
|
|
|
881
1017
|
if (playlist.isLive) playlist.refreshIntervalMs = (playlist.targetDuration || 5) * 2 * 1e3;
|
|
882
1018
|
return playlist;
|
|
883
1019
|
}
|
|
884
|
-
#parseKey(keyLine) {
|
|
1020
|
+
async #parseKey(keyLine) {
|
|
885
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);
|
|
886
1022
|
throw new Error("No key processor found");
|
|
887
1023
|
}
|
|
@@ -1016,4 +1152,4 @@ var StreamExtractor = class {
|
|
|
1016
1152
|
};
|
|
1017
1153
|
|
|
1018
1154
|
//#endregion
|
|
1019
|
-
export { ENCRYPT_METHODS, EXTRACTOR_TYPES, MEDIA_TYPES, ParserConfig, ROLE_TYPE, StreamExtractor };
|
|
1155
|
+
export { ContentProcessor, DefaultUrlProcessor, ENCRYPT_METHODS, EXTRACTOR_TYPES, KeyProcessor, MEDIA_TYPES, ParserConfig, ROLE_TYPE, StreamExtractor, UrlProcessor };
|