dasha 4.0.0-alpha.13 → 4.0.0-alpha.15
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 +39 -24
- package/dist/index.d.mts +491 -0
- package/dist/index.mjs +1751 -0
- package/package.json +8 -10
- package/dist/dasha.d.mts +0 -441
- package/dist/dasha.mjs +0 -1568
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
import { ADTS, CustomPathedSource, FilePathSource, HLS_FORMATS, Input as Input$1, InputFormat, InputTrack, MATROSKA, MP3, MP4, QTFF, UrlSource, WEBM, asc, desc, prefer } from "mediabunny";
|
|
2
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
3
|
+
import { Temporal } from "temporal-polyfill";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
//#region src/util.ts
|
|
7
|
+
const combineUrl = (baseUrl, relativeUrl) => {
|
|
8
|
+
if (!baseUrl.trim()) return relativeUrl;
|
|
9
|
+
const url1 = new URL(baseUrl);
|
|
10
|
+
return new URL(relativeUrl, url1).toString();
|
|
11
|
+
};
|
|
12
|
+
const parseMimes = (codecs) => codecs.toLowerCase().split(",").map((codec) => codec.trim().split(".")[0]);
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/audio.ts
|
|
15
|
+
const parseAudioCodecFromMime = (mime) => {
|
|
16
|
+
switch (mime.toLowerCase().trim().split(".")[0]) {
|
|
17
|
+
case "mp4a": return "aac";
|
|
18
|
+
case "ac-3": return "ac3";
|
|
19
|
+
case "ec-3": return "eac3";
|
|
20
|
+
case "opus": return "opus";
|
|
21
|
+
case "dtsc": return "dts";
|
|
22
|
+
case "alac": return "alac";
|
|
23
|
+
case "flac": return "flac";
|
|
24
|
+
default: throw new Error(`The MIME ${mime} is not supported as audio codec`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const parseAudioCodec = (codecs) => {
|
|
28
|
+
const mimes = parseMimes(codecs);
|
|
29
|
+
for (const mime of mimes) try {
|
|
30
|
+
return parseAudioCodecFromMime(mime);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`No MIME types matched any supported Audio Codecs in ${codecs}`);
|
|
35
|
+
};
|
|
36
|
+
const tryParseAudioCodec = (codecs) => {
|
|
37
|
+
try {
|
|
38
|
+
return parseAudioCodec(codecs);
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const getDolbyDigitalPlusComplexityIndex = (supplementalProps = []) => {
|
|
44
|
+
const targetScheme = "tag:dolby.com,2018:dash:EC3_ExtensionComplexityIndex:2018";
|
|
45
|
+
for (const prop of supplementalProps) if (prop.schemeIdUri === targetScheme && prop.value) return parseInt(prop.value);
|
|
46
|
+
};
|
|
47
|
+
const checkIsDescriptive = (accessibilities = []) => {
|
|
48
|
+
for (const accessibility of accessibilities) {
|
|
49
|
+
const { schemeIdUri, value } = accessibility;
|
|
50
|
+
if (schemeIdUri == "urn:mpeg:dash:role:2011" && value === "descriptive" || schemeIdUri == "urn:tva:metadata:cs:AudioPurposeCS:2007" && value === "1") return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
const parseChannels = (channels) => {
|
|
55
|
+
const isDigit = (char) => char >= "0" && char <= "9";
|
|
56
|
+
if (typeof channels === "string") {
|
|
57
|
+
if (channels.toUpperCase() == "A000") return 2;
|
|
58
|
+
else if (channels.toUpperCase() == "F801") return 5.1;
|
|
59
|
+
else if (isDigit(channels.replace("ch", "").replace(".", "")[0])) return parseFloat(channels.replace("ch", ""));
|
|
60
|
+
throw new Error(`Unsupported audio channels value, '${channels}'`);
|
|
61
|
+
}
|
|
62
|
+
return parseFloat(channels);
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/encrypt-method.ts
|
|
66
|
+
const ENCRYPT_METHODS = {
|
|
67
|
+
NONE: "none",
|
|
68
|
+
AES_128: "aes-128",
|
|
69
|
+
AES_128_ECB: "aes-128-ecb",
|
|
70
|
+
SAMPLE_AES: "sample-aes",
|
|
71
|
+
SAMPLE_AES_CTR: "sample-aes-ctr",
|
|
72
|
+
CENC: "cenc",
|
|
73
|
+
CHACHA20: "chacha20",
|
|
74
|
+
UNKNOWN: "unknown"
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/role-type.ts
|
|
78
|
+
const ROLE_TYPE = {
|
|
79
|
+
Subtitle: 0,
|
|
80
|
+
Main: 1,
|
|
81
|
+
Alternate: 2,
|
|
82
|
+
Supplementary: 3,
|
|
83
|
+
Commentary: 4,
|
|
84
|
+
Dub: 5,
|
|
85
|
+
Description: 6,
|
|
86
|
+
Sign: 7,
|
|
87
|
+
Metadata: 8,
|
|
88
|
+
ForcedSubtitle: 9
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/subtitle.ts
|
|
92
|
+
const parseSubtitleCodecFromMime = (mime) => {
|
|
93
|
+
switch (mime.toLowerCase().trim().split(".")[0]) {
|
|
94
|
+
case "srt":
|
|
95
|
+
case "x-subrip": return "srt";
|
|
96
|
+
case "ssa": return "ssa";
|
|
97
|
+
case "ass": return "ass";
|
|
98
|
+
case "ttml": return "ttml";
|
|
99
|
+
case "vtt": return "vtt";
|
|
100
|
+
case "stpp": return "stpp";
|
|
101
|
+
case "wvtt": return "wvtt";
|
|
102
|
+
default: throw new Error(`The MIME ${mime} is not supported as subtitle codec`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const parseSubtitleCodec = (codecs) => {
|
|
106
|
+
const mimes = parseMimes(codecs);
|
|
107
|
+
for (const mime of mimes) try {
|
|
108
|
+
return parseSubtitleCodecFromMime(mime);
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`No MIME types matched any supported Subtitle Codecs in ${codecs}`);
|
|
113
|
+
};
|
|
114
|
+
const tryParseSubtitleCodec = (codecs) => {
|
|
115
|
+
try {
|
|
116
|
+
return parseSubtitleCodec(codecs);
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const checkIsClosedCaption = (roles = []) => {
|
|
122
|
+
for (const role of roles) if (role.schemeIdUri === "urn:mpeg:dash:role:2011" && role.value === "caption") return true;
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
const checkIsSdh = (accessibilities = []) => {
|
|
126
|
+
for (const accessibility of accessibilities) {
|
|
127
|
+
const { schemeIdUri, value } = accessibility;
|
|
128
|
+
if (schemeIdUri === "urn:tva:metadata:cs:AudioPurposeCS:2007" && value === "2") return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/video.ts
|
|
134
|
+
const PRIMARIES = {
|
|
135
|
+
Unspecified: 0,
|
|
136
|
+
BT_709: 1,
|
|
137
|
+
BT_601_625: 5,
|
|
138
|
+
BT_601_525: 6,
|
|
139
|
+
BT_2020_and_2100: 9,
|
|
140
|
+
SMPTE_ST_2113_and_EG_4321: 12
|
|
141
|
+
};
|
|
142
|
+
const TRANSFER = {
|
|
143
|
+
Unspecified: 0,
|
|
144
|
+
BT_709: 1,
|
|
145
|
+
BT_601: 6,
|
|
146
|
+
BT_2020: 14,
|
|
147
|
+
BT_2100: 15,
|
|
148
|
+
BT_2100_PQ: 16,
|
|
149
|
+
BT_2100_HLG: 18
|
|
150
|
+
};
|
|
151
|
+
const MATRIX = {
|
|
152
|
+
RGB: 0,
|
|
153
|
+
YCbCr_BT_709: 1,
|
|
154
|
+
YCbCr_BT_601_625: 5,
|
|
155
|
+
YCbCr_BT_601_525: 6,
|
|
156
|
+
YCbCr_BT_2020_and_2100: 9,
|
|
157
|
+
ICtCp_BT_2100: 14
|
|
158
|
+
};
|
|
159
|
+
const parseVideoCodecFromMime = (mime) => {
|
|
160
|
+
const target = mime.toLowerCase().trim().split(".")[0];
|
|
161
|
+
const avc = [
|
|
162
|
+
"avc1",
|
|
163
|
+
"avc2",
|
|
164
|
+
"avc3",
|
|
165
|
+
"dva1",
|
|
166
|
+
"dvav"
|
|
167
|
+
];
|
|
168
|
+
const hevc = [
|
|
169
|
+
"hev1",
|
|
170
|
+
"hev2",
|
|
171
|
+
"hev3",
|
|
172
|
+
"hvc1",
|
|
173
|
+
"hvc2",
|
|
174
|
+
"hvc3",
|
|
175
|
+
"dvh1",
|
|
176
|
+
"dvhe",
|
|
177
|
+
"lhv1",
|
|
178
|
+
"lhe1"
|
|
179
|
+
];
|
|
180
|
+
const vc1 = ["vc-1"];
|
|
181
|
+
const vp8 = ["vp08", "vp8"];
|
|
182
|
+
const vp9 = ["vp09", "vp9"];
|
|
183
|
+
const av1 = ["av01"];
|
|
184
|
+
if (avc.includes(target)) return "avc";
|
|
185
|
+
if (hevc.includes(target)) return "hevc";
|
|
186
|
+
if (vc1.includes(target)) return "vc1";
|
|
187
|
+
if (vp8.includes(target)) return "vp8";
|
|
188
|
+
if (vp9.includes(target)) return "vp9";
|
|
189
|
+
if (av1.includes(target)) return "av1";
|
|
190
|
+
throw new Error(`The MIME ${mime} is not supported as video codec`);
|
|
191
|
+
};
|
|
192
|
+
const parseDynamicRangeFromCicp = (primaries, transfer, matrix) => {
|
|
193
|
+
if (transfer == 5) transfer = TRANSFER.BT_601;
|
|
194
|
+
if (primaries == PRIMARIES.Unspecified && transfer == TRANSFER.Unspecified && matrix == MATRIX.RGB) return "sdr";
|
|
195
|
+
else if ([PRIMARIES.BT_601_625, PRIMARIES.BT_601_525].includes(primaries)) return "sdr";
|
|
196
|
+
else if (TRANSFER.BT_2100_PQ === transfer) return "hdr10";
|
|
197
|
+
else if (TRANSFER.BT_2100_HLG === transfer) return "hlg";
|
|
198
|
+
else return "sdr";
|
|
199
|
+
};
|
|
200
|
+
const parseVideoCodec = (codecs) => {
|
|
201
|
+
for (const codec of codecs.toLowerCase().split(",")) {
|
|
202
|
+
const mime = codec.trim().split(".")[0];
|
|
203
|
+
try {
|
|
204
|
+
return parseVideoCodecFromMime(mime);
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`No MIME types matched any supported Video Codecs in ${codecs}`);
|
|
210
|
+
};
|
|
211
|
+
const tryParseVideoCodec = (codecs) => {
|
|
212
|
+
try {
|
|
213
|
+
return parseVideoCodec(codecs);
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const parseDynamicRange = (codecs, supplementalProps = [], essentialProps = []) => {
|
|
219
|
+
if ([
|
|
220
|
+
"dva1",
|
|
221
|
+
"dvav",
|
|
222
|
+
"dvhe",
|
|
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));
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/dash/dash-misc.ts
|
|
234
|
+
const DASH_MIME_TYPE = "application/dash+xml";
|
|
235
|
+
const DASH_TEMPLATE_REPRESENTATION_ID = "$RepresentationID$";
|
|
236
|
+
const DASH_TEMPLATE_BANDWIDTH = "$Bandwidth$";
|
|
237
|
+
const DASH_TEMPLATE_NUMBER = "$Number$";
|
|
238
|
+
const DASH_TEMPLATE_TIME = "$Time$";
|
|
239
|
+
const getDashTrackMatchKey = (track) => JSON.stringify({
|
|
240
|
+
type: track.type,
|
|
241
|
+
codecString: track.codecString,
|
|
242
|
+
groupId: track.groupId,
|
|
243
|
+
width: track.type === "video" ? track.width : null,
|
|
244
|
+
height: track.type === "video" ? track.height : null,
|
|
245
|
+
languageCode: track.languageCode ?? null,
|
|
246
|
+
name: track.name,
|
|
247
|
+
role: track.role ?? null,
|
|
248
|
+
extension: track.extension
|
|
249
|
+
});
|
|
250
|
+
const getSourcePath = (source) => {
|
|
251
|
+
if ("rootPath" in source && typeof source.rootPath === "string") return source.rootPath;
|
|
252
|
+
};
|
|
253
|
+
const normalizeHeaders = (headers) => {
|
|
254
|
+
if (!headers) return {};
|
|
255
|
+
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
256
|
+
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
257
|
+
return { ...headers };
|
|
258
|
+
};
|
|
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);
|
|
262
|
+
return {
|
|
263
|
+
...requestHeaders,
|
|
264
|
+
...optionHeaders
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
const parseOriginalUrlFromManifest = (text) => text.match(/<!--\s*URL:\s*([^\n]+?)\s*-->/)?.[1]?.trim();
|
|
268
|
+
const loadDashManifest = async (source) => {
|
|
269
|
+
const manifestPath = getSourcePath(source);
|
|
270
|
+
if (!manifestPath) throw new Error("DASH input currently requires a pathed source such as UrlSource.");
|
|
271
|
+
if (manifestPath.startsWith("http://") || manifestPath.startsWith("https://")) {
|
|
272
|
+
const response = await fetch(manifestPath, { headers: getSourceHeaders(source) });
|
|
273
|
+
if (!response.ok) throw new Error(`Failed to fetch DASH manifest: ${response.status} ${response.statusText} (${response.url})`);
|
|
274
|
+
return {
|
|
275
|
+
text: await response.text(),
|
|
276
|
+
url: response.url
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (manifestPath.startsWith("file:")) {
|
|
280
|
+
const text = await readFile(new URL(manifestPath), "utf8");
|
|
281
|
+
return {
|
|
282
|
+
text,
|
|
283
|
+
url: parseOriginalUrlFromManifest(text) ?? manifestPath
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const text = await readFile(manifestPath, "utf8");
|
|
287
|
+
return {
|
|
288
|
+
text,
|
|
289
|
+
url: parseOriginalUrlFromManifest(text) ?? manifestPath
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const isLikelyDashPath = (source) => {
|
|
293
|
+
const path = getSourcePath(source);
|
|
294
|
+
if (!path) return false;
|
|
295
|
+
return path.toLowerCase().split(/[?#]/, 1)[0]?.endsWith(".mpd") ?? false;
|
|
296
|
+
};
|
|
297
|
+
const isDashManifestText = (text) => /<MPD(?:\s|>)/i.test(text);
|
|
298
|
+
const replaceDashVariables = (text, variables) => {
|
|
299
|
+
let result = "";
|
|
300
|
+
for (let index = 0; index < text.length;) {
|
|
301
|
+
if (text[index] !== "$") {
|
|
302
|
+
result += text[index];
|
|
303
|
+
index += 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (text[index + 1] === "$") {
|
|
307
|
+
result += "$";
|
|
308
|
+
index += 2;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const endIndex = text.indexOf("$", index + 1);
|
|
312
|
+
if (endIndex < 0) {
|
|
313
|
+
result += "$";
|
|
314
|
+
index += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const match = text.slice(index + 1, endIndex).match(/^(RepresentationID|Bandwidth|Number|Time)(?:%([0-9]+)d)?$/);
|
|
318
|
+
if (!match) {
|
|
319
|
+
result += text.slice(index, endIndex + 1);
|
|
320
|
+
index = endIndex + 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const [, variableName, width] = match;
|
|
324
|
+
const value = variables[`$${variableName}$`];
|
|
325
|
+
if (value == null) {
|
|
326
|
+
result += text.slice(index, endIndex + 1);
|
|
327
|
+
index = endIndex + 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
result += !width || variableName === "RepresentationID" ? value : value.padStart(Number.parseInt(width, 10), "0");
|
|
331
|
+
index = endIndex + 1;
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
};
|
|
335
|
+
const createDashTrackDescriptor = (params) => {
|
|
336
|
+
const shouldUseCodecsFromMime = params.contentType === "text" && !params.mimeType?.includes("mp4");
|
|
337
|
+
const codecString = params.codecs ?? (shouldUseCodecsFromMime ? params.mimeType?.split("/")[1] : null);
|
|
338
|
+
if (codecString) {
|
|
339
|
+
const videoCodec = tryParseVideoCodec(codecString);
|
|
340
|
+
if (videoCodec) return {
|
|
341
|
+
type: "video",
|
|
342
|
+
codec: videoCodec,
|
|
343
|
+
codecString
|
|
344
|
+
};
|
|
345
|
+
const audioCodec = tryParseAudioCodec(codecString);
|
|
346
|
+
if (audioCodec) return {
|
|
347
|
+
type: "audio",
|
|
348
|
+
codec: audioCodec,
|
|
349
|
+
codecString
|
|
350
|
+
};
|
|
351
|
+
const subtitleCodec = tryParseSubtitleCodec(codecString);
|
|
352
|
+
if (subtitleCodec) return {
|
|
353
|
+
type: "subtitle",
|
|
354
|
+
codec: subtitleCodec,
|
|
355
|
+
codecString
|
|
356
|
+
};
|
|
357
|
+
} else {
|
|
358
|
+
const type = params.contentType || params.mimeType?.split("/")[0];
|
|
359
|
+
if (type === "video") return {
|
|
360
|
+
type: "video",
|
|
361
|
+
codecString: null
|
|
362
|
+
};
|
|
363
|
+
if (type === "audio") return {
|
|
364
|
+
type: "audio",
|
|
365
|
+
codecString: null
|
|
366
|
+
};
|
|
367
|
+
if (type === "text") {
|
|
368
|
+
const subtitleCodecString = params.mimeType?.split("/")[1] ?? null;
|
|
369
|
+
return {
|
|
370
|
+
type: "subtitle",
|
|
371
|
+
codec: subtitleCodecString ? tryParseSubtitleCodec(subtitleCodecString) : void 0,
|
|
372
|
+
codecString: subtitleCodecString
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
throw new Error("Unable to determine the type of a track, cannot continue...");
|
|
377
|
+
};
|
|
378
|
+
const getDirectDashChildren = (node, tag) => node.getElementsByTagName(tag).filter((child) => !!child.parentNode?.isSameNode(node));
|
|
379
|
+
const getDirectDashChild = (node, tag) => getDirectDashChildren(node, tag)[0];
|
|
380
|
+
const getInheritedDashChild = (tag, ...nodes) => {
|
|
381
|
+
for (const node of nodes) {
|
|
382
|
+
const child = getDirectDashChild(node, tag);
|
|
383
|
+
if (child) return child;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const getDashTagAttrs = (tag, ...elements) => {
|
|
387
|
+
for (const element of elements) {
|
|
388
|
+
const matches = getDirectDashChildren(element, tag);
|
|
389
|
+
if (!matches.length) continue;
|
|
390
|
+
return matches.flatMap((match) => {
|
|
391
|
+
const schemeIdUri = match.getAttribute("schemeIdUri");
|
|
392
|
+
if (!schemeIdUri) return [];
|
|
393
|
+
return {
|
|
394
|
+
schemeIdUri,
|
|
395
|
+
value: match.getAttribute("value") ?? void 0
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return [];
|
|
400
|
+
};
|
|
401
|
+
const extendDashBaseUrl = (node, baseUrl) => {
|
|
402
|
+
const target = getDirectDashChild(node, "BaseURL");
|
|
403
|
+
if (target?.textContent) return combineUrl(baseUrl, target.textContent);
|
|
404
|
+
return baseUrl;
|
|
405
|
+
};
|
|
406
|
+
const getDashFrameRate = (node) => {
|
|
407
|
+
const frameRate = node.getAttribute("frameRate");
|
|
408
|
+
if (!frameRate || !frameRate.includes("/")) return;
|
|
409
|
+
const value = Number(frameRate.split("/")[0]) / Number(frameRate.split("/")[1]);
|
|
410
|
+
return Number(value.toFixed(3));
|
|
411
|
+
};
|
|
412
|
+
const parseDashRange = (range) => {
|
|
413
|
+
const [startRange, end] = range.split("-").map(Number);
|
|
414
|
+
return [startRange, end - startRange + 1];
|
|
415
|
+
};
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/mediabunny-input.ts
|
|
418
|
+
const requireSync = (value, getterName, asyncName) => {
|
|
419
|
+
if (value instanceof Promise) throw new Error(`'${getterName}' is not available synchronously for this track. Use '${asyncName}()' instead.`);
|
|
420
|
+
return value;
|
|
421
|
+
};
|
|
422
|
+
const queryTracks = async (tracks, query) => {
|
|
423
|
+
let matched = tracks;
|
|
424
|
+
if (query?.filter) {
|
|
425
|
+
const filterMatches = tracks.map((track) => query.filter(track));
|
|
426
|
+
const resolvedFilterMatches = await Promise.all(filterMatches);
|
|
427
|
+
matched = tracks.filter((_, index) => resolvedFilterMatches[index]);
|
|
428
|
+
}
|
|
429
|
+
if (!query?.sortBy) return matched;
|
|
430
|
+
const resolvedSortValues = await Promise.all(matched.map((track) => query.sortBy(track)));
|
|
431
|
+
return matched.map((track, index) => ({
|
|
432
|
+
track,
|
|
433
|
+
sortValue: resolvedSortValues[index]
|
|
434
|
+
})).sort((left, right) => {
|
|
435
|
+
const leftValues = Array.isArray(left.sortValue) ? left.sortValue : [left.sortValue];
|
|
436
|
+
const rightValues = Array.isArray(right.sortValue) ? right.sortValue : [right.sortValue];
|
|
437
|
+
const maxLength = Math.max(leftValues.length, rightValues.length);
|
|
438
|
+
for (let index = 0; index < maxLength; index++) {
|
|
439
|
+
const leftValue = leftValues[index] ?? 0;
|
|
440
|
+
const rightValue = rightValues[index] ?? 0;
|
|
441
|
+
if (leftValue === rightValue) continue;
|
|
442
|
+
return leftValue - rightValue;
|
|
443
|
+
}
|
|
444
|
+
return 0;
|
|
445
|
+
}).map(({ track }) => track);
|
|
446
|
+
};
|
|
447
|
+
const BACKING_TYPE_SUBTITLE = "subtitle";
|
|
448
|
+
const BACKING_TYPE_AUDIO = "audio";
|
|
449
|
+
const BACKING_TYPE_VIDEO = "video";
|
|
450
|
+
const BASE_INPUT_PATCHED = Symbol.for("dasha.base-mediabunny-input-patched");
|
|
451
|
+
const PRESERVE_SUBTITLE_BACKINGS = Symbol.for("dasha.preserve-subtitle-backings");
|
|
452
|
+
const getBackingType = (backing) => backing.getType?.();
|
|
453
|
+
const queryWrappedTracks = (input, backings, query) => {
|
|
454
|
+
return queryTracks(backings.map((backing) => input._wrapBackingAsTrack(backing)), query);
|
|
455
|
+
};
|
|
456
|
+
const getTrackBackingsByType = async (input, type) => {
|
|
457
|
+
const backings = await input._getTrackBackings();
|
|
458
|
+
if (!type) return backings;
|
|
459
|
+
return backings.filter((backing) => getBackingType(backing) === type);
|
|
460
|
+
};
|
|
461
|
+
const patchBaseMediabunnyInput = () => {
|
|
462
|
+
const prototype = Input$1.prototype;
|
|
463
|
+
if (prototype[BASE_INPUT_PATCHED]) return;
|
|
464
|
+
prototype.getTracks = function(query) {
|
|
465
|
+
return getTrackBackingsByType(this).then((backings) => queryWrappedTracks(this, this[PRESERVE_SUBTITLE_BACKINGS] ? backings : backings.filter((backing) => getBackingType(backing) !== BACKING_TYPE_SUBTITLE), query));
|
|
466
|
+
};
|
|
467
|
+
prototype.getAudioTracks = function(query) {
|
|
468
|
+
return getTrackBackingsByType(this, BACKING_TYPE_AUDIO).then((backings) => queryWrappedTracks(this, backings, query));
|
|
469
|
+
};
|
|
470
|
+
prototype[BASE_INPUT_PATCHED] = true;
|
|
471
|
+
};
|
|
472
|
+
const getSegmentedInputForTrack = (track) => {
|
|
473
|
+
const backing = track._backing;
|
|
474
|
+
if ("getSegmentedInput" in backing && typeof backing.getSegmentedInput === "function") return backing.getSegmentedInput();
|
|
475
|
+
const internalTrack = backing.internalTrack;
|
|
476
|
+
return internalTrack.demuxer.getSegmentedInputForPath(internalTrack.fullPath);
|
|
477
|
+
};
|
|
478
|
+
const addSegmentAccess = (track) => new Proxy(track, { get(target, prop) {
|
|
479
|
+
if (prop === "getSegmentedInput") return () => getSegmentedInputForTrack(target);
|
|
480
|
+
if (prop === "getSegments") return async () => {
|
|
481
|
+
const segmentedInput = getSegmentedInputForTrack(target);
|
|
482
|
+
await segmentedInput.runUpdateSegments();
|
|
483
|
+
return segmentedInput.segments;
|
|
484
|
+
};
|
|
485
|
+
const value = Reflect.get(target, prop, target);
|
|
486
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
487
|
+
} });
|
|
488
|
+
var MediabunnyInputSubtitleTrack = class extends InputTrack {
|
|
489
|
+
#backing;
|
|
490
|
+
constructor(input, backing) {
|
|
491
|
+
super();
|
|
492
|
+
Object.assign(this, {
|
|
493
|
+
input,
|
|
494
|
+
_backing: backing
|
|
495
|
+
});
|
|
496
|
+
this.#backing = backing;
|
|
497
|
+
}
|
|
498
|
+
get type() {
|
|
499
|
+
return "subtitle";
|
|
500
|
+
}
|
|
501
|
+
async getCodec() {
|
|
502
|
+
return this.#backing.getCodec();
|
|
503
|
+
}
|
|
504
|
+
get codec() {
|
|
505
|
+
return requireSync(this.#backing.getCodec(), "codec", "getCodec");
|
|
506
|
+
}
|
|
507
|
+
async getCodecParameterString() {
|
|
508
|
+
return await this.#backing.getMetadataCodecParameterString?.() ?? null;
|
|
509
|
+
}
|
|
510
|
+
async canDecode() {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
async determinePacketType(_packet) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
async hasOnlyKeyPackets() {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
patchBaseMediabunnyInput();
|
|
521
|
+
const preserveSubtitleBackingsOnInput = (input) => {
|
|
522
|
+
Object.assign(input, { [PRESERVE_SUBTITLE_BACKINGS]: true });
|
|
523
|
+
return input;
|
|
524
|
+
};
|
|
525
|
+
var SegmentedMediabunnyInput = class extends Input$1 {
|
|
526
|
+
#trackCache = /* @__PURE__ */ new WeakMap();
|
|
527
|
+
#subtitleTrackCache = /* @__PURE__ */ new WeakMap();
|
|
528
|
+
async #queryTracks(query, type) {
|
|
529
|
+
const backings = await getTrackBackingsByType(this, type);
|
|
530
|
+
return queryWrappedTracks(this, backings, query);
|
|
531
|
+
}
|
|
532
|
+
_wrapBackingAsTrack(backing) {
|
|
533
|
+
const track = backing.getType?.() === "subtitle" ? this.#wrapSubtitleBacking(backing) : super._wrapBackingAsTrack(backing);
|
|
534
|
+
const existing = this.#trackCache.get(track);
|
|
535
|
+
if (existing) return existing;
|
|
536
|
+
const wrapped = addSegmentAccess(track);
|
|
537
|
+
this.#trackCache.set(track, wrapped);
|
|
538
|
+
return wrapped;
|
|
539
|
+
}
|
|
540
|
+
#wrapSubtitleBacking(backing) {
|
|
541
|
+
const existing = this.#subtitleTrackCache.get(backing);
|
|
542
|
+
if (existing) return existing;
|
|
543
|
+
const track = new MediabunnyInputSubtitleTrack(this, backing);
|
|
544
|
+
this.#subtitleTrackCache.set(backing, track);
|
|
545
|
+
return track;
|
|
546
|
+
}
|
|
547
|
+
async getTracks(query) {
|
|
548
|
+
return await this.#queryTracks(query);
|
|
549
|
+
}
|
|
550
|
+
async getVideoTracks(query) {
|
|
551
|
+
return await this.#queryTracks(query, BACKING_TYPE_VIDEO);
|
|
552
|
+
}
|
|
553
|
+
async getAudioTracks(query) {
|
|
554
|
+
return await this.#queryTracks(query, BACKING_TYPE_AUDIO);
|
|
555
|
+
}
|
|
556
|
+
async getPrimaryVideoTrack(query) {
|
|
557
|
+
return await super.getPrimaryVideoTrack(query);
|
|
558
|
+
}
|
|
559
|
+
async getPrimaryAudioTrack(query) {
|
|
560
|
+
return await super.getPrimaryAudioTrack(query);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/dash/dash-segmented-input.ts
|
|
565
|
+
const roundToDivisor = (value, multiple) => Math.round(value * multiple) / multiple;
|
|
566
|
+
const binarySearchLessOrEqual = (array, value, getValue) => {
|
|
567
|
+
let left = 0;
|
|
568
|
+
let right = array.length - 1;
|
|
569
|
+
let result = -1;
|
|
570
|
+
while (left <= right) {
|
|
571
|
+
const mid = left + right >> 1;
|
|
572
|
+
if (getValue(array[mid]) <= value) {
|
|
573
|
+
result = mid;
|
|
574
|
+
left = mid + 1;
|
|
575
|
+
} else right = mid - 1;
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
};
|
|
579
|
+
const getLeastRecentlyUsedIndex = (entries) => {
|
|
580
|
+
let bestIndex = -1;
|
|
581
|
+
let bestAge = Infinity;
|
|
582
|
+
for (const [index, entry] of entries.entries()) if (entry.age < bestAge) {
|
|
583
|
+
bestAge = entry.age;
|
|
584
|
+
bestIndex = index;
|
|
585
|
+
}
|
|
586
|
+
return bestIndex;
|
|
587
|
+
};
|
|
588
|
+
const getSegmentLocation = (segment) => ({
|
|
589
|
+
path: segment.url,
|
|
590
|
+
offset: segment.startRange ?? 0,
|
|
591
|
+
length: segment.expectLength ?? null
|
|
592
|
+
});
|
|
593
|
+
const createInitSegment = (segment) => ({
|
|
594
|
+
timestamp: 0,
|
|
595
|
+
duration: 0,
|
|
596
|
+
relativeToUnixEpoch: false,
|
|
597
|
+
firstSegment: null,
|
|
598
|
+
sequenceNumber: segment.sequenceNumber,
|
|
599
|
+
location: getSegmentLocation(segment),
|
|
600
|
+
encryption: segment.encryption,
|
|
601
|
+
initSegment: null,
|
|
602
|
+
lastProgramDateTimeSeconds: null
|
|
603
|
+
});
|
|
604
|
+
const trackToDashSegments = (internalTrack) => {
|
|
605
|
+
const mediaSegments = internalTrack.track.mediaSegments;
|
|
606
|
+
if (mediaSegments.length === 0) return [];
|
|
607
|
+
let nextTimestamp = 0;
|
|
608
|
+
const segments = [];
|
|
609
|
+
for (const mediaSegment of mediaSegments) {
|
|
610
|
+
const timestamp = mediaSegment.timestamp ?? nextTimestamp;
|
|
611
|
+
const dashSegment = {
|
|
612
|
+
timestamp,
|
|
613
|
+
duration: mediaSegment.duration,
|
|
614
|
+
relativeToUnixEpoch: false,
|
|
615
|
+
firstSegment: null,
|
|
616
|
+
sequenceNumber: mediaSegment.sequenceNumber,
|
|
617
|
+
location: getSegmentLocation(mediaSegment),
|
|
618
|
+
encryption: mediaSegment.encryption,
|
|
619
|
+
initSegment: null,
|
|
620
|
+
lastProgramDateTimeSeconds: null
|
|
621
|
+
};
|
|
622
|
+
segments.push(dashSegment);
|
|
623
|
+
nextTimestamp = timestamp + mediaSegment.duration;
|
|
624
|
+
}
|
|
625
|
+
const firstSegment = segments[0] ?? null;
|
|
626
|
+
const initSegment = internalTrack.track.initSegment ? createInitSegment(internalTrack.track.initSegment) : null;
|
|
627
|
+
for (const segment of segments) {
|
|
628
|
+
segment.firstSegment = firstSegment;
|
|
629
|
+
segment.initSegment = initSegment;
|
|
630
|
+
}
|
|
631
|
+
return segments;
|
|
632
|
+
};
|
|
633
|
+
var DashSegmentedInput = class {
|
|
634
|
+
internalTrack;
|
|
635
|
+
demuxer;
|
|
636
|
+
segments = [];
|
|
637
|
+
currentUpdateSegmentsPromise = null;
|
|
638
|
+
lastSegmentUpdateTime = -Infinity;
|
|
639
|
+
nextInputCacheAge = 0;
|
|
640
|
+
inputCache = [];
|
|
641
|
+
firstTrackPromise = null;
|
|
642
|
+
packetInfos = /* @__PURE__ */ new WeakMap();
|
|
643
|
+
firstSegmentFirstTimestamps = /* @__PURE__ */ new WeakMap();
|
|
644
|
+
firstTimestampCache = /* @__PURE__ */ new WeakMap();
|
|
645
|
+
constructor(internalTrack) {
|
|
646
|
+
this.internalTrack = internalTrack;
|
|
647
|
+
this.demuxer = internalTrack.demuxer;
|
|
648
|
+
}
|
|
649
|
+
runUpdateSegments() {
|
|
650
|
+
return this.currentUpdateSegmentsPromise ??= (async () => {
|
|
651
|
+
try {
|
|
652
|
+
const remainingWaitTimeMs = this.getRemainingWaitTimeMs();
|
|
653
|
+
if (remainingWaitTimeMs > 0) await setTimeout(remainingWaitTimeMs);
|
|
654
|
+
this.lastSegmentUpdateTime = performance.now();
|
|
655
|
+
await this.updateSegments();
|
|
656
|
+
} finally {
|
|
657
|
+
this.currentUpdateSegmentsPromise = null;
|
|
658
|
+
}
|
|
659
|
+
})();
|
|
660
|
+
}
|
|
661
|
+
async updateSegments() {
|
|
662
|
+
await this.demuxer.refreshTrackSegments(this.internalTrack);
|
|
663
|
+
this.segments = trackToDashSegments(this.internalTrack);
|
|
664
|
+
}
|
|
665
|
+
getRemainingWaitTimeMs() {
|
|
666
|
+
if (!this.internalTrack.track.isLive) return 0;
|
|
667
|
+
const elapsed = performance.now() - this.lastSegmentUpdateTime;
|
|
668
|
+
const result = Math.max(0, this.internalTrack.track.refreshIntervalMs - elapsed);
|
|
669
|
+
if (result <= 50) return 0;
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
async getFirstSegment() {
|
|
673
|
+
if (this.segments.length === 0) await this.runUpdateSegments();
|
|
674
|
+
return this.segments[0] ?? null;
|
|
675
|
+
}
|
|
676
|
+
async getSegmentAt(timestamp, options) {
|
|
677
|
+
if (this.segments.length === 0) await this.runUpdateSegments();
|
|
678
|
+
let isLazy = !!options.skipLiveWait && this.getRemainingWaitTimeMs() > 0;
|
|
679
|
+
while (true) {
|
|
680
|
+
const index = binarySearchLessOrEqual(this.segments, timestamp, (segment) => segment.timestamp);
|
|
681
|
+
if (index === -1) return null;
|
|
682
|
+
if (index < this.segments.length - 1 || !this.internalTrack.track.isLive || isLazy) return this.segments[index];
|
|
683
|
+
const segment = this.segments[index];
|
|
684
|
+
if (timestamp < segment.timestamp + segment.duration) return segment;
|
|
685
|
+
await this.runUpdateSegments();
|
|
686
|
+
if (options.skipLiveWait) isLazy = true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
getNextSegmentIndex(segment) {
|
|
690
|
+
const currentIndex = this.segments.indexOf(segment);
|
|
691
|
+
if (currentIndex !== -1) return currentIndex + 1;
|
|
692
|
+
if (segment.sequenceNumber !== null) {
|
|
693
|
+
const matchingSequenceIndex = this.segments.findIndex((candidate) => candidate.sequenceNumber === segment.sequenceNumber);
|
|
694
|
+
if (matchingSequenceIndex !== -1) return matchingSequenceIndex + 1;
|
|
695
|
+
return this.segments.findIndex((candidate) => candidate.sequenceNumber !== null && candidate.sequenceNumber > segment.sequenceNumber);
|
|
696
|
+
}
|
|
697
|
+
const matchingLocationIndex = this.segments.findIndex((candidate) => candidate.timestamp === segment.timestamp && candidate.duration === segment.duration && candidate.location.path === segment.location.path && candidate.location.offset === segment.location.offset && candidate.location.length === segment.location.length);
|
|
698
|
+
if (matchingLocationIndex !== -1) return matchingLocationIndex + 1;
|
|
699
|
+
return this.segments.findIndex((candidate) => candidate.timestamp > segment.timestamp);
|
|
700
|
+
}
|
|
701
|
+
async getNextSegment(segment, options) {
|
|
702
|
+
let isLazy = !!options.skipLiveWait && this.getRemainingWaitTimeMs() > 0;
|
|
703
|
+
while (true) {
|
|
704
|
+
const nextIndex = this.getNextSegmentIndex(segment);
|
|
705
|
+
if (nextIndex !== -1 && nextIndex < this.segments.length) return this.segments[nextIndex];
|
|
706
|
+
if (!this.internalTrack.track.isLive || isLazy) return null;
|
|
707
|
+
await this.runUpdateSegments();
|
|
708
|
+
if (options.skipLiveWait) isLazy = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async getPreviousSegment(segment) {
|
|
712
|
+
const index = this.segments.indexOf(segment);
|
|
713
|
+
if (index === -1) throw new Error("Segment was not created by this segmented input.");
|
|
714
|
+
return this.segments[index - 1] ?? null;
|
|
715
|
+
}
|
|
716
|
+
getInputForSegment(segment) {
|
|
717
|
+
const input = this.demuxer.input;
|
|
718
|
+
const cacheEntry = this.inputCache.find((entry) => entry.segment === segment);
|
|
719
|
+
if (cacheEntry) {
|
|
720
|
+
cacheEntry.age = this.nextInputCacheAge++;
|
|
721
|
+
return cacheEntry.input;
|
|
722
|
+
}
|
|
723
|
+
let initInput = null;
|
|
724
|
+
if (segment.initSegment && segment.initSegment !== segment) initInput = this.getInputForSegment(segment.initSegment);
|
|
725
|
+
const formatOptions = { ...input._formatOptions };
|
|
726
|
+
const segmentInput = preserveSubtitleBackingsOnInput(new Input$1({
|
|
727
|
+
source: new CustomPathedSource(segment.location.path, async (request) => {
|
|
728
|
+
if (!request.isRoot) throw new Error("Nested requests are not supported for DASH segments.");
|
|
729
|
+
const proxiedRequest = {
|
|
730
|
+
...request,
|
|
731
|
+
isRoot: false
|
|
732
|
+
};
|
|
733
|
+
let ref = await input._getSourceCached(proxiedRequest);
|
|
734
|
+
if (segment.location.offset > 0 || segment.location.length !== null) {
|
|
735
|
+
const sliceRef = ref.source.slice(segment.location.offset, segment.location.length ?? void 0).ref();
|
|
736
|
+
ref.free();
|
|
737
|
+
ref = sliceRef;
|
|
738
|
+
}
|
|
739
|
+
return ref;
|
|
740
|
+
}),
|
|
741
|
+
formats: input._formats.filter((format) => format.mimeType !== DASH_MIME_TYPE),
|
|
742
|
+
initInput: initInput ?? void 0,
|
|
743
|
+
formatOptions
|
|
744
|
+
}));
|
|
745
|
+
this.inputCache.push({
|
|
746
|
+
age: this.nextInputCacheAge++,
|
|
747
|
+
input: segmentInput,
|
|
748
|
+
segment
|
|
749
|
+
});
|
|
750
|
+
if (this.inputCache.length > 4) {
|
|
751
|
+
const minAgeIndex = getLeastRecentlyUsedIndex(this.inputCache);
|
|
752
|
+
if (minAgeIndex === -1) throw new Error("Failed to evict cached DASH segment input.");
|
|
753
|
+
this.inputCache.splice(minAgeIndex, 1);
|
|
754
|
+
}
|
|
755
|
+
return segmentInput;
|
|
756
|
+
}
|
|
757
|
+
async getTrackForSegment(segment) {
|
|
758
|
+
const matchingType = (await this.getInputForSegment(segment).getTracks()).filter((track) => track.type === this.internalTrack.info.type);
|
|
759
|
+
if (matchingType.length === 1) return matchingType[0];
|
|
760
|
+
if (this.internalTrack.track.codec) {
|
|
761
|
+
for (const track of matchingType) if (await track.getCodec() === this.internalTrack.track.codec) return track;
|
|
762
|
+
}
|
|
763
|
+
return matchingType[0] ?? null;
|
|
764
|
+
}
|
|
765
|
+
async getFirstTrack() {
|
|
766
|
+
return this.firstTrackPromise ??= (async () => {
|
|
767
|
+
const firstSegment = await this.getFirstSegment();
|
|
768
|
+
if (!firstSegment) throw new Error("Missing first DASH segment, cannot hydrate track.");
|
|
769
|
+
const track = await this.getTrackForSegment(firstSegment);
|
|
770
|
+
if (!track) throw new Error("No matching track found in DASH segment media data.");
|
|
771
|
+
return track;
|
|
772
|
+
})();
|
|
773
|
+
}
|
|
774
|
+
async getFirstTimestampForInput(input) {
|
|
775
|
+
const existing = this.firstTimestampCache.get(input);
|
|
776
|
+
if (existing !== void 0) return existing;
|
|
777
|
+
const firstTimestamp = await input.getFirstTimestamp();
|
|
778
|
+
this.firstTimestampCache.set(input, firstTimestamp);
|
|
779
|
+
return firstTimestamp;
|
|
780
|
+
}
|
|
781
|
+
async getMediaOffset(segment, input, track) {
|
|
782
|
+
const firstSegment = segment.firstSegment ?? segment;
|
|
783
|
+
let firstSegmentFirstTimestamp;
|
|
784
|
+
if (this.firstSegmentFirstTimestamps.has(firstSegment)) firstSegmentFirstTimestamp = this.firstSegmentFirstTimestamps.get(firstSegment);
|
|
785
|
+
else {
|
|
786
|
+
const firstInput = this.getInputForSegment(firstSegment);
|
|
787
|
+
firstSegmentFirstTimestamp = await this.getFirstTimestampForInput(firstInput);
|
|
788
|
+
this.firstSegmentFirstTimestamps.set(firstSegment, firstSegmentFirstTimestamp);
|
|
789
|
+
}
|
|
790
|
+
if (firstSegment === segment) return firstSegment.timestamp - firstSegmentFirstTimestamp;
|
|
791
|
+
const segmentFirstTimestamp = await this.getFirstTimestampForInput(input);
|
|
792
|
+
const segmentElapsed = segment.timestamp - firstSegment.timestamp;
|
|
793
|
+
const difference = segmentFirstTimestamp - firstSegmentFirstTimestamp - segmentElapsed;
|
|
794
|
+
if (Math.abs(difference) <= Math.min(.25, segmentElapsed)) return firstSegment.timestamp - firstSegmentFirstTimestamp;
|
|
795
|
+
return segment.timestamp - segmentFirstTimestamp;
|
|
796
|
+
}
|
|
797
|
+
async createAdjustedPacket(packet, segment, track) {
|
|
798
|
+
if (packet.sequenceNumber < 0) throw new Error("DASH packet sequence number must be non-negative.");
|
|
799
|
+
const input = track.input;
|
|
800
|
+
const mediaOffset = await this.getMediaOffset(segment, input, track);
|
|
801
|
+
const firstSegment = segment.firstSegment ?? segment;
|
|
802
|
+
const segmentTimestampRelativeToFirst = segment.timestamp - firstSegment.timestamp;
|
|
803
|
+
const modified = packet.clone({
|
|
804
|
+
timestamp: roundToDivisor(packet.timestamp + mediaOffset, await track.getTimeResolution()),
|
|
805
|
+
sequenceNumber: Math.floor(1e8 * segmentTimestampRelativeToFirst) + packet.sequenceNumber
|
|
806
|
+
});
|
|
807
|
+
this.packetInfos.set(modified, {
|
|
808
|
+
segment,
|
|
809
|
+
track,
|
|
810
|
+
sourcePacket: packet
|
|
811
|
+
});
|
|
812
|
+
return modified;
|
|
813
|
+
}
|
|
814
|
+
async getDecoderConfig() {
|
|
815
|
+
return (await this.getFirstTrack())._backing.getDecoderConfig();
|
|
816
|
+
}
|
|
817
|
+
async getHasOnlyKeyPackets() {
|
|
818
|
+
return await (await this.getFirstTrack())._backing.getHasOnlyKeyPackets?.() ?? null;
|
|
819
|
+
}
|
|
820
|
+
async getFirstPacket(options) {
|
|
821
|
+
const firstSegment = await this.getFirstSegment();
|
|
822
|
+
if (!firstSegment) return null;
|
|
823
|
+
const track = await this.getTrackForSegment(firstSegment);
|
|
824
|
+
if (!track) return null;
|
|
825
|
+
const packet = await track._backing.getFirstPacket(options);
|
|
826
|
+
if (!packet) return null;
|
|
827
|
+
return this.createAdjustedPacket(packet, firstSegment, track);
|
|
828
|
+
}
|
|
829
|
+
getNextPacket(packet, options) {
|
|
830
|
+
return this.getNextPacketInternal(packet, options, false);
|
|
831
|
+
}
|
|
832
|
+
getNextKeyPacket(packet, options) {
|
|
833
|
+
return this.getNextPacketInternal(packet, options, true);
|
|
834
|
+
}
|
|
835
|
+
async getNextPacketInternal(packet, options, keyframesOnly) {
|
|
836
|
+
const info = this.packetInfos.get(packet);
|
|
837
|
+
if (!info) throw new Error("Packet was not created from this DASH track.");
|
|
838
|
+
const nextPacket = keyframesOnly ? await info.track._backing.getNextKeyPacket(info.sourcePacket, options) : await info.track._backing.getNextPacket(info.sourcePacket, options);
|
|
839
|
+
if (nextPacket) return this.createAdjustedPacket(nextPacket, info.segment, info.track);
|
|
840
|
+
let currentSegment = info.segment;
|
|
841
|
+
while (true) {
|
|
842
|
+
const nextSegment = await this.getNextSegment(currentSegment, { skipLiveWait: options.skipLiveWait });
|
|
843
|
+
if (!nextSegment) return null;
|
|
844
|
+
const nextTrack = await this.getTrackForSegment(nextSegment);
|
|
845
|
+
if (!nextTrack) {
|
|
846
|
+
currentSegment = nextSegment;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const firstPacket = await nextTrack._backing.getFirstPacket(options);
|
|
850
|
+
if (!firstPacket) {
|
|
851
|
+
currentSegment = nextSegment;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
return this.createAdjustedPacket(firstPacket, nextSegment, nextTrack);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
getPacket(timestamp, options) {
|
|
858
|
+
return this.getPacketInternal(timestamp, options, false);
|
|
859
|
+
}
|
|
860
|
+
getKeyPacket(timestamp, options) {
|
|
861
|
+
return this.getPacketInternal(timestamp, options, true);
|
|
862
|
+
}
|
|
863
|
+
async getPacketInternal(timestamp, options, keyframesOnly) {
|
|
864
|
+
let currentSegment = await this.getSegmentAt(timestamp, { skipLiveWait: options.skipLiveWait });
|
|
865
|
+
if (!currentSegment) return null;
|
|
866
|
+
while (currentSegment) {
|
|
867
|
+
const track = await this.getTrackForSegment(currentSegment);
|
|
868
|
+
if (!track) {
|
|
869
|
+
currentSegment = await this.getPreviousSegment(currentSegment);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const input = track.input;
|
|
873
|
+
const offsetTimestamp = timestamp - await this.getMediaOffset(currentSegment, input, track);
|
|
874
|
+
const packet = keyframesOnly ? await track._backing.getKeyPacket(offsetTimestamp, options) : await track._backing.getPacket(offsetTimestamp, options);
|
|
875
|
+
if (!packet) {
|
|
876
|
+
currentSegment = await this.getPreviousSegment(currentSegment);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
return this.createAdjustedPacket(packet, currentSegment, track);
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
async getLiveRefreshInterval() {
|
|
884
|
+
if (this.getRemainingWaitTimeMs() === 0) await this.runUpdateSegments();
|
|
885
|
+
return this.internalTrack.track.isLive ? this.internalTrack.track.refreshIntervalMs / 1e3 : null;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/dash/dash-demuxer.ts
|
|
890
|
+
const DASH_NAMESPACE_MAP = new Map([
|
|
891
|
+
["cenc", "urn:mpeg:cenc:2013"],
|
|
892
|
+
["mspr", "urn:microsoft:playready"],
|
|
893
|
+
["mas", "urn:marlin:mas:1-0:services:schemas:mpd"]
|
|
894
|
+
]);
|
|
895
|
+
const WIDEVINE_SYSTEM_ID = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
|
|
896
|
+
const PLAYREADY_SYSTEM_ID = "9a04f079-9840-4286-ab92-e65be0885f95";
|
|
897
|
+
const DEFAULT_TRACK_DISPOSITION = {
|
|
898
|
+
commentary: false,
|
|
899
|
+
default: true,
|
|
900
|
+
forced: false,
|
|
901
|
+
hearingImpaired: false,
|
|
902
|
+
original: false,
|
|
903
|
+
primary: true,
|
|
904
|
+
visuallyImpaired: false
|
|
905
|
+
};
|
|
906
|
+
const isMissingNamespace = (rawText, tag) => !rawText.includes(`xmlns:${tag}`) && rawText.includes(`<${tag}:`);
|
|
907
|
+
const replaceFirst = (source, oldValue, newValue) => {
|
|
908
|
+
const index = source.indexOf(oldValue);
|
|
909
|
+
return index < 0 ? source : source.slice(0, index) + newValue + source.slice(index + oldValue.length);
|
|
910
|
+
};
|
|
911
|
+
const processDashContent = (mpdContent) => {
|
|
912
|
+
const missingNamespaceKeys = Array.from(DASH_NAMESPACE_MAP.keys().filter((key) => isMissingNamespace(mpdContent, key)));
|
|
913
|
+
if (!missingNamespaceKeys.length) return mpdContent;
|
|
914
|
+
return replaceFirst(mpdContent, "<MPD ", `<MPD ${missingNamespaceKeys.map((key) => `xmlns:${key}="${DASH_NAMESPACE_MAP.get(key)}"`).join(" ")} `);
|
|
915
|
+
};
|
|
916
|
+
const getDashRefreshIntervalMs = (timeShiftBufferDepth) => Temporal.Duration.from(timeShiftBufferDepth).total("milliseconds") / 2;
|
|
917
|
+
const getDisposition = (track) => ({
|
|
918
|
+
...DEFAULT_TRACK_DISPOSITION,
|
|
919
|
+
commentary: track.role === ROLE_TYPE.Commentary,
|
|
920
|
+
default: !!track.default,
|
|
921
|
+
forced: track.role === ROLE_TYPE.ForcedSubtitle || !!(track.type === "subtitle" && track.forced),
|
|
922
|
+
hearingImpaired: !!(track.type === "subtitle" && track.sdh),
|
|
923
|
+
visuallyImpaired: !!(track.type === "audio" && track.descriptive)
|
|
924
|
+
});
|
|
925
|
+
const canPairTracks = (left, right) => {
|
|
926
|
+
if (left === right || left.type === right.type) return false;
|
|
927
|
+
if (left.type === "video" && right.type === "audio") return !left.audioGroupId || left.audioGroupId === right.groupId;
|
|
928
|
+
if (left.type === "audio" && right.type === "video") return !right.audioGroupId || right.audioGroupId === left.groupId;
|
|
929
|
+
if (left.type === "video" && right.type === "subtitle") return !left.subtitleGroupId || left.subtitleGroupId === right.groupId;
|
|
930
|
+
if (left.type === "subtitle" && right.type === "video") return !right.subtitleGroupId || right.subtitleGroupId === left.groupId;
|
|
931
|
+
return false;
|
|
932
|
+
};
|
|
933
|
+
const createPairingMasks = (tracks) => {
|
|
934
|
+
const masks = /* @__PURE__ */ new Map();
|
|
935
|
+
let nextPairIndex = 0;
|
|
936
|
+
for (const [leftIndex, left] of tracks.entries()) for (const right of tracks.slice(leftIndex + 1)) {
|
|
937
|
+
if (!canPairTracks(left, right)) continue;
|
|
938
|
+
const bit = 1n << BigInt(nextPairIndex++);
|
|
939
|
+
masks.set(left, (masks.get(left) ?? 0n) | bit);
|
|
940
|
+
masks.set(right, (masks.get(right) ?? 0n) | bit);
|
|
941
|
+
}
|
|
942
|
+
return masks;
|
|
943
|
+
};
|
|
944
|
+
const createTrackInfo = (track) => {
|
|
945
|
+
if (track.type === "video") return {
|
|
946
|
+
type: "video",
|
|
947
|
+
width: track.width ?? null,
|
|
948
|
+
height: track.height ?? null
|
|
949
|
+
};
|
|
950
|
+
if (track.type === "audio") return {
|
|
951
|
+
type: "audio",
|
|
952
|
+
numberOfChannels: track.numberOfChannels ?? null
|
|
953
|
+
};
|
|
954
|
+
return { type: "subtitle" };
|
|
955
|
+
};
|
|
956
|
+
const createInternalTracks = (demuxer, tracks) => {
|
|
957
|
+
const pairingMasks = createPairingMasks(tracks);
|
|
958
|
+
return tracks.map((track, index) => ({
|
|
959
|
+
id: index + 1,
|
|
960
|
+
demuxer,
|
|
961
|
+
backingTrack: null,
|
|
962
|
+
pairingMask: pairingMasks.get(track) ?? 0n,
|
|
963
|
+
track,
|
|
964
|
+
info: createTrackInfo(track)
|
|
965
|
+
}));
|
|
966
|
+
};
|
|
967
|
+
const getTrackNumber = (internalTrack) => {
|
|
968
|
+
const internalTracks = internalTrack.demuxer.internalTracks;
|
|
969
|
+
if (!internalTracks) return 1;
|
|
970
|
+
let number = 0;
|
|
971
|
+
for (const track of internalTracks) {
|
|
972
|
+
if (track.info.type === internalTrack.info.type) number++;
|
|
973
|
+
if (track === internalTrack) break;
|
|
974
|
+
}
|
|
975
|
+
return number;
|
|
976
|
+
};
|
|
977
|
+
const addWholeResourceSegment = (track, url, duration) => {
|
|
978
|
+
track.mediaSegments.push({
|
|
979
|
+
sequenceNumber: 0,
|
|
980
|
+
duration,
|
|
981
|
+
url,
|
|
982
|
+
encryption: null
|
|
983
|
+
});
|
|
984
|
+
};
|
|
985
|
+
const appendDashSegment = (track, segment) => {
|
|
986
|
+
track.mediaSegments.push(segment);
|
|
987
|
+
};
|
|
988
|
+
const createDashRangedSegment = (url, sequenceNumber, range) => {
|
|
989
|
+
const segment = {
|
|
990
|
+
sequenceNumber,
|
|
991
|
+
duration: 0,
|
|
992
|
+
url,
|
|
993
|
+
encryption: null
|
|
994
|
+
};
|
|
995
|
+
if (range) {
|
|
996
|
+
const [start, expect] = parseDashRange(range);
|
|
997
|
+
segment.startRange = start;
|
|
998
|
+
segment.expectLength = expect;
|
|
999
|
+
}
|
|
1000
|
+
return segment;
|
|
1001
|
+
};
|
|
1002
|
+
const getSegmentTimelineEntries = (timeline, timescale, limit) => {
|
|
1003
|
+
const entries = [];
|
|
1004
|
+
let currentTime = 0;
|
|
1005
|
+
for (const entry of getDirectDashChildren(timeline, "S")) {
|
|
1006
|
+
const duration = Number(entry.getAttribute("d"));
|
|
1007
|
+
if (!Number.isFinite(duration)) continue;
|
|
1008
|
+
const startTime = entry.getAttribute("t");
|
|
1009
|
+
if (startTime) currentTime = Number(startTime);
|
|
1010
|
+
let repeatCount = Number(entry.getAttribute("r"));
|
|
1011
|
+
if (!Number.isFinite(repeatCount)) repeatCount = 0;
|
|
1012
|
+
const remaining = limit - entries.length;
|
|
1013
|
+
if (remaining <= 0) break;
|
|
1014
|
+
const totalEntries = repeatCount < 0 ? remaining : Math.min(repeatCount + 1, remaining);
|
|
1015
|
+
for (let i = 0; i < totalEntries; i++) {
|
|
1016
|
+
entries.push({
|
|
1017
|
+
duration: duration / timescale,
|
|
1018
|
+
timestamp: currentTime / timescale
|
|
1019
|
+
});
|
|
1020
|
+
currentTime += duration;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return entries;
|
|
1024
|
+
};
|
|
1025
|
+
const getRoleType = (roleValue) => {
|
|
1026
|
+
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
1027
|
+
return ROLE_TYPE[roleValue.split("-").map(capitalize).join("")];
|
|
1028
|
+
};
|
|
1029
|
+
const createDashTrack = (params) => {
|
|
1030
|
+
const { adaptationSet, contentType, frameRate, isLive, mimeType, period, representation, timeShiftBufferDepth } = params;
|
|
1031
|
+
const bitrate = params.bitrate;
|
|
1032
|
+
const descriptor = createDashTrackDescriptor({
|
|
1033
|
+
codecs: representation.getAttribute("codecs") || adaptationSet.getAttribute("codecs"),
|
|
1034
|
+
contentType,
|
|
1035
|
+
mimeType
|
|
1036
|
+
});
|
|
1037
|
+
const track = {
|
|
1038
|
+
type: descriptor.type,
|
|
1039
|
+
codec: descriptor.codec,
|
|
1040
|
+
codecString: descriptor.codecString,
|
|
1041
|
+
peakBitrate: bitrate,
|
|
1042
|
+
averageBitrate: bitrate,
|
|
1043
|
+
name: null,
|
|
1044
|
+
default: false,
|
|
1045
|
+
groupId: representation.getAttribute("id"),
|
|
1046
|
+
periodId: period.getAttribute("id"),
|
|
1047
|
+
extension: null,
|
|
1048
|
+
isLive,
|
|
1049
|
+
refreshIntervalMs: getDashRefreshIntervalMs(timeShiftBufferDepth),
|
|
1050
|
+
initSegment: null,
|
|
1051
|
+
mediaSegments: []
|
|
1052
|
+
};
|
|
1053
|
+
const roles = getDashTagAttrs("Role", representation, adaptationSet);
|
|
1054
|
+
const supplementalProps = getDashTagAttrs("SupplementalProperty", representation, adaptationSet);
|
|
1055
|
+
const essentialProps = getDashTagAttrs("EssentialProperty", representation, adaptationSet);
|
|
1056
|
+
const accessibilities = getDashTagAttrs("Accessibility", representation, adaptationSet);
|
|
1057
|
+
const channelsString = getDashTagAttrs("AudioChannelConfiguration", representation, adaptationSet)[0]?.value;
|
|
1058
|
+
const width = representation.getAttribute("width");
|
|
1059
|
+
const height = representation.getAttribute("height");
|
|
1060
|
+
track.languageCode = representation.getAttribute("lang") || adaptationSet.getAttribute("lang") || void 0;
|
|
1061
|
+
const volumeAdjust = representation.getAttribute("volumeAdjust");
|
|
1062
|
+
if (volumeAdjust) track.groupId = `${track.groupId}-${volumeAdjust}`;
|
|
1063
|
+
const actualMimeType = representation.getAttribute("mimeType") || adaptationSet.getAttribute("mimeType");
|
|
1064
|
+
if (actualMimeType) {
|
|
1065
|
+
const mimeTypeSplit = actualMimeType.split("/");
|
|
1066
|
+
track.extension = mimeTypeSplit.length === 2 ? mimeTypeSplit[1] : null;
|
|
1067
|
+
}
|
|
1068
|
+
if (track.type === "video") {
|
|
1069
|
+
if (width) track.width = Number(width);
|
|
1070
|
+
if (height) track.height = Number(height);
|
|
1071
|
+
track.frameRate = frameRate ?? getDashFrameRate(representation);
|
|
1072
|
+
if (track.codecString && supplementalProps && essentialProps) track.dynamicRange = parseDynamicRange(track.codecString, supplementalProps, essentialProps);
|
|
1073
|
+
} else if (track.type === "audio") {
|
|
1074
|
+
if (accessibilities) track.descriptive = checkIsDescriptive(accessibilities);
|
|
1075
|
+
if (supplementalProps) track.joc = getDolbyDigitalPlusComplexityIndex(supplementalProps);
|
|
1076
|
+
if (channelsString) track.numberOfChannels = parseChannels(channelsString);
|
|
1077
|
+
} else {
|
|
1078
|
+
if (roles) track.cc = checkIsClosedCaption(roles);
|
|
1079
|
+
if (accessibilities) track.sdh = checkIsSdh(accessibilities);
|
|
1080
|
+
}
|
|
1081
|
+
const role = roles[0];
|
|
1082
|
+
if (role?.value) track.role = getRoleType(role.value);
|
|
1083
|
+
return track;
|
|
1084
|
+
};
|
|
1085
|
+
const normalizeDashTrackExtension = (track) => {
|
|
1086
|
+
if (track.type === "subtitle" && track.extension === "mp4") track.extension = "m4s";
|
|
1087
|
+
if (track.type !== "subtitle" && (track.extension == null || track.mediaSegments.length > 1)) track.extension = "m4s";
|
|
1088
|
+
};
|
|
1089
|
+
const getDashSegmentSourceChild = (tag, representation, adaptationSet, period) => getInheritedDashChild(tag, representation, adaptationSet, period);
|
|
1090
|
+
const applySegmentBase = (params) => {
|
|
1091
|
+
const { adaptationSet, period, representation, track, segmentBaseUrl } = params;
|
|
1092
|
+
const segmentBaseElement = getDashSegmentSourceChild("SegmentBase", representation, adaptationSet, period);
|
|
1093
|
+
if (!segmentBaseElement) return;
|
|
1094
|
+
const initialization = getDirectDashChild(segmentBaseElement, "Initialization");
|
|
1095
|
+
if (!initialization) return;
|
|
1096
|
+
track.initSegment = createDashRangedSegment(combineUrl(segmentBaseUrl, initialization.getAttribute("sourceURL") || ""), -1, initialization.getAttribute("range"));
|
|
1097
|
+
};
|
|
1098
|
+
const applySegmentList = (params) => {
|
|
1099
|
+
const { adaptationSet, period, representation, track, segmentBaseUrl } = params;
|
|
1100
|
+
const segmentList = getDashSegmentSourceChild("SegmentList", representation, adaptationSet, period);
|
|
1101
|
+
if (!segmentList) return;
|
|
1102
|
+
const initialization = getDirectDashChild(segmentList, "Initialization");
|
|
1103
|
+
if (initialization) track.initSegment = createDashRangedSegment(combineUrl(segmentBaseUrl, initialization.getAttribute("sourceURL") || ""), -1, initialization.getAttribute("range"));
|
|
1104
|
+
const timescale = Number(segmentList.getAttribute("timescale") || "1");
|
|
1105
|
+
const segmentUrls = getDirectDashChildren(segmentList, "SegmentURL");
|
|
1106
|
+
const segmentTimeline = getDirectDashChild(segmentList, "SegmentTimeline");
|
|
1107
|
+
const timelineEntries = segmentTimeline ? getSegmentTimelineEntries(segmentTimeline, timescale, segmentUrls.length) : null;
|
|
1108
|
+
const fixedDurationAttr = segmentList.getAttribute("duration");
|
|
1109
|
+
const fixedDuration = fixedDurationAttr ? Number(fixedDurationAttr) : NaN;
|
|
1110
|
+
for (const [segmentIndex, segmentUrl] of segmentUrls.entries()) {
|
|
1111
|
+
const media = segmentUrl.getAttribute("media");
|
|
1112
|
+
if (!media) continue;
|
|
1113
|
+
const duration = timelineEntries?.[segmentIndex]?.duration ?? fixedDuration / timescale;
|
|
1114
|
+
if (!Number.isFinite(duration)) break;
|
|
1115
|
+
const segment = createDashRangedSegment(combineUrl(segmentBaseUrl, media), segmentIndex, segmentUrl.getAttribute("mediaRange"));
|
|
1116
|
+
segment.timestamp = timelineEntries?.[segmentIndex]?.timestamp;
|
|
1117
|
+
segment.duration = duration;
|
|
1118
|
+
appendDashSegment(track, segment);
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const appendTemplatedSegment = (params) => {
|
|
1122
|
+
const { currentTime, duration, index, mediaTemplate, track } = params;
|
|
1123
|
+
const { segmentNumber, segBaseUrl, timescale, variables } = params;
|
|
1124
|
+
variables[DASH_TEMPLATE_TIME] = String(currentTime);
|
|
1125
|
+
variables[DASH_TEMPLATE_NUMBER] = String(segmentNumber);
|
|
1126
|
+
appendDashSegment(track, {
|
|
1127
|
+
sequenceNumber: index,
|
|
1128
|
+
timestamp: currentTime / timescale,
|
|
1129
|
+
duration: duration / timescale,
|
|
1130
|
+
url: combineUrl(segBaseUrl, replaceDashVariables(mediaTemplate, variables)),
|
|
1131
|
+
encryption: null
|
|
1132
|
+
});
|
|
1133
|
+
};
|
|
1134
|
+
const applySegmentTimeline = (params) => {
|
|
1135
|
+
const { mediaTemplate, periodDurationSeconds, track, segBaseUrl } = params;
|
|
1136
|
+
const { startNumberString, timeline, timescaleString, variables } = params;
|
|
1137
|
+
const timelineEntries = getDirectDashChildren(timeline, "S");
|
|
1138
|
+
const timescale = Number(timescaleString);
|
|
1139
|
+
let segmentNumber = Number(startNumberString);
|
|
1140
|
+
let currentTime = 0;
|
|
1141
|
+
let segmentIndex = 0;
|
|
1142
|
+
for (const entry of timelineEntries) {
|
|
1143
|
+
const startTime = entry.getAttribute("t");
|
|
1144
|
+
if (startTime) currentTime = Number(startTime);
|
|
1145
|
+
const duration = Number(entry.getAttribute("d"));
|
|
1146
|
+
let repeatCount = Number(entry.getAttribute("r"));
|
|
1147
|
+
appendTemplatedSegment({
|
|
1148
|
+
currentTime,
|
|
1149
|
+
duration,
|
|
1150
|
+
index: segmentIndex++,
|
|
1151
|
+
mediaTemplate,
|
|
1152
|
+
track,
|
|
1153
|
+
segmentNumber: segmentNumber++,
|
|
1154
|
+
segBaseUrl,
|
|
1155
|
+
timescale,
|
|
1156
|
+
variables
|
|
1157
|
+
});
|
|
1158
|
+
if (repeatCount < 0) repeatCount = Math.ceil(periodDurationSeconds * timescale / duration) - 1;
|
|
1159
|
+
for (let i = 0; i < repeatCount; i++) {
|
|
1160
|
+
currentTime += duration;
|
|
1161
|
+
appendTemplatedSegment({
|
|
1162
|
+
currentTime,
|
|
1163
|
+
duration,
|
|
1164
|
+
index: segmentIndex++,
|
|
1165
|
+
mediaTemplate,
|
|
1166
|
+
track,
|
|
1167
|
+
segmentNumber: segmentNumber++,
|
|
1168
|
+
segBaseUrl,
|
|
1169
|
+
timescale,
|
|
1170
|
+
variables
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
currentTime += duration;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
const applyFixedDurationTemplate = (params) => {
|
|
1177
|
+
const { availabilityStartTime, durationString, isLive, mediaTemplate, periodDurationSeconds } = params;
|
|
1178
|
+
const { presentationTimeOffset, segBaseUrl, startNumberString, timeShiftBufferDepth } = params;
|
|
1179
|
+
const { timescaleString, track, variables } = params;
|
|
1180
|
+
const timescale = Number(timescaleString);
|
|
1181
|
+
const duration = Number(durationString);
|
|
1182
|
+
let startNumber = Number(startNumberString);
|
|
1183
|
+
let totalNumber = Math.ceil(periodDurationSeconds * timescale / duration);
|
|
1184
|
+
if (totalNumber === 0 && isLive) {
|
|
1185
|
+
if (!availabilityStartTime) throw new Error("Invalid live MPD: availabilityStartTime is required.");
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
const availableTime = new Date(availabilityStartTime);
|
|
1188
|
+
const offsetMs = Number(presentationTimeOffset) / 1e3;
|
|
1189
|
+
availableTime.setUTCMilliseconds(availableTime.getUTCMilliseconds() + offsetMs);
|
|
1190
|
+
const elapsedSeconds = (now - availableTime.getTime()) / 1e3;
|
|
1191
|
+
const updateWindowSeconds = Temporal.Duration.from(timeShiftBufferDepth).total("seconds");
|
|
1192
|
+
startNumber += (elapsedSeconds - updateWindowSeconds) * timescale / duration;
|
|
1193
|
+
totalNumber = updateWindowSeconds * timescale / duration;
|
|
1194
|
+
}
|
|
1195
|
+
for (let number = startNumber, segmentIndex = 0; number < startNumber + totalNumber; number++) {
|
|
1196
|
+
variables[DASH_TEMPLATE_TIME] = String((number - startNumber) * duration);
|
|
1197
|
+
variables[DASH_TEMPLATE_NUMBER] = String(number);
|
|
1198
|
+
appendDashSegment(track, {
|
|
1199
|
+
sequenceNumber: isLive ? number : segmentIndex++,
|
|
1200
|
+
timestamp: (number - startNumber) * (duration / timescale),
|
|
1201
|
+
duration: duration / timescale,
|
|
1202
|
+
url: combineUrl(segBaseUrl, replaceDashVariables(mediaTemplate, variables)),
|
|
1203
|
+
encryption: null
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
const applySegmentTemplate = (params) => {
|
|
1208
|
+
const { adaptationSet, availabilityStartTime, bitrate, isLive, period } = params;
|
|
1209
|
+
const { periodDurationSeconds, representation, representationId, segBaseUrl, track, timeShiftBufferDepth } = params;
|
|
1210
|
+
const segmentTemplates = [
|
|
1211
|
+
getDirectDashChild(representation, "SegmentTemplate"),
|
|
1212
|
+
getDirectDashChild(adaptationSet, "SegmentTemplate"),
|
|
1213
|
+
getDirectDashChild(period, "SegmentTemplate")
|
|
1214
|
+
].filter((template) => !!template);
|
|
1215
|
+
if (!segmentTemplates[0]) return;
|
|
1216
|
+
const getTemplateAttribute = (name) => {
|
|
1217
|
+
for (const template of segmentTemplates) {
|
|
1218
|
+
const value = template.getAttribute(name);
|
|
1219
|
+
if (value) return value;
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
};
|
|
1223
|
+
const getTemplateTimeline = () => {
|
|
1224
|
+
for (const template of segmentTemplates) {
|
|
1225
|
+
const timeline = getDirectDashChild(template, "SegmentTimeline");
|
|
1226
|
+
if (timeline) return timeline;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const variables = {
|
|
1230
|
+
[DASH_TEMPLATE_BANDWIDTH]: String(bitrate),
|
|
1231
|
+
[DASH_TEMPLATE_REPRESENTATION_ID]: representationId ?? ""
|
|
1232
|
+
};
|
|
1233
|
+
const presentationTimeOffset = getTemplateAttribute("presentationTimeOffset") || "0";
|
|
1234
|
+
const timescaleString = getTemplateAttribute("timescale") || "1";
|
|
1235
|
+
const durationString = getTemplateAttribute("duration");
|
|
1236
|
+
const startNumberString = getTemplateAttribute("startNumber") || "1";
|
|
1237
|
+
const initialization = getTemplateAttribute("initialization");
|
|
1238
|
+
if (initialization) track.initSegment = {
|
|
1239
|
+
sequenceNumber: -1,
|
|
1240
|
+
duration: 0,
|
|
1241
|
+
url: combineUrl(segBaseUrl, replaceDashVariables(initialization, variables)),
|
|
1242
|
+
encryption: null
|
|
1243
|
+
};
|
|
1244
|
+
const mediaTemplate = getTemplateAttribute("media");
|
|
1245
|
+
if (!mediaTemplate) return;
|
|
1246
|
+
const segmentTimeline = getTemplateTimeline();
|
|
1247
|
+
if (segmentTimeline) {
|
|
1248
|
+
applySegmentTimeline({
|
|
1249
|
+
mediaTemplate,
|
|
1250
|
+
periodDurationSeconds,
|
|
1251
|
+
track,
|
|
1252
|
+
segBaseUrl,
|
|
1253
|
+
startNumberString,
|
|
1254
|
+
timeline: segmentTimeline,
|
|
1255
|
+
timescaleString,
|
|
1256
|
+
variables
|
|
1257
|
+
});
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (!durationString) return;
|
|
1261
|
+
applyFixedDurationTemplate({
|
|
1262
|
+
availabilityStartTime,
|
|
1263
|
+
durationString,
|
|
1264
|
+
isLive,
|
|
1265
|
+
mediaTemplate,
|
|
1266
|
+
periodDurationSeconds,
|
|
1267
|
+
track,
|
|
1268
|
+
presentationTimeOffset,
|
|
1269
|
+
segBaseUrl,
|
|
1270
|
+
startNumberString,
|
|
1271
|
+
timeShiftBufferDepth,
|
|
1272
|
+
timescaleString,
|
|
1273
|
+
variables
|
|
1274
|
+
});
|
|
1275
|
+
};
|
|
1276
|
+
const ensureFallbackMediaSegment = (track, segBaseUrl, periodDurationSeconds) => {
|
|
1277
|
+
if (track.mediaSegments.length > 0) return;
|
|
1278
|
+
addWholeResourceSegment(track, segBaseUrl, periodDurationSeconds);
|
|
1279
|
+
};
|
|
1280
|
+
const cloneEncryption = (encryption) => encryption ? {
|
|
1281
|
+
method: encryption.method,
|
|
1282
|
+
key: encryption.key,
|
|
1283
|
+
iv: encryption.iv,
|
|
1284
|
+
drm: { ...encryption.drm }
|
|
1285
|
+
} : null;
|
|
1286
|
+
const applyContentProtection = (adaptationSet, representation, track) => {
|
|
1287
|
+
const representationProtections = getDirectDashChildren(representation, "ContentProtection");
|
|
1288
|
+
const adaptationSetProtections = getDirectDashChildren(adaptationSet, "ContentProtection");
|
|
1289
|
+
const contentProtections = representationProtections.length ? representationProtections : adaptationSetProtections;
|
|
1290
|
+
if (!contentProtections.length) return;
|
|
1291
|
+
const encryption = {
|
|
1292
|
+
method: ENCRYPT_METHODS.CENC,
|
|
1293
|
+
drm: {}
|
|
1294
|
+
};
|
|
1295
|
+
for (const contentProtection of contentProtections) {
|
|
1296
|
+
const schemeIdUri = contentProtection.getAttribute("schemeIdUri");
|
|
1297
|
+
const drmData = {
|
|
1298
|
+
keyId: contentProtection.getAttribute("cenc:default_KID") || void 0,
|
|
1299
|
+
pssh: getDirectDashChild(contentProtection, "cenc:pssh")?.textContent?.trim() || void 0
|
|
1300
|
+
};
|
|
1301
|
+
if (schemeIdUri?.includes(WIDEVINE_SYSTEM_ID)) encryption.drm.widevine = drmData;
|
|
1302
|
+
else if (schemeIdUri?.includes(PLAYREADY_SYSTEM_ID)) encryption.drm.playready = drmData;
|
|
1303
|
+
}
|
|
1304
|
+
if (track.initSegment) track.initSegment.encryption = cloneEncryption(encryption);
|
|
1305
|
+
for (const segment of track.mediaSegments) if (!segment.encryption) segment.encryption = cloneEncryption(encryption);
|
|
1306
|
+
};
|
|
1307
|
+
const mergeDashPeriodTrack = (tracks, track, isLive) => {
|
|
1308
|
+
const existingTrackIndex = tracks.findIndex((item) => item.type === track.type && item.periodId !== track.periodId && item.groupId === track.groupId && (item.type === "video" && track.type === "video" ? item.width === track.width && item.height === track.height : true));
|
|
1309
|
+
if (existingTrackIndex < 0) {
|
|
1310
|
+
tracks.push(track);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (isLive) return;
|
|
1314
|
+
const existingTrack = tracks[existingTrackIndex];
|
|
1315
|
+
if (!existingTrack) return;
|
|
1316
|
+
const lastSegment = existingTrack.mediaSegments.at(-1);
|
|
1317
|
+
const incomingSegments = track.mediaSegments;
|
|
1318
|
+
const incomingLastSegment = incomingSegments.at(-1);
|
|
1319
|
+
if (!lastSegment || !incomingLastSegment) return;
|
|
1320
|
+
if (lastSegment.url !== incomingLastSegment.url) {
|
|
1321
|
+
const startIndex = (lastSegment.sequenceNumber ?? 0) + 1;
|
|
1322
|
+
for (const segment of incomingSegments) if (segment.sequenceNumber !== null) segment.sequenceNumber += startIndex;
|
|
1323
|
+
existingTrack.mediaSegments.push(...incomingSegments);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
lastSegment.duration += incomingSegments.reduce((sum, segment) => sum + segment.duration, 0);
|
|
1327
|
+
};
|
|
1328
|
+
const linkDefaultDashGroups = (tracks) => {
|
|
1329
|
+
const audioList = tracks.filter((track) => track.type === "audio");
|
|
1330
|
+
const subtitleList = tracks.filter((track) => track.type === "subtitle");
|
|
1331
|
+
const videoList = tracks.filter((track) => track.type === "video");
|
|
1332
|
+
for (const video of videoList) {
|
|
1333
|
+
const audioGroupId = audioList.toSorted((a, b) => (b.peakBitrate || 0) - (a.peakBitrate || 0)).at(0)?.groupId;
|
|
1334
|
+
const subtitleGroupId = subtitleList.toSorted((a, b) => (b.peakBitrate || 0) - (a.peakBitrate || 0)).at(0)?.groupId;
|
|
1335
|
+
if (audioGroupId) video.audioGroupId = audioGroupId;
|
|
1336
|
+
if (subtitleGroupId) video.subtitleGroupId = subtitleGroupId;
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
var DashDemuxer = class {
|
|
1340
|
+
input;
|
|
1341
|
+
metadataPromise = null;
|
|
1342
|
+
trackBackings = null;
|
|
1343
|
+
internalTracks = null;
|
|
1344
|
+
segmentedInputs = [];
|
|
1345
|
+
manifestUrl = "";
|
|
1346
|
+
originalUrl = "";
|
|
1347
|
+
headers;
|
|
1348
|
+
mpdUrl = "";
|
|
1349
|
+
baseUrl = "";
|
|
1350
|
+
constructor(input) {
|
|
1351
|
+
this.input = input;
|
|
1352
|
+
this.headers = getSourceHeaders(input.source);
|
|
1353
|
+
}
|
|
1354
|
+
readMetadata() {
|
|
1355
|
+
return this.metadataPromise ??= (async () => {
|
|
1356
|
+
const { text, url } = await loadDashManifest(this.input.source);
|
|
1357
|
+
this.manifestUrl = url;
|
|
1358
|
+
this.originalUrl = url;
|
|
1359
|
+
this.resetManifestUrls();
|
|
1360
|
+
const tracks = this.extractTracks(text.trim());
|
|
1361
|
+
const internalTracks = createInternalTracks(this, tracks);
|
|
1362
|
+
this.internalTracks = internalTracks;
|
|
1363
|
+
this.trackBackings = createTrackBackings(internalTracks);
|
|
1364
|
+
})();
|
|
1365
|
+
}
|
|
1366
|
+
async getTrackBackings() {
|
|
1367
|
+
await this.readMetadata();
|
|
1368
|
+
if (!this.trackBackings) throw new Error("DASH track metadata did not initialize correctly.");
|
|
1369
|
+
return this.trackBackings;
|
|
1370
|
+
}
|
|
1371
|
+
getSegmentedInputForTrack(track) {
|
|
1372
|
+
let segmentedInput = this.segmentedInputs.find((value) => value.internalTrack === track);
|
|
1373
|
+
if (segmentedInput) return segmentedInput;
|
|
1374
|
+
segmentedInput = new DashSegmentedInput(track);
|
|
1375
|
+
this.segmentedInputs.push(segmentedInput);
|
|
1376
|
+
return segmentedInput;
|
|
1377
|
+
}
|
|
1378
|
+
async refreshTrackSegments(track) {
|
|
1379
|
+
await this.readMetadata();
|
|
1380
|
+
if (!track.track.isLive) return;
|
|
1381
|
+
if (!this.manifestUrl.startsWith("http://") && !this.manifestUrl.startsWith("https://")) return;
|
|
1382
|
+
const tracks = this.internalTracks?.map((internalTrack) => internalTrack.track) ?? [];
|
|
1383
|
+
await this.refreshTracks(tracks);
|
|
1384
|
+
}
|
|
1385
|
+
async getMimeType() {
|
|
1386
|
+
return DASH.mimeType;
|
|
1387
|
+
}
|
|
1388
|
+
async getMetadataTags() {
|
|
1389
|
+
return {};
|
|
1390
|
+
}
|
|
1391
|
+
dispose() {
|
|
1392
|
+
this.segmentedInputs.length = 0;
|
|
1393
|
+
}
|
|
1394
|
+
extractTracks(rawText) {
|
|
1395
|
+
const manifest = this.parseManifest(rawText);
|
|
1396
|
+
const tracks = [];
|
|
1397
|
+
for (const period of getDirectDashChildren(manifest.mpdElement, "Period")) this.appendPeriodTracks(tracks, manifest, period);
|
|
1398
|
+
linkDefaultDashGroups(tracks);
|
|
1399
|
+
return tracks;
|
|
1400
|
+
}
|
|
1401
|
+
parseManifest(rawText) {
|
|
1402
|
+
const mpdContent = processDashContent(rawText);
|
|
1403
|
+
const mpdElement = new DOMParser().parseFromString(mpdContent, "text/xml").getElementsByTagName("MPD")[0];
|
|
1404
|
+
const manifest = {
|
|
1405
|
+
mpdElement,
|
|
1406
|
+
isLive: mpdElement.getAttribute("type") === "dynamic",
|
|
1407
|
+
availabilityStartTime: mpdElement.getAttribute("availabilityStartTime"),
|
|
1408
|
+
timeShiftBufferDepth: mpdElement.getAttribute("timeShiftBufferDepth") || "PT1M",
|
|
1409
|
+
mediaPresentationDuration: mpdElement.getAttribute("mediaPresentationDuration")
|
|
1410
|
+
};
|
|
1411
|
+
const baseUrlElement = getDirectDashChild(mpdElement, "BaseURL");
|
|
1412
|
+
if (baseUrlElement?.textContent) {
|
|
1413
|
+
let baseUrl = baseUrlElement.textContent;
|
|
1414
|
+
if (baseUrl.includes("kkbox.com.tw/")) baseUrl = baseUrl.replace("//https:%2F%2F", "//");
|
|
1415
|
+
this.baseUrl = combineUrl(this.mpdUrl, baseUrl);
|
|
1416
|
+
}
|
|
1417
|
+
return manifest;
|
|
1418
|
+
}
|
|
1419
|
+
appendPeriodTracks(tracks, manifest, period) {
|
|
1420
|
+
const periodDurationSeconds = Temporal.Duration.from(period.getAttribute("duration") || manifest.mediaPresentationDuration || "PT0S").total("seconds");
|
|
1421
|
+
const periodBaseUrl = extendDashBaseUrl(period, this.baseUrl);
|
|
1422
|
+
for (const adaptationSet of getDirectDashChildren(period, "AdaptationSet")) this.appendAdaptationSetTracks({
|
|
1423
|
+
tracks,
|
|
1424
|
+
manifest,
|
|
1425
|
+
period,
|
|
1426
|
+
periodDurationSeconds,
|
|
1427
|
+
periodBaseUrl,
|
|
1428
|
+
adaptationSet
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
appendAdaptationSetTracks(params) {
|
|
1432
|
+
const { tracks, manifest, period, periodDurationSeconds, periodBaseUrl, adaptationSet } = params;
|
|
1433
|
+
const adaptationSetBaseUrl = extendDashBaseUrl(adaptationSet, periodBaseUrl);
|
|
1434
|
+
const adaptationSetFrameRate = getDashFrameRate(adaptationSet);
|
|
1435
|
+
let contentType = adaptationSet.getAttribute("contentType");
|
|
1436
|
+
let mimeType = adaptationSet.getAttribute("mimeType");
|
|
1437
|
+
for (const representation of getDirectDashChildren(adaptationSet, "Representation")) {
|
|
1438
|
+
const segmentBaseUrl = extendDashBaseUrl(representation, adaptationSetBaseUrl);
|
|
1439
|
+
contentType ||= representation.getAttribute("contentType");
|
|
1440
|
+
mimeType ||= representation.getAttribute("mimeType");
|
|
1441
|
+
const bitrate = Number(representation.getAttribute("bandwidth") ?? "");
|
|
1442
|
+
const track = this.createTrack({
|
|
1443
|
+
adaptationSet,
|
|
1444
|
+
representation,
|
|
1445
|
+
period,
|
|
1446
|
+
manifest,
|
|
1447
|
+
bitrate,
|
|
1448
|
+
contentType,
|
|
1449
|
+
mimeType,
|
|
1450
|
+
frameRate: adaptationSetFrameRate
|
|
1451
|
+
});
|
|
1452
|
+
this.populateTrackSegments({
|
|
1453
|
+
adaptationSet,
|
|
1454
|
+
period,
|
|
1455
|
+
representation,
|
|
1456
|
+
track,
|
|
1457
|
+
manifest,
|
|
1458
|
+
segmentBaseUrl,
|
|
1459
|
+
periodDurationSeconds,
|
|
1460
|
+
bitrate
|
|
1461
|
+
});
|
|
1462
|
+
mergeDashPeriodTrack(tracks, track, manifest.isLive);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
createTrack(params) {
|
|
1466
|
+
const { adaptationSet, representation, period, manifest, bitrate, contentType, mimeType, frameRate } = params;
|
|
1467
|
+
return createDashTrack({
|
|
1468
|
+
adaptationSet,
|
|
1469
|
+
bitrate,
|
|
1470
|
+
contentType,
|
|
1471
|
+
frameRate,
|
|
1472
|
+
isLive: manifest.isLive,
|
|
1473
|
+
mimeType,
|
|
1474
|
+
period,
|
|
1475
|
+
representation,
|
|
1476
|
+
timeShiftBufferDepth: manifest.timeShiftBufferDepth
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
populateTrackSegments(params) {
|
|
1480
|
+
const { adaptationSet, period, representation, track, manifest, segmentBaseUrl, periodDurationSeconds, bitrate } = params;
|
|
1481
|
+
applySegmentBase({
|
|
1482
|
+
adaptationSet,
|
|
1483
|
+
period,
|
|
1484
|
+
representation,
|
|
1485
|
+
track,
|
|
1486
|
+
segmentBaseUrl
|
|
1487
|
+
});
|
|
1488
|
+
applySegmentList({
|
|
1489
|
+
adaptationSet,
|
|
1490
|
+
period,
|
|
1491
|
+
representation,
|
|
1492
|
+
track,
|
|
1493
|
+
segmentBaseUrl
|
|
1494
|
+
});
|
|
1495
|
+
applySegmentTemplate({
|
|
1496
|
+
adaptationSet,
|
|
1497
|
+
availabilityStartTime: manifest.availabilityStartTime,
|
|
1498
|
+
bitrate,
|
|
1499
|
+
isLive: manifest.isLive,
|
|
1500
|
+
period,
|
|
1501
|
+
periodDurationSeconds,
|
|
1502
|
+
representationId: representation.getAttribute("id"),
|
|
1503
|
+
representation,
|
|
1504
|
+
segBaseUrl: segmentBaseUrl,
|
|
1505
|
+
track,
|
|
1506
|
+
timeShiftBufferDepth: manifest.timeShiftBufferDepth
|
|
1507
|
+
});
|
|
1508
|
+
ensureFallbackMediaSegment(track, segmentBaseUrl, periodDurationSeconds);
|
|
1509
|
+
normalizeDashTrackExtension(track);
|
|
1510
|
+
applyContentProtection(adaptationSet, representation, track);
|
|
1511
|
+
}
|
|
1512
|
+
findMatchingTrack(nextTracks, currentTrack) {
|
|
1513
|
+
let matchingTracks = nextTracks.filter((candidate) => getDashTrackMatchKey(candidate) === getDashTrackMatchKey(currentTrack));
|
|
1514
|
+
const currentInitSegmentUrl = currentTrack.initSegment?.url;
|
|
1515
|
+
if (!matchingTracks.length && currentInitSegmentUrl) matchingTracks = nextTracks.filter((candidate) => candidate.initSegment?.url === currentInitSegmentUrl);
|
|
1516
|
+
return matchingTracks[0];
|
|
1517
|
+
}
|
|
1518
|
+
async refreshTracks(tracks) {
|
|
1519
|
+
if (!tracks.length) return;
|
|
1520
|
+
const response = await this.fetchManifest(this.manifestUrl).catch(() => this.fetchManifest(this.originalUrl));
|
|
1521
|
+
const rawText = await response.text();
|
|
1522
|
+
this.manifestUrl = response.url;
|
|
1523
|
+
this.resetManifestUrls();
|
|
1524
|
+
const nextTracks = this.extractTracks(rawText);
|
|
1525
|
+
for (const track of tracks) {
|
|
1526
|
+
const nextTrack = this.findMatchingTrack(nextTracks, track);
|
|
1527
|
+
if (!nextTrack) continue;
|
|
1528
|
+
track.isLive = nextTrack.isLive;
|
|
1529
|
+
track.refreshIntervalMs = nextTrack.refreshIntervalMs;
|
|
1530
|
+
track.periodId = nextTrack.periodId;
|
|
1531
|
+
track.initSegment = nextTrack.initSegment;
|
|
1532
|
+
track.mediaSegments = nextTrack.mediaSegments;
|
|
1533
|
+
track.audioGroupId = nextTrack.audioGroupId;
|
|
1534
|
+
track.subtitleGroupId = nextTrack.subtitleGroupId;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
async fetchManifest(url) {
|
|
1538
|
+
const response = await fetch(url, { headers: this.headers });
|
|
1539
|
+
if (!response.ok) throw new Error(`Failed to fetch DASH manifest: ${response.status} ${response.statusText} (${response.url})`);
|
|
1540
|
+
return response;
|
|
1541
|
+
}
|
|
1542
|
+
resetManifestUrls() {
|
|
1543
|
+
this.mpdUrl = this.manifestUrl;
|
|
1544
|
+
this.baseUrl = this.mpdUrl;
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
var DashTrackBackingBase = class {
|
|
1548
|
+
internalTrack;
|
|
1549
|
+
hydratedTrackPromise = null;
|
|
1550
|
+
constructor(internalTrack) {
|
|
1551
|
+
this.internalTrack = internalTrack;
|
|
1552
|
+
}
|
|
1553
|
+
hydrate() {
|
|
1554
|
+
return this.hydratedTrackPromise ??= this.getSegmentedInput().getFirstTrack();
|
|
1555
|
+
}
|
|
1556
|
+
delegate(fn) {
|
|
1557
|
+
if (this.hydratedTrackPromise) return this.hydratedTrackPromise.then(fn);
|
|
1558
|
+
return this.hydrate().then(fn);
|
|
1559
|
+
}
|
|
1560
|
+
getId() {
|
|
1561
|
+
return this.internalTrack.id;
|
|
1562
|
+
}
|
|
1563
|
+
getNumber() {
|
|
1564
|
+
return getTrackNumber(this.internalTrack);
|
|
1565
|
+
}
|
|
1566
|
+
getCodec() {
|
|
1567
|
+
return this.internalTrack.track.codec ?? null;
|
|
1568
|
+
}
|
|
1569
|
+
getInternalCodecId() {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
getName() {
|
|
1573
|
+
return this.internalTrack.track.name;
|
|
1574
|
+
}
|
|
1575
|
+
getLanguageCode() {
|
|
1576
|
+
return this.internalTrack.track.languageCode ?? "und";
|
|
1577
|
+
}
|
|
1578
|
+
getTimeResolution() {
|
|
1579
|
+
return this.delegate((track) => track.getTimeResolution());
|
|
1580
|
+
}
|
|
1581
|
+
isRelativeToUnixEpoch() {
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
getDisposition() {
|
|
1585
|
+
return getDisposition(this.internalTrack.track);
|
|
1586
|
+
}
|
|
1587
|
+
getPairingMask() {
|
|
1588
|
+
return this.internalTrack.pairingMask;
|
|
1589
|
+
}
|
|
1590
|
+
getBitrate() {
|
|
1591
|
+
return this.internalTrack.track.peakBitrate;
|
|
1592
|
+
}
|
|
1593
|
+
getAverageBitrate() {
|
|
1594
|
+
return this.internalTrack.track.averageBitrate;
|
|
1595
|
+
}
|
|
1596
|
+
async getDurationFromMetadata(_options) {
|
|
1597
|
+
return this.internalTrack.track.mediaSegments.reduce((sum, segment) => sum + segment.duration, 0);
|
|
1598
|
+
}
|
|
1599
|
+
async getLiveRefreshInterval() {
|
|
1600
|
+
if (!this.internalTrack.track.isLive) return null;
|
|
1601
|
+
return this.internalTrack.track.refreshIntervalMs / 1e3;
|
|
1602
|
+
}
|
|
1603
|
+
getHasOnlyKeyPackets() {
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
async getDecoderConfig() {
|
|
1607
|
+
return this.getSegmentedInput().getDecoderConfig();
|
|
1608
|
+
}
|
|
1609
|
+
getMetadataCodecParameterString() {
|
|
1610
|
+
return this.internalTrack.track.codecString;
|
|
1611
|
+
}
|
|
1612
|
+
async getFirstPacket(options) {
|
|
1613
|
+
return this.getSegmentedInput().getFirstPacket(options);
|
|
1614
|
+
}
|
|
1615
|
+
async getPacket(timestamp, options) {
|
|
1616
|
+
return this.getSegmentedInput().getPacket(timestamp, options);
|
|
1617
|
+
}
|
|
1618
|
+
async getNextPacket(packet, options) {
|
|
1619
|
+
return this.getSegmentedInput().getNextPacket(packet, options);
|
|
1620
|
+
}
|
|
1621
|
+
async getKeyPacket(timestamp, options) {
|
|
1622
|
+
return this.getSegmentedInput().getKeyPacket(timestamp, options);
|
|
1623
|
+
}
|
|
1624
|
+
async getNextKeyPacket(packet, options) {
|
|
1625
|
+
return this.getSegmentedInput().getNextKeyPacket(packet, options);
|
|
1626
|
+
}
|
|
1627
|
+
getSegmentedInput() {
|
|
1628
|
+
return this.internalTrack.demuxer.getSegmentedInputForTrack(this.internalTrack);
|
|
1629
|
+
}
|
|
1630
|
+
async getSegments() {
|
|
1631
|
+
const segmentedInput = this.getSegmentedInput();
|
|
1632
|
+
await segmentedInput.runUpdateSegments();
|
|
1633
|
+
return segmentedInput.segments;
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
var DashInputVideoTrackBacking = class extends DashTrackBackingBase {
|
|
1637
|
+
internalTrack;
|
|
1638
|
+
constructor(internalTrack) {
|
|
1639
|
+
super(internalTrack);
|
|
1640
|
+
this.internalTrack = internalTrack;
|
|
1641
|
+
}
|
|
1642
|
+
getType() {
|
|
1643
|
+
return "video";
|
|
1644
|
+
}
|
|
1645
|
+
getCodec() {
|
|
1646
|
+
return this.internalTrack.track.codec ?? null;
|
|
1647
|
+
}
|
|
1648
|
+
getCodedWidth() {
|
|
1649
|
+
return this.internalTrack.info.width ?? this.delegate((track) => track.getCodedWidth());
|
|
1650
|
+
}
|
|
1651
|
+
getCodedHeight() {
|
|
1652
|
+
return this.internalTrack.info.height ?? this.delegate((track) => track.getCodedHeight());
|
|
1653
|
+
}
|
|
1654
|
+
getSquarePixelWidth() {
|
|
1655
|
+
return this.internalTrack.info.width ?? this.delegate((track) => track.getSquarePixelWidth());
|
|
1656
|
+
}
|
|
1657
|
+
getSquarePixelHeight() {
|
|
1658
|
+
return this.internalTrack.info.height ?? this.delegate((track) => track.getSquarePixelHeight());
|
|
1659
|
+
}
|
|
1660
|
+
getMetadataDisplayWidth() {
|
|
1661
|
+
return this.internalTrack.info.width;
|
|
1662
|
+
}
|
|
1663
|
+
getMetadataDisplayHeight() {
|
|
1664
|
+
return this.internalTrack.info.height;
|
|
1665
|
+
}
|
|
1666
|
+
getRotation() {
|
|
1667
|
+
return 0;
|
|
1668
|
+
}
|
|
1669
|
+
async getColorSpace() {
|
|
1670
|
+
return this.delegate((track) => track.getColorSpace());
|
|
1671
|
+
}
|
|
1672
|
+
async canBeTransparent() {
|
|
1673
|
+
return this.delegate((track) => track.canBeTransparent());
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var DashInputAudioTrackBacking = class extends DashTrackBackingBase {
|
|
1677
|
+
internalTrack;
|
|
1678
|
+
constructor(internalTrack) {
|
|
1679
|
+
super(internalTrack);
|
|
1680
|
+
this.internalTrack = internalTrack;
|
|
1681
|
+
}
|
|
1682
|
+
getType() {
|
|
1683
|
+
return "audio";
|
|
1684
|
+
}
|
|
1685
|
+
getCodec() {
|
|
1686
|
+
return this.internalTrack.track.codec ?? null;
|
|
1687
|
+
}
|
|
1688
|
+
getNumberOfChannels() {
|
|
1689
|
+
return this.internalTrack.info.numberOfChannels ?? this.delegate((track) => track.getNumberOfChannels());
|
|
1690
|
+
}
|
|
1691
|
+
getSampleRate() {
|
|
1692
|
+
return this.internalTrack.track.sampleRate ?? this.delegate((track) => track.getSampleRate());
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
var DashInputSubtitleTrackBacking = class extends DashTrackBackingBase {
|
|
1696
|
+
internalTrack;
|
|
1697
|
+
constructor(internalTrack) {
|
|
1698
|
+
super(internalTrack);
|
|
1699
|
+
this.internalTrack = internalTrack;
|
|
1700
|
+
}
|
|
1701
|
+
getType() {
|
|
1702
|
+
return "subtitle";
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
const createTrackBackings = (internalTracks) => internalTracks.map((internalTrack) => {
|
|
1706
|
+
const backing = internalTrack.info.type === "video" ? new DashInputVideoTrackBacking(internalTrack) : internalTrack.info.type === "audio" ? new DashInputAudioTrackBacking(internalTrack) : new DashInputSubtitleTrackBacking(internalTrack);
|
|
1707
|
+
internalTrack.backingTrack = backing;
|
|
1708
|
+
return backing;
|
|
1709
|
+
});
|
|
1710
|
+
var DashInputFormat = class extends InputFormat {
|
|
1711
|
+
get name() {
|
|
1712
|
+
return "Dynamic Adaptive Streaming over HTTP (DASH)";
|
|
1713
|
+
}
|
|
1714
|
+
get mimeType() {
|
|
1715
|
+
return DASH_MIME_TYPE;
|
|
1716
|
+
}
|
|
1717
|
+
async _canReadInput(input) {
|
|
1718
|
+
if (isLikelyDashPath(input.source)) return true;
|
|
1719
|
+
try {
|
|
1720
|
+
const { text } = await loadDashManifest(input.source);
|
|
1721
|
+
return isDashManifestText(text);
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
_createDemuxer(input) {
|
|
1727
|
+
return new DashDemuxer(input);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
const DASH = new DashInputFormat();
|
|
1731
|
+
const DASH_FORMATS = [
|
|
1732
|
+
DASH,
|
|
1733
|
+
MP4,
|
|
1734
|
+
QTFF,
|
|
1735
|
+
WEBM,
|
|
1736
|
+
MATROSKA,
|
|
1737
|
+
MP3,
|
|
1738
|
+
ADTS
|
|
1739
|
+
];
|
|
1740
|
+
//#endregion
|
|
1741
|
+
//#region src/index.ts
|
|
1742
|
+
var Input = class extends SegmentedMediabunnyInput {
|
|
1743
|
+
constructor(options) {
|
|
1744
|
+
super(options);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
const isInput = (value) => value instanceof Input;
|
|
1748
|
+
const getSegmentedInput = (track) => track.getSegmentedInput();
|
|
1749
|
+
const getSegments = async (track) => track.getSegments();
|
|
1750
|
+
//#endregion
|
|
1751
|
+
export { DASH, DASH_FORMATS, FilePathSource, HLS_FORMATS, Input, UrlSource, asc, desc, getSegmentedInput, getSegments, isInput, prefer, preserveSubtitleBackingsOnInput };
|