dasha 4.0.3 → 4.2.0
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 +38 -0
- package/dist/index.d.mts +189 -48
- package/dist/index.mjs +1062 -34
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ADTS, CustomPathedSource, FilePathSource, HLS_FORMATS, Input as Input$1, InputFormat, InputTrack, MATROSKA, MP3, MP4, QTFF, UrlSource, WEBM, asc, desc, prefer } from "mediabunny";
|
|
1
|
+
import { ADTS, ALL_FORMATS as ALL_FORMATS$1, CustomPathedSource, FilePathSource, HLS, HLS as HLS$1, HLS_FORMATS, Input as Input$1, InputFormat, InputTrack, InputVideoTrack, MATROSKA, MP3, MP3 as MP3$1, MP4, MP4 as MP4$1, QTFF, SourceRef, UrlSource, WEBM, asc, desc, prefer } from "mediabunny";
|
|
2
2
|
import { DOMParser } from "@xmldom/xmldom";
|
|
3
3
|
import { Temporal } from "temporal-polyfill";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
//#region src/util.ts
|
|
7
8
|
const combineUrl = (baseUrl, relativeUrl) => {
|
|
8
9
|
if (!baseUrl.trim()) return relativeUrl;
|
|
@@ -90,15 +91,16 @@ const ROLE_TYPE = {
|
|
|
90
91
|
//#endregion
|
|
91
92
|
//#region src/subtitle.ts
|
|
92
93
|
const parseSubtitleCodecFromMime = (mime) => {
|
|
93
|
-
switch (mime.toLowerCase().trim().split(
|
|
94
|
+
switch (mime.toLowerCase().trim().split(/[.+;]/)[0]) {
|
|
94
95
|
case "srt":
|
|
95
96
|
case "x-subrip": return "srt";
|
|
96
97
|
case "ssa": return "ssa";
|
|
97
98
|
case "ass": return "ass";
|
|
98
99
|
case "ttml": return "ttml";
|
|
99
|
-
case "
|
|
100
|
+
case "webvtt":
|
|
101
|
+
case "vtt":
|
|
102
|
+
case "wvtt": return "webvtt";
|
|
100
103
|
case "stpp": return "stpp";
|
|
101
|
-
case "wvtt": return "wvtt";
|
|
102
104
|
default: throw new Error(`The MIME ${mime} is not supported as subtitle codec`);
|
|
103
105
|
}
|
|
104
106
|
};
|
|
@@ -156,6 +158,32 @@ const MATRIX = {
|
|
|
156
158
|
YCbCr_BT_2020_and_2100: 9,
|
|
157
159
|
ICtCp_BT_2100: 14
|
|
158
160
|
};
|
|
161
|
+
const DOLBY_VISION_CODECS = [
|
|
162
|
+
"dva1",
|
|
163
|
+
"dvav",
|
|
164
|
+
"dvhe",
|
|
165
|
+
"dvh1"
|
|
166
|
+
];
|
|
167
|
+
const COLOR_PRIMARIES_MAP = new Map([
|
|
168
|
+
[PRIMARIES.BT_709, "bt709"],
|
|
169
|
+
[PRIMARIES.BT_601_625, "bt470bg"],
|
|
170
|
+
[PRIMARIES.BT_601_525, "smpte170m"],
|
|
171
|
+
[PRIMARIES.BT_2020_and_2100, "bt2020"],
|
|
172
|
+
[PRIMARIES.SMPTE_ST_2113_and_EG_4321, "smpte432"]
|
|
173
|
+
]);
|
|
174
|
+
const TRANSFER_CHARACTERISTICS_MAP = new Map([
|
|
175
|
+
[TRANSFER.BT_709, "bt709"],
|
|
176
|
+
[TRANSFER.BT_601, "smpte170m"],
|
|
177
|
+
[TRANSFER.BT_2100_PQ, "pq"],
|
|
178
|
+
[TRANSFER.BT_2100_HLG, "hlg"]
|
|
179
|
+
]);
|
|
180
|
+
const MATRIX_COEFFICIENTS_MAP = new Map([
|
|
181
|
+
[MATRIX.RGB, "rgb"],
|
|
182
|
+
[MATRIX.YCbCr_BT_709, "bt709"],
|
|
183
|
+
[MATRIX.YCbCr_BT_601_625, "bt470bg"],
|
|
184
|
+
[MATRIX.YCbCr_BT_601_525, "smpte170m"],
|
|
185
|
+
[MATRIX.YCbCr_BT_2020_and_2100, "bt2020-ncl"]
|
|
186
|
+
]);
|
|
159
187
|
const parseVideoCodecFromMime = (mime) => {
|
|
160
188
|
const target = mime.toLowerCase().trim().split(".")[0];
|
|
161
189
|
const avc = [
|
|
@@ -197,6 +225,52 @@ const parseDynamicRangeFromCicp = (primaries, transfer, matrix) => {
|
|
|
197
225
|
else if (TRANSFER.BT_2100_HLG === transfer) return "hlg";
|
|
198
226
|
else return "sdr";
|
|
199
227
|
};
|
|
228
|
+
const parseColorSpaceFromCicp = (primaries, transfer, matrix) => {
|
|
229
|
+
const normalizedTransfer = transfer == 5 ? TRANSFER.BT_601 : transfer;
|
|
230
|
+
return {
|
|
231
|
+
primaries: COLOR_PRIMARIES_MAP.get(primaries),
|
|
232
|
+
transfer: TRANSFER_CHARACTERISTICS_MAP.get(normalizedTransfer),
|
|
233
|
+
matrix: MATRIX_COEFFICIENTS_MAP.get(matrix)
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const parseDynamicRangeFromCodecString = (codecs) => {
|
|
237
|
+
const normalized = codecs?.trim().toLowerCase();
|
|
238
|
+
if (!normalized) return;
|
|
239
|
+
return DOLBY_VISION_CODECS.some((value) => normalized.startsWith(value)) ? "dv" : void 0;
|
|
240
|
+
};
|
|
241
|
+
const getCicpValues = (supplementalProps = [], essentialProps = []) => {
|
|
242
|
+
const primariesScheme = "urn:mpeg:mpegB:cicp:ColourPrimaries";
|
|
243
|
+
const transferScheme = "urn:mpeg:mpegB:cicp:TransferCharacteristics";
|
|
244
|
+
const matrixScheme = "urn:mpeg:mpegB:cicp:MatrixCoefficients";
|
|
245
|
+
const allProps = [...essentialProps, ...supplementalProps];
|
|
246
|
+
const getValues = (schemeIdUri) => allProps.filter((prop) => prop.schemeIdUri === schemeIdUri).flatMap((prop) => prop.value ? [Number.parseInt(prop.value, 10)] : []);
|
|
247
|
+
return {
|
|
248
|
+
primaries: getValues(primariesScheme)[0] ?? 0,
|
|
249
|
+
transfer: getValues(transferScheme)[0] ?? 0,
|
|
250
|
+
matrix: getValues(matrixScheme)[0] ?? 0
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
const parseColorSpace = (supplementalProps = [], essentialProps = []) => {
|
|
254
|
+
const { primaries, transfer, matrix } = getCicpValues(supplementalProps, essentialProps);
|
|
255
|
+
return parseColorSpaceFromCicp(primaries, transfer, matrix);
|
|
256
|
+
};
|
|
257
|
+
const parseDynamicRangeFromHlsVideoRange = (videoRange, codecs) => {
|
|
258
|
+
const normalized = videoRange?.replaceAll("\"", "").trim().toUpperCase();
|
|
259
|
+
if (!normalized) return;
|
|
260
|
+
const codecDynamicRange = parseDynamicRangeFromCodecString(codecs);
|
|
261
|
+
if (normalized === "PQ") return codecDynamicRange ?? "hdr10";
|
|
262
|
+
if (normalized === "HLG") return "hlg";
|
|
263
|
+
if (normalized === "SDR") return "sdr";
|
|
264
|
+
};
|
|
265
|
+
const parseDynamicRangeFromColorSpace = (colorSpace) => {
|
|
266
|
+
if (!colorSpace) return;
|
|
267
|
+
if (colorSpace.transfer === "pq") return "hdr10";
|
|
268
|
+
if (colorSpace.transfer === "hlg") return "hlg";
|
|
269
|
+
if (colorSpace.primaries != null || colorSpace.transfer != null || colorSpace.matrix != null || colorSpace.fullRange != null) return "sdr";
|
|
270
|
+
};
|
|
271
|
+
const inferDynamicRange = (params) => {
|
|
272
|
+
return parseDynamicRangeFromCodecString(params.codecs) ?? parseDynamicRangeFromHlsVideoRange(params.videoRange, params.codecs) ?? parseDynamicRangeFromColorSpace(params.colorSpace) ?? "sdr";
|
|
273
|
+
};
|
|
200
274
|
const parseVideoCodec = (codecs) => {
|
|
201
275
|
for (const codec of codecs.toLowerCase().split(",")) {
|
|
202
276
|
const mime = codec.trim().split(".")[0];
|
|
@@ -216,18 +290,10 @@ const tryParseVideoCodec = (codecs) => {
|
|
|
216
290
|
}
|
|
217
291
|
};
|
|
218
292
|
const parseDynamicRange = (codecs, supplementalProps = [], essentialProps = []) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"dvh1"
|
|
224
|
-
].some((value) => codecs.startsWith(value))) return "dv";
|
|
225
|
-
const primariesScheme = "urn:mpeg:mpegB:cicp:ColourPrimaries";
|
|
226
|
-
const transferScheme = "urn:mpeg:mpegB:cicp:TransferCharacteristics";
|
|
227
|
-
const matrixScheme = "urn:mpeg:mpegB:cicp:MatrixCoefficients";
|
|
228
|
-
const allProps = [...essentialProps, ...supplementalProps];
|
|
229
|
-
const getValues = (schemeIdUri) => allProps.filter((prop) => prop.schemeIdUri === schemeIdUri).flatMap((prop) => prop.value ? [parseInt(prop.value)] : []);
|
|
230
|
-
return parseDynamicRangeFromCicp(getValues(primariesScheme).reduce((acc, current) => acc + current, 0), getValues(transferScheme).reduce((acc, current) => acc + current, 0), getValues(matrixScheme).reduce((acc, current) => acc + current, 0));
|
|
293
|
+
const codecDynamicRange = parseDynamicRangeFromCodecString(codecs);
|
|
294
|
+
if (codecDynamicRange) return codecDynamicRange;
|
|
295
|
+
const { primaries, transfer, matrix } = getCicpValues(supplementalProps, essentialProps);
|
|
296
|
+
return parseDynamicRangeFromCicp(primaries, transfer, matrix);
|
|
231
297
|
};
|
|
232
298
|
//#endregion
|
|
233
299
|
//#region src/dash/dash-misc.ts
|
|
@@ -250,15 +316,15 @@ const getDashTrackMatchKey = (track) => JSON.stringify({
|
|
|
250
316
|
const getSourcePath = (source) => {
|
|
251
317
|
if ("rootPath" in source && typeof source.rootPath === "string") return source.rootPath;
|
|
252
318
|
};
|
|
253
|
-
const normalizeHeaders = (headers) => {
|
|
319
|
+
const normalizeHeaders$1 = (headers) => {
|
|
254
320
|
if (!headers) return {};
|
|
255
321
|
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
256
322
|
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
257
323
|
return { ...headers };
|
|
258
324
|
};
|
|
259
|
-
const getSourceHeaders = (source) => {
|
|
260
|
-
const requestHeaders = "_url" in source && source._url instanceof Request ? normalizeHeaders(source._url.headers) : {};
|
|
261
|
-
const optionHeaders = normalizeHeaders(("_options" in source && source._options && typeof source._options === "object" ? source._options : void 0)?.requestInit?.headers);
|
|
325
|
+
const getSourceHeaders$1 = (source) => {
|
|
326
|
+
const requestHeaders = "_url" in source && source._url instanceof Request ? normalizeHeaders$1(source._url.headers) : {};
|
|
327
|
+
const optionHeaders = normalizeHeaders$1(("_options" in source && source._options && typeof source._options === "object" ? source._options : void 0)?.requestInit?.headers);
|
|
262
328
|
return {
|
|
263
329
|
...requestHeaders,
|
|
264
330
|
...optionHeaders
|
|
@@ -269,7 +335,7 @@ const loadDashManifest = async (source) => {
|
|
|
269
335
|
const manifestPath = getSourcePath(source);
|
|
270
336
|
if (!manifestPath) throw new Error("DASH input currently requires a pathed source such as UrlSource.");
|
|
271
337
|
if (manifestPath.startsWith("http://") || manifestPath.startsWith("https://")) {
|
|
272
|
-
const response = await fetch(manifestPath, { headers: getSourceHeaders(source) });
|
|
338
|
+
const response = await fetch(manifestPath, { headers: getSourceHeaders$1(source) });
|
|
273
339
|
if (!response.ok) throw new Error(`Failed to fetch DASH manifest: ${response.status} ${response.statusText} (${response.url})`);
|
|
274
340
|
return {
|
|
275
341
|
text: await response.text(),
|
|
@@ -414,7 +480,872 @@ const parseDashRange = (range) => {
|
|
|
414
480
|
return [startRange, end - startRange + 1];
|
|
415
481
|
};
|
|
416
482
|
//#endregion
|
|
483
|
+
//#region src/hls/hls-subtitles.ts
|
|
484
|
+
const TAG_STREAM_INF = "#EXT-X-STREAM-INF:";
|
|
485
|
+
const TAG_MEDIA = "#EXT-X-MEDIA:";
|
|
486
|
+
const TAG_EXTINF = "#EXTINF:";
|
|
487
|
+
const TAG_MAP = "#EXT-X-MAP:";
|
|
488
|
+
const TAG_KEY = "#EXT-X-KEY:";
|
|
489
|
+
const TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE:";
|
|
490
|
+
const TAG_BYTERANGE = "#EXT-X-BYTERANGE:";
|
|
491
|
+
const TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME:";
|
|
492
|
+
const TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
|
|
493
|
+
const TAG_TARGETDURATION = "#EXT-X-TARGETDURATION:";
|
|
494
|
+
const TAG_ENDLIST = "#EXT-X-ENDLIST";
|
|
495
|
+
const TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE:";
|
|
496
|
+
const AES_128_BLOCK_SIZE = 16;
|
|
497
|
+
const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
|
|
498
|
+
const DEFAULT_TRACK_DISPOSITION$1 = {
|
|
499
|
+
commentary: false,
|
|
500
|
+
default: true,
|
|
501
|
+
forced: false,
|
|
502
|
+
hearingImpaired: false,
|
|
503
|
+
original: false,
|
|
504
|
+
primary: true,
|
|
505
|
+
visuallyImpaired: false
|
|
506
|
+
};
|
|
507
|
+
const subtitleBackingsCache = /* @__PURE__ */ new WeakMap();
|
|
508
|
+
const SRT_HEADER_REGEX = /(?:^|\n)\d+\s*\n(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}\s+-->\s+(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}/;
|
|
509
|
+
const TTML_MARKER_REGEX = /<tt(?:\s|>)/i;
|
|
510
|
+
const splitPlaylistLines = (text) => text.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0 && (!line.startsWith("#") || line.startsWith("#EXT")));
|
|
511
|
+
const normalizeHeaders = (headers) => {
|
|
512
|
+
if (!headers) return {};
|
|
513
|
+
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
514
|
+
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
515
|
+
return { ...headers };
|
|
516
|
+
};
|
|
517
|
+
const getSourceHeaders = (source) => {
|
|
518
|
+
const requestHeaders = source._url instanceof Request ? normalizeHeaders(source._url.headers) : {};
|
|
519
|
+
const optionHeaders = normalizeHeaders(source._options?.requestInit?.headers);
|
|
520
|
+
return {
|
|
521
|
+
...requestHeaders,
|
|
522
|
+
...optionHeaders
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
const joinHlsPath = (basePath, relativePath) => {
|
|
526
|
+
if (relativePath.includes("://")) return relativePath;
|
|
527
|
+
if (basePath.includes("://")) {
|
|
528
|
+
const queryIndex = basePath.indexOf("?");
|
|
529
|
+
if (queryIndex !== -1) basePath = basePath.slice(0, queryIndex);
|
|
530
|
+
}
|
|
531
|
+
let result;
|
|
532
|
+
if (relativePath.startsWith("/")) {
|
|
533
|
+
const protocolIndex = basePath.indexOf("://");
|
|
534
|
+
if (protocolIndex === -1) result = relativePath;
|
|
535
|
+
else {
|
|
536
|
+
const pathStart = basePath.indexOf("/", protocolIndex + 3);
|
|
537
|
+
result = pathStart === -1 ? basePath + relativePath : basePath.slice(0, pathStart) + relativePath;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
const lastSlash = basePath.lastIndexOf("/");
|
|
541
|
+
result = lastSlash === -1 ? relativePath : basePath.slice(0, lastSlash + 1) + relativePath;
|
|
542
|
+
}
|
|
543
|
+
let prefix = "";
|
|
544
|
+
const protocolIndex = result.indexOf("://");
|
|
545
|
+
if (protocolIndex !== -1) {
|
|
546
|
+
const pathStart = result.indexOf("/", protocolIndex + 3);
|
|
547
|
+
if (pathStart !== -1) {
|
|
548
|
+
prefix = result.slice(0, pathStart);
|
|
549
|
+
result = result.slice(pathStart);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const normalized = [];
|
|
553
|
+
for (const segment of result.split("/")) if (segment === "..") {
|
|
554
|
+
if (normalized.length === 0 || normalized.length === 1 && normalized[0] === "") throw new RangeError(`Invalid HLS path '${relativePath}': parent traversal exceeds root for base '${basePath}'.`);
|
|
555
|
+
normalized.pop();
|
|
556
|
+
} else if (segment !== ".") normalized.push(segment);
|
|
557
|
+
return prefix + normalized.join("/");
|
|
558
|
+
};
|
|
559
|
+
const toSegmentPath = (path) => path.includes("://") ? path : pathToFileURL(path).toString();
|
|
560
|
+
const parseAttributeBoolean = (value) => value?.toUpperCase() === "YES";
|
|
561
|
+
const stripQueryAndHash = (value) => value.split("#", 1)[0]?.split("?", 1)[0] ?? "";
|
|
562
|
+
const inferSubtitleCodecStringFromPath = (path) => {
|
|
563
|
+
const normalized = stripQueryAndHash(path).toLowerCase();
|
|
564
|
+
if (normalized.endsWith(".webvtt")) return "webvtt";
|
|
565
|
+
if (normalized.endsWith(".vtt")) return "vtt";
|
|
566
|
+
if (normalized.endsWith(".srt")) return "srt";
|
|
567
|
+
if (normalized.endsWith(".ttml")) return "ttml";
|
|
568
|
+
if (normalized.endsWith(".dfxp")) return "dfxp";
|
|
569
|
+
return null;
|
|
570
|
+
};
|
|
571
|
+
const parseDetectedSubtitleCodec = (codecString) => {
|
|
572
|
+
if (!codecString) return null;
|
|
573
|
+
const codec = tryParseSubtitleCodec(codecString);
|
|
574
|
+
if (!codec) return null;
|
|
575
|
+
return {
|
|
576
|
+
codec,
|
|
577
|
+
codecString
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
const sniffSubtitleCodecFromText = (text) => {
|
|
581
|
+
const normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trimStart();
|
|
582
|
+
if (normalized.startsWith("WEBVTT")) return "webvtt";
|
|
583
|
+
if (TTML_MARKER_REGEX.test(normalized)) return "ttml";
|
|
584
|
+
if (SRT_HEADER_REGEX.test(normalized)) return "srt";
|
|
585
|
+
return null;
|
|
586
|
+
};
|
|
587
|
+
var AttributeList = class {
|
|
588
|
+
#attributes = {};
|
|
589
|
+
constructor(text) {
|
|
590
|
+
let key = "";
|
|
591
|
+
let value = "";
|
|
592
|
+
let inValue = false;
|
|
593
|
+
let inQuotes = false;
|
|
594
|
+
for (const char of text) {
|
|
595
|
+
if (char === "\"") {
|
|
596
|
+
inQuotes = !inQuotes;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (char === "=" && !inValue && !inQuotes) {
|
|
600
|
+
inValue = true;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (char === "," && !inQuotes) {
|
|
604
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
605
|
+
key = "";
|
|
606
|
+
value = "";
|
|
607
|
+
inValue = false;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (inValue) value += char;
|
|
611
|
+
else key += char;
|
|
612
|
+
}
|
|
613
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
614
|
+
}
|
|
615
|
+
get(name) {
|
|
616
|
+
return this.#attributes[name.toLowerCase()] ?? null;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const loadPlaylistText = async (source, path) => {
|
|
620
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
621
|
+
const response = await fetch(path, { headers: getSourceHeaders(source) });
|
|
622
|
+
if (!response.ok) throw new Error(`Failed to fetch HLS playlist: ${response.status} ${response.statusText} (${response.url})`);
|
|
623
|
+
return {
|
|
624
|
+
path: response.url,
|
|
625
|
+
text: await response.text()
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (path.startsWith("file:")) return {
|
|
629
|
+
path,
|
|
630
|
+
text: await readFile(new URL(path), "utf8")
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
path,
|
|
634
|
+
text: await readFile(path, "utf8")
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
const detectSubtitleCodecFromUri = async (source, uri) => {
|
|
638
|
+
const fromPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(uri));
|
|
639
|
+
if (fromPath) return fromPath;
|
|
640
|
+
const loaded = await loadPlaylistText(source, uri);
|
|
641
|
+
const fromText = parseDetectedSubtitleCodec(sniffSubtitleCodecFromText(loaded.text));
|
|
642
|
+
if (fromText) return fromText;
|
|
643
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
644
|
+
if (lines[0] === "#EXTM3U") for (const line of lines.slice(1)) {
|
|
645
|
+
if (!line.startsWith("#")) {
|
|
646
|
+
const segmentUri = joinHlsPath(loaded.path, line);
|
|
647
|
+
const fromSegmentPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(segmentUri));
|
|
648
|
+
if (fromSegmentPath) return fromSegmentPath;
|
|
649
|
+
const fromSegmentText = parseDetectedSubtitleCodec(sniffSubtitleCodecFromText((await loadPlaylistText(source, segmentUri)).text));
|
|
650
|
+
if (fromSegmentText) return fromSegmentText;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (!line.startsWith(TAG_MAP)) continue;
|
|
654
|
+
const mapUri = new AttributeList(line.slice(11)).get("uri");
|
|
655
|
+
if (!mapUri) continue;
|
|
656
|
+
const fromMapPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(joinHlsPath(loaded.path, mapUri)));
|
|
657
|
+
if (fromMapPath) return fromMapPath;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
codec: "webvtt",
|
|
661
|
+
codecString: "webvtt"
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
const parseMediaRange = (value) => {
|
|
665
|
+
const separatorIndex = value.indexOf("@");
|
|
666
|
+
const length = Number(separatorIndex === -1 ? value : value.slice(0, separatorIndex));
|
|
667
|
+
if (!Number.isInteger(length) || length < 0) throw new Error(`Invalid #EXT-X-BYTERANGE length '${value}'.`);
|
|
668
|
+
const offsetValue = separatorIndex === -1 ? null : Number(value.slice(separatorIndex + 1));
|
|
669
|
+
if (offsetValue !== null && (!Number.isInteger(offsetValue) || offsetValue < 0)) throw new Error(`Invalid #EXT-X-BYTERANGE offset '${value}'.`);
|
|
670
|
+
return {
|
|
671
|
+
length,
|
|
672
|
+
offset: offsetValue
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
const parseHexIv = (value) => {
|
|
676
|
+
if (!IV_STRING_REGEX.test(value)) throw new Error(`Unsupported IV format '${value}'.`);
|
|
677
|
+
let hex = value.slice(2);
|
|
678
|
+
hex = hex.padStart(AES_128_BLOCK_SIZE * 2, "0");
|
|
679
|
+
const iv = new Uint8Array(AES_128_BLOCK_SIZE);
|
|
680
|
+
for (let index = 0; index < AES_128_BLOCK_SIZE; index++) {
|
|
681
|
+
const startIndex = index * 2;
|
|
682
|
+
iv[index] = Number.parseInt(hex.slice(startIndex, startIndex + 2), 16);
|
|
683
|
+
}
|
|
684
|
+
return iv;
|
|
685
|
+
};
|
|
686
|
+
const createSequenceIv = (sequenceNumber) => {
|
|
687
|
+
const iv = new Uint8Array(AES_128_BLOCK_SIZE);
|
|
688
|
+
const view = new DataView(iv.buffer, iv.byteOffset, iv.byteLength);
|
|
689
|
+
view.setUint32(8, Math.floor(sequenceNumber / 2 ** 32));
|
|
690
|
+
view.setUint32(12, sequenceNumber);
|
|
691
|
+
return iv;
|
|
692
|
+
};
|
|
693
|
+
var HlsSubtitlePlaylist = class {
|
|
694
|
+
segments = [];
|
|
695
|
+
#source;
|
|
696
|
+
#playlistPath;
|
|
697
|
+
#refreshIntervalSeconds = 5;
|
|
698
|
+
#streamHasEnded = false;
|
|
699
|
+
#currentUpdatePromise = null;
|
|
700
|
+
#lastUpdateTime = -Infinity;
|
|
701
|
+
constructor(source, playlistPath) {
|
|
702
|
+
this.#source = source;
|
|
703
|
+
this.#playlistPath = playlistPath;
|
|
704
|
+
}
|
|
705
|
+
runUpdateSegments() {
|
|
706
|
+
return this.#currentUpdatePromise ??= (async () => {
|
|
707
|
+
try {
|
|
708
|
+
await this.#maybeWaitForRefresh();
|
|
709
|
+
await this.#reloadSegments();
|
|
710
|
+
} finally {
|
|
711
|
+
this.#currentUpdatePromise = null;
|
|
712
|
+
}
|
|
713
|
+
})();
|
|
714
|
+
}
|
|
715
|
+
async getDurationFromMetadata(_options) {
|
|
716
|
+
await this.runUpdateSegments();
|
|
717
|
+
const lastSegment = this.segments.at(-1);
|
|
718
|
+
return lastSegment ? lastSegment.timestamp + lastSegment.duration : null;
|
|
719
|
+
}
|
|
720
|
+
async getLiveRefreshInterval() {
|
|
721
|
+
await this.runUpdateSegments();
|
|
722
|
+
return this.#streamHasEnded ? null : this.#refreshIntervalSeconds;
|
|
723
|
+
}
|
|
724
|
+
async isRelativeToUnixEpoch() {
|
|
725
|
+
await this.runUpdateSegments();
|
|
726
|
+
return this.segments.some((segment) => segment.relativeToUnixEpoch);
|
|
727
|
+
}
|
|
728
|
+
async #maybeWaitForRefresh() {
|
|
729
|
+
if (this.#streamHasEnded || this.#lastUpdateTime === -Infinity) return;
|
|
730
|
+
const elapsed = performance.now() - this.#lastUpdateTime;
|
|
731
|
+
const remaining = Math.max(0, this.#refreshIntervalSeconds * 1e3 - elapsed);
|
|
732
|
+
if (remaining > 50) await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
733
|
+
}
|
|
734
|
+
async #reloadSegments() {
|
|
735
|
+
this.#lastUpdateTime = performance.now();
|
|
736
|
+
const loaded = await loadPlaylistText(this.#source, this.#playlistPath);
|
|
737
|
+
this.#playlistPath = loaded.path;
|
|
738
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
739
|
+
if (lines[0] !== "#EXTM3U") throw new Error("Invalid M3U8 file; expected first line to be #EXTM3U.");
|
|
740
|
+
let accumulatedTime = 0;
|
|
741
|
+
let nextDuration = null;
|
|
742
|
+
let currentKey = null;
|
|
743
|
+
let nextSequenceNumber = 0;
|
|
744
|
+
let currentFirstSegment = null;
|
|
745
|
+
let currentInitSegment = null;
|
|
746
|
+
let nextByteRange = null;
|
|
747
|
+
let lastByteRangeEnd = null;
|
|
748
|
+
let lastProgramDateTimeSeconds = null;
|
|
749
|
+
let targetDuration = null;
|
|
750
|
+
let segmentSeen = false;
|
|
751
|
+
const segments = [];
|
|
752
|
+
let streamHasEnded = false;
|
|
753
|
+
for (const line of lines.slice(1)) {
|
|
754
|
+
if (!line.startsWith("#")) {
|
|
755
|
+
if (nextDuration === null) throw new Error("Invalid M3U8 file; a segment must be preceded by an #EXTINF tag.");
|
|
756
|
+
const location = {
|
|
757
|
+
path: toSegmentPath(joinHlsPath(this.#playlistPath, line)),
|
|
758
|
+
offset: nextByteRange?.offset ?? 0,
|
|
759
|
+
length: nextByteRange?.length ?? null
|
|
760
|
+
};
|
|
761
|
+
const encryption = currentKey?.method === "AES-128" && !currentKey.iv ? {
|
|
762
|
+
...currentKey,
|
|
763
|
+
iv: createSequenceIv(nextSequenceNumber)
|
|
764
|
+
} : currentKey;
|
|
765
|
+
const segment = {
|
|
766
|
+
timestamp: accumulatedTime,
|
|
767
|
+
duration: nextDuration,
|
|
768
|
+
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
|
769
|
+
firstSegment: currentFirstSegment,
|
|
770
|
+
sequenceNumber: nextSequenceNumber,
|
|
771
|
+
location,
|
|
772
|
+
encryption,
|
|
773
|
+
initSegment: currentInitSegment,
|
|
774
|
+
lastProgramDateTimeSeconds
|
|
775
|
+
};
|
|
776
|
+
currentFirstSegment ??= segment;
|
|
777
|
+
accumulatedTime += nextDuration;
|
|
778
|
+
segments.push(segment);
|
|
779
|
+
nextDuration = null;
|
|
780
|
+
nextSequenceNumber += 1;
|
|
781
|
+
if (nextByteRange === null) lastByteRangeEnd = null;
|
|
782
|
+
else nextByteRange = null;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (line.startsWith(TAG_EXTINF)) {
|
|
786
|
+
if (!segmentSeen) {
|
|
787
|
+
if (lastProgramDateTimeSeconds === null && nextSequenceNumber > 0 && targetDuration !== null) accumulatedTime = nextSequenceNumber * targetDuration;
|
|
788
|
+
segmentSeen = true;
|
|
789
|
+
}
|
|
790
|
+
const content = line.slice(8);
|
|
791
|
+
const commaIndex = content.indexOf(",");
|
|
792
|
+
const durationString = commaIndex === -1 ? content : content.slice(0, commaIndex);
|
|
793
|
+
const duration = Number(durationString);
|
|
794
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid #EXTINF tag duration '${durationString}'.`);
|
|
795
|
+
nextDuration = duration;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
|
|
799
|
+
const value = Number(line.slice(22));
|
|
800
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`Invalid EXT-X-MEDIA-SEQUENCE value '${line.slice(22)}'.`);
|
|
801
|
+
nextSequenceNumber = value;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (line.startsWith(TAG_KEY)) {
|
|
805
|
+
const attributes = new AttributeList(line.slice(11));
|
|
806
|
+
const method = attributes.get("method");
|
|
807
|
+
if (method === "NONE") {
|
|
808
|
+
currentKey = null;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (method === "AES-128") {
|
|
812
|
+
const uri = attributes.get("uri");
|
|
813
|
+
if (!uri) throw new Error("Invalid #EXT-X-KEY: AES-128 requires a URI attribute.");
|
|
814
|
+
const iv = attributes.get("iv");
|
|
815
|
+
const keyFormat = attributes.get("keyformat") ?? "identity";
|
|
816
|
+
if (keyFormat !== "identity") throw new Error("For AES-128 encryption, only the 'identity' KEYFORMAT is currently supported.");
|
|
817
|
+
currentKey = {
|
|
818
|
+
method,
|
|
819
|
+
keyUri: joinHlsPath(this.#playlistPath, uri),
|
|
820
|
+
iv: iv ? parseHexIv(iv) : null,
|
|
821
|
+
keyFormat
|
|
822
|
+
};
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (method === "SAMPLE-AES" || method === "SAMPLE-AES-CTR") {
|
|
826
|
+
currentKey = { method };
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
throw new Error(`Unsupported encryption method '${method}'.`);
|
|
830
|
+
}
|
|
831
|
+
if (line.startsWith(TAG_BYTERANGE)) {
|
|
832
|
+
const range = parseMediaRange(line.slice(17));
|
|
833
|
+
const nextOffset = range.offset ?? lastByteRangeEnd ?? 0;
|
|
834
|
+
nextByteRange = {
|
|
835
|
+
offset: nextOffset,
|
|
836
|
+
length: range.length
|
|
837
|
+
};
|
|
838
|
+
lastByteRangeEnd = nextOffset + range.length;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (line.startsWith(TAG_MAP)) {
|
|
842
|
+
const attributes = new AttributeList(line.slice(11));
|
|
843
|
+
const uri = attributes.get("uri");
|
|
844
|
+
if (uri === null) throw new Error("Invalid M3U8 file; #EXT-X-MAP tag requires a URI attribute.");
|
|
845
|
+
const byterange = attributes.get("byterange");
|
|
846
|
+
const range = byterange ? parseMediaRange(byterange) : null;
|
|
847
|
+
currentInitSegment = {
|
|
848
|
+
timestamp: 0,
|
|
849
|
+
duration: 0,
|
|
850
|
+
relativeToUnixEpoch: false,
|
|
851
|
+
firstSegment: null,
|
|
852
|
+
sequenceNumber: -1,
|
|
853
|
+
location: {
|
|
854
|
+
path: toSegmentPath(joinHlsPath(this.#playlistPath, uri)),
|
|
855
|
+
offset: range?.offset ?? 0,
|
|
856
|
+
length: range?.length ?? null
|
|
857
|
+
},
|
|
858
|
+
encryption: null,
|
|
859
|
+
initSegment: null,
|
|
860
|
+
lastProgramDateTimeSeconds: null
|
|
861
|
+
};
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
|
|
865
|
+
const dateTimeString = line.slice(25);
|
|
866
|
+
const dateTimeSeconds = new Date(dateTimeString).getTime() / 1e3;
|
|
867
|
+
if (!Number.isFinite(dateTimeSeconds)) throw new Error(`Invalid EXT-X-PROGRAM-DATE-TIME value '${dateTimeString}'.`);
|
|
868
|
+
if (segments.length > 0 && lastProgramDateTimeSeconds === null) {
|
|
869
|
+
const lastSegment = segments.at(-1);
|
|
870
|
+
if (!lastSegment) throw new Error("Expected at least one prior HLS segment.");
|
|
871
|
+
const offset = dateTimeSeconds - (lastSegment.timestamp + lastSegment.duration);
|
|
872
|
+
for (const segment of segments) {
|
|
873
|
+
segment.timestamp += offset;
|
|
874
|
+
segment.relativeToUnixEpoch = true;
|
|
875
|
+
}
|
|
876
|
+
accumulatedTime += offset;
|
|
877
|
+
}
|
|
878
|
+
lastProgramDateTimeSeconds = dateTimeSeconds;
|
|
879
|
+
accumulatedTime = dateTimeSeconds;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (line === TAG_DISCONTINUITY) {
|
|
883
|
+
currentFirstSegment = null;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (line.startsWith(TAG_TARGETDURATION)) {
|
|
887
|
+
const duration = Number(line.slice(22));
|
|
888
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid EXT-X-TARGETDURATION value '${line.slice(22)}'.`);
|
|
889
|
+
this.#refreshIntervalSeconds = duration;
|
|
890
|
+
targetDuration = duration;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (line === TAG_ENDLIST) {
|
|
894
|
+
streamHasEnded = true;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
if (line.startsWith(TAG_PLAYLIST_TYPE)) {
|
|
898
|
+
if (line.slice(21).toLowerCase() === "vod") streamHasEnded = true;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
this.segments = segments;
|
|
902
|
+
this.#streamHasEnded = streamHasEnded;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
var ExternalSubtitleSegmentedInput = class {
|
|
906
|
+
segments = [];
|
|
907
|
+
#source;
|
|
908
|
+
#rootPath;
|
|
909
|
+
#delegate = null;
|
|
910
|
+
#loadPromise = null;
|
|
911
|
+
constructor(source, rootPath) {
|
|
912
|
+
this.#source = source;
|
|
913
|
+
this.#rootPath = rootPath;
|
|
914
|
+
}
|
|
915
|
+
runUpdateSegments() {
|
|
916
|
+
return this.#loadPromise ??= (async () => {
|
|
917
|
+
if (this.#delegate) {
|
|
918
|
+
await this.#delegate.runUpdateSegments();
|
|
919
|
+
this.segments = this.#delegate.segments;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const loaded = await loadPlaylistText(this.#source, this.#rootPath);
|
|
923
|
+
if (splitPlaylistLines(loaded.text)[0] === "#EXTM3U") {
|
|
924
|
+
this.#delegate = new HlsSubtitlePlaylist(this.#source, loaded.path);
|
|
925
|
+
await this.#delegate.runUpdateSegments();
|
|
926
|
+
this.segments = this.#delegate.segments;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const segment = {
|
|
930
|
+
timestamp: 0,
|
|
931
|
+
duration: 0,
|
|
932
|
+
relativeToUnixEpoch: false,
|
|
933
|
+
firstSegment: null,
|
|
934
|
+
sequenceNumber: 0,
|
|
935
|
+
location: {
|
|
936
|
+
path: toSegmentPath(loaded.path),
|
|
937
|
+
offset: 0,
|
|
938
|
+
length: null
|
|
939
|
+
},
|
|
940
|
+
encryption: null,
|
|
941
|
+
initSegment: null,
|
|
942
|
+
lastProgramDateTimeSeconds: null
|
|
943
|
+
};
|
|
944
|
+
segment.firstSegment = segment;
|
|
945
|
+
this.segments = [segment];
|
|
946
|
+
this.#delegate = {
|
|
947
|
+
segments: this.segments,
|
|
948
|
+
runUpdateSegments: async () => {},
|
|
949
|
+
getDurationFromMetadata: async (_options) => null,
|
|
950
|
+
getLiveRefreshInterval: async () => null,
|
|
951
|
+
isRelativeToUnixEpoch: async () => false
|
|
952
|
+
};
|
|
953
|
+
})().finally(() => {
|
|
954
|
+
this.#loadPromise = null;
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
async getDurationFromMetadata(options) {
|
|
958
|
+
await this.runUpdateSegments();
|
|
959
|
+
if (this.#delegate) return this.#delegate.getDurationFromMetadata(options);
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
async getLiveRefreshInterval() {
|
|
963
|
+
await this.runUpdateSegments();
|
|
964
|
+
if (this.#delegate) return this.#delegate.getLiveRefreshInterval();
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
async isRelativeToUnixEpoch() {
|
|
968
|
+
await this.runUpdateSegments();
|
|
969
|
+
if (this.#delegate) return this.#delegate.isRelativeToUnixEpoch();
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var HlsSubtitleTrackBacking = class {
|
|
974
|
+
#id;
|
|
975
|
+
#number;
|
|
976
|
+
#pairingMask;
|
|
977
|
+
#track;
|
|
978
|
+
#segmentedInput;
|
|
979
|
+
constructor(params) {
|
|
980
|
+
this.#id = params.id;
|
|
981
|
+
this.#number = params.number;
|
|
982
|
+
this.#pairingMask = params.pairingMask;
|
|
983
|
+
this.#track = params.track;
|
|
984
|
+
this.#segmentedInput = new HlsSubtitlePlaylist(params.source, params.track.uri);
|
|
985
|
+
}
|
|
986
|
+
getType() {
|
|
987
|
+
return "subtitle";
|
|
988
|
+
}
|
|
989
|
+
getId() {
|
|
990
|
+
return this.#id;
|
|
991
|
+
}
|
|
992
|
+
getNumber() {
|
|
993
|
+
return this.#number;
|
|
994
|
+
}
|
|
995
|
+
getCodec() {
|
|
996
|
+
return this.#track.codec;
|
|
997
|
+
}
|
|
998
|
+
getInternalCodecId() {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
getName() {
|
|
1002
|
+
return this.#track.name;
|
|
1003
|
+
}
|
|
1004
|
+
getLanguageCode() {
|
|
1005
|
+
return this.#track.languageCode || "und";
|
|
1006
|
+
}
|
|
1007
|
+
getTimeResolution() {
|
|
1008
|
+
return 1e3;
|
|
1009
|
+
}
|
|
1010
|
+
isRelativeToUnixEpoch() {
|
|
1011
|
+
return this.#segmentedInput.isRelativeToUnixEpoch();
|
|
1012
|
+
}
|
|
1013
|
+
getDisposition() {
|
|
1014
|
+
return {
|
|
1015
|
+
...DEFAULT_TRACK_DISPOSITION$1,
|
|
1016
|
+
default: this.#track.autoselect,
|
|
1017
|
+
primary: this.#track.default,
|
|
1018
|
+
forced: this.#track.forced,
|
|
1019
|
+
hearingImpaired: this.#track.hearingImpaired
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
getPairingMask() {
|
|
1023
|
+
return this.#pairingMask;
|
|
1024
|
+
}
|
|
1025
|
+
getBitrate() {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
getAverageBitrate() {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
getDurationFromMetadata(options) {
|
|
1032
|
+
return this.#segmentedInput.getDurationFromMetadata(options);
|
|
1033
|
+
}
|
|
1034
|
+
getLiveRefreshInterval() {
|
|
1035
|
+
return this.#segmentedInput.getLiveRefreshInterval();
|
|
1036
|
+
}
|
|
1037
|
+
getHasOnlyKeyPackets() {
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
async getDecoderConfig() {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
getMetadataCodecParameterString() {
|
|
1044
|
+
return this.#track.codecString;
|
|
1045
|
+
}
|
|
1046
|
+
async getFirstPacket(_options) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
async getPacket(_timestamp, _options) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
async getNextPacket(_packet, _options) {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
async getKeyPacket(_timestamp, _options) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
async getNextKeyPacket(_packet, _options) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
getSegmentedInput() {
|
|
1062
|
+
return this.#segmentedInput;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
var ExternalSubtitleTrackBacking = class {
|
|
1066
|
+
#id;
|
|
1067
|
+
#number;
|
|
1068
|
+
#pairingMask;
|
|
1069
|
+
#source;
|
|
1070
|
+
#segmentedInput;
|
|
1071
|
+
#languageCode;
|
|
1072
|
+
#name;
|
|
1073
|
+
#disposition;
|
|
1074
|
+
#codec;
|
|
1075
|
+
#codecString;
|
|
1076
|
+
#codecInfoPromise = null;
|
|
1077
|
+
constructor(params) {
|
|
1078
|
+
this.#id = params.id;
|
|
1079
|
+
this.#number = params.number;
|
|
1080
|
+
this.#pairingMask = params.pairingMask;
|
|
1081
|
+
this.#source = params.source;
|
|
1082
|
+
this.#segmentedInput = new ExternalSubtitleSegmentedInput(params.source, params.source.rootPath);
|
|
1083
|
+
this.#languageCode = params.languageCode ?? "und";
|
|
1084
|
+
this.#name = params.name ?? null;
|
|
1085
|
+
this.#codec = params.codec ?? tryParseSubtitleCodec(params.codecString ?? "") ?? void 0;
|
|
1086
|
+
this.#codecString = params.codecString ?? params.codec ?? void 0;
|
|
1087
|
+
this.#disposition = {
|
|
1088
|
+
...DEFAULT_TRACK_DISPOSITION$1,
|
|
1089
|
+
...params.disposition
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
getType() {
|
|
1093
|
+
return "subtitle";
|
|
1094
|
+
}
|
|
1095
|
+
getId() {
|
|
1096
|
+
return this.#id;
|
|
1097
|
+
}
|
|
1098
|
+
getNumber() {
|
|
1099
|
+
return this.#number;
|
|
1100
|
+
}
|
|
1101
|
+
async #getCodecInfo() {
|
|
1102
|
+
if (this.#codec !== void 0 && this.#codecString !== void 0) return {
|
|
1103
|
+
codec: this.#codec,
|
|
1104
|
+
codecString: this.#codecString
|
|
1105
|
+
};
|
|
1106
|
+
const info = await (this.#codecInfoPromise ??= detectSubtitleCodecFromUri(this.#source, this.#source.rootPath));
|
|
1107
|
+
this.#codec = info.codec;
|
|
1108
|
+
this.#codecString = info.codecString;
|
|
1109
|
+
return info;
|
|
1110
|
+
}
|
|
1111
|
+
getCodec() {
|
|
1112
|
+
if (this.#codec !== void 0) return this.#codec;
|
|
1113
|
+
return this.#getCodecInfo().then((info) => info.codec);
|
|
1114
|
+
}
|
|
1115
|
+
getInternalCodecId() {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
getName() {
|
|
1119
|
+
return this.#name;
|
|
1120
|
+
}
|
|
1121
|
+
getLanguageCode() {
|
|
1122
|
+
return this.#languageCode;
|
|
1123
|
+
}
|
|
1124
|
+
getTimeResolution() {
|
|
1125
|
+
return 1e3;
|
|
1126
|
+
}
|
|
1127
|
+
isRelativeToUnixEpoch() {
|
|
1128
|
+
return this.#segmentedInput.isRelativeToUnixEpoch();
|
|
1129
|
+
}
|
|
1130
|
+
getDisposition() {
|
|
1131
|
+
return this.#disposition;
|
|
1132
|
+
}
|
|
1133
|
+
getPairingMask() {
|
|
1134
|
+
return this.#pairingMask;
|
|
1135
|
+
}
|
|
1136
|
+
getBitrate() {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
getAverageBitrate() {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
getDurationFromMetadata(options) {
|
|
1143
|
+
return this.#segmentedInput.getDurationFromMetadata(options);
|
|
1144
|
+
}
|
|
1145
|
+
getLiveRefreshInterval() {
|
|
1146
|
+
return this.#segmentedInput.getLiveRefreshInterval();
|
|
1147
|
+
}
|
|
1148
|
+
getHasOnlyKeyPackets() {
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
async getDecoderConfig() {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
async getMetadataCodecParameterString() {
|
|
1155
|
+
if (this.#codecString !== void 0) return this.#codecString;
|
|
1156
|
+
return this.#getCodecInfo().then((info) => info.codecString);
|
|
1157
|
+
}
|
|
1158
|
+
async getFirstPacket(_options) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
async getPacket(_timestamp, _options) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
async getNextPacket(_packet, _options) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
async getKeyPacket(_timestamp, _options) {
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
async getNextKeyPacket(_packet, _options) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
getSegmentedInput() {
|
|
1174
|
+
return this.#segmentedInput;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
const parseMasterPlaylistSubtitles = async (input) => {
|
|
1178
|
+
const source = input.source;
|
|
1179
|
+
if (!("rootPath" in source) || typeof source.rootPath !== "string") return [];
|
|
1180
|
+
const loaded = await loadPlaylistText(source, source.rootPath);
|
|
1181
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
1182
|
+
if (lines[0] !== "#EXTM3U") return [];
|
|
1183
|
+
const subtitleMediaTags = [];
|
|
1184
|
+
const pairingMasks = /* @__PURE__ */ new Map();
|
|
1185
|
+
let nextPairIndex = 0n;
|
|
1186
|
+
for (let index = 1; index < lines.length; index++) {
|
|
1187
|
+
const line = lines[index];
|
|
1188
|
+
if (!line) continue;
|
|
1189
|
+
if (line.startsWith(TAG_EXTINF)) return [];
|
|
1190
|
+
if (line.startsWith(TAG_STREAM_INF)) {
|
|
1191
|
+
const groupId = new AttributeList(line.slice(18)).get("subtitles");
|
|
1192
|
+
if (groupId !== null) pairingMasks.set(groupId, (pairingMasks.get(groupId) ?? 0n) | 1n << nextPairIndex);
|
|
1193
|
+
nextPairIndex += 1n;
|
|
1194
|
+
index += 1;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (!line.startsWith(TAG_MEDIA)) continue;
|
|
1198
|
+
const attributes = new AttributeList(line.slice(13));
|
|
1199
|
+
if (attributes.get("type")?.toLowerCase() !== "subtitles") continue;
|
|
1200
|
+
const groupId = attributes.get("group-id");
|
|
1201
|
+
const uri = attributes.get("uri");
|
|
1202
|
+
if (!groupId || !uri) continue;
|
|
1203
|
+
const characteristics = (attributes.get("characteristics") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
1204
|
+
const name = attributes.get("name")?.trim() ?? null;
|
|
1205
|
+
const hearingImpaired = characteristics.includes("public.accessibility.transcribes-spoken-dialog") || characteristics.includes("public.accessibility.describes-music-and-sound") || name?.toLowerCase().includes("sdh") === true;
|
|
1206
|
+
subtitleMediaTags.push({
|
|
1207
|
+
autoselect: parseAttributeBoolean(attributes.get("default")) || parseAttributeBoolean(attributes.get("autoselect")),
|
|
1208
|
+
default: parseAttributeBoolean(attributes.get("default")),
|
|
1209
|
+
forced: parseAttributeBoolean(attributes.get("forced")),
|
|
1210
|
+
groupId,
|
|
1211
|
+
hearingImpaired,
|
|
1212
|
+
languageCode: attributes.get("language") ?? "und",
|
|
1213
|
+
name,
|
|
1214
|
+
uri: joinHlsPath(loaded.path, uri)
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
const detectedSubtitleMediaTags = await Promise.all(subtitleMediaTags.map(async (track) => ({
|
|
1218
|
+
...track,
|
|
1219
|
+
...await detectSubtitleCodecFromUri(source, track.uri)
|
|
1220
|
+
})));
|
|
1221
|
+
const nativeTrackCount = await input._getTrackBackings().then((backings) => backings.length);
|
|
1222
|
+
return detectedSubtitleMediaTags.map((track, index) => new HlsSubtitleTrackBacking({
|
|
1223
|
+
id: nativeTrackCount + index + 1,
|
|
1224
|
+
number: index + 1,
|
|
1225
|
+
pairingMask: pairingMasks.get(track.groupId) ?? 0n,
|
|
1226
|
+
source,
|
|
1227
|
+
track
|
|
1228
|
+
}));
|
|
1229
|
+
};
|
|
1230
|
+
const getHlsSubtitleTrackBackings = (input) => {
|
|
1231
|
+
const existing = subtitleBackingsCache.get(input);
|
|
1232
|
+
if (existing) return existing;
|
|
1233
|
+
const promise = parseMasterPlaylistSubtitles(input);
|
|
1234
|
+
subtitleBackingsCache.set(input, promise);
|
|
1235
|
+
return promise;
|
|
1236
|
+
};
|
|
1237
|
+
//#endregion
|
|
417
1238
|
//#region src/mediabunny-input.ts
|
|
1239
|
+
const CUSTOM_SUBTITLE_TRACK_ID_OFFSET = 1e9;
|
|
1240
|
+
const CUSTOM_PAIRING_BIT_START = 1024n;
|
|
1241
|
+
const EXTRA_PAIRING_MASK = Symbol.for("dasha.extra-pairing-mask");
|
|
1242
|
+
const ORIGINAL_GET_PAIRING_MASK = Symbol.for("dasha.original-get-pairing-mask");
|
|
1243
|
+
const HLS_VARIANT_INF_LINE = "#EXT-X-STREAM-INF:";
|
|
1244
|
+
const HLS_DEMUXER_PATCHED = Symbol.for("dasha.hls-demuxer-patched");
|
|
1245
|
+
const HLS_DEMUXER_METADATA_PATCH = Symbol.for("dasha.hls-demuxer-metadata-patch");
|
|
1246
|
+
const HLS_VIDEO_RANGE_APPLIED = Symbol.for("dasha.hls-video-range-applied");
|
|
1247
|
+
var HlsAttributeList = class {
|
|
1248
|
+
#attributes = {};
|
|
1249
|
+
constructor(str) {
|
|
1250
|
+
let key = "";
|
|
1251
|
+
let value = "";
|
|
1252
|
+
let inValue = false;
|
|
1253
|
+
let inQuotes = false;
|
|
1254
|
+
for (const char of str) if (char === "\"") inQuotes = !inQuotes;
|
|
1255
|
+
else if (char === "=" && !inValue && !inQuotes) inValue = true;
|
|
1256
|
+
else if (char === "," && !inQuotes) {
|
|
1257
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
1258
|
+
key = "";
|
|
1259
|
+
value = "";
|
|
1260
|
+
inValue = false;
|
|
1261
|
+
} else if (inValue) value += char;
|
|
1262
|
+
else key += char;
|
|
1263
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
1264
|
+
}
|
|
1265
|
+
get(name) {
|
|
1266
|
+
return this.#attributes[name.toLowerCase()] ?? null;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
const extractHlsVideoRanges = (text) => {
|
|
1270
|
+
const lines = text.split(/\r?\n/);
|
|
1271
|
+
const videoRanges = [];
|
|
1272
|
+
for (const rawLine of lines) {
|
|
1273
|
+
const line = rawLine.trim();
|
|
1274
|
+
if (!line.startsWith(HLS_VARIANT_INF_LINE)) continue;
|
|
1275
|
+
const attributes = new HlsAttributeList(line.slice(18));
|
|
1276
|
+
videoRanges.push(attributes.get("video-range"));
|
|
1277
|
+
}
|
|
1278
|
+
return videoRanges;
|
|
1279
|
+
};
|
|
1280
|
+
const getPairingMaskIndexes = (pairingMask) => {
|
|
1281
|
+
const indexes = [];
|
|
1282
|
+
let value = pairingMask;
|
|
1283
|
+
let index = 0;
|
|
1284
|
+
while (value > 0n) {
|
|
1285
|
+
if ((value & 1n) === 1n) indexes.push(index);
|
|
1286
|
+
value >>= 1n;
|
|
1287
|
+
index++;
|
|
1288
|
+
}
|
|
1289
|
+
return indexes;
|
|
1290
|
+
};
|
|
1291
|
+
const applyHlsVideoRangeMetadata = async (demuxer) => {
|
|
1292
|
+
if (demuxer[HLS_VIDEO_RANGE_APPLIED] || !demuxer.hasMasterPlaylist || !demuxer.internalTracks?.length) {
|
|
1293
|
+
demuxer[HLS_VIDEO_RANGE_APPLIED] = true;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const slice = await demuxer.input?._reader?.requestEntireFile();
|
|
1297
|
+
if (!slice) {
|
|
1298
|
+
demuxer[HLS_VIDEO_RANGE_APPLIED] = true;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const bytes = slice instanceof Uint8Array ? slice : "bytes" in slice ? slice.bytes : slice.data;
|
|
1302
|
+
const videoRanges = extractHlsVideoRanges(new TextDecoder().decode(bytes));
|
|
1303
|
+
if (!videoRanges.length) {
|
|
1304
|
+
demuxer[HLS_VIDEO_RANGE_APPLIED] = true;
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
for (const track of demuxer.internalTracks) {
|
|
1308
|
+
if (track.info.type !== "video") continue;
|
|
1309
|
+
const matchedRanges = getPairingMaskIndexes(track.pairingMask).map((index) => videoRanges[index] ?? null).filter((value) => value != null);
|
|
1310
|
+
const uniqueRanges = [...new Set(matchedRanges)];
|
|
1311
|
+
track.videoRange = uniqueRanges.length === 1 ? uniqueRanges[0] : null;
|
|
1312
|
+
}
|
|
1313
|
+
demuxer[HLS_VIDEO_RANGE_APPLIED] = true;
|
|
1314
|
+
};
|
|
1315
|
+
const patchHlsDemuxer = (demuxer) => {
|
|
1316
|
+
if (!(demuxer instanceof Object) || !("readMetadata" in demuxer) || typeof demuxer.readMetadata !== "function" || !("input" in demuxer) || demuxer[HLS_DEMUXER_PATCHED]) return demuxer;
|
|
1317
|
+
const candidate = demuxer;
|
|
1318
|
+
if (candidate.constructor?.name !== "HlsDemuxer") return demuxer;
|
|
1319
|
+
const originalReadMetadata = candidate.readMetadata.bind(candidate);
|
|
1320
|
+
candidate.readMetadata = () => {
|
|
1321
|
+
if (candidate[HLS_DEMUXER_METADATA_PATCH]) return candidate[HLS_DEMUXER_METADATA_PATCH];
|
|
1322
|
+
const promise = Promise.resolve(originalReadMetadata()).then(() => applyHlsVideoRangeMetadata(candidate));
|
|
1323
|
+
candidate[HLS_DEMUXER_METADATA_PATCH] = promise.catch((error) => {
|
|
1324
|
+
if (candidate[HLS_DEMUXER_METADATA_PATCH] === promise) candidate[HLS_DEMUXER_METADATA_PATCH] = void 0;
|
|
1325
|
+
throw error;
|
|
1326
|
+
});
|
|
1327
|
+
return candidate[HLS_DEMUXER_METADATA_PATCH];
|
|
1328
|
+
};
|
|
1329
|
+
candidate[HLS_DEMUXER_PATCHED] = true;
|
|
1330
|
+
return demuxer;
|
|
1331
|
+
};
|
|
1332
|
+
const getDynamicRangeForTrack = async (track) => {
|
|
1333
|
+
const backing = track._backing;
|
|
1334
|
+
const manifestDynamicRange = backing?.internalTrack?.track?.dynamicRange;
|
|
1335
|
+
if (manifestDynamicRange) return manifestDynamicRange;
|
|
1336
|
+
const codecString = await track.getCodecParameterString().catch(() => null);
|
|
1337
|
+
const videoRange = backing?.internalTrack?.videoRange ?? null;
|
|
1338
|
+
const fromMetadata = inferDynamicRange({
|
|
1339
|
+
codecs: codecString,
|
|
1340
|
+
videoRange
|
|
1341
|
+
});
|
|
1342
|
+
if (fromMetadata !== "sdr" || videoRange != null) return fromMetadata;
|
|
1343
|
+
return inferDynamicRange({
|
|
1344
|
+
codecs: codecString,
|
|
1345
|
+
colorSpace: await track.getColorSpace().catch(() => null),
|
|
1346
|
+
videoRange
|
|
1347
|
+
});
|
|
1348
|
+
};
|
|
418
1349
|
const requireSync = (value, getterName, asyncName) => {
|
|
419
1350
|
if (value instanceof Promise) throw new Error(`'${getterName}' is not available synchronously for this track. Use '${asyncName}()' instead.`);
|
|
420
1351
|
return value;
|
|
@@ -454,9 +1385,10 @@ const queryWrappedTracks = (input, backings, query) => {
|
|
|
454
1385
|
return queryTracks(backings.map((backing) => input._wrapBackingAsTrack(backing)), query);
|
|
455
1386
|
};
|
|
456
1387
|
const getTrackBackingsByType = async (input, type) => {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
1388
|
+
const nativeBackings = await input._getTrackBackings();
|
|
1389
|
+
const syntheticBackings = await input._getSyntheticTrackBackings?.(type) ?? [];
|
|
1390
|
+
const backings = [...nativeBackings, ...syntheticBackings];
|
|
1391
|
+
return type ? backings.filter((backing) => getBackingType(backing) === type) : backings;
|
|
460
1392
|
};
|
|
461
1393
|
const patchBaseMediabunnyInput = () => {
|
|
462
1394
|
const prototype = Input$1.prototype;
|
|
@@ -467,6 +1399,10 @@ const patchBaseMediabunnyInput = () => {
|
|
|
467
1399
|
prototype.getAudioTracks = function(query) {
|
|
468
1400
|
return getTrackBackingsByType(this, BACKING_TYPE_AUDIO).then((backings) => queryWrappedTracks(this, backings, query));
|
|
469
1401
|
};
|
|
1402
|
+
const originalGetDemuxer = prototype._getDemuxer;
|
|
1403
|
+
prototype._getDemuxer = function() {
|
|
1404
|
+
return originalGetDemuxer.call(this).then((demuxer) => patchHlsDemuxer(demuxer));
|
|
1405
|
+
};
|
|
470
1406
|
prototype[BASE_INPUT_PATCHED] = true;
|
|
471
1407
|
};
|
|
472
1408
|
const getSegmentedInputForTrack = (track) => {
|
|
@@ -476,6 +1412,7 @@ const getSegmentedInputForTrack = (track) => {
|
|
|
476
1412
|
return internalTrack.demuxer.getSegmentedInputForPath(internalTrack.fullPath);
|
|
477
1413
|
};
|
|
478
1414
|
const addSegmentAccess = (track) => new Proxy(track, { get(target, prop) {
|
|
1415
|
+
if (prop === "getDynamicRange" && target instanceof InputVideoTrack) return () => getDynamicRangeForTrack(target);
|
|
479
1416
|
if (prop === "getSegmentedInput") return () => getSegmentedInputForTrack(target);
|
|
480
1417
|
if (prop === "getSegments") return async () => {
|
|
481
1418
|
const segmentedInput = getSegmentedInputForTrack(target);
|
|
@@ -525,9 +1462,14 @@ const preserveSubtitleBackingsOnInput = (input) => {
|
|
|
525
1462
|
var SegmentedMediabunnyInput = class extends Input$1 {
|
|
526
1463
|
#trackCache = /* @__PURE__ */ new WeakMap();
|
|
527
1464
|
#subtitleTrackCache = /* @__PURE__ */ new WeakMap();
|
|
1465
|
+
#hlsSubtitleBackingsPromise = null;
|
|
1466
|
+
#customSubtitleBackings = [];
|
|
1467
|
+
#nextCustomSubtitleTrackId = CUSTOM_SUBTITLE_TRACK_ID_OFFSET;
|
|
1468
|
+
#nextCustomSubtitleTrackNumber = CUSTOM_SUBTITLE_TRACK_ID_OFFSET;
|
|
1469
|
+
#nextPairingBitIndex = null;
|
|
528
1470
|
async #queryTracks(query, type) {
|
|
529
|
-
const
|
|
530
|
-
return queryWrappedTracks(
|
|
1471
|
+
const internalInput = this;
|
|
1472
|
+
return queryWrappedTracks(internalInput, await getTrackBackingsByType(internalInput, type), query);
|
|
531
1473
|
}
|
|
532
1474
|
_wrapBackingAsTrack(backing) {
|
|
533
1475
|
const track = backing.getType?.() === "subtitle" ? this.#wrapSubtitleBacking(backing) : super._wrapBackingAsTrack(backing);
|
|
@@ -537,6 +1479,19 @@ var SegmentedMediabunnyInput = class extends Input$1 {
|
|
|
537
1479
|
this.#trackCache.set(track, wrapped);
|
|
538
1480
|
return wrapped;
|
|
539
1481
|
}
|
|
1482
|
+
async _getSyntheticTrackBackings(type) {
|
|
1483
|
+
if (type && type !== BACKING_TYPE_SUBTITLE) return [];
|
|
1484
|
+
const backings = [...this.#customSubtitleBackings];
|
|
1485
|
+
if (await this.getFormat() !== HLS$1) return backings;
|
|
1486
|
+
if (!this.#hlsSubtitleBackingsPromise) {
|
|
1487
|
+
const promise = getHlsSubtitleTrackBackings(this).catch((error) => {
|
|
1488
|
+
if (this.#hlsSubtitleBackingsPromise === promise) this.#hlsSubtitleBackingsPromise = null;
|
|
1489
|
+
throw error;
|
|
1490
|
+
});
|
|
1491
|
+
this.#hlsSubtitleBackingsPromise = promise;
|
|
1492
|
+
}
|
|
1493
|
+
return [...backings, ...await this.#hlsSubtitleBackingsPromise];
|
|
1494
|
+
}
|
|
540
1495
|
#wrapSubtitleBacking(backing) {
|
|
541
1496
|
const existing = this.#subtitleTrackCache.get(backing);
|
|
542
1497
|
if (existing) return existing;
|
|
@@ -553,12 +1508,83 @@ var SegmentedMediabunnyInput = class extends Input$1 {
|
|
|
553
1508
|
async getAudioTracks(query) {
|
|
554
1509
|
return await this.#queryTracks(query, BACKING_TYPE_AUDIO);
|
|
555
1510
|
}
|
|
1511
|
+
async getSubtitleTracks(query) {
|
|
1512
|
+
return await this.#queryTracks(query, BACKING_TYPE_SUBTITLE);
|
|
1513
|
+
}
|
|
556
1514
|
async getPrimaryVideoTrack(query) {
|
|
557
1515
|
return await super.getPrimaryVideoTrack(query);
|
|
558
1516
|
}
|
|
559
1517
|
async getPrimaryAudioTrack(query) {
|
|
560
1518
|
return await super.getPrimaryAudioTrack(query);
|
|
561
1519
|
}
|
|
1520
|
+
addSubtitleTrack(source, metadata = {}) {
|
|
1521
|
+
const sourceWithRootPath = this.#takeSubtitleSourceRef(source).source;
|
|
1522
|
+
if (typeof sourceWithRootPath.rootPath !== "string") throw new TypeError("source must provide a string rootPath.");
|
|
1523
|
+
const pairWith = this.#toPairableVideoTracks(metadata.pairWith);
|
|
1524
|
+
const backing = new ExternalSubtitleTrackBacking({
|
|
1525
|
+
id: this.#nextCustomSubtitleTrackId++,
|
|
1526
|
+
number: this.#nextCustomSubtitleTrackNumber++,
|
|
1527
|
+
pairingMask: 0n,
|
|
1528
|
+
source: sourceWithRootPath,
|
|
1529
|
+
codec: metadata.codec,
|
|
1530
|
+
codecString: metadata.codecString,
|
|
1531
|
+
disposition: metadata.disposition,
|
|
1532
|
+
languageCode: metadata.languageCode,
|
|
1533
|
+
name: metadata.name
|
|
1534
|
+
});
|
|
1535
|
+
this.#pairSubtitleBacking(backing, pairWith);
|
|
1536
|
+
this.#customSubtitleBackings.push(backing);
|
|
1537
|
+
return this._wrapBackingAsTrack(backing);
|
|
1538
|
+
}
|
|
1539
|
+
#takeSubtitleSourceRef(source) {
|
|
1540
|
+
const rawSource = source instanceof SourceRef ? source.source : source;
|
|
1541
|
+
if (!(rawSource instanceof Object) || !("rootPath" in rawSource) || !("ref" in rawSource) || typeof rawSource.ref !== "function") throw new TypeError("source must be a pathed source such as UrlSource or FilePathSource.");
|
|
1542
|
+
const ref = rawSource.ref();
|
|
1543
|
+
this._sourceRefs.push(ref);
|
|
1544
|
+
return ref;
|
|
1545
|
+
}
|
|
1546
|
+
#toPairableVideoTracks(pairWith) {
|
|
1547
|
+
if (!pairWith) return [];
|
|
1548
|
+
const tracks = this.#isIterable(pairWith) ? [...pairWith] : [pairWith];
|
|
1549
|
+
for (const track of tracks) {
|
|
1550
|
+
if (track.input !== this) throw new TypeError("pairWith tracks must belong to the same input instance.");
|
|
1551
|
+
if (track.type !== "video") throw new TypeError("pairWith only accepts video tracks.");
|
|
1552
|
+
}
|
|
1553
|
+
return tracks;
|
|
1554
|
+
}
|
|
1555
|
+
#isIterable(value) {
|
|
1556
|
+
return typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
1557
|
+
}
|
|
1558
|
+
#pairSubtitleBacking(subtitleBacking, videoTracks) {
|
|
1559
|
+
for (const track of videoTracks) {
|
|
1560
|
+
const bit = this.#allocatePairingBit();
|
|
1561
|
+
this.#appendPairingMask(subtitleBacking, bit);
|
|
1562
|
+
this.#appendPairingMask(track._backing, bit);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
#allocatePairingBit() {
|
|
1566
|
+
const nextIndex = this.#nextPairingBitIndex ?? this.#getInitialPairingBitIndex();
|
|
1567
|
+
this.#nextPairingBitIndex = nextIndex + 1n;
|
|
1568
|
+
return 1n << nextIndex;
|
|
1569
|
+
}
|
|
1570
|
+
#getInitialPairingBitIndex() {
|
|
1571
|
+
const loadedBackings = [...this._trackBackingsCache ?? []];
|
|
1572
|
+
let maxBitIndex = -1n;
|
|
1573
|
+
for (const backing of [...loadedBackings, ...this.#customSubtitleBackings]) {
|
|
1574
|
+
const mask = backing.getPairingMask?.() ?? 0n;
|
|
1575
|
+
if (mask === 0n) continue;
|
|
1576
|
+
const bitIndex = BigInt(mask.toString(2).length - 1);
|
|
1577
|
+
if (bitIndex > maxBitIndex) maxBitIndex = bitIndex;
|
|
1578
|
+
}
|
|
1579
|
+
return maxBitIndex >= 0n ? maxBitIndex + 1n : CUSTOM_PAIRING_BIT_START;
|
|
1580
|
+
}
|
|
1581
|
+
#appendPairingMask(backing, mask) {
|
|
1582
|
+
const patchedBacking = backing;
|
|
1583
|
+
patchedBacking[EXTRA_PAIRING_MASK] = (patchedBacking[EXTRA_PAIRING_MASK] ?? 0n) | mask;
|
|
1584
|
+
if (patchedBacking[ORIGINAL_GET_PAIRING_MASK]) return;
|
|
1585
|
+
patchedBacking[ORIGINAL_GET_PAIRING_MASK] = backing.getPairingMask?.bind(backing) ?? (() => 0n);
|
|
1586
|
+
Object.assign(backing, { getPairingMask: () => (patchedBacking[ORIGINAL_GET_PAIRING_MASK]?.() ?? 0n) | (patchedBacking[EXTRA_PAIRING_MASK] ?? 0n) });
|
|
1587
|
+
}
|
|
562
1588
|
};
|
|
563
1589
|
//#endregion
|
|
564
1590
|
//#region src/dash/dash-segmented-input.ts
|
|
@@ -650,7 +1676,7 @@ var DashSegmentedInput = class {
|
|
|
650
1676
|
return this.currentUpdateSegmentsPromise ??= (async () => {
|
|
651
1677
|
try {
|
|
652
1678
|
const remainingWaitTimeMs = this.getRemainingWaitTimeMs();
|
|
653
|
-
if (remainingWaitTimeMs > 0) await setTimeout(remainingWaitTimeMs);
|
|
1679
|
+
if (remainingWaitTimeMs > 0) await setTimeout$1(remainingWaitTimeMs);
|
|
654
1680
|
this.lastSegmentUpdateTime = performance.now();
|
|
655
1681
|
await this.updateSegments();
|
|
656
1682
|
} finally {
|
|
@@ -1069,7 +2095,8 @@ const createDashTrack = (params) => {
|
|
|
1069
2095
|
if (width) track.width = Number(width);
|
|
1070
2096
|
if (height) track.height = Number(height);
|
|
1071
2097
|
track.frameRate = frameRate ?? getDashFrameRate(representation);
|
|
1072
|
-
if (
|
|
2098
|
+
if (supplementalProps.length > 0 || essentialProps.length > 0) track.colorSpace = parseColorSpace(supplementalProps, essentialProps);
|
|
2099
|
+
if (track.codecString && (supplementalProps.length > 0 || essentialProps.length > 0)) track.dynamicRange = parseDynamicRange(track.codecString, supplementalProps, essentialProps);
|
|
1073
2100
|
} else if (track.type === "audio") {
|
|
1074
2101
|
if (accessibilities) track.descriptive = checkIsDescriptive(accessibilities);
|
|
1075
2102
|
if (supplementalProps) track.joc = getDolbyDigitalPlusComplexityIndex(supplementalProps);
|
|
@@ -1349,7 +2376,7 @@ var DashDemuxer = class {
|
|
|
1349
2376
|
baseUrl = "";
|
|
1350
2377
|
constructor(input) {
|
|
1351
2378
|
this.input = input;
|
|
1352
|
-
this.headers = getSourceHeaders(input.source);
|
|
2379
|
+
this.headers = getSourceHeaders$1(input.source);
|
|
1353
2380
|
}
|
|
1354
2381
|
readMetadata() {
|
|
1355
2382
|
return this.metadataPromise ??= (async () => {
|
|
@@ -1667,7 +2694,7 @@ var DashInputVideoTrackBacking = class extends DashTrackBackingBase {
|
|
|
1667
2694
|
return 0;
|
|
1668
2695
|
}
|
|
1669
2696
|
async getColorSpace() {
|
|
1670
|
-
return this.delegate((track) => track.getColorSpace());
|
|
2697
|
+
return this.internalTrack.track.colorSpace ?? this.delegate((track) => track.getColorSpace());
|
|
1671
2698
|
}
|
|
1672
2699
|
async canBeTransparent() {
|
|
1673
2700
|
return this.delegate((track) => track.canBeTransparent());
|
|
@@ -1730,11 +2757,11 @@ var DashInputFormat = class extends InputFormat {
|
|
|
1730
2757
|
const DASH = new DashInputFormat();
|
|
1731
2758
|
const DASH_FORMATS = [
|
|
1732
2759
|
DASH,
|
|
1733
|
-
MP4,
|
|
2760
|
+
MP4$1,
|
|
1734
2761
|
QTFF,
|
|
1735
2762
|
WEBM,
|
|
1736
2763
|
MATROSKA,
|
|
1737
|
-
MP3,
|
|
2764
|
+
MP3$1,
|
|
1738
2765
|
ADTS
|
|
1739
2766
|
];
|
|
1740
2767
|
//#endregion
|
|
@@ -1747,5 +2774,6 @@ var Input = class extends SegmentedMediabunnyInput {
|
|
|
1747
2774
|
const isInput = (value) => value instanceof Input;
|
|
1748
2775
|
const getSegmentedInput = (track) => track.getSegmentedInput();
|
|
1749
2776
|
const getSegments = async (track) => track.getSegments();
|
|
2777
|
+
const ALL_FORMATS = [...ALL_FORMATS$1, DASH];
|
|
1750
2778
|
//#endregion
|
|
1751
|
-
export { DASH, DASH_FORMATS, FilePathSource, HLS_FORMATS, Input, UrlSource, asc, desc, getSegmentedInput, getSegments, isInput, prefer, preserveSubtitleBackingsOnInput };
|
|
2779
|
+
export { ALL_FORMATS, DASH, DASH_FORMATS, FilePathSource, HLS, HLS_FORMATS, Input, MP3, MP4, UrlSource, asc, desc, getSegmentedInput, getSegments, isInput, prefer, preserveSubtitleBackingsOnInput };
|