dasha 3.0.3 → 3.0.5

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # dasha
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
4
- [![npm downloads/month](https://img.shields.io/npm/dm/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
5
- [![npm downloads](https://img.shields.io/npm/dt/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
3
+ [![npm version](https://img.shields.io/npm/v/dasha?style=flat&color=black)](https://www.npmjs.com/package/dasha)
4
+ [![npm downloads/month](https://img.shields.io/npm/dm/dasha?style=flat&color=black)](https://www.npmjs.com/package/dasha)
5
+ [![npm downloads](https://img.shields.io/npm/dt/dasha?style=flat&color=black)](https://www.npmjs.com/package/dasha)
6
6
 
7
- Library for parsing MPEG-DASH and HLS manifests. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments.
7
+ Library for parsing MPEG-DASH (.mpd) and HLS (.m3u8) manifests. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments.
8
8
 
9
9
  ## Install
10
10
 
@@ -15,9 +15,17 @@ npm i dasha
15
15
  ## Quick start
16
16
 
17
17
  ```js
18
+ import fs from 'node:fs/promises';
18
19
  import { parse } from 'dasha';
19
20
 
20
21
  const url = 'https://dash.akamaized.net/dash264/TestCases/1a/sony/SNE_DASH_SD_CASE1A_REVISED.mpd';
21
22
  const body = await fetch(url).then((res) => res.text());
22
23
  const manifest = await parse(body, url);
24
+
25
+ for (const track of manifest.tracks.all) {
26
+ for (const segment of track.segments) {
27
+ const content = await fetch(url).then((res) => res.arrayBuffer());
28
+ await fs.appendFile(`${track.id}.mp4`, content);
29
+ }
30
+ }
23
31
  ```
package/dasha.js CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  const dash = require('./lib/dash');
4
4
  const hls = require('./lib/hls');
5
+ const {
6
+ filterByResolution,
7
+ filterByQuality,
8
+ filterByCodecs,
9
+ filterByLanguages,
10
+ filterByChannels,
11
+ } = require('./lib/track');
5
12
 
6
13
  const parse = (text, url, fallbackLanguage) => {
7
14
  if (text.includes('<MPD')) return dash.parseManifest(text, url, fallbackLanguage);
@@ -9,4 +16,11 @@ const parse = (text, url, fallbackLanguage) => {
9
16
  else throw new Error('Invalid manifest');
10
17
  };
11
18
 
12
- module.exports = { parse };
19
+ module.exports = {
20
+ parse,
21
+ filterByResolution,
22
+ filterByQuality,
23
+ filterByCodecs,
24
+ filterByLanguages,
25
+ filterByChannels,
26
+ };
package/lib/dash.js CHANGED
@@ -21,6 +21,9 @@ const {
21
21
  createVideoQualityFilter,
22
22
  createAudioLanguageFilter,
23
23
  createSubtitleLanguageFilter,
24
+ createVideoCodecFilter,
25
+ createAudioCodecFilter,
26
+ createAudioChannelsFilter,
24
27
  } = require('./track');
25
28
 
26
29
  const appendUtils = (element) => {
@@ -87,7 +90,7 @@ const parseLanguage = (representation, adaptationSet, fallbackLanguage) => {
87
90
  options.push(lang);
88
91
  if (id) {
89
92
  const m = id.match(/\w+_(\w+)=\d+/);
90
- if (m) options.push(m.group(1));
93
+ if (m && m[1]) options.push(m[1]);
91
94
  }
92
95
  }
93
96
  options.push(adaptationSet.get('lang'));
@@ -173,10 +176,14 @@ const parseSegmentsFromTemplate = (
173
176
  const startNumber = Number(segmentTemplate.get('startNumber') || 1);
174
177
  const segmentTimeline = segmentTemplate.get('SegmentTimeline');
175
178
  resolveSegmentTemplateUrls(segmentTemplate, baseUrl, manifestUrl);
176
- if (!duration) throw new Error('Duration of the Period was unable to be determined.');
177
179
  const segmentDuration = parseFloat(segmentTemplate.get('duration'));
178
180
  const segmentTimescale = parseFloat(segmentTemplate.get('timescale') || 1);
179
- const segmentsCount = Math.ceil(duration / (segmentDuration / segmentTimescale));
181
+ // TODO: Support live manifests with type=dynamic
182
+ const DEFAULT_SEGMENTS_COUNT = 35;
183
+ // if (!duration) throw new Error('Duration of the Period was unable to be determined.');
184
+ const segmentsCount = duration
185
+ ? Math.ceil(duration / (segmentDuration / segmentTimescale))
186
+ : DEFAULT_SEGMENTS_COUNT;
180
187
  const bandwidth = representation.get('bandwidth');
181
188
  const id = representation.get('id');
182
189
  const segments = [];
@@ -222,12 +229,13 @@ const parseSegmentFromBase = async (segmentBase, baseUrl) => {
222
229
  const initialization = segmentBase.get('Initialization');
223
230
  let mediaRange = '';
224
231
  if (initialization) {
225
- const range = initialization.get('range');
226
- const headers = range ? { Range: `bytes=${range}` } : undefined;
227
- const response = await fetch(baseUrl, headers);
228
- const initData = await response.arrayBuffer();
229
- const totalSize = response.headers.get('Content-Range').split('/')[-1];
230
- if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
232
+ // const range = initialization.get('range');
233
+ // const headers = range ? { Range: `bytes=${range}` } : undefined;
234
+ // const response = await fetch(baseUrl, headers);
235
+ // const initData = await response.arrayBuffer();
236
+ // console.log(response.headers);
237
+ // const totalSize = response.headers.get('Content-Range').split('/')[-1];
238
+ // if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
231
239
  }
232
240
  return { url: baseUrl, range: mediaRange };
233
241
  };
@@ -398,6 +406,9 @@ const parseManifest = async (text, url, fallbackLanguage) => {
398
406
  }
399
407
  }
400
408
  }
409
+
410
+ videos.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
411
+
401
412
  return {
402
413
  duration,
403
414
  tracks: {
@@ -406,8 +417,11 @@ const parseManifest = async (text, url, fallbackLanguage) => {
406
417
  audios,
407
418
  subtitles,
408
419
  withResolution: createResolutionFilter(videos),
420
+ withVideoCodecs: createVideoCodecFilter(videos),
409
421
  withVideoQuality: createVideoQualityFilter(videos),
422
+ withAudioCodecs: createAudioCodecFilter(audios),
410
423
  withAudioLanguages: createAudioLanguageFilter(audios),
424
+ withAudioChannels: createAudioChannelsFilter(audios),
411
425
  withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
412
426
  },
413
427
  };
package/lib/hls.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { dirname, basename } = require('node:path');
3
4
  const m3u8Parser = require('m3u8-parser');
4
5
  const { parseBitrate, getQualityLabel } = require('./util');
5
6
  const {
@@ -7,7 +8,12 @@ const {
7
8
  createVideoQualityFilter,
8
9
  createAudioLanguageFilter,
9
10
  createSubtitleLanguageFilter,
11
+ createVideoCodecFilter,
12
+ createAudioCodecFilter,
13
+ createAudioChannelsFilter,
10
14
  } = require('./track');
15
+ const { createAudioTrack } = require('./audio');
16
+ const { createVideoTrack } = require('./video');
11
17
 
12
18
  const parseM3u8 = (manifestString) => {
13
19
  const parser = new m3u8Parser.Parser();
@@ -22,9 +28,9 @@ const fetchPlaylist = async (url) =>
22
28
  .then(parseM3u8);
23
29
 
24
30
  const parseUrl = (playlistUri, manifestUri) => {
25
- if (playlistUri.includes('https://')) return playlistUri;
26
- const uri = new URL(manifestUri);
27
- return uri.origin + playlistUri;
31
+ let value = playlistUri;
32
+ if (!value.startsWith('https://')) value = new URL(value, manifestUri).toString();
33
+ return value;
28
34
  };
29
35
 
30
36
  const urlsSame = (url1, url2) => {
@@ -51,18 +57,22 @@ const parseMediaGroup = (groups, manifestUri) => {
51
57
  };
52
58
 
53
59
  const getAudioPlaylists = (m3u8, manifestUri) => {
60
+ if (!m3u8.mediaGroups) return [];
54
61
  return parseMediaGroup(m3u8.mediaGroups.AUDIO, manifestUri);
55
62
  };
56
63
 
57
64
  const getSubtitlePlaylists = (m3u8, manifestUri) => {
65
+ if (!m3u8.mediaGroups) return [];
58
66
  return parseMediaGroup(m3u8.mediaGroups.SUBTITLES, manifestUri);
59
67
  };
60
68
 
61
69
  const getVideoPlaylists = (m3u8, manifestUri) => {
70
+ if (!m3u8.playlists) return [];
62
71
  return m3u8.playlists.map((data) => {
63
- const bandwidth = data.attributes.BANDWIDTH;
72
+ const bandwidth = data.attributes?.BANDWIDTH;
64
73
  const url = data.resolvedUri || parseUrl(data.uri, manifestUri);
65
74
  const track = { bitrate: parseBitrate(bandwidth), url };
75
+ track.type = 'video';
66
76
  if (data.attributes.RESOLUTION) {
67
77
  track.resolution = data.attributes.RESOLUTION;
68
78
  track.quality = getQualityLabel(track.resolution);
@@ -73,13 +83,20 @@ const getVideoPlaylists = (m3u8, manifestUri) => {
73
83
  });
74
84
  };
75
85
 
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
- });
86
+ const segmentsDto = (data = [], track) => {
87
+ const mapSegment = (item) => {
88
+ let url = item.resolvedUri || item.uri;
89
+ if (!url.startsWith('https://')) {
90
+ const baseUrl = dirname(track.url) + '/';
91
+ url = new URL(url, baseUrl).toString();
92
+ }
93
+ return {
94
+ url,
95
+ duration: item.duration,
96
+ number: item.number,
97
+ presentationTime: item.presentationTime,
98
+ };
99
+ };
83
100
  const segments = data.map(mapSegment);
84
101
  if (data.length && data[0].map?.resolvedUri)
85
102
  segments.unshift({
@@ -92,21 +109,25 @@ const segmentsDto = (data = []) => {
92
109
  return segments;
93
110
  };
94
111
 
112
+ const parseSegments = (playlist, track) => {
113
+ track.segments = segmentsDto(playlist.segments, track);
114
+ if (playlist.contentProtection) {
115
+ track.protection = {};
116
+ const fairplayLegacy = playlist.contentProtection['com.apple.fps.1_0'];
117
+ if (fairplayLegacy)
118
+ track.protection.fairplay = {
119
+ keyFormat: fairplayLegacy.attributes.KEYFORMAT,
120
+ uri: fairplayLegacy.attributes.URI,
121
+ method: fairplayLegacy.attributes.METHOD,
122
+ };
123
+ }
124
+ };
125
+
95
126
  const fetchTrackSegments = (tracks) => {
96
127
  return Promise.all(
97
128
  tracks.map(async (track) => {
98
129
  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
- }
130
+ parseSegments(playlist, track);
110
131
  })
111
132
  );
112
133
  };
@@ -117,11 +138,51 @@ const parseManifest = async (manifestString, manifestUri) => {
117
138
  const audios = getAudioPlaylists(m3u8, manifestUri);
118
139
  const subtitles = getSubtitlePlaylists(m3u8, manifestUri);
119
140
 
120
- await Promise.all([
121
- fetchTrackSegments(videos),
122
- fetchTrackSegments(audios),
123
- fetchTrackSegments(subtitles),
124
- ]);
141
+ if (!m3u8.playlists && m3u8.segments) {
142
+ // TODO: Handle audio-only manifests
143
+ const { pathname } = new URL(manifestUri);
144
+ const isAudio =
145
+ pathname.includes('.m4a') || pathname.includes('.mp3') || pathname.includes('.opus');
146
+ if (isAudio) {
147
+ const track = createAudioTrack({
148
+ id: 'audio' + basename(pathname),
149
+ label: 'audio',
150
+ type: 'audio',
151
+ codec: '',
152
+ channels: 2,
153
+ jointObjectCoding: '',
154
+ isDescriptive: false,
155
+ bitrate: NaN,
156
+ duration: NaN,
157
+ language: '',
158
+ });
159
+ parseSegments(m3u8, track);
160
+ audios.push(track);
161
+ } else {
162
+ const track = createVideoTrack({
163
+ id: 'video' + basename(pathname),
164
+ label: 'video',
165
+ type: 'video',
166
+ codec: '',
167
+ dynamicRange: '',
168
+ contentProtection: '',
169
+ bitrate: NaN,
170
+ duration: NaN,
171
+ width: NaN,
172
+ height: NaN,
173
+ fps: NaN,
174
+ language: '',
175
+ });
176
+ parseSegments(m3u8, track);
177
+ videos.push(track);
178
+ }
179
+ } else {
180
+ await Promise.all([
181
+ fetchTrackSegments(videos),
182
+ fetchTrackSegments(audios),
183
+ fetchTrackSegments(subtitles),
184
+ ]);
185
+ }
125
186
 
126
187
  const manifest = {
127
188
  tracks: {
@@ -130,8 +191,11 @@ const parseManifest = async (manifestString, manifestUri) => {
130
191
  audios,
131
192
  subtitles,
132
193
  withResolution: createResolutionFilter(videos),
194
+ withVideoCodecs: createVideoCodecFilter(videos),
133
195
  withVideoQuality: createVideoQualityFilter(videos),
196
+ withAudioCodecs: createAudioCodecFilter(audios),
134
197
  withAudioLanguages: createAudioLanguageFilter(audios),
198
+ withAudioChannels: createAudioChannelsFilter(audios),
135
199
  withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
136
200
  },
137
201
  };
package/lib/subtitle.js CHANGED
@@ -18,6 +18,7 @@ const parseSubtitleCodecFromMime = (mime) => {
18
18
  const target = mime.toLowerCase().trim().split('.')[0];
19
19
  switch (target) {
20
20
  case 'srt':
21
+ case 'x-subrip':
21
22
  return SUBTITLE_CODECS.SubRip;
22
23
  case 'ssa':
23
24
  return SUBTITLE_CODECS.SubStationAlpha;
package/lib/track.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { getWidth, getBestTrack } = require('./util');
3
+ const { getBestTrack } = require('./util');
4
4
 
5
5
  const parseMimes = (codecs) =>
6
6
  codecs
@@ -16,15 +16,29 @@ const createResolutionFilter = (videos) => {
16
16
  };
17
17
  };
18
18
 
19
+ const createCodecFilter = (tracks) => {
20
+ return (codecs) => {
21
+ if (!codecs?.length) return tracks;
22
+ const results = tracks.filter((track) => codecs.includes(track.codec));
23
+ results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
24
+ return results;
25
+ };
26
+ };
27
+
28
+ const createVideoCodecFilter = (videos) => createCodecFilter(videos);
29
+
19
30
  const createVideoQualityFilter = (videos) => {
20
31
  return (quality) => {
21
32
  if (!quality) return [getBestTrack(videos)];
22
33
  const trackQuality = String(quality).includes('p') ? quality : `${quality}p`;
23
34
  const results = videos.filter((track) => track.quality === trackQuality);
35
+ results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
24
36
  return results.length ? results : [getBestTrack(videos)];
25
37
  };
26
38
  };
27
39
 
40
+ const createAudioCodecFilter = (audios) => createCodecFilter(audios);
41
+
28
42
  const createAudioLanguageFilter = (audios) => {
29
43
  return (languages = [], maxTracksPerLanguage) => {
30
44
  if (!languages.length) {
@@ -50,6 +64,14 @@ const createAudioLanguageFilter = (audios) => {
50
64
  };
51
65
  };
52
66
 
67
+ const createAudioChannelsFilter = (audios) => {
68
+ return (channels) => {
69
+ if (!channels) return audios;
70
+ const value = typeof channels === 'string' ? parseFloat(channels) : channels;
71
+ return audios.filter((track) => track.channels === value);
72
+ };
73
+ };
74
+
53
75
  const createSubtitleLanguageFilter = (subtitles) => {
54
76
  return (languages) => {
55
77
  if (!languages.length) return subtitles;
@@ -61,10 +83,25 @@ const createSubtitleLanguageFilter = (subtitles) => {
61
83
  };
62
84
  };
63
85
 
86
+ const filterByResolution = (tracks, resolution) => createResolutionFilter(tracks)(resolution);
87
+ const filterByQuality = (tracks, quality) => createVideoQualityFilter(tracks)(quality);
88
+ const filterByCodecs = (tracks, codecs) => createCodecFilter(tracks)(codecs);
89
+ const filterByLanguages = (tracks, languages, maxTracksPerLanguage) =>
90
+ createAudioLanguageFilter(tracks)(languages, maxTracksPerLanguage);
91
+ const filterByChannels = (tracks, channels) => createAudioChannelsFilter(tracks)(channels);
92
+
64
93
  module.exports = {
65
94
  parseMimes,
66
95
  createResolutionFilter,
96
+ createVideoCodecFilter,
67
97
  createVideoQualityFilter,
98
+ createAudioCodecFilter,
68
99
  createAudioLanguageFilter,
100
+ createAudioChannelsFilter,
69
101
  createSubtitleLanguageFilter,
102
+ filterByResolution,
103
+ filterByQuality,
104
+ filterByCodecs,
105
+ filterByLanguages,
106
+ filterByChannels,
70
107
  };
package/lib/util.js CHANGED
@@ -33,6 +33,7 @@ const qualities = [
33
33
  { width: 2560, height: 1440 },
34
34
  { width: 1920, height: 1080 },
35
35
  { width: 1280, height: 720 },
36
+ { width: 1024, height: 576 },
36
37
  { width: 854, height: 480 },
37
38
  { width: 640, height: 360 },
38
39
  { width: 426, height: 240 },
package/lib/xml.js CHANGED
@@ -28,7 +28,7 @@ function parse(text, options = {}) {
28
28
 
29
29
  var closeTag = text.substring(closeStart, pos);
30
30
  if (closeTag.indexOf(tagName) == -1) {
31
- var parsedText = text.substring(0, pos).split('\n');
31
+ const parsedText = text.substring(0, pos).split('\n');
32
32
  throw new Error(
33
33
  'Unexpected close tag\nLine: ' +
34
34
  (parsedText.length - 1) +
@@ -103,7 +103,7 @@ function parse(text, options = {}) {
103
103
  node.children = [];
104
104
  }
105
105
  } else {
106
- var parsedText = parseText();
106
+ const parsedText = parseText();
107
107
  if (keepWhitespace) {
108
108
  if (parsedText.length > 0) {
109
109
  children.push(parsedText);
@@ -191,12 +191,12 @@ function parse(text, options = {}) {
191
191
  // optional parsing of children
192
192
  if (text.charCodeAt(pos - 1) !== slashCC) {
193
193
  if (tagName == 'script') {
194
- var start = pos + 1;
194
+ const start = pos + 1;
195
195
  pos = text.indexOf('</script>', pos);
196
196
  children = [text.slice(start, pos)];
197
197
  pos += 9;
198
198
  } else if (tagName == 'style') {
199
- var start = pos + 1;
199
+ const start = pos + 1;
200
200
  pos = text.indexOf('</style>', pos);
201
201
  children = [text.slice(start, pos)];
202
202
  pos += 8;
@@ -241,10 +241,10 @@ function parse(text, options = {}) {
241
241
  }
242
242
  }
243
243
 
244
- var out = null;
244
+ let out = null;
245
245
  if (options.attrValue !== undefined) {
246
246
  options.attrName = options.attrName || 'id';
247
- var out = [];
247
+ out = [];
248
248
 
249
249
  while ((pos = findElements()) !== -1) {
250
250
  pos = text.lastIndexOf('<', pos);
@@ -264,10 +264,6 @@ function parse(text, options = {}) {
264
264
  out = filter(out, options.filter);
265
265
  }
266
266
 
267
- if (options.simplify) {
268
- return simplify(Array.isArray(out) ? out : [out]);
269
- }
270
-
271
267
  if (options.setPos) {
272
268
  out.pos = pos;
273
269
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dasha",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "author": "Vitaly Gashkov <vitalygashkov@vk.com>",
5
- "description": "Parser for MPEG-DASH & HLS manifests",
5
+ "description": "Streaming manifest parser",
6
6
  "license": "AGPL-3.0",
7
7
  "keywords": [
8
8
  "mpeg",
@@ -11,7 +11,8 @@
11
11
  "adaptive",
12
12
  "mpd",
13
13
  "m3u8",
14
- "manifest"
14
+ "manifest",
15
+ "playlist"
15
16
  ],
16
17
  "readmeFilename": "README.md",
17
18
  "repository": {
@@ -45,16 +46,16 @@
45
46
  "build:bun": "bun build ./dasha.js --outdir ./dist --format cjs"
46
47
  },
47
48
  "dependencies": {
48
- "m3u8-parser": "^7.1.0"
49
+ "m3u8-parser": "^7.2.0"
49
50
  },
50
51
  "devDependencies": {
51
- "@types/node": "^20.12.7",
52
+ "@types/node": "^22.5.5",
52
53
  "eslint": "^8.57.0",
53
54
  "eslint-config-prettier": "^9.1.0",
54
- "eslint-plugin-import": "^2.29.1",
55
- "eslint-plugin-prettier": "^5.1.3",
56
- "prettier": "^3.2.5",
57
- "tsup": "^8.0.2",
58
- "typescript": "^5.4.5"
55
+ "eslint-plugin-import-x": "^4.2.1",
56
+ "eslint-plugin-prettier": "^5.2.1",
57
+ "prettier": "^3.3.3",
58
+ "tsup": "^8.2.4",
59
+ "typescript": "^5.6.2"
59
60
  }
60
61
  }
package/types/dasha.d.ts CHANGED
@@ -8,12 +8,25 @@ export interface Manifest {
8
8
  audios: AudioTrack[];
9
9
  subtitles: SubtitleTrack[];
10
10
  withResolution(resolution: { width?: string; height?: string }): VideoTrack[];
11
+ withVideoCodecs(codecs: VideoCodec[]): VideoTrack[];
11
12
  withVideoQuality(quality: number | string): VideoTrack[];
13
+ withAudioCodecs(codecs: AudioCodec[]): AudioTrack[];
12
14
  withAudioLanguages(languages: string[], maxTracksPerLanguage?: number): AudioTrack[];
13
15
  withSubtitleLanguages(languages: string[]): SubtitleTrack[];
14
16
  };
15
17
  }
16
18
 
19
+ export function filterByResolution(resolution: { width?: string; height?: string }): VideoTrack[];
20
+ export function filterByCodecs(tracks: VideoTrack[], codecs: VideoCodec[]): VideoTrack[];
21
+ export function filterByCodecs(tracks: AudioTrack[], codecs: AudioCodec[]): AudioTrack[];
22
+ export function filterByQuality(tracks: VideoTrack[], quality: number | string): VideoTrack[];
23
+ export function filterByLanguages(
24
+ tracks: AudioTrack[],
25
+ languages: string[],
26
+ maxTracksPerLanguage?: number
27
+ ): AudioTrack[];
28
+ export function filterByChannels(tracks: AudioTrack[], channels: number | string): AudioTrack[];
29
+
17
30
  export interface Track {
18
31
  id: string;
19
32
  type: 'video' | 'audio' | 'text';