dasha 4.0.0-alpha.2 → 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 CHANGED
@@ -7,13 +7,13 @@
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
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).
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
11
 
12
12
 
13
13
  ## Install
14
14
 
15
15
  ```shell
16
- npm i dasha@4.0.0-alpha.1
16
+ npm i dasha@alpha
17
17
  ```
18
18
 
19
19
  ## Usage
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
- const node_fs = __toESM(require("node:fs"));
26
- const node_fs_promises = __toESM(require("node:fs/promises"));
27
- const node_url = __toESM(require("node:url"));
28
- const node_path = __toESM(require("node:path"));
29
- const temporal_polyfill = __toESM(require("temporal-polyfill"));
30
- const __xmldom_xmldom = __toESM(require("@xmldom/xmldom"));
31
- const node_crypto = __toESM(require("node:crypto"));
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: 0,
45
- AES_128: 1,
46
- AES_128_ECB: 2,
47
- SAMPLE_AES: 3,
48
- SAMPLE_AES_CTR: 4,
49
- CENC: 5,
50
- CHACHA20: 6,
51
- UNKNOWN: 7
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
@@ -77,17 +85,13 @@ const ROLE_TYPE = {
77
85
 
78
86
  //#endregion
79
87
  //#region lib/processor.ts
80
- var ContentProcessor = class {};
81
- var KeyProcessor = class {};
82
- var UrlProcessor = class {};
83
- var DefaultUrlProcessor = class extends UrlProcessor {
88
+ var DefaultUrlProcessor = class {
84
89
  canProcess(_extractorType, _originalUrl, parserConfig) {
85
90
  return parserConfig.appendUrlParams;
86
91
  }
87
92
  process(url, parserConfig) {
88
93
  if (!url.startsWith("http")) return url;
89
- const urlFromConfig = new URL(parserConfig.url);
90
- const urlFromConfigQuery = urlFromConfig.searchParams;
94
+ const urlFromConfigQuery = new URL(parserConfig.url).searchParams;
91
95
  const oldUrl = new URL(url);
92
96
  const newQuery = oldUrl.searchParams;
93
97
  for (const [key, value] of urlFromConfigQuery) if (newQuery.has(key)) newQuery.set(key, value);
@@ -102,7 +106,7 @@ var DefaultUrlProcessor = class extends UrlProcessor {
102
106
 
103
107
  //#endregion
104
108
  //#region lib/dash/dash-content-processor.ts
105
- var DefaultDashContentProcessor = class extends ContentProcessor {
109
+ var DefaultDashContentProcessor = class {
106
110
  canProcess(extractorType, mpdContent) {
107
111
  if (extractorType !== EXTRACTOR_TYPES.MPEG_DASH) return false;
108
112
  return mpdContent.includes("<mas:") && !mpdContent.includes("xmlns:mas");
@@ -147,7 +151,7 @@ const HLS_TAGS = {
147
151
 
148
152
  //#endregion
149
153
  //#region lib/hls/hls-content-processor.ts
150
- var DefaultHlsContentProcessor = class DefaultHlsContentProcessor extends ContentProcessor {
154
+ var DefaultHlsContentProcessor = class DefaultHlsContentProcessor {
151
155
  static YkDVRegex = /#EXT-X-DISCONTINUITY\s+#EXT-X-MAP:URI="(.*?)",BYTERANGE="(.*?)"/g;
152
156
  static DNSPRegex = /#EXT-X-MAP:URI=".*?BUMPER\/[\s\S]+?#EXT-X-DISCONTINUITY/;
153
157
  static DNSPSubRegex = /#EXTINF:.*?,\s+.*BUMPER.*\s+#EXT-X-DISCONTINUITY/;
@@ -186,8 +190,10 @@ var EncryptInfo = class {
186
190
  method = ENCRYPT_METHODS.NONE;
187
191
  key;
188
192
  iv;
193
+ drm;
189
194
  constructor(method) {
190
195
  this.method = this.parseMethod(method);
196
+ this.drm = {};
191
197
  }
192
198
  parseMethod(method) {
193
199
  if (method) return ENCRYPT_METHODS[method.replace("-", "_")];
@@ -197,7 +203,7 @@ var EncryptInfo = class {
197
203
 
198
204
  //#endregion
199
205
  //#region lib/hls/hls-key-processor.ts
200
- var DefaultHlsKeyProcessor = class extends KeyProcessor {
206
+ var DefaultHlsKeyProcessor = class {
201
207
  canProcess(extractorType) {
202
208
  return extractorType === EXTRACTOR_TYPES.HLS;
203
209
  }
@@ -205,17 +211,16 @@ var DefaultHlsKeyProcessor = class extends KeyProcessor {
205
211
  const iv = this.getAttribute(keyLine, "IV");
206
212
  const method = this.getAttribute(keyLine, "METHOD");
207
213
  const uri = this.getAttribute(keyLine, "URI");
208
- console.debug(`METHOD:${method}, URI:${uri}, IV:${iv}`);
209
214
  const encryptInfo = new EncryptInfo(method);
210
- if (iv) encryptInfo.iv = Buffer.from(iv, "hex");
215
+ if (iv) encryptInfo.iv = barsic.b.hex().encode(iv);
211
216
  if (parserConfig.customIv && parserConfig.customIv.length > 0) encryptInfo.iv = parserConfig.customIv;
212
217
  try {
213
218
  if (parserConfig.customKey && parserConfig.customKey.length > 0) encryptInfo.key = parserConfig.customKey;
214
219
  else if (uri) {
215
220
  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");
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));
219
224
  else if ((0, node_fs.existsSync)(uri)) encryptInfo.key = (0, node_fs.readFileSync)(uri);
220
225
  else {
221
226
  const processedUrl = this.preProcessUrl(new URL(uri, m3u8Url).toString(), parserConfig);
@@ -233,15 +238,15 @@ var DefaultHlsKeyProcessor = class extends KeyProcessor {
233
238
  return encryptInfo;
234
239
  }
235
240
  getAttribute(line, attrName) {
236
- const regex = new RegExp(`${attrName}="([^"]+)"`, "i");
241
+ const regex = new RegExp(`${attrName}=(?:"([^"]+)"|([^,]+))`, "i");
237
242
  const match = line.match(regex);
238
- return match?.[1] ?? null;
243
+ return match?.[1] ?? match?.[2] ?? null;
239
244
  }
240
245
  async fetchKeyWithRetry(url, parserConfig) {
241
246
  let retryCount = parserConfig.keyRetryCount ?? 3;
242
247
  while (retryCount >= 0) try {
243
248
  const response = await fetch(url, { headers: parserConfig.headers });
244
- return Buffer.from(await response.arrayBuffer());
249
+ return new Uint8Array(await response.arrayBuffer());
245
250
  } catch (error) {
246
251
  if (error.message.includes("scheme is not supported")) throw error;
247
252
  console.warn(`Error fetching key: ${error.message}. Retries left: ${retryCount}`);
@@ -362,8 +367,7 @@ const DASH_TAGS = {
362
367
  const combineUrl = (baseUrl, relativeUrl) => {
363
368
  if (!baseUrl.trim()) return relativeUrl;
364
369
  const url1 = new URL(baseUrl);
365
- const url2 = new URL(relativeUrl, url1);
366
- return url2.toString();
370
+ return new URL(relativeUrl, url1).toString();
367
371
  };
368
372
  const replaceVars = (text, dict) => {
369
373
  let result = text;
@@ -402,7 +406,7 @@ const getAttribute = (line, key = "") => {
402
406
  return result;
403
407
  };
404
408
  const distinctBy = (array, callbackfn) => {
405
- const seen = new Set();
409
+ const seen = /* @__PURE__ */ new Set();
406
410
  return array.filter((item) => {
407
411
  const value = callbackfn(item);
408
412
  if (seen.has(value)) return false;
@@ -481,8 +485,7 @@ var MediaSegment = class MediaSegment {
481
485
  */
482
486
  const parseRange = (range) => {
483
487
  const [startRange, end] = range.split("-").map(Number);
484
- const expectLength = end - startRange + 1;
485
- return [startRange, expectLength];
488
+ return [startRange, end - startRange + 1];
486
489
  };
487
490
 
488
491
  //#endregion
@@ -518,11 +521,9 @@ var DashExtractor = class DashExtractor {
518
521
  async extractStreams(rawText) {
519
522
  const streamList = [];
520
523
  this.#mpdContent = rawText;
521
- const document = new __xmldom_xmldom.DOMParser().parseFromString(this.#mpdContent, "text/xml");
522
- const mpdElement = document.getElementsByTagName("MPD")[0];
523
- const type = mpdElement.getAttribute("type");
524
- const isLive = type === "dynamic";
525
- 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");
526
527
  const availabilityStartTime = mpdElement.getAttribute("availabilityStartTime");
527
528
  const timeShiftBufferDepth = mpdElement.getAttribute("timeShiftBufferDepth") || "PT1M";
528
529
  const publishTime = mpdElement.getAttribute("publishTime");
@@ -581,8 +582,7 @@ var DashExtractor = class DashExtractor {
581
582
  if (role) {
582
583
  const roleValue = role.getAttribute("value");
583
584
  const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
584
- const roleTypeKey = roleValue.split("-").map(capitalize).join("");
585
- const roleType = ROLE_TYPE[roleTypeKey];
585
+ const roleType = ROLE_TYPE[roleValue.split("-").map(capitalize).join("")];
586
586
  streamSpec.role = roleType;
587
587
  if (roleType === ROLE_TYPE.Subtitle) {
588
588
  streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
@@ -713,8 +713,7 @@ var DashExtractor = class DashExtractor {
713
713
  varDic[DASH_TAGS.TemplateNumber] = segNumber++;
714
714
  const _hashTime = mediaTemplate?.includes(DASH_TAGS.TemplateTime);
715
715
  const _media = replaceVars(mediaTemplate, varDic);
716
- const _mediaUrl = combineUrl(segBaseUrl, _media);
717
- _mediaSegment.url = _mediaUrl;
716
+ _mediaSegment.url = combineUrl(segBaseUrl, _media);
718
717
  _mediaSegment.index = segIndex++;
719
718
  _mediaSegment.duration = _duration / timescale;
720
719
  if (_hashTime) _mediaSegment.nameFromVar = currentTime.toString();
@@ -758,27 +757,37 @@ var DashExtractor = class DashExtractor {
758
757
  mediaSegment.duration = periodDurationSeconds;
759
758
  streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
760
759
  }
761
- const contentProtection = adaptationSet.getElementsByTagName("ContentProtection")[0] || representation.getElementsByTagName("ContentProtection")[0];
762
- if (contentProtection) {
760
+ const adaptationSetProtections = adaptationSet.getElementsByTagName("ContentProtection");
761
+ const representationProtections = representation.getElementsByTagName("ContentProtection");
762
+ const contentProtections = representationProtections[0] ? representationProtections : adaptationSetProtections;
763
+ if (contentProtections.length) {
763
764
  const encryptInfo = new EncryptInfo();
764
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
+ }
765
778
  if (streamSpec.playlist.mediaInit) streamSpec.playlist.mediaInit.encryptInfo = encryptInfo;
766
779
  const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
767
780
  for (const segment of segments) if (!segment.encryptInfo) segment.encryptInfo = encryptInfo;
768
781
  }
769
782
  const _index = streamList.findIndex((item) => item.periodId !== streamSpec.periodId && item.groupId === streamSpec.groupId && item.resolution === streamSpec.resolution && item.mediaType === streamSpec.mediaType);
770
- if (_index > -1) if (isLive) {} else {
771
- const url1 = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).url;
772
- const url2 = streamSpec.playlist.mediaParts[0].mediaSegments.at(-1)?.url;
773
- if (url1 !== url2) {
774
- const startIndex = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).index + 1;
775
- const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
776
- for (const segment of segments) segment.index += startIndex;
777
- const mediaPart = new MediaPart();
778
- mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
779
- streamList[_index].playlist.mediaParts.push(mediaPart);
780
- } else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
781
- }
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);
782
791
  else {
783
792
  if (streamSpec.mediaType === MEDIA_TYPES.SUBTITLES && streamSpec.extension === "mp4") streamSpec.extension = "m4s";
784
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";
@@ -793,8 +802,8 @@ var DashExtractor = class DashExtractor {
793
802
  const subtitleList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.SUBTITLES);
794
803
  const videoList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.VIDEO);
795
804
  for (const video of videoList) {
796
- const audioGroupId = audioList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
797
- 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;
798
807
  if (audioGroupId) video.audioId = audioGroupId;
799
808
  if (subtitleGroupId) video.subtitleId = subtitleGroupId;
800
809
  }
@@ -804,17 +813,38 @@ var DashExtractor = class DashExtractor {
804
813
  if (!v) return;
805
814
  return v;
806
815
  }
807
- fetchPlayList(streamSpecs) {
808
- throw new Error("Method not implemented.");
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
+ }
809
838
  }
810
- refreshPlayList(streamSpecs) {
811
- throw new Error("Method not implemented.");
839
+ async fetchPlayList(streamSpecs) {
840
+ this.#processUrl(streamSpecs);
812
841
  }
813
842
  preProcessUrl(url) {
814
- throw new Error("Method not implemented.");
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;
815
845
  }
816
846
  preProcessContent() {
817
- throw new Error("Method not implemented.");
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);
818
848
  }
819
849
  };
820
850
 
@@ -893,8 +923,7 @@ var HlsExtractor = class {
893
923
  expectPlaylist = true;
894
924
  } else if (line.startsWith(HLS_TAGS.extXMedia)) {
895
925
  streamSpec = new StreamSpec();
896
- const type = getAttribute(line, "TYPE").replace("-", "_");
897
- const mediaType = MEDIA_TYPES[type];
926
+ const mediaType = MEDIA_TYPES[getAttribute(line, "TYPE").replace("-", "_")];
898
927
  if (mediaType) streamSpec.mediaType = mediaType;
899
928
  if (mediaType === MEDIA_TYPES.CLOSED_CAPTIONS) continue;
900
929
  let url = getAttribute(line, "URI");
@@ -945,8 +974,7 @@ var HlsExtractor = class {
945
974
  for (const line of lines) {
946
975
  if (!line.trim()) continue;
947
976
  if (line.startsWith(HLS_TAGS.extXByterange)) {
948
- const p = getAttribute(line);
949
- const [n, o] = getRange(p);
977
+ const [n, o] = getRange(getAttribute(line));
950
978
  segment.expectLength = n;
951
979
  segment.startRange = o || (segments.at(-1)?.startRange || 0) + (segments.at(-1)?.expectLength || 0);
952
980
  expectSegment = true;
@@ -968,9 +996,7 @@ var HlsExtractor = class {
968
996
  mediaParts.push(new MediaPart(segments));
969
997
  segments = [];
970
998
  } else if (line.startsWith(HLS_TAGS.extXKey)) {
971
- const uri = getAttribute(line, "URI");
972
- const uriLast = getAttribute(lastKeyLine, "URI");
973
- if (uri !== uriLast) {
999
+ if (getAttribute(line, "URI") !== getAttribute(lastKeyLine, "URI")) {
974
1000
  const parsedInfo = await this.#parseKey(line);
975
1001
  currentEncryptInfo.method = parsedInfo.method;
976
1002
  currentEncryptInfo.key = parsedInfo.key;
@@ -998,8 +1024,7 @@ var HlsExtractor = class {
998
1024
  mediaSegment.index = -1;
999
1025
  playlist.mediaInit = mediaSegment;
1000
1026
  if (line.includes("BYTERANGE")) {
1001
- const p = getAttribute(line, "BYTERANGE");
1002
- const [n, o] = getRange(p);
1027
+ const [n, o] = getRange(getAttribute(line, "BYTERANGE"));
1003
1028
  mediaSegment.expectLength = n;
1004
1029
  mediaSegment.startRange = o || 0;
1005
1030
  }
@@ -1048,10 +1073,7 @@ var HlsExtractor = class {
1048
1073
  async extractStreams(rawText) {
1049
1074
  this.#m3u8Content = rawText;
1050
1075
  this.preProcessContent();
1051
- if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) {
1052
- console.log("Master m3u8 found");
1053
- return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
1054
- }
1076
+ if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
1055
1077
  const playlist = await this.#parseList();
1056
1078
  const streamSpec = new StreamSpec();
1057
1079
  streamSpec.url = this.parserConfig.url;
@@ -1061,8 +1083,7 @@ var HlsExtractor = class {
1061
1083
  }
1062
1084
  async #loadM3u8FromUrl(url) {
1063
1085
  if (url.startsWith("file:")) {
1064
- const uri = new URL(url);
1065
- const filePath = uri.pathname;
1086
+ const filePath = new URL(url).pathname;
1066
1087
  this.#m3u8Content = await (0, node_fs_promises.readFile)(filePath, "utf8");
1067
1088
  } else if (url.startsWith("http")) try {
1068
1089
  const response = await fetch(url, { headers: this.parserConfig.headers });
@@ -1102,9 +1123,9 @@ var HlsExtractor = class {
1102
1123
  else list.playlist = newPlaylist;
1103
1124
  if (list.mediaType === MEDIA_TYPES.SUBTITLES) {
1104
1125
  const a = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".ttml")));
1105
- 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")));
1106
1127
  if (a) list.extension = "ttml";
1107
- if (b) list.extension = "vtt";
1128
+ if (b$1) list.extension = "vtt";
1108
1129
  } else list.extension = list.playlist.mediaInit ? "m4s" : "ts";
1109
1130
  }
1110
1131
  }
@@ -1132,8 +1153,7 @@ var StreamExtractor = class {
1132
1153
  }
1133
1154
  async loadSourceFromUrl(url) {
1134
1155
  if (url.startsWith("file:")) {
1135
- const uri = new URL(url);
1136
- const filePath = uri.pathname;
1156
+ const filePath = new URL(url).pathname;
1137
1157
  this.#rawText = await (0, node_fs_promises.readFile)(filePath, "utf8");
1138
1158
  this.#setUrl(url);
1139
1159
  } else if (url.startsWith("http")) {
@@ -1160,8 +1180,11 @@ var StreamExtractor = class {
1160
1180
  } else if (this.#rawText.includes("</MPD>") && this.#rawText.includes("<MPD")) {
1161
1181
  this.#extractor = new DashExtractor(this.#parserConfig);
1162
1182
  rawType = "mpd";
1163
- } else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia")) rawType = "ism";
1164
- else if (rawText === "<RE_LIVE_TS>") {} else throw new Error("Unsupported stream type");
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");
1165
1188
  this.#rawFiles[`raw.${rawType}`] = rawText;
1166
1189
  }
1167
1190
  async extractStreams() {
@@ -1176,13 +1199,10 @@ var StreamExtractor = class {
1176
1199
  };
1177
1200
 
1178
1201
  //#endregion
1179
- exports.ContentProcessor = ContentProcessor
1180
- exports.DefaultUrlProcessor = DefaultUrlProcessor
1181
- exports.ENCRYPT_METHODS = ENCRYPT_METHODS
1182
- exports.EXTRACTOR_TYPES = EXTRACTOR_TYPES
1183
- exports.KeyProcessor = KeyProcessor
1184
- exports.MEDIA_TYPES = MEDIA_TYPES
1185
- exports.ParserConfig = ParserConfig
1186
- exports.ROLE_TYPE = ROLE_TYPE
1187
- exports.StreamExtractor = StreamExtractor
1188
- exports.UrlProcessor = UrlProcessor
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;
package/dist/dasha.d.cts CHANGED
@@ -6,21 +6,19 @@ declare const MEDIA_TYPES: {
6
6
  readonly CLOSED_CAPTIONS: "closed-captions";
7
7
  };
8
8
  type MediaType = (typeof MEDIA_TYPES)[keyof typeof MEDIA_TYPES];
9
-
10
9
  //#endregion
11
10
  //#region lib/shared/encrypt-method.d.ts
12
11
  declare const ENCRYPT_METHODS: {
13
- NONE: number;
14
- AES_128: number;
15
- AES_128_ECB: number;
16
- SAMPLE_AES: number;
17
- SAMPLE_AES_CTR: number;
18
- CENC: number;
19
- CHACHA20: number;
20
- UNKNOWN: number;
12
+ readonly NONE: "none";
13
+ readonly AES_128: "aes-128";
14
+ readonly AES_128_ECB: "aes-128-ecb";
15
+ readonly SAMPLE_AES: "sample-aes";
16
+ readonly SAMPLE_AES_CTR: "sample-aes-ctr";
17
+ readonly CENC: "cenc";
18
+ readonly CHACHA20: "chacha20";
19
+ readonly UNKNOWN: "unknown";
21
20
  };
22
21
  type EncryptMethod = (typeof ENCRYPT_METHODS)[keyof typeof ENCRYPT_METHODS];
23
-
24
22
  //#endregion
25
23
  //#region lib/shared/extractor-type.d.ts
26
24
  declare const EXTRACTOR_TYPES: {
@@ -30,7 +28,6 @@ declare const EXTRACTOR_TYPES: {
30
28
  readonly MSS: "MSS";
31
29
  };
32
30
  type ExtractorType = (typeof EXTRACTOR_TYPES)[keyof typeof EXTRACTOR_TYPES];
33
-
34
31
  //#endregion
35
32
  //#region lib/shared/role-type.d.ts
36
33
  declare const ROLE_TYPE: {
@@ -46,36 +43,38 @@ declare const ROLE_TYPE: {
46
43
  ForcedSubtitle: number;
47
44
  };
48
45
  type RoleType = (typeof ROLE_TYPE)[keyof typeof ROLE_TYPE];
49
-
50
46
  //#endregion
51
47
  //#region lib/shared/encrypt-info.d.ts
48
+ type DrmType = 'widevine' | 'playready' | 'fairplay';
52
49
  declare class EncryptInfo {
53
50
  method: EncryptMethod;
54
- key?: Buffer;
55
- iv?: Buffer;
51
+ key?: Uint8Array;
52
+ iv?: Uint8Array;
53
+ drm: { [key in DrmType]?: {
54
+ keyId?: string;
55
+ pssh?: string;
56
+ } };
56
57
  constructor(method?: string | null);
57
58
  parseMethod(method?: string | null): EncryptMethod;
58
59
  }
59
-
60
60
  //#endregion
61
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;
62
+ interface ContentProcessor {
63
+ canProcess(extractorType: ExtractorType, rawText: string, parserConfig: ParserConfig): boolean;
64
+ process(rawText: string, parserConfig: ParserConfig): string;
65
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>;
66
+ interface KeyProcessor {
67
+ canProcess(extractorType: ExtractorType, keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): boolean;
68
+ process(keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): Promise<EncryptInfo>;
69
69
  }
70
- declare abstract class UrlProcessor {
71
- abstract canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
72
- abstract process(originalUrl: string, parserConfig: ParserConfig): string;
70
+ interface UrlProcessor {
71
+ canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
72
+ process(originalUrl: string, parserConfig: ParserConfig): string;
73
73
  }
74
- declare class DefaultUrlProcessor extends UrlProcessor {
74
+ declare class DefaultUrlProcessor implements UrlProcessor {
75
75
  canProcess(_extractorType: ExtractorType, _originalUrl: string, parserConfig: ParserConfig): boolean;
76
76
  process(url: string, parserConfig: ParserConfig): string;
77
77
  }
78
-
79
78
  //#endregion
80
79
  //#region lib/parser-config.d.ts
81
80
  declare class ParserConfig {
@@ -88,13 +87,12 @@ declare class ParserConfig {
88
87
  urlProcessors: UrlProcessor[];
89
88
  keyProcessors: KeyProcessor[];
90
89
  customMethod?: EncryptMethod;
91
- customKey?: Buffer;
92
- customIv?: Buffer;
90
+ customKey?: Uint8Array;
91
+ customIv?: Uint8Array;
93
92
  urlProcessorArgs?: string;
94
93
  appendUrlParams: boolean;
95
94
  keyRetryCount: number;
96
95
  }
97
-
98
96
  //#endregion
99
97
  //#region lib/shared/media-segment.d.ts
100
98
  declare class MediaSegment {
@@ -112,14 +110,12 @@ declare class MediaSegment {
112
110
  equals(segment: unknown): boolean;
113
111
  getHashCode(): string;
114
112
  }
115
-
116
113
  //#endregion
117
114
  //#region lib/shared/media-part.d.ts
118
115
  declare class MediaPart {
119
116
  mediaSegments: MediaSegment[];
120
117
  constructor(segments?: MediaSegment[]);
121
118
  }
122
-
123
119
  //#endregion
124
120
  //#region lib/shared/playlist.d.ts
125
121
  declare class Playlist {
@@ -131,7 +127,6 @@ declare class Playlist {
131
127
  mediaInit?: MediaSegment;
132
128
  mediaParts: MediaPart[];
133
129
  }
134
-
135
130
  //#endregion
136
131
  //#region lib/shared/stream-spec.d.ts
137
132
  declare class StreamSpec {
@@ -161,7 +156,6 @@ declare class StreamSpec {
161
156
  get segmentsCount(): number;
162
157
  toShortString(): string;
163
158
  }
164
-
165
159
  //#endregion
166
160
  //#region lib/stream-extractor.d.ts
167
161
  declare class StreamExtractor {
@@ -174,6 +168,5 @@ declare class StreamExtractor {
174
168
  fetchPlayList(streamSpecs: StreamSpec[]): Promise<void>;
175
169
  refreshPlayList(streamSpecs: StreamSpec[]): Promise<void>;
176
170
  }
177
-
178
171
  //#endregion
179
172
  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
@@ -6,21 +6,19 @@ declare const MEDIA_TYPES: {
6
6
  readonly CLOSED_CAPTIONS: "closed-captions";
7
7
  };
8
8
  type MediaType = (typeof MEDIA_TYPES)[keyof typeof MEDIA_TYPES];
9
-
10
9
  //#endregion
11
10
  //#region lib/shared/encrypt-method.d.ts
12
11
  declare const ENCRYPT_METHODS: {
13
- NONE: number;
14
- AES_128: number;
15
- AES_128_ECB: number;
16
- SAMPLE_AES: number;
17
- SAMPLE_AES_CTR: number;
18
- CENC: number;
19
- CHACHA20: number;
20
- UNKNOWN: number;
12
+ readonly NONE: "none";
13
+ readonly AES_128: "aes-128";
14
+ readonly AES_128_ECB: "aes-128-ecb";
15
+ readonly SAMPLE_AES: "sample-aes";
16
+ readonly SAMPLE_AES_CTR: "sample-aes-ctr";
17
+ readonly CENC: "cenc";
18
+ readonly CHACHA20: "chacha20";
19
+ readonly UNKNOWN: "unknown";
21
20
  };
22
21
  type EncryptMethod = (typeof ENCRYPT_METHODS)[keyof typeof ENCRYPT_METHODS];
23
-
24
22
  //#endregion
25
23
  //#region lib/shared/extractor-type.d.ts
26
24
  declare const EXTRACTOR_TYPES: {
@@ -30,7 +28,6 @@ declare const EXTRACTOR_TYPES: {
30
28
  readonly MSS: "MSS";
31
29
  };
32
30
  type ExtractorType = (typeof EXTRACTOR_TYPES)[keyof typeof EXTRACTOR_TYPES];
33
-
34
31
  //#endregion
35
32
  //#region lib/shared/role-type.d.ts
36
33
  declare const ROLE_TYPE: {
@@ -46,36 +43,38 @@ declare const ROLE_TYPE: {
46
43
  ForcedSubtitle: number;
47
44
  };
48
45
  type RoleType = (typeof ROLE_TYPE)[keyof typeof ROLE_TYPE];
49
-
50
46
  //#endregion
51
47
  //#region lib/shared/encrypt-info.d.ts
48
+ type DrmType = 'widevine' | 'playready' | 'fairplay';
52
49
  declare class EncryptInfo {
53
50
  method: EncryptMethod;
54
- key?: Buffer;
55
- iv?: Buffer;
51
+ key?: Uint8Array;
52
+ iv?: Uint8Array;
53
+ drm: { [key in DrmType]?: {
54
+ keyId?: string;
55
+ pssh?: string;
56
+ } };
56
57
  constructor(method?: string | null);
57
58
  parseMethod(method?: string | null): EncryptMethod;
58
59
  }
59
-
60
60
  //#endregion
61
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;
62
+ interface ContentProcessor {
63
+ canProcess(extractorType: ExtractorType, rawText: string, parserConfig: ParserConfig): boolean;
64
+ process(rawText: string, parserConfig: ParserConfig): string;
65
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>;
66
+ interface KeyProcessor {
67
+ canProcess(extractorType: ExtractorType, keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): boolean;
68
+ process(keyLine: string, m3u8Url: string, m3u8Content: string, parserConfig: ParserConfig): Promise<EncryptInfo>;
69
69
  }
70
- declare abstract class UrlProcessor {
71
- abstract canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
72
- abstract process(originalUrl: string, parserConfig: ParserConfig): string;
70
+ interface UrlProcessor {
71
+ canProcess(extractorType: ExtractorType, originalUrl: string, parserConfig: ParserConfig): boolean;
72
+ process(originalUrl: string, parserConfig: ParserConfig): string;
73
73
  }
74
- declare class DefaultUrlProcessor extends UrlProcessor {
74
+ declare class DefaultUrlProcessor implements UrlProcessor {
75
75
  canProcess(_extractorType: ExtractorType, _originalUrl: string, parserConfig: ParserConfig): boolean;
76
76
  process(url: string, parserConfig: ParserConfig): string;
77
77
  }
78
-
79
78
  //#endregion
80
79
  //#region lib/parser-config.d.ts
81
80
  declare class ParserConfig {
@@ -88,13 +87,12 @@ declare class ParserConfig {
88
87
  urlProcessors: UrlProcessor[];
89
88
  keyProcessors: KeyProcessor[];
90
89
  customMethod?: EncryptMethod;
91
- customKey?: Buffer;
92
- customIv?: Buffer;
90
+ customKey?: Uint8Array;
91
+ customIv?: Uint8Array;
93
92
  urlProcessorArgs?: string;
94
93
  appendUrlParams: boolean;
95
94
  keyRetryCount: number;
96
95
  }
97
-
98
96
  //#endregion
99
97
  //#region lib/shared/media-segment.d.ts
100
98
  declare class MediaSegment {
@@ -112,14 +110,12 @@ declare class MediaSegment {
112
110
  equals(segment: unknown): boolean;
113
111
  getHashCode(): string;
114
112
  }
115
-
116
113
  //#endregion
117
114
  //#region lib/shared/media-part.d.ts
118
115
  declare class MediaPart {
119
116
  mediaSegments: MediaSegment[];
120
117
  constructor(segments?: MediaSegment[]);
121
118
  }
122
-
123
119
  //#endregion
124
120
  //#region lib/shared/playlist.d.ts
125
121
  declare class Playlist {
@@ -131,7 +127,6 @@ declare class Playlist {
131
127
  mediaInit?: MediaSegment;
132
128
  mediaParts: MediaPart[];
133
129
  }
134
-
135
130
  //#endregion
136
131
  //#region lib/shared/stream-spec.d.ts
137
132
  declare class StreamSpec {
@@ -161,7 +156,6 @@ declare class StreamSpec {
161
156
  get segmentsCount(): number;
162
157
  toShortString(): string;
163
158
  }
164
-
165
159
  //#endregion
166
160
  //#region lib/stream-extractor.d.ts
167
161
  declare class StreamExtractor {
@@ -174,6 +168,5 @@ declare class StreamExtractor {
174
168
  fetchPlayList(streamSpecs: StreamSpec[]): Promise<void>;
175
169
  refreshPlayList(streamSpecs: StreamSpec[]): Promise<void>;
176
170
  }
177
-
178
171
  //#endregion
179
172
  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,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { b } from "barsic";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import path from "node:path";
@@ -17,14 +18,14 @@ const MEDIA_TYPES = {
17
18
  //#endregion
18
19
  //#region lib/shared/encrypt-method.ts
19
20
  const ENCRYPT_METHODS = {
20
- NONE: 0,
21
- AES_128: 1,
22
- AES_128_ECB: 2,
23
- SAMPLE_AES: 3,
24
- SAMPLE_AES_CTR: 4,
25
- CENC: 5,
26
- CHACHA20: 6,
27
- UNKNOWN: 7
21
+ NONE: "none",
22
+ AES_128: "aes-128",
23
+ AES_128_ECB: "aes-128-ecb",
24
+ SAMPLE_AES: "sample-aes",
25
+ SAMPLE_AES_CTR: "sample-aes-ctr",
26
+ CENC: "cenc",
27
+ CHACHA20: "chacha20",
28
+ UNKNOWN: "unknown"
28
29
  };
29
30
 
30
31
  //#endregion
@@ -53,17 +54,13 @@ const ROLE_TYPE = {
53
54
 
54
55
  //#endregion
55
56
  //#region lib/processor.ts
56
- var ContentProcessor = class {};
57
- var KeyProcessor = class {};
58
- var UrlProcessor = class {};
59
- var DefaultUrlProcessor = class extends UrlProcessor {
57
+ var DefaultUrlProcessor = class {
60
58
  canProcess(_extractorType, _originalUrl, parserConfig) {
61
59
  return parserConfig.appendUrlParams;
62
60
  }
63
61
  process(url, parserConfig) {
64
62
  if (!url.startsWith("http")) return url;
65
- const urlFromConfig = new URL(parserConfig.url);
66
- const urlFromConfigQuery = urlFromConfig.searchParams;
63
+ const urlFromConfigQuery = new URL(parserConfig.url).searchParams;
67
64
  const oldUrl = new URL(url);
68
65
  const newQuery = oldUrl.searchParams;
69
66
  for (const [key, value] of urlFromConfigQuery) if (newQuery.has(key)) newQuery.set(key, value);
@@ -78,7 +75,7 @@ var DefaultUrlProcessor = class extends UrlProcessor {
78
75
 
79
76
  //#endregion
80
77
  //#region lib/dash/dash-content-processor.ts
81
- var DefaultDashContentProcessor = class extends ContentProcessor {
78
+ var DefaultDashContentProcessor = class {
82
79
  canProcess(extractorType, mpdContent) {
83
80
  if (extractorType !== EXTRACTOR_TYPES.MPEG_DASH) return false;
84
81
  return mpdContent.includes("<mas:") && !mpdContent.includes("xmlns:mas");
@@ -123,7 +120,7 @@ const HLS_TAGS = {
123
120
 
124
121
  //#endregion
125
122
  //#region lib/hls/hls-content-processor.ts
126
- var DefaultHlsContentProcessor = class DefaultHlsContentProcessor extends ContentProcessor {
123
+ var DefaultHlsContentProcessor = class DefaultHlsContentProcessor {
127
124
  static YkDVRegex = /#EXT-X-DISCONTINUITY\s+#EXT-X-MAP:URI="(.*?)",BYTERANGE="(.*?)"/g;
128
125
  static DNSPRegex = /#EXT-X-MAP:URI=".*?BUMPER\/[\s\S]+?#EXT-X-DISCONTINUITY/;
129
126
  static DNSPSubRegex = /#EXTINF:.*?,\s+.*BUMPER.*\s+#EXT-X-DISCONTINUITY/;
@@ -162,8 +159,10 @@ var EncryptInfo = class {
162
159
  method = ENCRYPT_METHODS.NONE;
163
160
  key;
164
161
  iv;
162
+ drm;
165
163
  constructor(method) {
166
164
  this.method = this.parseMethod(method);
165
+ this.drm = {};
167
166
  }
168
167
  parseMethod(method) {
169
168
  if (method) return ENCRYPT_METHODS[method.replace("-", "_")];
@@ -173,7 +172,7 @@ var EncryptInfo = class {
173
172
 
174
173
  //#endregion
175
174
  //#region lib/hls/hls-key-processor.ts
176
- var DefaultHlsKeyProcessor = class extends KeyProcessor {
175
+ var DefaultHlsKeyProcessor = class {
177
176
  canProcess(extractorType) {
178
177
  return extractorType === EXTRACTOR_TYPES.HLS;
179
178
  }
@@ -181,17 +180,16 @@ var DefaultHlsKeyProcessor = class extends KeyProcessor {
181
180
  const iv = this.getAttribute(keyLine, "IV");
182
181
  const method = this.getAttribute(keyLine, "METHOD");
183
182
  const uri = this.getAttribute(keyLine, "URI");
184
- console.debug(`METHOD:${method}, URI:${uri}, IV:${iv}`);
185
183
  const encryptInfo = new EncryptInfo(method);
186
- if (iv) encryptInfo.iv = Buffer.from(iv, "hex");
184
+ if (iv) encryptInfo.iv = b.hex().encode(iv);
187
185
  if (parserConfig.customIv && parserConfig.customIv.length > 0) encryptInfo.iv = parserConfig.customIv;
188
186
  try {
189
187
  if (parserConfig.customKey && parserConfig.customKey.length > 0) encryptInfo.key = parserConfig.customKey;
190
188
  else if (uri) {
191
189
  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");
190
+ if (lowerUri.startsWith("base64:")) encryptInfo.key = b.base64().encode(uri.slice(7));
191
+ else if (lowerUri.startsWith("data:;base64,")) encryptInfo.key = b.base64().encode(uri.slice(13));
192
+ else if (lowerUri.startsWith("data:text/plain;base64,")) encryptInfo.key = b.base64().encode(uri.slice(23));
195
193
  else if (existsSync(uri)) encryptInfo.key = readFileSync(uri);
196
194
  else {
197
195
  const processedUrl = this.preProcessUrl(new URL(uri, m3u8Url).toString(), parserConfig);
@@ -209,15 +207,15 @@ var DefaultHlsKeyProcessor = class extends KeyProcessor {
209
207
  return encryptInfo;
210
208
  }
211
209
  getAttribute(line, attrName) {
212
- const regex = new RegExp(`${attrName}="([^"]+)"`, "i");
210
+ const regex = new RegExp(`${attrName}=(?:"([^"]+)"|([^,]+))`, "i");
213
211
  const match = line.match(regex);
214
- return match?.[1] ?? null;
212
+ return match?.[1] ?? match?.[2] ?? null;
215
213
  }
216
214
  async fetchKeyWithRetry(url, parserConfig) {
217
215
  let retryCount = parserConfig.keyRetryCount ?? 3;
218
216
  while (retryCount >= 0) try {
219
217
  const response = await fetch(url, { headers: parserConfig.headers });
220
- return Buffer.from(await response.arrayBuffer());
218
+ return new Uint8Array(await response.arrayBuffer());
221
219
  } catch (error) {
222
220
  if (error.message.includes("scheme is not supported")) throw error;
223
221
  console.warn(`Error fetching key: ${error.message}. Retries left: ${retryCount}`);
@@ -338,8 +336,7 @@ const DASH_TAGS = {
338
336
  const combineUrl = (baseUrl, relativeUrl) => {
339
337
  if (!baseUrl.trim()) return relativeUrl;
340
338
  const url1 = new URL(baseUrl);
341
- const url2 = new URL(relativeUrl, url1);
342
- return url2.toString();
339
+ return new URL(relativeUrl, url1).toString();
343
340
  };
344
341
  const replaceVars = (text, dict) => {
345
342
  let result = text;
@@ -378,7 +375,7 @@ const getAttribute = (line, key = "") => {
378
375
  return result;
379
376
  };
380
377
  const distinctBy = (array, callbackfn) => {
381
- const seen = new Set();
378
+ const seen = /* @__PURE__ */ new Set();
382
379
  return array.filter((item) => {
383
380
  const value = callbackfn(item);
384
381
  if (seen.has(value)) return false;
@@ -457,8 +454,7 @@ var MediaSegment = class MediaSegment {
457
454
  */
458
455
  const parseRange = (range) => {
459
456
  const [startRange, end] = range.split("-").map(Number);
460
- const expectLength = end - startRange + 1;
461
- return [startRange, expectLength];
457
+ return [startRange, end - startRange + 1];
462
458
  };
463
459
 
464
460
  //#endregion
@@ -494,11 +490,9 @@ var DashExtractor = class DashExtractor {
494
490
  async extractStreams(rawText) {
495
491
  const streamList = [];
496
492
  this.#mpdContent = rawText;
497
- const document = new DOMParser().parseFromString(this.#mpdContent, "text/xml");
498
- const mpdElement = document.getElementsByTagName("MPD")[0];
499
- const type = mpdElement.getAttribute("type");
500
- const isLive = type === "dynamic";
501
- const maxSegmentDuration = mpdElement.getAttribute("maxSegmentDuration");
493
+ const mpdElement = new DOMParser().parseFromString(this.#mpdContent, "text/xml").getElementsByTagName("MPD")[0];
494
+ const isLive = mpdElement.getAttribute("type") === "dynamic";
495
+ mpdElement.getAttribute("maxSegmentDuration");
502
496
  const availabilityStartTime = mpdElement.getAttribute("availabilityStartTime");
503
497
  const timeShiftBufferDepth = mpdElement.getAttribute("timeShiftBufferDepth") || "PT1M";
504
498
  const publishTime = mpdElement.getAttribute("publishTime");
@@ -557,8 +551,7 @@ var DashExtractor = class DashExtractor {
557
551
  if (role) {
558
552
  const roleValue = role.getAttribute("value");
559
553
  const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
560
- const roleTypeKey = roleValue.split("-").map(capitalize).join("");
561
- const roleType = ROLE_TYPE[roleTypeKey];
554
+ const roleType = ROLE_TYPE[roleValue.split("-").map(capitalize).join("")];
562
555
  streamSpec.role = roleType;
563
556
  if (roleType === ROLE_TYPE.Subtitle) {
564
557
  streamSpec.mediaType = MEDIA_TYPES.SUBTITLES;
@@ -689,8 +682,7 @@ var DashExtractor = class DashExtractor {
689
682
  varDic[DASH_TAGS.TemplateNumber] = segNumber++;
690
683
  const _hashTime = mediaTemplate?.includes(DASH_TAGS.TemplateTime);
691
684
  const _media = replaceVars(mediaTemplate, varDic);
692
- const _mediaUrl = combineUrl(segBaseUrl, _media);
693
- _mediaSegment.url = _mediaUrl;
685
+ _mediaSegment.url = combineUrl(segBaseUrl, _media);
694
686
  _mediaSegment.index = segIndex++;
695
687
  _mediaSegment.duration = _duration / timescale;
696
688
  if (_hashTime) _mediaSegment.nameFromVar = currentTime.toString();
@@ -734,27 +726,37 @@ var DashExtractor = class DashExtractor {
734
726
  mediaSegment.duration = periodDurationSeconds;
735
727
  streamSpec.playlist.mediaParts[0].mediaSegments.push(mediaSegment);
736
728
  }
737
- const contentProtection = adaptationSet.getElementsByTagName("ContentProtection")[0] || representation.getElementsByTagName("ContentProtection")[0];
738
- if (contentProtection) {
729
+ const adaptationSetProtections = adaptationSet.getElementsByTagName("ContentProtection");
730
+ const representationProtections = representation.getElementsByTagName("ContentProtection");
731
+ const contentProtections = representationProtections[0] ? representationProtections : adaptationSetProtections;
732
+ if (contentProtections.length) {
739
733
  const encryptInfo = new EncryptInfo();
740
734
  encryptInfo.method = DashExtractor.#DEFAULT_METHOD;
735
+ const widevineSystemId = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
736
+ const playreadySystemId = "9a04f079-9840-4286-ab92-e65be0885f95";
737
+ for (const contentProtection of contentProtections) {
738
+ const schemeIdUri = contentProtection.getAttribute("schemeIdUri");
739
+ const drmData = {
740
+ keyId: contentProtection.getAttribute("cenc:default_KID") || void 0,
741
+ pssh: contentProtection.getElementsByTagName("cenc:pssh")[0]?.textContent || void 0
742
+ };
743
+ if (schemeIdUri?.includes(widevineSystemId)) encryptInfo.drm.widevine = drmData;
744
+ else if (schemeIdUri?.includes(playreadySystemId)) encryptInfo.drm.playready = drmData;
745
+ else continue;
746
+ }
741
747
  if (streamSpec.playlist.mediaInit) streamSpec.playlist.mediaInit.encryptInfo = encryptInfo;
742
748
  const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
743
749
  for (const segment of segments) if (!segment.encryptInfo) segment.encryptInfo = encryptInfo;
744
750
  }
745
751
  const _index = streamList.findIndex((item) => item.periodId !== streamSpec.periodId && item.groupId === streamSpec.groupId && item.resolution === streamSpec.resolution && item.mediaType === streamSpec.mediaType);
746
- if (_index > -1) if (isLive) {} else {
747
- const url1 = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).url;
748
- const url2 = streamSpec.playlist.mediaParts[0].mediaSegments.at(-1)?.url;
749
- if (url1 !== url2) {
750
- const startIndex = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).index + 1;
751
- const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
752
- for (const segment of segments) segment.index += startIndex;
753
- const mediaPart = new MediaPart();
754
- mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
755
- streamList[_index].playlist.mediaParts.push(mediaPart);
756
- } else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
757
- }
752
+ 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) {
753
+ const startIndex = streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).index + 1;
754
+ const segments = streamSpec.playlist.mediaParts[0].mediaSegments;
755
+ for (const segment of segments) segment.index += startIndex;
756
+ const mediaPart = new MediaPart();
757
+ mediaPart.mediaSegments = streamList[_index].playlist.mediaParts[0].mediaSegments;
758
+ streamList[_index].playlist.mediaParts.push(mediaPart);
759
+ } else streamList[_index].playlist.mediaParts.at(-1).mediaSegments.at(-1).duration += streamSpec.playlist.mediaParts[0].mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
758
760
  else {
759
761
  if (streamSpec.mediaType === MEDIA_TYPES.SUBTITLES && streamSpec.extension === "mp4") streamSpec.extension = "m4s";
760
762
  if (streamSpec.mediaType !== MEDIA_TYPES.SUBTITLES && (streamSpec.extension == null || streamSpec.playlist.mediaParts.reduce((sum, part) => sum + part.mediaSegments.length, 0) > 1)) streamSpec.extension = "m4s";
@@ -769,8 +771,8 @@ var DashExtractor = class DashExtractor {
769
771
  const subtitleList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.SUBTITLES);
770
772
  const videoList = streamList.filter((stream) => stream.mediaType === MEDIA_TYPES.VIDEO);
771
773
  for (const video of videoList) {
772
- const audioGroupId = audioList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
773
- const subtitleGroupId = subtitleList.toSorted((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
774
+ const audioGroupId = audioList.toSorted((a, b$1) => (b$1.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
775
+ const subtitleGroupId = subtitleList.toSorted((a, b$1) => (b$1.bandwidth || 0) - (a.bandwidth || 0)).at(0)?.groupId;
774
776
  if (audioGroupId) video.audioId = audioGroupId;
775
777
  if (subtitleGroupId) video.subtitleId = subtitleGroupId;
776
778
  }
@@ -780,17 +782,38 @@ var DashExtractor = class DashExtractor {
780
782
  if (!v) return;
781
783
  return v;
782
784
  }
783
- fetchPlayList(streamSpecs) {
784
- throw new Error("Method not implemented.");
785
+ async refreshPlayList(streamSpecs) {
786
+ if (!streamSpecs.length) return;
787
+ const response = await fetch(this.#parserConfig.url, this.#parserConfig.headers).catch(() => fetch(this.#parserConfig.originalUrl, this.#parserConfig.headers));
788
+ const rawText = await response.text();
789
+ const url = response.url;
790
+ this.#parserConfig.url = url;
791
+ this.#setInitUrl();
792
+ const newStreams = await this.extractStreams(rawText);
793
+ for (const streamSpec of streamSpecs) {
794
+ let results = newStreams.filter((n) => n.toShortString() === streamSpec.toShortString());
795
+ if (!results.length) results = newStreams.filter((n) => n.playlist?.mediaInit?.url === streamSpec.playlist?.mediaInit?.url);
796
+ if (results.length) streamSpec.playlist.mediaParts = results.at(0).playlist.mediaParts;
797
+ }
798
+ await this.#processUrl(streamSpecs);
799
+ }
800
+ async #processUrl(streamSpecs) {
801
+ for (const spec of streamSpecs) {
802
+ const playlist = spec.playlist;
803
+ if (!playlist) continue;
804
+ if (playlist.mediaInit) playlist.mediaInit.url = this.preProcessUrl(playlist.mediaInit.url);
805
+ for (const part of playlist.mediaParts) for (const segment of part.mediaSegments) segment.url = this.preProcessUrl(segment.url);
806
+ }
785
807
  }
786
- refreshPlayList(streamSpecs) {
787
- throw new Error("Method not implemented.");
808
+ async fetchPlayList(streamSpecs) {
809
+ this.#processUrl(streamSpecs);
788
810
  }
789
811
  preProcessUrl(url) {
790
- throw new Error("Method not implemented.");
812
+ for (const processor of this.#parserConfig.urlProcessors) if (processor.canProcess(this.extractorType, url, this.#parserConfig)) url = processor.process(url, this.#parserConfig);
813
+ return url;
791
814
  }
792
815
  preProcessContent() {
793
- throw new Error("Method not implemented.");
816
+ for (const processor of this.#parserConfig.contentProcessors) if (processor.canProcess(this.extractorType, this.#mpdContent, this.#parserConfig)) this.#mpdContent = processor.process(this.#mpdContent, this.#parserConfig);
794
817
  }
795
818
  };
796
819
 
@@ -869,8 +892,7 @@ var HlsExtractor = class {
869
892
  expectPlaylist = true;
870
893
  } else if (line.startsWith(HLS_TAGS.extXMedia)) {
871
894
  streamSpec = new StreamSpec();
872
- const type = getAttribute(line, "TYPE").replace("-", "_");
873
- const mediaType = MEDIA_TYPES[type];
895
+ const mediaType = MEDIA_TYPES[getAttribute(line, "TYPE").replace("-", "_")];
874
896
  if (mediaType) streamSpec.mediaType = mediaType;
875
897
  if (mediaType === MEDIA_TYPES.CLOSED_CAPTIONS) continue;
876
898
  let url = getAttribute(line, "URI");
@@ -921,8 +943,7 @@ var HlsExtractor = class {
921
943
  for (const line of lines) {
922
944
  if (!line.trim()) continue;
923
945
  if (line.startsWith(HLS_TAGS.extXByterange)) {
924
- const p = getAttribute(line);
925
- const [n, o] = getRange(p);
946
+ const [n, o] = getRange(getAttribute(line));
926
947
  segment.expectLength = n;
927
948
  segment.startRange = o || (segments.at(-1)?.startRange || 0) + (segments.at(-1)?.expectLength || 0);
928
949
  expectSegment = true;
@@ -944,9 +965,7 @@ var HlsExtractor = class {
944
965
  mediaParts.push(new MediaPart(segments));
945
966
  segments = [];
946
967
  } else if (line.startsWith(HLS_TAGS.extXKey)) {
947
- const uri = getAttribute(line, "URI");
948
- const uriLast = getAttribute(lastKeyLine, "URI");
949
- if (uri !== uriLast) {
968
+ if (getAttribute(line, "URI") !== getAttribute(lastKeyLine, "URI")) {
950
969
  const parsedInfo = await this.#parseKey(line);
951
970
  currentEncryptInfo.method = parsedInfo.method;
952
971
  currentEncryptInfo.key = parsedInfo.key;
@@ -974,8 +993,7 @@ var HlsExtractor = class {
974
993
  mediaSegment.index = -1;
975
994
  playlist.mediaInit = mediaSegment;
976
995
  if (line.includes("BYTERANGE")) {
977
- const p = getAttribute(line, "BYTERANGE");
978
- const [n, o] = getRange(p);
996
+ const [n, o] = getRange(getAttribute(line, "BYTERANGE"));
979
997
  mediaSegment.expectLength = n;
980
998
  mediaSegment.startRange = o || 0;
981
999
  }
@@ -1024,10 +1042,7 @@ var HlsExtractor = class {
1024
1042
  async extractStreams(rawText) {
1025
1043
  this.#m3u8Content = rawText;
1026
1044
  this.preProcessContent();
1027
- if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) {
1028
- console.log("Master m3u8 found");
1029
- return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
1030
- }
1045
+ if (this.#m3u8Content.includes(HLS_TAGS.extXStreamInf)) return this.#parseMasterList().then((lists) => distinctBy(lists, (list) => list.url));
1031
1046
  const playlist = await this.#parseList();
1032
1047
  const streamSpec = new StreamSpec();
1033
1048
  streamSpec.url = this.parserConfig.url;
@@ -1037,8 +1052,7 @@ var HlsExtractor = class {
1037
1052
  }
1038
1053
  async #loadM3u8FromUrl(url) {
1039
1054
  if (url.startsWith("file:")) {
1040
- const uri = new URL(url);
1041
- const filePath = uri.pathname;
1055
+ const filePath = new URL(url).pathname;
1042
1056
  this.#m3u8Content = await readFile(filePath, "utf8");
1043
1057
  } else if (url.startsWith("http")) try {
1044
1058
  const response = await fetch(url, { headers: this.parserConfig.headers });
@@ -1078,9 +1092,9 @@ var HlsExtractor = class {
1078
1092
  else list.playlist = newPlaylist;
1079
1093
  if (list.mediaType === MEDIA_TYPES.SUBTITLES) {
1080
1094
  const a = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".ttml")));
1081
- const b = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".vtt") || segment.url.includes(".webvtt")));
1095
+ const b$1 = list.playlist.mediaParts.some((part) => part.mediaSegments.some((segment) => segment.url.includes(".vtt") || segment.url.includes(".webvtt")));
1082
1096
  if (a) list.extension = "ttml";
1083
- if (b) list.extension = "vtt";
1097
+ if (b$1) list.extension = "vtt";
1084
1098
  } else list.extension = list.playlist.mediaInit ? "m4s" : "ts";
1085
1099
  }
1086
1100
  }
@@ -1108,8 +1122,7 @@ var StreamExtractor = class {
1108
1122
  }
1109
1123
  async loadSourceFromUrl(url) {
1110
1124
  if (url.startsWith("file:")) {
1111
- const uri = new URL(url);
1112
- const filePath = uri.pathname;
1125
+ const filePath = new URL(url).pathname;
1113
1126
  this.#rawText = await readFile(filePath, "utf8");
1114
1127
  this.#setUrl(url);
1115
1128
  } else if (url.startsWith("http")) {
@@ -1136,8 +1149,11 @@ var StreamExtractor = class {
1136
1149
  } else if (this.#rawText.includes("</MPD>") && this.#rawText.includes("<MPD")) {
1137
1150
  this.#extractor = new DashExtractor(this.#parserConfig);
1138
1151
  rawType = "mpd";
1139
- } else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia")) rawType = "ism";
1140
- else if (rawText === "<RE_LIVE_TS>") {} else throw new Error("Unsupported stream type");
1152
+ } else if (this.#rawText.includes("</SmoothStreamingMedia>") && this.#rawText.includes("<SmoothStreamingMedia")) {
1153
+ rawType = "ism";
1154
+ throw new Error("Smooth Streaming is not supported yet");
1155
+ } else if (rawText === "<RE_LIVE_TS>") throw new Error("Live TS is not supported yet");
1156
+ else throw new Error("Unsupported stream type");
1141
1157
  this.#rawFiles[`raw.${rawType}`] = rawText;
1142
1158
  }
1143
1159
  async extractStreams() {
@@ -1152,4 +1168,4 @@ var StreamExtractor = class {
1152
1168
  };
1153
1169
 
1154
1170
  //#endregion
1155
- export { ContentProcessor, DefaultUrlProcessor, ENCRYPT_METHODS, EXTRACTOR_TYPES, KeyProcessor, MEDIA_TYPES, ParserConfig, ROLE_TYPE, StreamExtractor, UrlProcessor };
1171
+ export { DefaultUrlProcessor, ENCRYPT_METHODS, EXTRACTOR_TYPES, MEDIA_TYPES, ParserConfig, ROLE_TYPE, StreamExtractor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dasha",
3
- "version": "4.0.0-alpha.2",
3
+ "version": "4.0.0-alpha.3",
4
4
  "description": "Streaming manifest parser",
5
5
  "files": [
6
6
  "dist"
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
33
- "url": "https://github.com/streamyx-labs/dasha"
33
+ "url": "https://github.com/azot-labs/dasha"
34
34
  },
35
35
  "keywords": [
36
36
  "mpeg",
@@ -48,37 +48,34 @@
48
48
  "license": "MIT",
49
49
  "readmeFilename": "README.md",
50
50
  "bugs": {
51
- "url": "https://github.com/streamyx-labs/dasha/issues",
51
+ "url": "https://github.com/azot-labs/dasha/issues",
52
52
  "email": "vitalygashkov@vk.com"
53
53
  },
54
54
  "funding": [
55
55
  {
56
56
  "type": "individual",
57
- "url": "https://boosty.to/vitalygashkov"
58
- },
59
- {
60
- "type": "patreon",
61
- "url": "https://www.patreon.com/vitalygashkov"
57
+ "url": "https://t.me/tribute/app?startapp=dqW2"
62
58
  }
63
59
  ],
64
60
  "engines": {
65
- "node": "20 || 21 || 22"
61
+ "node": ">=20"
66
62
  },
67
63
  "dependencies": {
68
64
  "@xmldom/xmldom": "^0.9.8",
65
+ "barsic": "^0.2.0",
69
66
  "temporal-polyfill": "^0.3.0"
70
67
  },
71
68
  "devDependencies": {
72
- "@eslint/js": "^9.25.1",
73
- "@types/node": "^22.15.3",
74
- "eslint": "^9.25.1",
75
- "eslint-config-prettier": "^10.1.2",
76
- "eslint-plugin-prettier": "^5.2.6",
77
- "globals": "^16.0.0",
78
- "prettier": "^3.5.3",
79
- "tsdown": "^0.10.2",
80
- "typescript": "^5.8.3",
81
- "typescript-eslint": "^8.31.1",
82
- "vitest": "^3.1.2"
69
+ "@eslint/js": "^9.39.1",
70
+ "@types/node": "^24.10.0",
71
+ "eslint": "^9.39.1",
72
+ "eslint-config-prettier": "^10.1.8",
73
+ "eslint-plugin-prettier": "^5.5.4",
74
+ "globals": "^16.5.0",
75
+ "prettier": "^3.6.2",
76
+ "tsdown": "^0.15.12",
77
+ "typescript": "^5.9.3",
78
+ "typescript-eslint": "^8.46.3",
79
+ "vitest": "^4.0.6"
83
80
  }
84
81
  }