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/lib/hls.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const m3u8Parser = require('m3u8-parser');
|
|
4
|
+
const { parseBitrate, getQualityLabel } = require('./util');
|
|
5
|
+
const {
|
|
6
|
+
createResolutionFilter,
|
|
7
|
+
createVideoQualityFilter,
|
|
8
|
+
createAudioLanguageFilter,
|
|
9
|
+
createSubtitleLanguageFilter,
|
|
10
|
+
} = require('./track');
|
|
11
|
+
|
|
12
|
+
const parseM3u8 = (manifestString) => {
|
|
13
|
+
const parser = new m3u8Parser.Parser();
|
|
14
|
+
parser.push(manifestString);
|
|
15
|
+
parser.end();
|
|
16
|
+
return parser.manifest;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const fetchPlaylist = async (url) =>
|
|
20
|
+
fetch(url)
|
|
21
|
+
.then((response) => response.text())
|
|
22
|
+
.then(parseM3u8);
|
|
23
|
+
|
|
24
|
+
const parseUrl = (playlistUri, manifestUri) => {
|
|
25
|
+
if (playlistUri.includes('https://')) return playlistUri;
|
|
26
|
+
const uri = new URL(manifestUri);
|
|
27
|
+
return uri.origin + playlistUri;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const urlsSame = (url1, url2) => {
|
|
31
|
+
return new URL(url1).pathname === new URL(url2).pathname;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const parseMediaGroup = (groups, manifestUri) => {
|
|
35
|
+
const results = [];
|
|
36
|
+
if (!groups) return results;
|
|
37
|
+
for (const [groupId, group] of Object.entries(groups)) {
|
|
38
|
+
for (const [label, entity] of Object.entries(group)) {
|
|
39
|
+
const url = parseUrl(entity.uri, manifestUri);
|
|
40
|
+
const existing = results.find((result) => urlsSame(result.url, url));
|
|
41
|
+
if (!existing)
|
|
42
|
+
results.push({
|
|
43
|
+
groupId,
|
|
44
|
+
label,
|
|
45
|
+
language: entity.language,
|
|
46
|
+
url,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getAudioPlaylists = (m3u8, manifestUri) => {
|
|
54
|
+
return parseMediaGroup(m3u8.mediaGroups.AUDIO, manifestUri);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getSubtitlePlaylists = (m3u8, manifestUri) => {
|
|
58
|
+
return parseMediaGroup(m3u8.mediaGroups.SUBTITLES, manifestUri);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getVideoPlaylists = (m3u8, manifestUri) => {
|
|
62
|
+
return m3u8.playlists.map((data) => {
|
|
63
|
+
const bandwidth = data.attributes.BANDWIDTH;
|
|
64
|
+
const url = data.resolvedUri || parseUrl(data.uri, manifestUri);
|
|
65
|
+
const track = { bitrate: parseBitrate(bandwidth), url };
|
|
66
|
+
if (data.attributes.RESOLUTION) {
|
|
67
|
+
track.resolution = data.attributes.RESOLUTION;
|
|
68
|
+
track.quality = getQualityLabel(track.resolution);
|
|
69
|
+
}
|
|
70
|
+
if (data.attributes.CODECS) track.codecs = data.attributes.CODECS;
|
|
71
|
+
if (data.attributes['FRAME-RATE']) track.frameRate = data.attributes['FRAME-RATE'];
|
|
72
|
+
return track;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const segmentsDto = (data = []) => {
|
|
77
|
+
const mapSegment = (item) => ({
|
|
78
|
+
url: item.resolvedUri || item.uri,
|
|
79
|
+
duration: item.duration,
|
|
80
|
+
number: item.number,
|
|
81
|
+
presentationTime: item.presentationTime,
|
|
82
|
+
});
|
|
83
|
+
const segments = data.map(mapSegment);
|
|
84
|
+
if (data.length && data[0].map?.resolvedUri)
|
|
85
|
+
segments.unshift({
|
|
86
|
+
url: data[0].map.resolvedUri,
|
|
87
|
+
init: true,
|
|
88
|
+
duration: 0,
|
|
89
|
+
number: 0,
|
|
90
|
+
presentationTime: 0,
|
|
91
|
+
});
|
|
92
|
+
return segments;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const fetchTrackSegments = (tracks) => {
|
|
96
|
+
return Promise.all(
|
|
97
|
+
tracks.map(async (track) => {
|
|
98
|
+
const playlist = await fetchPlaylist(track.url);
|
|
99
|
+
track.segments = segmentsDto(playlist.segments);
|
|
100
|
+
if (playlist.contentProtection) {
|
|
101
|
+
track.protection = {};
|
|
102
|
+
const fairplayLegacy = playlist.contentProtection['com.apple.fps.1_0'];
|
|
103
|
+
if (fairplayLegacy)
|
|
104
|
+
track.protection.fairplay = {
|
|
105
|
+
keyFormat: fairplayLegacy.attributes.KEYFORMAT,
|
|
106
|
+
uri: fairplayLegacy.attributes.URI,
|
|
107
|
+
method: fairplayLegacy.attributes.METHOD,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const parseManifest = async (manifestString, manifestUri) => {
|
|
115
|
+
const m3u8 = parseM3u8(manifestString);
|
|
116
|
+
const videos = getVideoPlaylists(m3u8, manifestUri);
|
|
117
|
+
const audios = getAudioPlaylists(m3u8, manifestUri);
|
|
118
|
+
const subtitles = getSubtitlePlaylists(m3u8, manifestUri);
|
|
119
|
+
|
|
120
|
+
await Promise.all([
|
|
121
|
+
fetchTrackSegments(videos),
|
|
122
|
+
fetchTrackSegments(audios),
|
|
123
|
+
fetchTrackSegments(subtitles),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const manifest = {
|
|
127
|
+
tracks: {
|
|
128
|
+
all: videos.concat(audios).concat(subtitles),
|
|
129
|
+
videos,
|
|
130
|
+
audios,
|
|
131
|
+
subtitles,
|
|
132
|
+
withResolution: createResolutionFilter(videos),
|
|
133
|
+
withVideoQuality: createVideoQualityFilter(videos),
|
|
134
|
+
withAudioLanguages: createAudioLanguageFilter(audios),
|
|
135
|
+
withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return manifest;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
module.exports = { parseManifest };
|
package/lib/subtitle.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseMimes } = require('./track');
|
|
4
|
+
const { parseBitrate, parseSize } = require('./util');
|
|
5
|
+
|
|
6
|
+
const SUBTITLE_CODECS = {
|
|
7
|
+
SubRip: 'SRT', // https://wikipedia.org/wiki/SubRip
|
|
8
|
+
SubStationAlpha: 'SSA', // https://wikipedia.org/wiki/SubStation_Alpha
|
|
9
|
+
SubStationAlphav4: 'ASS', // https://wikipedia.org/wiki/SubStation_Alpha#Advanced_SubStation_Alpha=
|
|
10
|
+
TimedTextMarkupLang: 'TTML', // https://wikipedia.org/wiki/Timed_Text_Markup_Language
|
|
11
|
+
WebVTT: 'VTT', // https://wikipedia.org/wiki/WebVTT
|
|
12
|
+
// MPEG-DASH box-encapsulated subtitle formats
|
|
13
|
+
fTTML: 'STPP', // https://www.w3.org/TR/2018/REC-ttml-imsc1.0.1-20180424
|
|
14
|
+
fVTT: 'WVTT', // https://www.w3.org/TR/webvtt1
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseSubtitleCodecFromMime = (mime) => {
|
|
18
|
+
const target = mime.toLowerCase().trim().split('.')[0];
|
|
19
|
+
switch (target) {
|
|
20
|
+
case 'srt':
|
|
21
|
+
return SUBTITLE_CODECS.SubRip;
|
|
22
|
+
case 'ssa':
|
|
23
|
+
return SUBTITLE_CODECS.SubStationAlpha;
|
|
24
|
+
case 'ass':
|
|
25
|
+
return SUBTITLE_CODECS.SubStationAlphav4;
|
|
26
|
+
case 'ttml':
|
|
27
|
+
return SUBTITLE_CODECS.TimedTextMarkupLang;
|
|
28
|
+
case 'vtt':
|
|
29
|
+
return SUBTITLE_CODECS.WebVTT;
|
|
30
|
+
case 'stpp':
|
|
31
|
+
return SUBTITLE_CODECS.fTTML;
|
|
32
|
+
case 'wvtt':
|
|
33
|
+
return SUBTITLE_CODECS.fVTT;
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`The MIME ${mime} is not supported as subtitle codec`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseSubtitleCodec = (codecs) => {
|
|
40
|
+
const mimes = parseMimes(codecs);
|
|
41
|
+
for (const mime of mimes) {
|
|
42
|
+
try {
|
|
43
|
+
return parseSubtitleCodecFromMime(mime);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`No MIME types matched any supported Subtitle Codecs in ${codecs}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const checkIsClosedCaption = (roles = []) => {
|
|
52
|
+
for (const role of roles) {
|
|
53
|
+
const isClosedCaption =
|
|
54
|
+
role.attributes.schemeIdUri === 'urn:mpeg:dash:role:2011' &&
|
|
55
|
+
role.attributes.value === 'caption';
|
|
56
|
+
if (isClosedCaption) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const checkIsSdh = (accessibilities = []) => {
|
|
62
|
+
for (const accessibility of accessibilities) {
|
|
63
|
+
const { schemeIdUri, value } = accessibility.attributes;
|
|
64
|
+
const isSdh = schemeIdUri === 'urn:tva:metadata:cs:AudioPurposeCS:2007' && value === '2';
|
|
65
|
+
if (isSdh) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const checkIsForced = (roles = []) => {
|
|
71
|
+
for (const role of roles) {
|
|
72
|
+
const isForced =
|
|
73
|
+
role.attributes.schemeIdUri === 'urn:mpeg:dash:role:2011' &&
|
|
74
|
+
(role.attributes.value === 'forced-subtitle' || role.attributes.value === 'forced_subtitle');
|
|
75
|
+
if (isForced) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const createSubtitleTrack = ({
|
|
81
|
+
id,
|
|
82
|
+
label,
|
|
83
|
+
bitrate,
|
|
84
|
+
duration,
|
|
85
|
+
type,
|
|
86
|
+
codec,
|
|
87
|
+
isClosedCaption,
|
|
88
|
+
isSdh,
|
|
89
|
+
isForced,
|
|
90
|
+
language,
|
|
91
|
+
segments,
|
|
92
|
+
}) => {
|
|
93
|
+
const parsedBitrate = parseBitrate(Number(bitrate));
|
|
94
|
+
const size = duration ? parseSize(Number(bitrate), Number(duration)) : undefined;
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
label,
|
|
98
|
+
bitrate: parsedBitrate,
|
|
99
|
+
size,
|
|
100
|
+
type,
|
|
101
|
+
codec,
|
|
102
|
+
isClosedCaption,
|
|
103
|
+
isSdh,
|
|
104
|
+
isForced,
|
|
105
|
+
segments,
|
|
106
|
+
language,
|
|
107
|
+
toString() {
|
|
108
|
+
return ['SUBTITLE', `[${codec}]`, language].join(' | ');
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
parseSubtitleCodec,
|
|
115
|
+
checkIsClosedCaption,
|
|
116
|
+
checkIsSdh,
|
|
117
|
+
checkIsForced,
|
|
118
|
+
createSubtitleTrack,
|
|
119
|
+
};
|
package/lib/track.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getWidth, getBestTrack } = require('./util');
|
|
4
|
+
|
|
5
|
+
const parseMimes = (codecs) =>
|
|
6
|
+
codecs
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((codec) => codec.trim().split('.')[0]);
|
|
10
|
+
|
|
11
|
+
const createResolutionFilter = (videos) => {
|
|
12
|
+
return ({ width, height }) => {
|
|
13
|
+
return videos.filter(
|
|
14
|
+
(track) => (!width || track.width === width) && (!height || track.height === height)
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const createVideoQualityFilter = (videos) => {
|
|
20
|
+
return (quality) => {
|
|
21
|
+
if (!quality) return [getBestTrack(videos)];
|
|
22
|
+
const trackQuality = String(quality).includes('p') ? quality : `${quality}p`;
|
|
23
|
+
const results = videos.filter((track) => track.quality === trackQuality);
|
|
24
|
+
return results.length ? results : [getBestTrack(videos)];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createAudioLanguageFilter = (audios) => {
|
|
29
|
+
return (languages = [], maxTracksPerLanguage) => {
|
|
30
|
+
if (!languages.length) {
|
|
31
|
+
for (const audio of audios) {
|
|
32
|
+
const alreadyAdded = languages.includes(audio.language);
|
|
33
|
+
if (!alreadyAdded) languages.push(audio.language);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const language of languages) {
|
|
38
|
+
const tracks = audios.filter((track) => track.language?.startsWith(language));
|
|
39
|
+
if (tracks.length > maxTracksPerLanguage) {
|
|
40
|
+
results.push(...tracks.slice(0, maxTracksPerLanguage));
|
|
41
|
+
} else {
|
|
42
|
+
results.push(...tracks);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createSubtitleLanguageFilter = (subtitles) => {
|
|
50
|
+
return (languages) => {
|
|
51
|
+
if (!languages.length) return subtitles;
|
|
52
|
+
return subtitles.filter((track) =>
|
|
53
|
+
languages.some(
|
|
54
|
+
(language) => track.language?.startsWith(language) || track.label?.startsWith(language)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
parseMimes,
|
|
62
|
+
createResolutionFilter,
|
|
63
|
+
createVideoQualityFilter,
|
|
64
|
+
createAudioLanguageFilter,
|
|
65
|
+
createSubtitleLanguageFilter,
|
|
66
|
+
};
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const formatBytes = (bytes, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']) => {
|
|
4
|
+
if (bytes == 0) return `0 ${sizes[0]}`;
|
|
5
|
+
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
6
|
+
if (i == 0) return bytes + ' ' + sizes[i];
|
|
7
|
+
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const parseSize = (bandwidth, duration) => ({
|
|
11
|
+
b: bandwidth * duration,
|
|
12
|
+
kb: (bandwidth / 1000) * duration,
|
|
13
|
+
mb: (bandwidth / 8e6) * duration,
|
|
14
|
+
gb: (bandwidth / 8e9) * duration,
|
|
15
|
+
toString() {
|
|
16
|
+
return formatBytes(bandwidth * duration);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const parseBitrate = (bandwidth) => ({
|
|
21
|
+
bps: bandwidth,
|
|
22
|
+
kbps: bandwidth / 1000,
|
|
23
|
+
mbps: bandwidth / 8e6,
|
|
24
|
+
gbps: bandwidth / 8e9,
|
|
25
|
+
toString() {
|
|
26
|
+
return formatBytes(bandwidth, ['bps', 'Kbps', 'Mbps', 'Gbps']);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const qualities = [
|
|
31
|
+
{ width: 7680, height: 4320 },
|
|
32
|
+
{ width: 3840, height: 2160 },
|
|
33
|
+
{ width: 2560, height: 1440 },
|
|
34
|
+
{ width: 1920, height: 1080 },
|
|
35
|
+
{ width: 1280, height: 720 },
|
|
36
|
+
{ width: 854, height: 480 },
|
|
37
|
+
{ width: 640, height: 360 },
|
|
38
|
+
{ width: 426, height: 240 },
|
|
39
|
+
{ width: 256, height: 144 },
|
|
40
|
+
];
|
|
41
|
+
const getWidth = (height) => qualities.find((q) => q.height === height)?.width;
|
|
42
|
+
const getHeight = (width) => qualities.find((q) => q.width === width)?.height;
|
|
43
|
+
const getQualityLabel = (resolution) => `${getHeight(resolution.width) || resolution.height}p`;
|
|
44
|
+
|
|
45
|
+
const getBestTrack = (tracks) => {
|
|
46
|
+
const maxBitrate = Math.max(...tracks.map((track) => track.bitrate.bps));
|
|
47
|
+
return tracks.find((track) => track.bitrate.bps === maxBitrate);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const parseDuration = (str) => {
|
|
51
|
+
const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
|
|
52
|
+
const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
|
|
53
|
+
const SECONDS_IN_DAY = 24 * 60 * 60;
|
|
54
|
+
const SECONDS_IN_HOUR = 60 * 60;
|
|
55
|
+
const SECONDS_IN_MIN = 60;
|
|
56
|
+
|
|
57
|
+
// P10Y10M10DT10H10M10.1S
|
|
58
|
+
const durationRegex =
|
|
59
|
+
/P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
|
|
60
|
+
const match = durationRegex.exec(str);
|
|
61
|
+
|
|
62
|
+
if (!match) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [year, month, day, hour, minute, second] = match.slice(1);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
parseFloat(year || 0) * SECONDS_IN_YEAR +
|
|
70
|
+
parseFloat(month || 0) * SECONDS_IN_MONTH +
|
|
71
|
+
parseFloat(day || 0) * SECONDS_IN_DAY +
|
|
72
|
+
parseFloat(hour || 0) * SECONDS_IN_HOUR +
|
|
73
|
+
parseFloat(minute || 0) * SECONDS_IN_MIN +
|
|
74
|
+
parseFloat(second || 0)
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isLanguageTagValid = (value) => {
|
|
79
|
+
try {
|
|
80
|
+
Intl.getCanonicalLocales(value);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
parseSize,
|
|
89
|
+
parseBitrate,
|
|
90
|
+
getWidth,
|
|
91
|
+
getHeight,
|
|
92
|
+
getQualityLabel,
|
|
93
|
+
getBestTrack,
|
|
94
|
+
parseDuration,
|
|
95
|
+
isLanguageTagValid,
|
|
96
|
+
};
|
package/lib/video.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const { parseBitrate, getQualityLabel, parseSize } = require('./util');
|
|
2
|
+
|
|
3
|
+
const VIDEO_CODECS = {
|
|
4
|
+
avc: 'H.264',
|
|
5
|
+
hevc: 'H.265',
|
|
6
|
+
vc1: 'VC-1',
|
|
7
|
+
vp8: 'VP8',
|
|
8
|
+
vp9: 'VP9',
|
|
9
|
+
av1: 'AV1',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DYNAMIC_RANGE = {
|
|
13
|
+
sdr: 'SDR', // Standart Dynamic Range
|
|
14
|
+
hlg: 'HLG', // Hybrid log-gamma (HDR)
|
|
15
|
+
hdr10: 'HDR10',
|
|
16
|
+
hdr10p: 'HDR10+',
|
|
17
|
+
dv: 'DV', // Dolby Vision
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PRIMARIES = {
|
|
21
|
+
Unspecified: 0,
|
|
22
|
+
BT_709: 1,
|
|
23
|
+
BT_601_625: 5,
|
|
24
|
+
BT_601_525: 6,
|
|
25
|
+
BT_2020_and_2100: 9,
|
|
26
|
+
SMPTE_ST_2113_and_EG_4321: 12, // P3D65
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TRANSFER = {
|
|
30
|
+
Unspecified: 0,
|
|
31
|
+
BT_709: 1,
|
|
32
|
+
BT_601: 6,
|
|
33
|
+
BT_2020: 14,
|
|
34
|
+
BT_2100: 15,
|
|
35
|
+
BT_2100_PQ: 16,
|
|
36
|
+
BT_2100_HLG: 18,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const MATRIX = {
|
|
40
|
+
RGB: 0,
|
|
41
|
+
YCbCr_BT_709: 1,
|
|
42
|
+
YCbCr_BT_601_625: 5,
|
|
43
|
+
YCbCr_BT_601_525: 6,
|
|
44
|
+
YCbCr_BT_2020_and_2100: 9, // YCbCr BT.2100 shares the same CP
|
|
45
|
+
ICtCp_BT_2100: 14,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parseVideoCodecFromMime = (mime) => {
|
|
49
|
+
const target = mime.toLowerCase().trim().split('.')[0];
|
|
50
|
+
const avc = ['avc1', 'avc2', 'avc3', 'dva1', 'dvav'];
|
|
51
|
+
const hevc = ['hev1', 'hev2', 'hev3', 'hvc1', 'hvc2', 'hvc3', 'dvh1', 'dvhe', 'lhv1', 'lhe1'];
|
|
52
|
+
const vc1 = ['vc-1'];
|
|
53
|
+
const vp8 = ['vp08', 'vp8'];
|
|
54
|
+
const vp9 = ['vp09', 'vp9'];
|
|
55
|
+
const av1 = ['av01'];
|
|
56
|
+
if (avc.includes(target)) return VIDEO_CODECS.avc;
|
|
57
|
+
if (hevc.includes(target)) return VIDEO_CODECS.hevc;
|
|
58
|
+
if (vc1.includes(target)) return VIDEO_CODECS.hevc;
|
|
59
|
+
if (vp8.includes(target)) return VIDEO_CODECS.vp8;
|
|
60
|
+
if (vp9.includes(target)) return VIDEO_CODECS.vp9;
|
|
61
|
+
if (av1.includes(target)) return VIDEO_CODECS.av1;
|
|
62
|
+
throw new Error(`The MIME ${mime} is not supported as video codec`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const parseDynamicRangeFromCicp = (primaries, transfer, matrix) => {
|
|
66
|
+
// While not part of any standard, it is typically used as a PAL variant of Transfer.BT_601=6.
|
|
67
|
+
// i.e. where Transfer 6 would be for BT.601-NTSC and Transfer 5 would be for BT.601-PAL.
|
|
68
|
+
// The codebase is currently agnostic to either, so a manual conversion to 6 is done.
|
|
69
|
+
if (transfer == 5) transfer = TRANSFER.BT_601;
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
primaries == PRIMARIES.Unspecified &&
|
|
73
|
+
transfer == TRANSFER.Unspecified &&
|
|
74
|
+
matrix == MATRIX.RGB
|
|
75
|
+
)
|
|
76
|
+
return DYNAMIC_RANGE.sdr;
|
|
77
|
+
else if ([PRIMARIES.BT_601_625, PRIMARIES.BT_601_525].includes(primaries))
|
|
78
|
+
return DYNAMIC_RANGE.sdr;
|
|
79
|
+
else if (TRANSFER.BT_2100_PQ === transfer) return DYNAMIC_RANGE.hdr10;
|
|
80
|
+
else if (TRANSFER.BT_2100_HLG === transfer) return DYNAMIC_RANGE.hlg;
|
|
81
|
+
else return DYNAMIC_RANGE.sdr;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const createVideoTrack = ({
|
|
85
|
+
id,
|
|
86
|
+
label,
|
|
87
|
+
type,
|
|
88
|
+
codec,
|
|
89
|
+
dynamicRange,
|
|
90
|
+
contentProtection,
|
|
91
|
+
bitrate,
|
|
92
|
+
duration,
|
|
93
|
+
width,
|
|
94
|
+
height,
|
|
95
|
+
fps,
|
|
96
|
+
language,
|
|
97
|
+
segments,
|
|
98
|
+
}) => {
|
|
99
|
+
const parsedBitrate = parseBitrate(Number(bitrate));
|
|
100
|
+
const parsedWidth = Number(width);
|
|
101
|
+
const parsedHeight = Number(height);
|
|
102
|
+
const size = duration ? parseSize(Number(bitrate), Number(duration)) : undefined;
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
label,
|
|
106
|
+
type,
|
|
107
|
+
codec,
|
|
108
|
+
bitrate: parsedBitrate,
|
|
109
|
+
size,
|
|
110
|
+
protection: contentProtection,
|
|
111
|
+
segments,
|
|
112
|
+
dynamicRange,
|
|
113
|
+
language,
|
|
114
|
+
width: parsedWidth,
|
|
115
|
+
height: parsedHeight,
|
|
116
|
+
fps: Number(fps),
|
|
117
|
+
quality: getQualityLabel({ width: parsedWidth, height: parsedHeight }),
|
|
118
|
+
toString() {
|
|
119
|
+
return [
|
|
120
|
+
'VIDEO',
|
|
121
|
+
`[${codec}, ${dynamicRange}]`,
|
|
122
|
+
language,
|
|
123
|
+
`${width}x${height} @ ${parsedBitrate.kbps} kb/s, ${fps} FPS`,
|
|
124
|
+
].join(' | ');
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseVideoCodec = (codecs) => {
|
|
130
|
+
for (const codec of codecs.toLowerCase().split(',')) {
|
|
131
|
+
const mime = codec.trim().split('.')[0];
|
|
132
|
+
try {
|
|
133
|
+
return parseVideoCodecFromMime(mime);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`No MIME types matched any supported Video Codecs in ${codecs}`);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const parseDynamicRange = (codecs, supplementalProps = [], essentialProps = []) => {
|
|
142
|
+
const dv = ['dva1', 'dvav', 'dvhe', 'dvh1'];
|
|
143
|
+
if (dv.some((value) => codecs.startsWith(value))) return DYNAMIC_RANGE.dv;
|
|
144
|
+
const primariesScheme = 'urn:mpeg:mpegB:cicp:ColourPrimaries';
|
|
145
|
+
const transferScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
|
|
146
|
+
const matrixScheme = 'urn:mpeg:mpegB:cicp:MatrixCoefficients';
|
|
147
|
+
const allProps = [...essentialProps, ...supplementalProps];
|
|
148
|
+
const getValues = (scheme) =>
|
|
149
|
+
allProps
|
|
150
|
+
.filter((prop) => prop.attributes.schemeIdUri === scheme)
|
|
151
|
+
.map((prop) => parseInt(prop.attributes.value));
|
|
152
|
+
const primaries = getValues(primariesScheme).reduce((acc, current) => acc + current, 0);
|
|
153
|
+
const transfer = getValues(transferScheme).reduce((acc, current) => acc + current, 0);
|
|
154
|
+
const matrix = getValues(matrixScheme).reduce((acc, current) => acc + current, 0);
|
|
155
|
+
return parseDynamicRangeFromCicp(primaries, transfer, matrix);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
module.exports = { parseVideoCodec, parseDynamicRange, createVideoTrack, VIDEO_CODECS };
|