dasha 2.3.6 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -201
- package/README.md +15 -42
- package/dasha.js +11 -3
- package/lib/audio.js +128 -0
- package/lib/dash.js +417 -0
- package/lib/hls.js +142 -0
- package/lib/subtitle.js +119 -0
- package/lib/track.js +66 -0
- package/lib/util.js +96 -0
- package/lib/video.js +158 -0
- package/lib/xml.js +277 -109
- package/package.json +35 -21
- package/types/dasha.d.ts +98 -2
- package/lib/constants.js +0 -16
- package/lib/manifest.js +0 -262
- package/lib/processor.js +0 -99
- package/lib/utils.js +0 -63
- package/types/constants.d.ts +0 -5
- package/types/manifest.d.ts +0 -52
- package/types/processor.d.ts +0 -63
- package/types/utils.d.ts +0 -7
- package/types/xml.d.ts +0 -3
package/README.md
CHANGED
|
@@ -1,50 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dasha
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/dasha)
|
|
4
|
-
[](https://www.npmjs.com/package/dasha)
|
|
5
|
-
[](https://www.npmjs.com/package/dasha)
|
|
6
|
-
[](https://github.com/vitnore/dasha/blob/main/LICENSE)
|
|
3
|
+
[](https://www.npmjs.com/package/dasha)
|
|
4
|
+
[](https://www.npmjs.com/package/dasha)
|
|
5
|
+
[](https://www.npmjs.com/package/dasha)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Library for parsing MPEG-DASH and HLS manifests. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments.
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## Install
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```javascript
|
|
16
|
-
const { parseManifest } = require('./dasha');
|
|
17
|
-
|
|
18
|
-
const rawManifest = `
|
|
19
|
-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
20
|
-
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
21
|
-
...
|
|
22
|
-
</MPD>
|
|
23
|
-
`;
|
|
24
|
-
const manifest = parseManifest(rawManifest);
|
|
25
|
-
const targetHeight = 1080;
|
|
26
|
-
const targetAudioLanguages = ['rus'];
|
|
27
|
-
const videoTrack = manifest.getVideoTrack(targetHeight);
|
|
28
|
-
const audioTracks = manifest.getAudioTracks(targetAudioLanguages);
|
|
29
|
-
const tracks = [videoTrack, ...audioTracks];
|
|
11
|
+
```shell
|
|
12
|
+
npm i dasha
|
|
30
13
|
```
|
|
31
14
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
bitrate: number; // Kbps
|
|
37
|
-
size: number; // MB
|
|
38
|
-
width: number;
|
|
39
|
-
height: number;
|
|
40
|
-
qualityLabel: '144p' | '240p' | '360p' | '480p' | '576p' | '720p' | '1080p' | '2160p';
|
|
41
|
-
};
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { parse } from 'dasha';
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
bitrate: number; // Kbps
|
|
47
|
-
size: number; // MB
|
|
48
|
-
audioSampleRate: number; // kHz
|
|
49
|
-
};
|
|
20
|
+
const url = 'https://dash.akamaized.net/dash264/TestCases/1a/sony/SNE_DASH_SD_CASE1A_REVISED.mpd';
|
|
21
|
+
const body = await fetch(url).then((res) => res.text());
|
|
22
|
+
const manifest = await parse(body, url);
|
|
50
23
|
```
|
package/dasha.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const dash = require('./lib/dash');
|
|
4
|
+
const hls = require('./lib/hls');
|
|
5
|
+
|
|
6
|
+
const parse = (text, url, fallbackLanguage) => {
|
|
7
|
+
if (text.includes('<MPD')) return dash.parseManifest(text, url, fallbackLanguage);
|
|
8
|
+
else if (text.includes('#EXTM3U')) return hls.parseManifest(text, url);
|
|
9
|
+
else throw new Error('Invalid manifest');
|
|
4
10
|
};
|
|
11
|
+
|
|
12
|
+
module.exports = { parse };
|
package/lib/audio.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseMimes } = require('./track');
|
|
4
|
+
const { parseBitrate, parseSize } = require('./util');
|
|
5
|
+
|
|
6
|
+
const AUDIO_CODECS = {
|
|
7
|
+
AAC: 'AAC', // https://wikipedia.org/wiki/Advanced_Audio_Coding
|
|
8
|
+
AC3: 'DD', // https://wikipedia.org/wiki/Dolby_Digital
|
|
9
|
+
EC3: 'DD+', // https://wikipedia.org/wiki/Dolby_Digital_Plus
|
|
10
|
+
OPUS: 'OPUS', // https://wikipedia.org/wiki/Opus_(audio_format)
|
|
11
|
+
OGG: 'VORB', // https://wikipedia.org/wiki/Vorbis
|
|
12
|
+
DTS: 'DTS', // https://en.wikipedia.org/wiki/DTS_(company)#DTS_Digital_Surround
|
|
13
|
+
ALAC: 'ALAC', // https://en.wikipedia.org/wiki/Apple_Lossless_Audio_Codec
|
|
14
|
+
FLAC: 'FLAC', // https://en.wikipedia.org/wiki/FLAC
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseAudioCodecFromMime = (mime) => {
|
|
18
|
+
const target = mime.toLowerCase().trim().split('.')[0];
|
|
19
|
+
switch (target) {
|
|
20
|
+
case 'mp4a':
|
|
21
|
+
return AUDIO_CODECS.AAC;
|
|
22
|
+
case 'ac-3':
|
|
23
|
+
return AUDIO_CODECS.AC3;
|
|
24
|
+
case 'ec-3':
|
|
25
|
+
return AUDIO_CODECS.EC3;
|
|
26
|
+
case 'opus':
|
|
27
|
+
return AUDIO_CODECS.OPUS;
|
|
28
|
+
case 'dtsc':
|
|
29
|
+
return AUDIO_CODECS.DTS;
|
|
30
|
+
case 'alac':
|
|
31
|
+
return AUDIO_CODECS.ALAC;
|
|
32
|
+
case 'flac':
|
|
33
|
+
return AUDIO_CODECS.FLAC;
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`The MIME ${mime} is not supported as audio codec`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseAudioCodec = (codecs) => {
|
|
40
|
+
const mimes = parseMimes(codecs);
|
|
41
|
+
for (const mime of mimes) {
|
|
42
|
+
try {
|
|
43
|
+
return parseAudioCodecFromMime(mime);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`No MIME types matched any supported Audio Codecs in ${codecs}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// https://professionalsupport.dolby.com/s/article/What-is-Dolby-Digital-Plus-JOC-Joint-Object-Coding?language=en_US
|
|
52
|
+
const getDolbyDigitalPlusComplexityIndex = (supplementalProps = []) => {
|
|
53
|
+
const targetScheme = 'tag:dolby.com,2018:dash:EC3_ExtensionComplexityIndex:2018';
|
|
54
|
+
for (const prop of supplementalProps)
|
|
55
|
+
if (prop.attributes.schemeIdUri === targetScheme) return parseInt(prop.attributes.value);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const checkIsDescriptive = (accessibilities = []) => {
|
|
59
|
+
for (const accessibility of accessibilities) {
|
|
60
|
+
const { schemeIdUri, value } = accessibility.attributes;
|
|
61
|
+
const firstMatch = schemeIdUri == 'urn:mpeg:dash:role:2011' && value === 'descriptive';
|
|
62
|
+
const secondMatch = schemeIdUri == 'urn:tva:metadata:cs:AudioPurposeCS:2007' && value === '1';
|
|
63
|
+
const isDescriptive = firstMatch || secondMatch;
|
|
64
|
+
if (isDescriptive) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const parseChannels = (channels) => {
|
|
70
|
+
const isDigit = (char) => char >= '0' && char <= '9';
|
|
71
|
+
if (typeof channels === 'string') {
|
|
72
|
+
if (channels.toUpperCase() == 'A000') return 2.0;
|
|
73
|
+
else if (channels.toUpperCase() == 'F801') return 5.1;
|
|
74
|
+
else if (isDigit(channels.replace('ch', '').replace('.', '')[0]))
|
|
75
|
+
// e.g., '2ch', '2', '2.0', '5.1ch', '5.1'
|
|
76
|
+
return parseFloat(channels.replace('ch', ''));
|
|
77
|
+
throw new Error(`Unsupported audio channels value, '${channels}'`);
|
|
78
|
+
}
|
|
79
|
+
return parseFloat(channels);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const createAudioTrack = ({
|
|
83
|
+
id,
|
|
84
|
+
label,
|
|
85
|
+
type,
|
|
86
|
+
codec,
|
|
87
|
+
channels,
|
|
88
|
+
bitrate,
|
|
89
|
+
duration,
|
|
90
|
+
jointObjectCoding = 0,
|
|
91
|
+
isDescriptive = false,
|
|
92
|
+
language,
|
|
93
|
+
segments,
|
|
94
|
+
}) => {
|
|
95
|
+
const parsedBitrate = parseBitrate(Number(bitrate));
|
|
96
|
+
const parsedChannels = parseChannels(channels);
|
|
97
|
+
const size = duration ? parseSize(Number(bitrate), Number(duration)) : undefined;
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
label,
|
|
101
|
+
type,
|
|
102
|
+
codec,
|
|
103
|
+
bitrate: parsedBitrate,
|
|
104
|
+
size,
|
|
105
|
+
language,
|
|
106
|
+
segments,
|
|
107
|
+
channels: parsedChannels,
|
|
108
|
+
jointObjectCoding,
|
|
109
|
+
isDescriptive,
|
|
110
|
+
toString() {
|
|
111
|
+
return [
|
|
112
|
+
'AUDIO',
|
|
113
|
+
`[${codec}]`,
|
|
114
|
+
`${parsedChannels || '?'}` + (jointObjectCoding ? ` (JOC ${jointObjectCoding})` : ''),
|
|
115
|
+
`${parsedBitrate.kbps} kb/s`,
|
|
116
|
+
language,
|
|
117
|
+
].join(' | ');
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
AUDIO_CODECS,
|
|
124
|
+
parseAudioCodec,
|
|
125
|
+
createAudioTrack,
|
|
126
|
+
getDolbyDigitalPlusComplexityIndex,
|
|
127
|
+
checkIsDescriptive,
|
|
128
|
+
};
|
package/lib/dash.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const xml = require('./xml');
|
|
4
|
+
const { parseDuration, isLanguageTagValid } = require('./util');
|
|
5
|
+
const { parseVideoCodec, parseDynamicRange, createVideoTrack } = require('./video');
|
|
6
|
+
const {
|
|
7
|
+
parseAudioCodec,
|
|
8
|
+
createAudioTrack,
|
|
9
|
+
getDolbyDigitalPlusComplexityIndex,
|
|
10
|
+
checkIsDescriptive,
|
|
11
|
+
} = require('./audio');
|
|
12
|
+
const {
|
|
13
|
+
parseSubtitleCodec,
|
|
14
|
+
checkIsClosedCaption,
|
|
15
|
+
checkIsSdh,
|
|
16
|
+
checkIsForced,
|
|
17
|
+
createSubtitleTrack,
|
|
18
|
+
} = require('./subtitle');
|
|
19
|
+
const {
|
|
20
|
+
createResolutionFilter,
|
|
21
|
+
createVideoQualityFilter,
|
|
22
|
+
createAudioLanguageFilter,
|
|
23
|
+
createSubtitleLanguageFilter,
|
|
24
|
+
} = require('./track');
|
|
25
|
+
|
|
26
|
+
const appendUtils = (element) => {
|
|
27
|
+
if (!element) return element;
|
|
28
|
+
if (Array.isArray(element)) {
|
|
29
|
+
element.get = (name) => appendUtils(element.find((item) => item.tagName === name));
|
|
30
|
+
} else {
|
|
31
|
+
element.getAttr = (name) => element.attributes[name];
|
|
32
|
+
element.getChild = (name) => {
|
|
33
|
+
const tag = element.children.find((item) => item.tagName === name);
|
|
34
|
+
const isString = !name && typeof element.children?.[0] === 'string';
|
|
35
|
+
return isString ? element.children[0] : appendUtils(tag);
|
|
36
|
+
};
|
|
37
|
+
element.set = (name, value) => (element.attributes[name] = value);
|
|
38
|
+
element.get = (name) => element.getAttr(name) || element.getChild(name);
|
|
39
|
+
element.getNumber = (name) => Number(element.find(name));
|
|
40
|
+
element.getAll = (name) =>
|
|
41
|
+
element.children.filter((item) => item.tagName === name).map(appendUtils);
|
|
42
|
+
element.getBaseUrls = () => element.getAll('BaseURL').map((item) => item.children[0]);
|
|
43
|
+
element.getBaseUrl = () => element.getBaseUrls()[0];
|
|
44
|
+
}
|
|
45
|
+
return element;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const combineGetters = (representation, adaptationSet) => {
|
|
49
|
+
const prevGet = representation.get;
|
|
50
|
+
const prevGetAll = representation.getAll;
|
|
51
|
+
const get = (name) => prevGet(name) || adaptationSet.get(name);
|
|
52
|
+
const getAll = (name) => [...prevGetAll(name), ...adaptationSet.getAll(name)].filter(Boolean);
|
|
53
|
+
representation.get = get;
|
|
54
|
+
representation.getAll = getAll;
|
|
55
|
+
return { get, getAll };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parseBaseUrl = (manifestUrl, mpd, period, representation) => {
|
|
59
|
+
let manifestBaseUrl = mpd.getBaseUrl();
|
|
60
|
+
if (!manifestBaseUrl) manifestBaseUrl = manifestUrl;
|
|
61
|
+
else if (!manifestBaseUrl.startsWith('https://'))
|
|
62
|
+
manifestBaseUrl = new URL(manifestBaseUrl, manifestUrl).toString();
|
|
63
|
+
const periodBaseUrl = new URL(period.getBaseUrl() || '', manifestBaseUrl).toString();
|
|
64
|
+
const representationBaseUrl = new URL(
|
|
65
|
+
representation.getBaseUrl() || '',
|
|
66
|
+
periodBaseUrl
|
|
67
|
+
).toString();
|
|
68
|
+
return representationBaseUrl;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const parseContentTypes = (representation) => {
|
|
72
|
+
const mimeType = representation.get('mimeType');
|
|
73
|
+
const contentType = representation.get('contentType') || mimeType?.split('/')[0];
|
|
74
|
+
if (!contentType && !mimeType)
|
|
75
|
+
throw new Error('Unable to determine the format of a Representation, cannot continue...');
|
|
76
|
+
return { contentType, mimeType };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parseCodecs = (representation, contentType, mimeType) => {
|
|
80
|
+
const shouldUseCodecsFromMime = contentType === 'text' && !mimeType.includes('mp4');
|
|
81
|
+
const codecs = shouldUseCodecsFromMime ? mimeType.split('/')[1] : representation.get('codecs');
|
|
82
|
+
return codecs;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const parseLanguage = (representation, adaptationSet, fallbackLanguage) => {
|
|
86
|
+
let language = '';
|
|
87
|
+
const options = [];
|
|
88
|
+
const lang = representation.get('lang');
|
|
89
|
+
const id = representation.get('id');
|
|
90
|
+
if (representation) {
|
|
91
|
+
options.push(lang);
|
|
92
|
+
if (id) {
|
|
93
|
+
const m = id.match(/\w+_(\w+)=\d+/);
|
|
94
|
+
if (m) options.push(m.group(1));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
options.push(adaptationSet.get('lang'));
|
|
98
|
+
if (fallbackLanguage) options.push(fallbackLanguage);
|
|
99
|
+
for (const option of options) {
|
|
100
|
+
const value = (option || '').trim();
|
|
101
|
+
if (!isLanguageTagValid(value) || value.startsWith('und')) continue;
|
|
102
|
+
language = value;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!language) {
|
|
106
|
+
// Language information could not be derived from a Representation.
|
|
107
|
+
// TODO: Throw error if language not found
|
|
108
|
+
}
|
|
109
|
+
return language;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const buildSegmentUrl = (template, fields) => {
|
|
113
|
+
let result = template;
|
|
114
|
+
for (const [key, value] of Object.entries(fields))
|
|
115
|
+
result = result.replace('$' + key + '$', value);
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const resolveSegmentTemplateUrls = (segmentTemplate, baseUrl, manifestUrl) => {
|
|
120
|
+
for (const type of ['initialization', 'media']) {
|
|
121
|
+
let value = segmentTemplate.get(type);
|
|
122
|
+
if (!value) continue;
|
|
123
|
+
if (!value.startsWith('https://')) {
|
|
124
|
+
if (!baseUrl)
|
|
125
|
+
throw new Error(`Resolved Segment URL is not absolute, and no Base URL is available.`);
|
|
126
|
+
value = new URL(value, baseUrl).toString();
|
|
127
|
+
}
|
|
128
|
+
if (!new URL(value).search) {
|
|
129
|
+
const manifestUrlQuery = new URL(manifestUrl).search;
|
|
130
|
+
if (manifestUrlQuery) value += `?${manifestUrlQuery}`;
|
|
131
|
+
}
|
|
132
|
+
segmentTemplate.set(type, value);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const parseSegmentsFromTimeline = (
|
|
137
|
+
segmentTimeline,
|
|
138
|
+
segmentTemplate,
|
|
139
|
+
representation,
|
|
140
|
+
startNumber
|
|
141
|
+
) => {
|
|
142
|
+
const times = [];
|
|
143
|
+
let currentTime = 0;
|
|
144
|
+
for (const s of segmentTimeline.getAll('S')) {
|
|
145
|
+
const t = Number(s.get('t'));
|
|
146
|
+
const r = Number(s.get('r') || 0);
|
|
147
|
+
const d = Number(s.get('d'));
|
|
148
|
+
if (t) currentTime = t;
|
|
149
|
+
for (let i = 0; i < r + 1; i++) {
|
|
150
|
+
times.push(currentTime);
|
|
151
|
+
currentTime += d;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const segments = [];
|
|
155
|
+
const numbers = [...Array(times.length).keys()].map((n) => n + startNumber);
|
|
156
|
+
for (let i = 0; i < times.length; i++) {
|
|
157
|
+
const t = times[i];
|
|
158
|
+
const n = numbers[i];
|
|
159
|
+
const url = buildSegmentUrl(segmentTemplate.get('media'), {
|
|
160
|
+
Bandwidth: representation.get('bandwidth'),
|
|
161
|
+
RepresentationID: representation.get('id'),
|
|
162
|
+
Number: n,
|
|
163
|
+
Time: t,
|
|
164
|
+
});
|
|
165
|
+
segments.push({ url });
|
|
166
|
+
}
|
|
167
|
+
return segments;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const parseSegmentsFromTemplate = (
|
|
171
|
+
segmentTemplate,
|
|
172
|
+
baseUrl,
|
|
173
|
+
manifestUrl,
|
|
174
|
+
duration,
|
|
175
|
+
representation
|
|
176
|
+
) => {
|
|
177
|
+
const startNumber = Number(segmentTemplate.get('startNumber') || 1);
|
|
178
|
+
const segmentTimeline = segmentTemplate.get('SegmentTimeline');
|
|
179
|
+
resolveSegmentTemplateUrls(segmentTemplate, baseUrl, manifestUrl);
|
|
180
|
+
if (!duration) throw new Error('Duration of the Period was unable to be determined.');
|
|
181
|
+
const segmentDuration = parseFloat(segmentTemplate.get('duration'));
|
|
182
|
+
const segmentTimescale = parseFloat(segmentTemplate.get('timescale') || 1);
|
|
183
|
+
const segmentsCount = Math.ceil(duration / (segmentDuration / segmentTimescale));
|
|
184
|
+
const bandwidth = representation.get('bandwidth');
|
|
185
|
+
const id = representation.get('id');
|
|
186
|
+
const segments = [];
|
|
187
|
+
if (segmentTimeline) {
|
|
188
|
+
segments.push(
|
|
189
|
+
...parseSegmentsFromTimeline(segmentTimeline, segmentTemplate, representation, startNumber)
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
for (let i = startNumber; i < startNumber + segmentsCount; i++) {
|
|
193
|
+
const url = buildSegmentUrl(segmentTemplate.get('media'), {
|
|
194
|
+
Bandwidth: bandwidth,
|
|
195
|
+
RepresentationID: id,
|
|
196
|
+
Number: i,
|
|
197
|
+
Time: i,
|
|
198
|
+
});
|
|
199
|
+
segments.push({ url });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const initialization = segmentTemplate.get('initialization');
|
|
203
|
+
if (initialization) {
|
|
204
|
+
const url = buildSegmentUrl(initialization, {
|
|
205
|
+
Bandwidth: bandwidth,
|
|
206
|
+
RepresentationID: id,
|
|
207
|
+
});
|
|
208
|
+
segments.unshift({ url, init: true });
|
|
209
|
+
}
|
|
210
|
+
return segments;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const parseSegmentsFromList = (segmentList, baseUrl) => {
|
|
214
|
+
const segmentUrls = segmentList.get('SegmentURL');
|
|
215
|
+
const segments = [];
|
|
216
|
+
for (const segmentUrl of segmentUrls) {
|
|
217
|
+
let mediaUrl = segmentUrl.get('media');
|
|
218
|
+
if (!mediaUrl) mediaUrl = baseUrl;
|
|
219
|
+
else if (!mediaUrl.startsWith('https://')) mediaUrl = new URL(mediaUrl, baseUrl).toString();
|
|
220
|
+
segments.push({ url: mediaUrl, range: segmentUrl.get('mediaRange') });
|
|
221
|
+
}
|
|
222
|
+
return segments;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const parseSegmentFromBase = async (segmentBase, baseUrl) => {
|
|
226
|
+
const initialization = segmentBase.get('Initialization');
|
|
227
|
+
let mediaRange = '';
|
|
228
|
+
if (initialization) {
|
|
229
|
+
const range = initialization.get('range');
|
|
230
|
+
const headers = range ? { Range: `bytes=${range}` } : undefined;
|
|
231
|
+
const response = await fetch(baseUrl, headers);
|
|
232
|
+
const initData = await response.arrayBuffer();
|
|
233
|
+
const totalSize = response.headers.get('Content-Range').split('/')[-1];
|
|
234
|
+
if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
|
|
235
|
+
}
|
|
236
|
+
return { url: baseUrl, range: mediaRange };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const parseContentProtection = (contentProtections) => {
|
|
240
|
+
const protection = {};
|
|
241
|
+
const commonId = 'urn:mpeg:dash:mp4protection:2011';
|
|
242
|
+
const playreadyId = 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95';
|
|
243
|
+
const widevineId = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed';
|
|
244
|
+
for (const contentProtection of contentProtections) {
|
|
245
|
+
const id = contentProtection.get('schemeIdUri')?.toLowerCase();
|
|
246
|
+
switch (id) {
|
|
247
|
+
case commonId:
|
|
248
|
+
protection.common = {
|
|
249
|
+
id,
|
|
250
|
+
value: contentProtection.get('value'),
|
|
251
|
+
keyId: contentProtection.get('cenc:default_KID'),
|
|
252
|
+
};
|
|
253
|
+
continue;
|
|
254
|
+
case playreadyId:
|
|
255
|
+
protection.playready = {
|
|
256
|
+
id,
|
|
257
|
+
value: contentProtection.get('value'),
|
|
258
|
+
pssh: contentProtection.get('cenc:pssh')?.get(),
|
|
259
|
+
};
|
|
260
|
+
continue;
|
|
261
|
+
case widevineId:
|
|
262
|
+
protection.widevine = {
|
|
263
|
+
id,
|
|
264
|
+
pssh: contentProtection.get('cenc:pssh')?.get(),
|
|
265
|
+
};
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return protection;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const parseManifest = async (text, url, fallbackLanguage) => {
|
|
273
|
+
const mpd = appendUtils(xml.parse(text)).get('MPD');
|
|
274
|
+
const period = mpd.get('Period');
|
|
275
|
+
const durationString = period.get('duration') || mpd.get('mediaPresentationDuration');
|
|
276
|
+
const duration = parseDuration(durationString);
|
|
277
|
+
|
|
278
|
+
const videos = [];
|
|
279
|
+
const audios = [];
|
|
280
|
+
const subtitles = [];
|
|
281
|
+
|
|
282
|
+
for (const adaptationSet of period.getAll('AdaptationSet')) {
|
|
283
|
+
for (const representation of adaptationSet.getAll('Representation')) {
|
|
284
|
+
const { get, getAll } = combineGetters(representation, adaptationSet);
|
|
285
|
+
const { contentType, mimeType } = parseContentTypes(representation);
|
|
286
|
+
const codecs = parseCodecs(representation, contentType, mimeType);
|
|
287
|
+
const language = parseLanguage(representation, adaptationSet, fallbackLanguage);
|
|
288
|
+
|
|
289
|
+
const baseUrl = parseBaseUrl(url, mpd, period, representation);
|
|
290
|
+
const segmentTemplate = get('SegmentTemplate');
|
|
291
|
+
const segmentList = get('SegmentList');
|
|
292
|
+
const segmentBase = get('SegmentBase');
|
|
293
|
+
const segments = [];
|
|
294
|
+
|
|
295
|
+
if (segmentTemplate) {
|
|
296
|
+
const segmentsFromTemplate = parseSegmentsFromTemplate(
|
|
297
|
+
segmentTemplate,
|
|
298
|
+
baseUrl,
|
|
299
|
+
url,
|
|
300
|
+
duration,
|
|
301
|
+
representation
|
|
302
|
+
);
|
|
303
|
+
segments.push(...segmentsFromTemplate);
|
|
304
|
+
} else if (segmentList) {
|
|
305
|
+
const segmentsFromList = parseSegmentsFromList(segmentList, baseUrl);
|
|
306
|
+
segments.push(...segmentsFromList);
|
|
307
|
+
} else if (segmentBase) {
|
|
308
|
+
const segmentFromBase = await parseSegmentFromBase(segmentBase, baseUrl);
|
|
309
|
+
segments.push(segmentFromBase);
|
|
310
|
+
} else if (baseUrl) {
|
|
311
|
+
segments.push({ url: baseUrl });
|
|
312
|
+
} else {
|
|
313
|
+
throw new Error('Could not find a way to get segments from this MPD manifest.');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const label = get('label');
|
|
317
|
+
const fps = get('frameRate') ?? segmentBase?.attributes.timescale;
|
|
318
|
+
const width = get('width') ?? 0;
|
|
319
|
+
const height = get('height') ?? 0;
|
|
320
|
+
const bitrate = get('bandwidth');
|
|
321
|
+
const supplementalProps = getAll('SupplementalProperty');
|
|
322
|
+
const essentialProps = getAll('EssentialProperty');
|
|
323
|
+
const accessibilities = adaptationSet.getAll('Accessibility');
|
|
324
|
+
const roles = adaptationSet.getAll('Role');
|
|
325
|
+
const contentProtections = adaptationSet.getAll('ContentProtection');
|
|
326
|
+
|
|
327
|
+
const id = [
|
|
328
|
+
new URL(baseUrl).hostname,
|
|
329
|
+
codecs,
|
|
330
|
+
bitrate,
|
|
331
|
+
language,
|
|
332
|
+
mpd.get('id'),
|
|
333
|
+
period.get('id'),
|
|
334
|
+
get('id'),
|
|
335
|
+
get('audioTrackId'),
|
|
336
|
+
]
|
|
337
|
+
.filter(Boolean)
|
|
338
|
+
.join('-')
|
|
339
|
+
.replaceAll('/', '-');
|
|
340
|
+
|
|
341
|
+
switch (contentType) {
|
|
342
|
+
case 'video': {
|
|
343
|
+
const track = createVideoTrack({
|
|
344
|
+
id,
|
|
345
|
+
label,
|
|
346
|
+
type: contentType,
|
|
347
|
+
codec: parseVideoCodec(codecs),
|
|
348
|
+
dynamicRange: parseDynamicRange(codecs, supplementalProps, essentialProps),
|
|
349
|
+
contentProtection: parseContentProtection(contentProtections),
|
|
350
|
+
bitrate,
|
|
351
|
+
duration,
|
|
352
|
+
width,
|
|
353
|
+
height,
|
|
354
|
+
fps,
|
|
355
|
+
language,
|
|
356
|
+
segments,
|
|
357
|
+
});
|
|
358
|
+
videos.push(track);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case 'audio': {
|
|
362
|
+
const track = createAudioTrack({
|
|
363
|
+
id,
|
|
364
|
+
label,
|
|
365
|
+
type: contentType,
|
|
366
|
+
codec: parseAudioCodec(codecs),
|
|
367
|
+
channels: get('AudioChannelConfiguration')?.get('value'),
|
|
368
|
+
jointObjectCoding: getDolbyDigitalPlusComplexityIndex(supplementalProps),
|
|
369
|
+
isDescriptive: checkIsDescriptive(accessibilities),
|
|
370
|
+
bitrate,
|
|
371
|
+
duration,
|
|
372
|
+
language,
|
|
373
|
+
segments,
|
|
374
|
+
});
|
|
375
|
+
audios.push(track);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case 'text': {
|
|
379
|
+
const track = createSubtitleTrack({
|
|
380
|
+
id,
|
|
381
|
+
label,
|
|
382
|
+
type: contentType,
|
|
383
|
+
codec: parseSubtitleCodec(codecs || 'vtt'),
|
|
384
|
+
isClosedCaption: checkIsClosedCaption(roles),
|
|
385
|
+
isSdh: checkIsSdh(accessibilities),
|
|
386
|
+
isForced: checkIsForced(roles),
|
|
387
|
+
bitrate,
|
|
388
|
+
duration,
|
|
389
|
+
language,
|
|
390
|
+
segments,
|
|
391
|
+
});
|
|
392
|
+
subtitles.push(track);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'image':
|
|
396
|
+
break;
|
|
397
|
+
default:
|
|
398
|
+
throw new Error(`Unknown content type: ${contentType}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
duration,
|
|
404
|
+
tracks: {
|
|
405
|
+
all: videos.concat(audios).concat(subtitles),
|
|
406
|
+
videos,
|
|
407
|
+
audios,
|
|
408
|
+
subtitles,
|
|
409
|
+
withResolution: createResolutionFilter(videos),
|
|
410
|
+
withVideoQuality: createVideoQualityFilter(videos),
|
|
411
|
+
withAudioLanguages: createAudioLanguageFilter(audios),
|
|
412
|
+
withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
module.exports = { parseManifest };
|