dasha 3.1.5 → 4.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/dash.js DELETED
@@ -1,516 +0,0 @@
1
- 'use strict';
2
-
3
- const xml = require('./xml');
4
- const { parseDuration, isLanguageTagValid } = require('./util');
5
- const {
6
- parseVideoCodec,
7
- tryParseVideoCodec,
8
- parseDynamicRange,
9
- createVideoTrack,
10
- } = require('./video');
11
- const {
12
- parseAudioCodec,
13
- tryParseAudioCodec,
14
- createAudioTrack,
15
- getDolbyDigitalPlusComplexityIndex,
16
- checkIsDescriptive,
17
- } = require('./audio');
18
- const {
19
- parseSubtitleCodec,
20
- tryParseSubtitleCodec,
21
- checkIsClosedCaption,
22
- checkIsSdh,
23
- checkIsForced,
24
- createSubtitleTrack,
25
- } = require('./subtitle');
26
- const {
27
- createResolutionFilter,
28
- createVideoQualityFilter,
29
- createAudioLanguageFilter,
30
- createSubtitleLanguageFilter,
31
- createVideoCodecFilter,
32
- createAudioCodecFilter,
33
- createAudioChannelsFilter,
34
- } = require('./track');
35
-
36
- const appendUtils = (element) => {
37
- if (!element) return element;
38
- if (Array.isArray(element)) {
39
- element.get = (name) =>
40
- appendUtils(element.find((item) => item.tagName === name));
41
- } else {
42
- element.getAttr = (name) => element.attributes[name];
43
- element.getChild = (name) => {
44
- const tag = element.children.find((item) => item.tagName === name);
45
- const isString = !name && typeof element.children?.[0] === 'string';
46
- return isString ? element.children[0] : appendUtils(tag);
47
- };
48
- element.set = (name, value) => (element.attributes[name] = value);
49
- element.get = (name) => element.getAttr(name) || element.getChild(name);
50
- element.getNumber = (name) => Number(element.find(name));
51
- element.getAll = (name) =>
52
- element.children.filter((item) => item.tagName === name).map(appendUtils);
53
- element.getBaseUrls = () =>
54
- element.getAll('BaseURL').map((item) => item.children[0]);
55
- element.getBaseUrl = () => element.getBaseUrls()[0];
56
- }
57
- return element;
58
- };
59
-
60
- const combineGetters = (representation, adaptationSet) => {
61
- const prevGet = representation.get;
62
- const prevGetAll = representation.getAll;
63
- const get = (name) => prevGet(name) || adaptationSet.get(name);
64
- const getAll = (name) =>
65
- [...prevGetAll(name), ...adaptationSet.getAll(name)].filter(Boolean);
66
- representation.get = get;
67
- representation.getAll = getAll;
68
- return { get, getAll };
69
- };
70
-
71
- const parseBaseUrl = (manifestUrl, mpd, period, representation) => {
72
- let base = mpd.getBaseUrl();
73
- if (!base) base = manifestUrl;
74
- else if (!base.startsWith('https://'))
75
- base = new URL(base, manifestUrl).toString();
76
- if (!!period.getBaseUrl() || !!base)
77
- base = new URL(period.getBaseUrl() || '', base).toString();
78
- const baseUrl = new URL(representation.getBaseUrl() || '', base).toString();
79
- return baseUrl;
80
- };
81
-
82
- const getTrackTypeByCodecs = (codecs) => {
83
- if (tryParseVideoCodec(codecs)) return 'video';
84
- else if (tryParseAudioCodec(codecs)) return 'audio';
85
- else if (tryParseSubtitleCodec(codecs)) return 'text';
86
- return null;
87
- };
88
-
89
- const parseContentTypes = (representation) => {
90
- const codecs = representation.get('codecs');
91
- const mimeType = representation.get('mimeType');
92
- const contentTypeByCodecs = getTrackTypeByCodecs(codecs);
93
- const contentType =
94
- representation.get('contentType') || mimeType?.split('/')[0];
95
- if (!contentType && !mimeType)
96
- throw new Error(
97
- 'Unable to determine the format of a Representation, cannot continue...',
98
- );
99
- return { contentType: contentTypeByCodecs || contentType, mimeType };
100
- };
101
-
102
- const parseCodecs = (representation, contentType, mimeType) => {
103
- const shouldUseCodecsFromMime =
104
- contentType === 'text' && !mimeType.includes('mp4');
105
- const codecs = shouldUseCodecsFromMime
106
- ? mimeType.split('/')[1]
107
- : representation.get('codecs');
108
- return codecs;
109
- };
110
-
111
- const parseLanguage = (representation, adaptationSet, fallbackLanguage) => {
112
- let language = '';
113
- const options = [];
114
- const lang = representation.get('lang');
115
- const id = representation.get('id');
116
- if (representation) {
117
- options.push(lang);
118
- if (id) {
119
- const m = id.match(/\w+_(\w+)=\d+/);
120
- if (m && m[1]) options.push(m[1]);
121
- }
122
- }
123
- options.push(adaptationSet.get('lang'));
124
- if (fallbackLanguage) options.push(fallbackLanguage);
125
- for (const option of options) {
126
- const value = (option || '').trim();
127
- if (!isLanguageTagValid(value) || value.startsWith('und')) continue;
128
- language = value;
129
- continue;
130
- }
131
- if (!language) {
132
- // Language information could not be derived from a Representation.
133
- // TODO: Throw error if language not found
134
- }
135
- return language;
136
- };
137
-
138
- const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
139
-
140
- const identifierReplacement =
141
- (values) => (match, identifier, format, width) => {
142
- if (match === '$$') return '$';
143
- if (typeof values[identifier] === 'undefined') return match;
144
- const value = '' + values[identifier];
145
- if (identifier === 'RepresentationID') return value;
146
- if (!format) width = 1;
147
- else width = parseInt(width, 10);
148
- if (value.length >= width) return value;
149
- return value.padStart(width, '0');
150
- };
151
-
152
- const buildSegmentUrl = (template, fields) => {
153
- return template.replace(identifierPattern, identifierReplacement(fields));
154
- };
155
-
156
- const resolveSegmentTemplateUrls = (segmentTemplate, baseUrl, manifestUrl) => {
157
- for (const type of ['initialization', 'media']) {
158
- let value = segmentTemplate.get(type);
159
- if (!value) continue;
160
- if (!value.startsWith('https://')) {
161
- if (!baseUrl)
162
- throw new Error(
163
- `Resolved Segment URL is not absolute, and no Base URL is available.`,
164
- );
165
- value = new URL(value, baseUrl).toString();
166
- }
167
- if (!new URL(value).search) {
168
- const manifestUrlQuery = new URL(manifestUrl).search;
169
- if (manifestUrlQuery) value += `?${manifestUrlQuery}`;
170
- }
171
- segmentTemplate.set(type, value);
172
- }
173
- };
174
-
175
- const parseSegmentsFromTimeline = (
176
- segmentTimeline,
177
- segmentTemplate,
178
- representation,
179
- startNumber,
180
- ) => {
181
- const times = [];
182
- let currentTime = 0;
183
- for (const s of segmentTimeline.getAll('S')) {
184
- const t = Number(s.get('t'));
185
- const r = Number(s.get('r') || 0);
186
- const d = Number(s.get('d'));
187
- if (t) currentTime = t;
188
- for (let i = 0; i < r + 1; i++) {
189
- times.push(currentTime);
190
- currentTime += d;
191
- }
192
- }
193
- const segments = [];
194
- const numbers = [...Array(times.length).keys()].map((n) => n + startNumber);
195
- for (let i = 0; i < times.length; i++) {
196
- const t = times[i];
197
- const n = numbers[i];
198
- const url = buildSegmentUrl(segmentTemplate.get('media'), {
199
- Bandwidth: representation.get('bandwidth'),
200
- RepresentationID: representation.get('id'),
201
- Number: n,
202
- Time: t,
203
- });
204
- segments.push({ url });
205
- }
206
- return segments;
207
- };
208
-
209
- const parseSegmentsFromTemplate = (
210
- segmentTemplate,
211
- baseUrl,
212
- manifestUrl,
213
- duration,
214
- representation,
215
- ) => {
216
- const startNumber = Number(segmentTemplate.get('startNumber') || 1);
217
- const segmentTimeline = segmentTemplate.get('SegmentTimeline');
218
- resolveSegmentTemplateUrls(segmentTemplate, baseUrl, manifestUrl);
219
- const segmentDuration = parseFloat(segmentTemplate.get('duration'));
220
- const segmentTimescale = parseFloat(segmentTemplate.get('timescale') || 1);
221
- // TODO: Support live manifests with type=dynamic
222
- const DEFAULT_SEGMENTS_COUNT = 35;
223
- // if (!duration) throw new Error('Duration of the Period was unable to be determined.');
224
- const segmentsCount = duration
225
- ? Math.ceil(duration / (segmentDuration / segmentTimescale))
226
- : DEFAULT_SEGMENTS_COUNT;
227
- const bandwidth = representation.get('bandwidth');
228
- const id = representation.get('id');
229
- const segments = [];
230
- if (segmentTimeline) {
231
- segments.push(
232
- ...parseSegmentsFromTimeline(
233
- segmentTimeline,
234
- segmentTemplate,
235
- representation,
236
- startNumber,
237
- ),
238
- );
239
- } else {
240
- for (let i = startNumber; i < startNumber + segmentsCount; i++) {
241
- const url = buildSegmentUrl(segmentTemplate.get('media'), {
242
- Bandwidth: bandwidth,
243
- RepresentationID: id,
244
- Number: i,
245
- Time: i,
246
- });
247
- segments.push({ url });
248
- }
249
- }
250
- const initialization = segmentTemplate.get('initialization');
251
- if (initialization) {
252
- const url = buildSegmentUrl(initialization, {
253
- Bandwidth: bandwidth,
254
- RepresentationID: id,
255
- });
256
- segments.unshift({ url, init: true });
257
- }
258
- return segments;
259
- };
260
-
261
- const parseSegmentsFromList = (segmentList, baseUrl) => {
262
- const segmentUrls = segmentList.getAll('SegmentURL');
263
- const segments = [];
264
- for (const segmentUrl of segmentUrls) {
265
- let mediaUrl = segmentUrl.get('media');
266
- if (!mediaUrl) {
267
- mediaUrl = baseUrl;
268
- } else if (!mediaUrl.startsWith('https://')) {
269
- mediaUrl = new URL(mediaUrl, baseUrl).toString();
270
- }
271
- segments.push({ url: mediaUrl, range: segmentUrl.get('mediaRange') });
272
- }
273
- const initialization = segmentList.get('Initialization');
274
- if (initialization) {
275
- let mediaUrl = initialization.get('sourceURL');
276
- if (!mediaUrl) {
277
- mediaUrl = baseUrl;
278
- } else if (!mediaUrl.startsWith('https://')) {
279
- mediaUrl = new URL(mediaUrl, baseUrl).toString();
280
- }
281
- if (mediaUrl) {
282
- segments.unshift({
283
- url: mediaUrl,
284
- range: initialization.get('range'),
285
- init: true,
286
- });
287
- }
288
- }
289
- return segments;
290
- };
291
-
292
- const parseSegmentFromBase = async (segmentBase, baseUrl) => {
293
- const initialization = segmentBase.get('Initialization');
294
- let mediaRange = '';
295
- if (initialization) {
296
- // const range = initialization.get('range');
297
- // const headers = range ? { Range: `bytes=${range}` } : undefined;
298
- // const response = await fetch(baseUrl, headers);
299
- // const initData = await response.arrayBuffer();
300
- // console.log(response.headers);
301
- // const totalSize = response.headers.get('Content-Range').split('/')[-1];
302
- // if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
303
- }
304
- return { url: baseUrl, range: mediaRange };
305
- };
306
-
307
- const transformSegmentUrls = (segments) => {
308
- for (const segment of segments) {
309
- const hasHtmlEscapeCode = segment.url.includes('&amp;');
310
- if (hasHtmlEscapeCode) {
311
- const url = new URL(segment.url);
312
- const entries = new URLSearchParams(
313
- url.searchParams.toString(),
314
- ).entries();
315
- for (const [key, value] of entries) {
316
- url.searchParams.delete(key);
317
- url.searchParams.append(key.replaceAll('amp;', ''), value);
318
- }
319
- segment.url = url.toString();
320
- }
321
- }
322
- };
323
-
324
- const protectionSchemas = {
325
- 'urn:mpeg:dash:mp4protection:2011': 'common',
326
- 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'playready',
327
- 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'widevine',
328
- };
329
-
330
- const parseContentProtection = (contentProtections) => {
331
- const protection = {};
332
- for (const contentProtection of contentProtections) {
333
- const id = contentProtection.get('schemeIdUri')?.toLowerCase();
334
- const value = contentProtection.get('value');
335
- const pssh =
336
- contentProtection.get('cenc:pssh')?.get() ||
337
- contentProtection.get('pssh')?.get();
338
- const defaultKeyId =
339
- contentProtection.get('cenc:default_KID') ||
340
- contentProtection.get('kid')?.get();
341
- const data = { id, value, pssh, defaultKeyId };
342
- protection[protectionSchemas[id]] = data;
343
- }
344
- return protection;
345
- };
346
-
347
- const parseManifest = async (text, url, fallbackLanguage) => {
348
- const mpd = appendUtils(xml.parse(text)).get('MPD');
349
- const period = mpd.get('Period');
350
- const durationString =
351
- period.get('duration') || mpd.get('mediaPresentationDuration');
352
- const duration = parseDuration(durationString);
353
-
354
- const videos = [];
355
- const audios = [];
356
- const subtitles = [];
357
-
358
- for (const adaptationSet of period.getAll('AdaptationSet')) {
359
- for (const representation of adaptationSet.getAll('Representation')) {
360
- const { get, getAll } = combineGetters(representation, adaptationSet);
361
- const { contentType, mimeType } = parseContentTypes(representation);
362
- const codecs = parseCodecs(representation, contentType, mimeType);
363
- const language = parseLanguage(
364
- representation,
365
- adaptationSet,
366
- fallbackLanguage,
367
- );
368
-
369
- const baseUrl = parseBaseUrl(url, mpd, period, representation);
370
- const segmentTemplate = get('SegmentTemplate');
371
- const segmentList = get('SegmentList');
372
- const segmentBase = get('SegmentBase');
373
- const segments = [];
374
-
375
- if (segmentTemplate) {
376
- const segmentsFromTemplate = parseSegmentsFromTemplate(
377
- segmentTemplate,
378
- baseUrl,
379
- url,
380
- duration,
381
- representation,
382
- );
383
- segments.push(...segmentsFromTemplate);
384
- } else if (segmentList) {
385
- const segmentsFromList = parseSegmentsFromList(segmentList, baseUrl);
386
- segments.push(...segmentsFromList);
387
- } else if (segmentBase) {
388
- const segmentFromBase = await parseSegmentFromBase(
389
- segmentBase,
390
- baseUrl,
391
- );
392
- segments.push(segmentFromBase);
393
- } else if (baseUrl) {
394
- segments.push({ url: baseUrl });
395
- } else {
396
- throw new Error(
397
- 'Could not find a way to get segments from this MPD manifest.',
398
- );
399
- }
400
- transformSegmentUrls(segments);
401
-
402
- const label = get('label');
403
- const fps = get('frameRate') ?? segmentBase?.attributes.timescale;
404
- const width = get('width') ?? 0;
405
- const height = get('height') ?? 0;
406
- const bitrate = get('bandwidth');
407
- const supplementalProps = getAll('SupplementalProperty');
408
- const essentialProps = getAll('EssentialProperty');
409
- const accessibilities = adaptationSet.getAll('Accessibility');
410
- const roles = adaptationSet.getAll('Role');
411
- const contentProtections = getAll('ContentProtection');
412
-
413
- const id = [
414
- new URL(baseUrl).hostname,
415
- contentType,
416
- codecs,
417
- bitrate,
418
- language,
419
- mpd.get('id'),
420
- period.get('id'),
421
- get('id'),
422
- get('audioTrackId'),
423
- ]
424
- .filter(Boolean)
425
- .join('-')
426
- .replaceAll('/', '-');
427
-
428
- switch (contentType) {
429
- case 'video': {
430
- const track = createVideoTrack({
431
- id,
432
- label,
433
- type: contentType,
434
- codec: parseVideoCodec(codecs),
435
- dynamicRange: parseDynamicRange(
436
- codecs,
437
- supplementalProps,
438
- essentialProps,
439
- ),
440
- contentProtection: parseContentProtection(contentProtections),
441
- bitrate,
442
- duration,
443
- width,
444
- height,
445
- fps,
446
- language,
447
- segments,
448
- });
449
- videos.push(track);
450
- break;
451
- }
452
- case 'audio': {
453
- const track = createAudioTrack({
454
- id,
455
- label,
456
- type: contentType,
457
- codec: parseAudioCodec(codecs),
458
- channels: get('AudioChannelConfiguration')?.get('value'),
459
- jointObjectCoding:
460
- getDolbyDigitalPlusComplexityIndex(supplementalProps),
461
- isDescriptive: checkIsDescriptive(accessibilities),
462
- contentProtection: parseContentProtection(contentProtections),
463
- bitrate,
464
- duration,
465
- language,
466
- segments,
467
- });
468
- audios.push(track);
469
- break;
470
- }
471
- case 'text': {
472
- const track = createSubtitleTrack({
473
- id,
474
- label,
475
- type: contentType,
476
- codec: parseSubtitleCodec(codecs || 'vtt'),
477
- isClosedCaption: checkIsClosedCaption(roles),
478
- isSdh: checkIsSdh(accessibilities),
479
- isForced: checkIsForced(roles),
480
- bitrate,
481
- duration,
482
- language,
483
- segments,
484
- });
485
- subtitles.push(track);
486
- break;
487
- }
488
- case 'image':
489
- break;
490
- default:
491
- throw new Error(`Unknown content type: ${contentType}`);
492
- }
493
- }
494
- }
495
-
496
- videos.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
497
-
498
- return {
499
- duration,
500
- tracks: {
501
- all: videos.concat(audios).concat(subtitles),
502
- videos,
503
- audios,
504
- subtitles,
505
- withResolution: createResolutionFilter(videos),
506
- withVideoCodecs: createVideoCodecFilter(videos),
507
- withVideoQuality: createVideoQualityFilter(videos),
508
- withAudioCodecs: createAudioCodecFilter(audios),
509
- withAudioLanguages: createAudioLanguageFilter(audios),
510
- withAudioChannels: createAudioChannelsFilter(audios),
511
- withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
512
- },
513
- };
514
- };
515
-
516
- module.exports = { parseManifest };