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 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 body = await fetch(url).then((res) => res.text());
23
- const manifest = await parse(body, url);
24
-
25
- for (const track of manifest.tracks.all) {
26
- for (const segment of track.segments) {
27
- const content = await fetch(url).then((res) => res.arrayBuffer());
28
- await fs.appendFile(`${track.id}.mp4`, content);
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/parser-config.ts
80
- var ParserConfig = class {
81
- url = "";
82
- originalUrl = "";
83
- baseUrl;
84
- customParserArgs = {};
85
- headers = {};
86
- contentProcessors = [];
87
- urlProcessors = [];
88
- keyProcessors = [];
89
- customMethod;
90
- customKey;
91
- customIv;
92
- urlProcessorArgs;
93
- appendUrlParams = false;
94
- keyRetryCount = 3;
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: any[];
59
- urlProcessors: any[];
60
- keyProcessors: any[];
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: any[];
59
- urlProcessors: any[];
60
- keyProcessors: any[];
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/parser-config.ts
56
- var ParserConfig = class {
57
- url = "";
58
- originalUrl = "";
59
- baseUrl;
60
- customParserArgs = {};
61
- headers = {};
62
- contentProcessors = [];
63
- urlProcessors = [];
64
- keyProcessors = [];
65
- customMethod;
66
- customKey;
67
- customIv;
68
- urlProcessorArgs;
69
- appendUrlParams = false;
70
- keyRetryCount = 3;
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dasha",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.2",
4
4
  "description": "Streaming manifest parser",
5
5
  "files": [
6
6
  "dist"