dasha 2.3.1 → 2.3.3

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/manifest.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { parseXml } = require('./xml');
4
4
  const { processManifest } = require('./processor');
5
- const { getQualityLabel, getHeightByWidth } = require('./utils');
5
+ const { getQualityLabel, getHeightByWidth, getClosest, sanitizeBaseUrl } = require('./utils');
6
6
  const { CONTENT_TYPE, KEY_SYSTEMS } = require('./constants');
7
7
 
8
8
  const isUrl = (value) => {
@@ -19,8 +19,9 @@ class Manifest {
19
19
  }
20
20
 
21
21
  addBaseUrl(value) {
22
- if (Array.isArray(this.baseUrls)) this.baseUrls.push({ value });
23
- else this.baseUrls = [{ value }];
22
+ const sanitizedValue = sanitizeBaseUrl(value);
23
+ if (Array.isArray(this.baseUrls)) this.baseUrls.push(sanitizedValue);
24
+ else this.baseUrls = [sanitizedValue];
24
25
  }
25
26
 
26
27
  getVideoTrack(height) {
@@ -34,11 +35,31 @@ class Manifest {
34
35
  .flat(1)
35
36
  .filter(isVideoAdaptationSet);
36
37
  const representations = adaptationSets.map((a) => a.representations).flat(2);
37
- const matchesInHeight = representations.filter((r) => getHeightByWidth(r.width) === height);
38
- const matchesInId = representations.filter((r) => r.id.includes(height));
39
- const matches = matchesInHeight?.length ? matchesInHeight : matchesInId;
40
- const representation = this.findBestRepresentation(matches.length ? matches : representations);
38
+
39
+ let suitableRepresentations = [];
40
+ if (height) {
41
+ const resolutions = representations.map((r) => ({
42
+ height: getHeightByWidth(r.width),
43
+ width: r.width,
44
+ }));
45
+ const heights = resolutions.map((r) => r.height);
46
+ const matchedHeight = height ? getClosest(height, heights) : Math.max(...heights);
47
+ const matchedResolution = resolutions.find((r) => r.height === matchedHeight);
48
+ const matchesInHeight = representations.filter((r) => matchedResolution.width === r.width);
49
+ const matchesInId = representations.filter((r) => r.id.includes(height.toString()));
50
+ suitableRepresentations = matchesInHeight?.length ? matchesInHeight : matchesInId;
51
+ }
52
+
53
+ const representation = this.findBestRepresentation(
54
+ suitableRepresentations.length ? suitableRepresentations : representations
55
+ );
41
56
  const adaptationSet = adaptationSets.find((a) => a.representations.includes(representation));
57
+ let codec = 'x264';
58
+ if (representation.codecs.includes('hvc') || representation.codecs.includes('hev'))
59
+ codec = 'x265';
60
+ let bitDepth = '8bit';
61
+ if (representation.codecs.includes('.2.4.')) bitDepth = '10bit';
62
+ if (representation.codecs.includes('.4.16.')) bitDepth = '12bit';
42
63
  return {
43
64
  id: 0,
44
65
  type: CONTENT_TYPE.video,
@@ -50,6 +71,10 @@ class Manifest {
50
71
  width: representation.width,
51
72
  height: representation.height,
52
73
  qualityLabel: getQualityLabel(representation.width),
74
+ hevc: representation.codecs.includes('hvc') || representation.codecs.includes('hev'),
75
+ codec,
76
+ codecs: representation.codecs,
77
+ bitDepth,
53
78
  };
54
79
  }
55
80
 
@@ -70,6 +95,7 @@ class Manifest {
70
95
  const tracks = [];
71
96
  for (const adaptationSet of adaptationSets) {
72
97
  const representation = this.findBestRepresentation(adaptationSet.representations);
98
+ let codec = '';
73
99
  const track = {
74
100
  id: tracks.length,
75
101
  type: CONTENT_TYPE.audio,
@@ -80,7 +106,8 @@ class Manifest {
80
106
  segments: this.getSegments(adaptationSet, representation),
81
107
  bitrate: Math.round(representation.bandwidth / 1000), // Kbps
82
108
  size: Math.ceil((representation.bandwidth / 8e6) * this.mediaPresentationDuration), // MB
83
- audioSampleRate: representation.audioSamplingRate / 1000,
109
+ audioSamplingRate: representation.audioSamplingRate / 1000,
110
+ codecs: representation.codecs,
84
111
  };
85
112
  tracks.push(track);
86
113
  }
@@ -107,8 +134,8 @@ class Manifest {
107
134
  const tracks = [];
108
135
  for (const representation of selectedRepresentations) {
109
136
  const adaptationSet = adaptationSets.find((a) => a.representations.includes(representation));
110
- const baseUrl = this.baseUrls?.[0]?.value;
111
- const representationBaseUrl = representation.baseUrls?.[0]?.value;
137
+ const baseUrl = this.baseUrls?.[0];
138
+ const representationBaseUrl = representation.baseUrls?.[0];
112
139
  const url = isUrl(representationBaseUrl)
113
140
  ? representationBaseUrl
114
141
  : `${baseUrl || ''}${representationBaseUrl || ''}`;
@@ -151,7 +178,7 @@ class Manifest {
151
178
  const segmentTemplate = representation.segmentTemplate || adaptationSet.segmentTemplate;
152
179
  const baseUrls = representation.baseUrls || this.baseUrls;
153
180
  const isBaseUrlRequired = !segmentTemplate.media.includes(`https://`);
154
- const baseUrl = baseUrls?.map((url) => url.value)?.[0];
181
+ const baseUrl = baseUrls?.[0];
155
182
  const initTemplate = segmentTemplate.initialization;
156
183
  const initUrl = initTemplate
157
184
  ?.replace(/\$Bandwidth\$/i, representation.bandwidth)
@@ -163,10 +190,12 @@ class Manifest {
163
190
  let time = 0;
164
191
  let index = parseInt(segmentTemplate.startNumber) || 0;
165
192
 
166
- if (!Array.isArray(segmentTemplate['SegmentTimeline']['S']))
167
- segmentTemplate['SegmentTimeline']['S'] = [segmentTemplate['SegmentTimeline']['S']];
193
+ let s = segmentTemplate['SegmentTimeline']?.['S'];
194
+ if (!!s && !Array.isArray(segmentTemplate['SegmentTimeline']?.['S']))
195
+ s = [segmentTemplate['SegmentTimeline']?.['S']];
168
196
 
169
- for (const segment of segmentTemplate['SegmentTimeline']['S']) {
197
+ for (let i = segmentTemplate.start || 1; i < (s?.length || segmentTemplate.duration); i++) {
198
+ const segment = s?.[i] || {};
170
199
  const repeats = parseInt(segment.r || '0') + 1;
171
200
  if (segment.t) time = parseInt(segment.t);
172
201
  const duration = parseInt(segment.d || segmentTemplate.timescale || '0');
@@ -189,6 +218,14 @@ class Manifest {
189
218
  let pssh = null;
190
219
  for (const period of this.periods)
191
220
  for (const adaptationSet of period.adaptationSets) {
221
+ for (const representation of adaptationSet.representations) {
222
+ if (!representation.contentProtections) continue;
223
+ for (const contentProtection of representation.contentProtections)
224
+ if (contentProtection.cencPssh && contentProtection.schemeIdUri === schemeIdUri) {
225
+ pssh = contentProtection.cencPssh;
226
+ break;
227
+ }
228
+ }
192
229
  if (!adaptationSet.contentProtections) continue;
193
230
  for (const contentProtection of adaptationSet.contentProtections)
194
231
  if (contentProtection.cencPssh && contentProtection.schemeIdUri === schemeIdUri) {
package/lib/processor.js CHANGED
@@ -1,4 +1,4 @@
1
- const { parseDuration } = require('./utils');
1
+ const { parseDuration, sanitizeBaseUrl } = require('./utils');
2
2
 
3
3
  const processElement = (element, type) => {
4
4
  if (!element) return;
@@ -15,9 +15,13 @@ const processElement = (element, type) => {
15
15
  };
16
16
 
17
17
  const processRepresentation = (representation) => {
18
+ const contentProtectionsList = processElement(representation?.['ContentProtection'], 'array');
19
+ const contentProtections = contentProtectionsList?.map((c) => processContentProtection(c));
18
20
  return {
19
21
  id: representation['id'],
20
- baseUrls: processElement(representation['BaseURL'], 'array'),
22
+ baseUrls: processElement(representation['BaseURL'], 'array')?.map((url) =>
23
+ sanitizeBaseUrl(url.value)
24
+ ),
21
25
  codecs: representation['codecs'],
22
26
  bandwidth: processElement(representation['bandwidth'], 'number'),
23
27
  frameRate: representation['frameRate'],
@@ -28,6 +32,7 @@ const processRepresentation = (representation) => {
28
32
  sar: representation['sar'],
29
33
  startWithSAP: representation['startWithSAP'],
30
34
  segmentTemplate: representation['SegmentTemplate'],
35
+ contentProtections,
31
36
  };
32
37
  };
33
38
 
@@ -50,7 +55,9 @@ const processAdaptationSet = (adaptationSet) => {
50
55
  return {
51
56
  id: adaptationSet['id'],
52
57
  group: processElement(adaptationSet['group'], 'number'),
53
- baseUrls: processElement(adaptationSet['BaseURL'], 'array'),
58
+ baseUrls: processElement(adaptationSet['BaseURL'], 'array')?.map((url) =>
59
+ sanitizeBaseUrl(url.value)
60
+ ),
54
61
  segmentAlignment: adaptationSet['segmentAlignment'],
55
62
  lang: adaptationSet['lang'],
56
63
  maxWidth: processElement(adaptationSet['maxWidth'], 'number'),
@@ -82,7 +89,7 @@ const processManifest = (parsedXml) => {
82
89
  if (!periodsList.length) return null;
83
90
  return {
84
91
  periods: periodsList.map((p) => processPeriod(p)),
85
- baseUrls: processElement(mpd['BaseURL'], 'array'),
92
+ baseUrls: processElement(mpd['BaseURL'], 'array')?.map((url) => sanitizeBaseUrl(url.value)),
86
93
  locations: processElement(mpd['Location'], 'array'),
87
94
  profiles: mpd['profiles'],
88
95
  mediaPresentationDuration: processElement(mpd['mediaPresentationDuration'], 'duration'),
package/lib/utils.js CHANGED
@@ -49,6 +49,15 @@ const getHeightByWidth = (videoWidth) => {
49
49
  return height;
50
50
  };
51
51
 
52
+ const getClosest = (value, array) =>
53
+ array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
54
+
52
55
  const getQualityLabel = (videoWidth) => getHeightByWidth(videoWidth) + 'p';
53
56
 
54
- module.exports = { parseDuration, getHeightByWidth, getQualityLabel };
57
+ const sanitizeBaseUrl = (value) => {
58
+ if (!value.includes('.mpd')) return value;
59
+ const manifestFilename = value.split('.mpd')[0].split('/').at(-1) + '.mpd';
60
+ return value.replace(manifestFilename, '');
61
+ };
62
+
63
+ module.exports = { parseDuration, getHeightByWidth, getQualityLabel, getClosest, sanitizeBaseUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dasha",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "author": "Vitaly Gashkov <vitalygashkov@bk.ru>",
5
5
  "description": "MPD-manifest parser for MPEG DASH",
6
6
  "license": "Apache-2.0",
@@ -34,9 +34,13 @@ interface VideoTrack extends MediaTrack {
34
34
  interface AudioTrack extends MediaTrack {
35
35
  type: 'audio';
36
36
  audioSampleRate: number;
37
+ language: string;
37
38
  }
38
39
 
39
- interface SubtitleTrack {}
40
+ interface SubtitleTrack extends MediaTrack {
41
+ type: 'text';
42
+ language: string;
43
+ }
40
44
 
41
45
  interface Segment {
42
46
  url: string;
@@ -45,4 +49,4 @@ interface Segment {
45
49
 
46
50
  declare const parseManifest: (text: string) => Manifest | null;
47
51
 
48
- export { parseManifest, Manifest, VideoTrack, AudioTrack };
52
+ export { parseManifest, Manifest, VideoTrack, AudioTrack, SubtitleTrack };