dasha 3.1.5 → 4.0.0-alpha.1

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/hls.js DELETED
@@ -1,234 +0,0 @@
1
- 'use strict';
2
-
3
- const { dirname, basename } = require('node:path');
4
- const m3u8Parser = require('m3u8-parser');
5
- const { parseBitrate, getQualityLabel } = require('./util');
6
- const {
7
- createResolutionFilter,
8
- createVideoQualityFilter,
9
- createAudioLanguageFilter,
10
- createSubtitleLanguageFilter,
11
- createVideoCodecFilter,
12
- createAudioCodecFilter,
13
- createAudioChannelsFilter,
14
- } = require('./track');
15
- const { createAudioTrack } = require('./audio');
16
- const { createVideoTrack } = require('./video');
17
-
18
- const parseM3u8 = (manifestString) => {
19
- const parser = new m3u8Parser.Parser();
20
- parser.push(manifestString);
21
- parser.end();
22
- return parser.manifest;
23
- };
24
-
25
- const fetchPlaylist = async (url) => {
26
- const response = await fetch(url);
27
- if (!response.ok)
28
- throw new Error(`Failed to fetch playlist (${response.status}): ${url}`);
29
- const text = await response.text();
30
- return parseM3u8(text);
31
- };
32
-
33
- const parseUrl = (playlistUri, manifestUri) => {
34
- let value = playlistUri;
35
- if (!value.startsWith('https://'))
36
- value =
37
- new URL(value, manifestUri).toString() + new URL(manifestUri).search;
38
- return value;
39
- };
40
-
41
- const urlsSame = (url1, url2) => {
42
- return new URL(url1).pathname === new URL(url2).pathname;
43
- };
44
-
45
- const parseMediaGroup = (groups, manifestUri) => {
46
- const results = [];
47
- if (!groups) return results;
48
- for (const [groupId, group] of Object.entries(groups)) {
49
- for (const [label, entity] of Object.entries(group)) {
50
- const url = parseUrl(entity.uri, manifestUri);
51
- const existing = results.find((result) => urlsSame(result.url, url));
52
- if (!existing)
53
- results.push({
54
- groupId,
55
- id: entity.uri.replace('/', ''),
56
- type: groupId,
57
- label,
58
- language: entity.language,
59
- url,
60
- default: entity.default,
61
- });
62
- }
63
- }
64
- return results;
65
- };
66
-
67
- const getAudioPlaylists = (m3u8, manifestUri) => {
68
- if (!m3u8.mediaGroups) return [];
69
- return parseMediaGroup(m3u8.mediaGroups.AUDIO, manifestUri);
70
- };
71
-
72
- const getSubtitlePlaylists = (m3u8, manifestUri) => {
73
- if (!m3u8.mediaGroups) return [];
74
- return parseMediaGroup(m3u8.mediaGroups.SUBTITLES, manifestUri);
75
- };
76
-
77
- const getVideoPlaylists = (m3u8, manifestUri) => {
78
- if (!m3u8.playlists) return [];
79
- return m3u8.playlists.map((data) => {
80
- const bandwidth = data.attributes?.BANDWIDTH;
81
- const url = data.resolvedUri || parseUrl(data.uri, manifestUri);
82
- const track = {
83
- id: data.uri.replace('/', ''),
84
- bitrate: parseBitrate(bandwidth),
85
- url,
86
- };
87
- track.type = 'video';
88
- if (data.attributes.RESOLUTION) {
89
- track.resolution = data.attributes.RESOLUTION;
90
- track.quality = getQualityLabel(track.resolution);
91
- }
92
- if (data.attributes['VIDEO-RANGE'])
93
- track.dynamicRange = data.attributes['VIDEO-RANGE'];
94
- if (data.attributes.CODECS) track.codecs = data.attributes.CODECS;
95
- if (data.attributes['FRAME-RATE'])
96
- track.frameRate = data.attributes['FRAME-RATE'];
97
- return track;
98
- });
99
- };
100
-
101
- const segmentsDto = (data = [], track) => {
102
- const mapSegment = (item) => {
103
- let url = item.resolvedUri || item.uri;
104
- if (!url.startsWith('https://') && track.url) {
105
- const baseUrl = dirname(track.url) + '/';
106
- url = new URL(url, baseUrl).toString();
107
- }
108
- return {
109
- url,
110
- duration: item.duration,
111
- number: item.number,
112
- presentationTime: item.presentationTime,
113
- };
114
- };
115
- const segments = data.map(mapSegment);
116
- const init = data[0].map;
117
- if (data.length && (init?.resolvedUri || init?.uri)) {
118
- const url = init?.resolvedUri || parseUrl(init.uri, track.url);
119
- segments.unshift({
120
- url: url,
121
- init: true,
122
- duration: 0,
123
- number: 0,
124
- presentationTime: 0,
125
- });
126
- }
127
- return segments;
128
- };
129
-
130
- const parseSegments = (playlist, track) => {
131
- track.segments = segmentsDto(playlist.segments, track);
132
- if (playlist.contentProtection) {
133
- track.protection = {};
134
- const fairplayLegacy = playlist.contentProtection['com.apple.fps.1_0'];
135
- if (fairplayLegacy)
136
- track.protection.fairplay = {
137
- keyFormat: fairplayLegacy.attributes.KEYFORMAT,
138
- uri: fairplayLegacy.attributes.URI,
139
- method: fairplayLegacy.attributes.METHOD,
140
- };
141
- const widevine = playlist.contentProtection['com.widevine.alpha'];
142
- if (widevine) {
143
- track.protection.widevine = {
144
- pssh: widevine.pssh,
145
- uri: widevine.attributes.schemeIdUri,
146
- keyId: widevine.attributes.keyId,
147
- };
148
- }
149
- }
150
- };
151
-
152
- const fetchTrackSegments = (tracks) => {
153
- return Promise.all(
154
- tracks.map(async (track) => {
155
- const playlist = await fetchPlaylist(track.url);
156
- parseSegments(playlist, track);
157
- }),
158
- );
159
- };
160
-
161
- const parseManifest = async (manifestString, manifestUri) => {
162
- const m3u8 = parseM3u8(manifestString);
163
- const videos = getVideoPlaylists(m3u8, manifestUri);
164
- const audios = getAudioPlaylists(m3u8, manifestUri);
165
- const subtitles = getSubtitlePlaylists(m3u8, manifestUri);
166
-
167
- if (!m3u8.playlists && m3u8.segments) {
168
- // TODO: Handle audio-only manifests
169
- const { pathname } = new URL(manifestUri);
170
- const isAudio =
171
- pathname.includes('.m4a') ||
172
- pathname.includes('.mp3') ||
173
- pathname.includes('.opus');
174
- if (isAudio) {
175
- const track = createAudioTrack({
176
- id: 'audio' + basename(pathname),
177
- label: 'audio',
178
- type: 'audio',
179
- codec: '',
180
- channels: 2,
181
- jointObjectCoding: '',
182
- isDescriptive: false,
183
- bitrate: NaN,
184
- duration: NaN,
185
- language: '',
186
- });
187
- parseSegments(m3u8, track);
188
- audios.push(track);
189
- } else {
190
- const track = createVideoTrack({
191
- id: 'video' + basename(pathname),
192
- label: 'video',
193
- type: 'video',
194
- codec: '',
195
- dynamicRange: '',
196
- contentProtection: '',
197
- bitrate: NaN,
198
- duration: NaN,
199
- width: NaN,
200
- height: NaN,
201
- fps: NaN,
202
- language: '',
203
- });
204
- parseSegments(m3u8, track);
205
- videos.push(track);
206
- }
207
- } else {
208
- await Promise.all([
209
- fetchTrackSegments(videos),
210
- fetchTrackSegments(audios),
211
- fetchTrackSegments(subtitles),
212
- ]);
213
- }
214
-
215
- const manifest = {
216
- tracks: {
217
- all: videos.concat(audios).concat(subtitles),
218
- videos,
219
- audios,
220
- subtitles,
221
- withResolution: createResolutionFilter(videos),
222
- withVideoCodecs: createVideoCodecFilter(videos),
223
- withVideoQuality: createVideoQualityFilter(videos),
224
- withAudioCodecs: createAudioCodecFilter(audios),
225
- withAudioLanguages: createAudioLanguageFilter(audios),
226
- withAudioChannels: createAudioChannelsFilter(audios),
227
- withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
228
- },
229
- };
230
-
231
- return manifest;
232
- };
233
-
234
- module.exports = { parseManifest };
package/lib/subtitle.js DELETED
@@ -1,137 +0,0 @@
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
- case 'x-subrip':
22
- return SUBTITLE_CODECS.SubRip;
23
- case 'ssa':
24
- return SUBTITLE_CODECS.SubStationAlpha;
25
- case 'ass':
26
- return SUBTITLE_CODECS.SubStationAlphav4;
27
- case 'ttml':
28
- return SUBTITLE_CODECS.TimedTextMarkupLang;
29
- case 'vtt':
30
- return SUBTITLE_CODECS.WebVTT;
31
- case 'stpp':
32
- return SUBTITLE_CODECS.fTTML;
33
- case 'wvtt':
34
- return SUBTITLE_CODECS.fVTT;
35
- default:
36
- throw new Error(`The MIME ${mime} is not supported as subtitle codec`);
37
- }
38
- };
39
-
40
- const parseSubtitleCodec = (codecs) => {
41
- const mimes = parseMimes(codecs);
42
- for (const mime of mimes) {
43
- try {
44
- return parseSubtitleCodecFromMime(mime);
45
- } catch (e) {
46
- continue;
47
- }
48
- }
49
- throw new Error(
50
- `No MIME types matched any supported Subtitle Codecs in ${codecs}`,
51
- );
52
- };
53
-
54
- const tryParseSubtitleCodec = (codecs) => {
55
- try {
56
- return parseSubtitleCodec(codecs);
57
- } catch (e) {
58
- return null;
59
- }
60
- };
61
-
62
- const checkIsClosedCaption = (roles = []) => {
63
- for (const role of roles) {
64
- const isClosedCaption =
65
- role.attributes.schemeIdUri === 'urn:mpeg:dash:role:2011' &&
66
- role.attributes.value === 'caption';
67
- if (isClosedCaption) return true;
68
- }
69
- return false;
70
- };
71
-
72
- const checkIsSdh = (accessibilities = []) => {
73
- for (const accessibility of accessibilities) {
74
- const { schemeIdUri, value } = accessibility.attributes;
75
- const isSdh =
76
- schemeIdUri === 'urn:tva:metadata:cs:AudioPurposeCS:2007' &&
77
- value === '2';
78
- if (isSdh) return true;
79
- }
80
- return false;
81
- };
82
-
83
- const checkIsForced = (roles = []) => {
84
- for (const role of roles) {
85
- const isForced =
86
- role.attributes.schemeIdUri === 'urn:mpeg:dash:role:2011' &&
87
- (role.attributes.value === 'forced-subtitle' ||
88
- role.attributes.value === 'forced_subtitle');
89
- if (isForced) return true;
90
- }
91
- return false;
92
- };
93
-
94
- const createSubtitleTrack = ({
95
- id,
96
- label,
97
- bitrate,
98
- duration,
99
- type,
100
- codec,
101
- isClosedCaption,
102
- isSdh,
103
- isForced,
104
- language,
105
- segments,
106
- }) => {
107
- const parsedBitrate = parseBitrate(Number(bitrate));
108
- const size = duration
109
- ? parseSize(Number(bitrate), Number(duration))
110
- : undefined;
111
- return {
112
- id,
113
- label,
114
- bitrate: parsedBitrate,
115
- size,
116
- type,
117
- codec,
118
- isClosedCaption,
119
- isSdh,
120
- isForced,
121
- segments,
122
- language,
123
- toString() {
124
- return ['SUBTITLE', `[${codec}]`, language].join(' | ');
125
- },
126
- };
127
- };
128
-
129
- module.exports = {
130
- SUBTITLE_CODECS,
131
- parseSubtitleCodec,
132
- tryParseSubtitleCodec,
133
- checkIsClosedCaption,
134
- checkIsSdh,
135
- checkIsForced,
136
- createSubtitleTrack,
137
- };
package/lib/track.js DELETED
@@ -1,127 +0,0 @@
1
- 'use strict';
2
-
3
- const { 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) =>
15
- (!width || track.width === width) &&
16
- (!height || track.height === height),
17
- );
18
- };
19
- };
20
-
21
- const createCodecFilter = (tracks) => {
22
- return (codecs) => {
23
- if (!codecs?.length) return tracks;
24
- const results = tracks.filter((track) => codecs.includes(track.codec));
25
- results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
26
- return results;
27
- };
28
- };
29
-
30
- const createVideoCodecFilter = (videos) => createCodecFilter(videos);
31
-
32
- const createVideoQualityFilter = (videos) => {
33
- return (quality) => {
34
- if (!quality) return [getBestTrack(videos)];
35
- const trackQuality = String(quality).includes('p')
36
- ? quality
37
- : `${quality}p`;
38
- const results = videos.filter((track) => track.quality === trackQuality);
39
- results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
40
- return results.length ? results : [getBestTrack(videos)];
41
- };
42
- };
43
-
44
- const createAudioCodecFilter = (audios) => createCodecFilter(audios);
45
-
46
- const createAudioLanguageFilter = (audios) => {
47
- return (languages = [], maxTracksPerLanguage) => {
48
- if (!languages.length) {
49
- const audiosWithLanguage = audios.filter((track) => track.language);
50
- if (audiosWithLanguage.length) {
51
- for (const audio of audiosWithLanguage) {
52
- const alreadyAdded = languages.includes(audio.language);
53
- if (!alreadyAdded) languages.push(audio.language);
54
- }
55
- } else {
56
- audios.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
57
- return audios.slice(0, maxTracksPerLanguage);
58
- }
59
- }
60
- const filtered = [];
61
- for (const language of languages) {
62
- const tracks = audios.filter((track) =>
63
- track.language?.startsWith(language),
64
- );
65
- filtered.push(...tracks);
66
- }
67
- const results = [];
68
- const filteredLanguages = [
69
- ...new Set(filtered.map((track) => `${track.language}:${track.label}`)),
70
- ];
71
- for (const language of filteredLanguages) {
72
- const tracks = filtered
73
- .filter((track) => `${track.language}:${track.label}` === language)
74
- .slice(0, maxTracksPerLanguage);
75
- results.push(...tracks);
76
- }
77
- return results;
78
- };
79
- };
80
-
81
- const createAudioChannelsFilter = (audios) => {
82
- return (channels) => {
83
- if (!channels) return audios;
84
- const value =
85
- typeof channels === 'string' ? parseFloat(channels) : channels;
86
- return audios.filter((track) => track.channels === value);
87
- };
88
- };
89
-
90
- const createSubtitleLanguageFilter = (subtitles) => {
91
- return (languages) => {
92
- if (!languages.length) return subtitles;
93
- return subtitles.filter((track) =>
94
- languages.some(
95
- (language) =>
96
- track.language?.startsWith(language) ||
97
- track.label?.startsWith(language),
98
- ),
99
- );
100
- };
101
- };
102
-
103
- const filterByResolution = (tracks, resolution) =>
104
- createResolutionFilter(tracks)(resolution);
105
- const filterByQuality = (tracks, quality) =>
106
- createVideoQualityFilter(tracks)(quality);
107
- const filterByCodecs = (tracks, codecs) => createCodecFilter(tracks)(codecs);
108
- const filterByLanguages = (tracks, languages, maxTracksPerLanguage) =>
109
- createAudioLanguageFilter(tracks)(languages, maxTracksPerLanguage);
110
- const filterByChannels = (tracks, channels) =>
111
- createAudioChannelsFilter(tracks)(channels);
112
-
113
- module.exports = {
114
- parseMimes,
115
- createResolutionFilter,
116
- createVideoCodecFilter,
117
- createVideoQualityFilter,
118
- createAudioCodecFilter,
119
- createAudioLanguageFilter,
120
- createAudioChannelsFilter,
121
- createSubtitleLanguageFilter,
122
- filterByResolution,
123
- filterByQuality,
124
- filterByCodecs,
125
- filterByLanguages,
126
- filterByChannels,
127
- };
package/lib/util.js DELETED
@@ -1,98 +0,0 @@
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: 1024, height: 576 },
37
- { width: 854, height: 480 },
38
- { width: 640, height: 360 },
39
- { width: 426, height: 240 },
40
- { width: 256, height: 144 },
41
- ];
42
- const getWidth = (height) => qualities.find((q) => q.height === height)?.width;
43
- const getHeight = (width) => qualities.find((q) => q.width === width)?.height;
44
- const getQualityLabel = (resolution) =>
45
- `${getHeight(resolution.width) || resolution.height}p`;
46
-
47
- const getBestTrack = (tracks) => {
48
- const maxBitrate = Math.max(...tracks.map((track) => track.bitrate.bps));
49
- return tracks.find((track) => track.bitrate.bps === maxBitrate);
50
- };
51
-
52
- const parseDuration = (str) => {
53
- const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
54
- const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
55
- const SECONDS_IN_DAY = 24 * 60 * 60;
56
- const SECONDS_IN_HOUR = 60 * 60;
57
- const SECONDS_IN_MIN = 60;
58
-
59
- // P10Y10M10DT10H10M10.1S
60
- const durationRegex =
61
- /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
62
- const match = durationRegex.exec(str);
63
-
64
- if (!match) {
65
- return 0;
66
- }
67
-
68
- const [year, month, day, hour, minute, second] = match.slice(1);
69
-
70
- return (
71
- parseFloat(year || 0) * SECONDS_IN_YEAR +
72
- parseFloat(month || 0) * SECONDS_IN_MONTH +
73
- parseFloat(day || 0) * SECONDS_IN_DAY +
74
- parseFloat(hour || 0) * SECONDS_IN_HOUR +
75
- parseFloat(minute || 0) * SECONDS_IN_MIN +
76
- parseFloat(second || 0)
77
- );
78
- };
79
-
80
- const isLanguageTagValid = (value) => {
81
- try {
82
- Intl.getCanonicalLocales(value);
83
- return true;
84
- } catch (e) {
85
- return;
86
- }
87
- };
88
-
89
- module.exports = {
90
- parseSize,
91
- parseBitrate,
92
- getWidth,
93
- getHeight,
94
- getQualityLabel,
95
- getBestTrack,
96
- parseDuration,
97
- isLanguageTagValid,
98
- };