Skip to content

Commit f7105d8

Browse files
fix: support multiple audio/subtitle playlists and export generateSidxKey (#123)
1 parent f0da2cc commit f7105d8

File tree

12 files changed

+3754
-2485
lines changed

12 files changed

+3754
-2485
lines changed

src/index.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { version } from '../package.json';
2-
import { toM3u8 } from './toM3u8';
2+
import { toM3u8, generateSidxKey } from './toM3u8';
33
import { toPlaylists } from './toPlaylists';
44
import { inheritAttributes } from './inheritAttributes';
55
import { stringToMpdXml } from './stringToMpdXml';
66
import { parseUTCTimingScheme } from './parseUTCTimingScheme';
7-
import {addSegmentsToPlaylist} from './segment/segmentBase.js';
7+
import {addSidxSegmentsToPlaylist} from './segment/segmentBase.js';
88

99
const VERSION = version;
1010

@@ -26,8 +26,6 @@ const parse = (manifestString, options = {}) => {
2626
const parseUTCTiming = (manifestString) =>
2727
parseUTCTimingScheme(stringToMpdXml(manifestString));
2828

29-
const addSidxSegmentsToPlaylist = addSegmentsToPlaylist;
30-
3129
export {
3230
VERSION,
3331
parse,
@@ -36,5 +34,6 @@ export {
3634
inheritAttributes,
3735
toPlaylists,
3836
toM3u8,
39-
addSidxSegmentsToPlaylist
37+
addSidxSegmentsToPlaylist,
38+
generateSidxKey
4039
};

src/segment/segmentBase.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const segmentsFromBase = (attributes) => {
6767
* @param {Object} sidx the parsed sidx box
6868
* @return {Object} the playlist object with the updated sidx information
6969
*/
70-
export const addSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
70+
export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
7171
// Retain init segment information
7272
const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
7373
// Retain source duration from initial master manifest parsing

src/toM3u8.js

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { values } from './utils/object';
22
import { findIndexes } from './utils/list';
3-
import { addSegmentsToPlaylist } from './segment/segmentBase';
3+
import { addSidxSegmentsToPlaylist as addSidxSegmentsToPlaylist_ } from './segment/segmentBase';
44
import { byteRangeToString } from './segment/urlType';
55

6+
export const generateSidxKey = (sidx) => sidx &&
7+
sidx.uri + '-' + byteRangeToString(sidx.byterange);
8+
69
const mergeDiscontiguousPlaylists = playlists => {
710
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
811
// assuming playlist IDs are the same across periods
@@ -40,31 +43,30 @@ const mergeDiscontiguousPlaylists = playlists => {
4043
});
4144
};
4245

43-
const addSegmentInfoFromSidx = (playlists, sidxMapping = {}) => {
46+
export const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
47+
const sidxKey = generateSidxKey(playlist.sidx);
48+
const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
49+
50+
if (sidxMatch) {
51+
addSidxSegmentsToPlaylist_(playlist, sidxMatch, playlist.sidx.resolvedUri);
52+
}
53+
54+
return playlist;
55+
};
56+
57+
export const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
4458
if (!Object.keys(sidxMapping).length) {
4559
return playlists;
4660
}
4761

4862
for (const i in playlists) {
49-
const playlist = playlists[i];
50-
51-
if (!playlist.sidx) {
52-
continue;
53-
}
54-
55-
const sidxKey = playlist.sidx.uri + '-' +
56-
byteRangeToString(playlist.sidx.byterange);
57-
const sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
58-
59-
if (playlist.sidx && sidxMatch) {
60-
addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri);
61-
}
63+
playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
6264
}
6365

6466
return playlists;
6567
};
6668

67-
export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
69+
export const formatAudioPlaylist = ({ attributes, segments, sidx }, isAudioOnly) => {
6870
const playlist = {
6971
attributes: {
7072
NAME: attributes.id,
@@ -89,6 +91,11 @@ export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
8991
playlist.sidx = sidx;
9092
}
9193

94+
if (isAudioOnly) {
95+
playlist.attributes.AUDIO = 'audio';
96+
playlist.attributes.SUBTITLES = 'subs';
97+
}
98+
9299
return playlist;
93100
};
94101

@@ -127,7 +134,7 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
127134
};
128135
};
129136

130-
export const organizeAudioPlaylists = (playlists, sidxMapping = {}) => {
137+
export const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
131138
let mainPlaylist;
132139

133140
const formattedPlaylists = playlists.reduce((a, playlist) => {
@@ -143,23 +150,19 @@ export const organizeAudioPlaylists = (playlists, sidxMapping = {}) => {
143150
label = `${playlist.attributes.lang}${roleLabel}`;
144151
}
145152

146-
// skip if we already have the highest quality audio for a language
147-
if (a[label] &&
148-
a[label].playlists[0].attributes.BANDWIDTH >
149-
playlist.attributes.bandwidth) {
150-
return a;
153+
if (!a[label]) {
154+
a[label] = {
155+
language,
156+
autoselect: true,
157+
default: role === 'main',
158+
playlists: [],
159+
uri: ''
160+
};
151161
}
152162

153-
a[label] = {
154-
language,
155-
autoselect: true,
156-
default: role === 'main',
157-
playlists: addSegmentInfoFromSidx(
158-
[formatAudioPlaylist(playlist)],
159-
sidxMapping
160-
),
161-
uri: ''
162-
};
163+
const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
164+
165+
a[label].playlists.push(formatted);
163166

164167
if (typeof mainPlaylist === 'undefined' && role === 'main') {
165168
mainPlaylist = playlist;
@@ -183,21 +186,16 @@ export const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
183186
return playlists.reduce((a, playlist) => {
184187
const label = playlist.attributes.lang || 'text';
185188

186-
// skip if we already have subtitles
187-
if (a[label]) {
188-
return a;
189+
if (!a[label]) {
190+
a[label] = {
191+
language: label,
192+
default: false,
193+
autoselect: false,
194+
playlists: [],
195+
uri: ''
196+
};
189197
}
190-
191-
a[label] = {
192-
language: label,
193-
default: false,
194-
autoselect: false,
195-
playlists: addSegmentInfoFromSidx(
196-
[formatVttPlaylist(playlist)],
197-
sidxMapping
198-
),
199-
uri: ''
200-
};
198+
a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
201199

202200
return a;
203201
}, {});
@@ -237,6 +235,13 @@ export const formatVideoPlaylist = ({ attributes, segments, sidx }) => {
237235
return playlist;
238236
};
239237

238+
const videoOnly = ({ attributes }) =>
239+
attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
240+
const audioOnly = ({ attributes }) =>
241+
attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
242+
const vttOnly = ({ attributes }) =>
243+
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
244+
240245
export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
241246
if (!dashPlaylists.length) {
242247
return {};
@@ -250,13 +255,6 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
250255
minimumUpdatePeriod
251256
} = dashPlaylists[0].attributes;
252257

253-
const videoOnly = ({ attributes }) =>
254-
attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
255-
const audioOnly = ({ attributes }) =>
256-
attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
257-
const vttOnly = ({ attributes }) =>
258-
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
259-
260258
const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
261259
const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
262260
const vttPlaylists = dashPlaylists.filter(vttOnly);
@@ -274,7 +272,7 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
274272
},
275273
uri: '',
276274
duration,
277-
playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping)
275+
playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
278276
};
279277

280278
if (minimumUpdatePeriod >= 0) {
@@ -289,8 +287,10 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
289287
master.suggestedPresentationDelay = suggestedPresentationDelay;
290288
}
291289

290+
const isAudioOnly = master.playlists.length === 0;
291+
292292
if (audioPlaylists.length) {
293-
master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping);
293+
master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly);
294294
}
295295

296296
if (vttPlaylists.length) {

test/index.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import locationsTemplate from './manifests/locations.mpd';
1313
import multiperiod from './manifests/multiperiod.mpd';
1414
import webmsegments from './manifests/webmsegments.mpd';
1515
import multiperiodDynamic from './manifests/multiperiod-dynamic.mpd';
16+
import audioOnly from './manifests/audio-only.mpd';
1617
import {
1718
parsedManifest as maatVttSegmentTemplateManifest
1819
} from './manifests/maat_vtt_segmentTemplate.js';
@@ -42,6 +43,10 @@ import {
4243
parsedManifest as vttCodecsManifest
4344
} from './manifests/vtt_codecs.js';
4445

46+
import {
47+
parsedManifest as audioOnlyManifest
48+
} from './manifests/audio-only.js';
49+
4550
QUnit.module('mpd-parser');
4651

4752
QUnit.test('has VERSION', function(assert) {
@@ -88,6 +93,10 @@ QUnit.test('has parse', function(assert) {
8893
name: 'vtt_codecs',
8994
input: vttCodecsTemplate,
9095
expected: vttCodecsManifest
96+
}, {
97+
name: 'audio-only',
98+
input: audioOnly,
99+
expected: audioOnlyManifest
91100
}].forEach(({ name, input, expected }) => {
92101
QUnit.test(`${name} test manifest`, function(assert) {
93102
const actual = parse(input);

test/manifests/audio-only.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
export const parsedManifest = {
2+
allowCache: true,
3+
discontinuityStarts: [],
4+
segments: [],
5+
endList: true,
6+
mediaGroups: {
7+
'AUDIO': {
8+
audio: {
9+
en: {
10+
language: 'en',
11+
autoselect: true,
12+
default: true,
13+
playlists: [
14+
{
15+
attributes: {
16+
'NAME': '0',
17+
'BANDWIDTH': 130803,
18+
'CODECS': 'mp4a.40.2',
19+
'PROGRAM-ID': 1,
20+
'AUDIO': 'audio',
21+
'SUBTITLES': 'subs'
22+
},
23+
uri: '',
24+
endList: true,
25+
timeline: 0,
26+
resolvedUri: '',
27+
targetDuration: 60,
28+
segments: [],
29+
mediaSequence: 1,
30+
sidx: {
31+
uri: 'http://example.com/audio_en_2c_128k_aac.mp4',
32+
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
33+
byterange: {
34+
length: 224,
35+
offset: 786
36+
},
37+
map: {
38+
uri: '',
39+
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
40+
byterange: {
41+
length: 786,
42+
offset: 0
43+
}
44+
},
45+
duration: 60,
46+
timeline: 0,
47+
number: 0
48+
}
49+
}
50+
],
51+
uri: ''
52+
},
53+
es: {
54+
language: 'es',
55+
autoselect: true,
56+
default: false,
57+
playlists: [
58+
{
59+
attributes: {
60+
'NAME': '1',
61+
'BANDWIDTH': 130405,
62+
'CODECS': 'mp4a.40.2',
63+
'PROGRAM-ID': 1,
64+
'AUDIO': 'audio',
65+
'SUBTITLES': 'subs'
66+
},
67+
uri: '',
68+
endList: true,
69+
timeline: 0,
70+
resolvedUri: '',
71+
targetDuration: 60,
72+
segments: [],
73+
mediaSequence: 1,
74+
sidx: {
75+
uri: 'http://example.com/audio_es_2c_128k_aac.mp4',
76+
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
77+
byterange: {
78+
length: 224,
79+
offset: 786
80+
},
81+
map: {
82+
uri: '',
83+
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
84+
byterange: {
85+
length: 786,
86+
offset: 0
87+
}
88+
},
89+
duration: 60,
90+
timeline: 0,
91+
number: 0
92+
}
93+
}
94+
],
95+
uri: ''
96+
}
97+
}
98+
},
99+
'VIDEO': {},
100+
'CLOSED-CAPTIONS': {},
101+
'SUBTITLES': {}
102+
},
103+
uri: '',
104+
duration: 60,
105+
playlists: []
106+
};

test/manifests/audio-only.mpd

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--Generated with https://github.com/google/shaka-packager version v2.4.1-c731217-release-->
3+
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT60S">
4+
<Period id="0">
5+
<AdaptationSet id="0" contentType="audio" lang="en" subsegmentAlignment="true">
6+
<Representation id="0" bandwidth="130803" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
7+
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
8+
<BaseURL>http://example.com/audio_en_2c_128k_aac.mp4</BaseURL>
9+
<SegmentBase indexRange="786-1009" timescale="48000">
10+
<Initialization range="0-785"/>
11+
</SegmentBase>
12+
</Representation>
13+
</AdaptationSet>
14+
<AdaptationSet id="1" contentType="audio" lang="es" subsegmentAlignment="true">
15+
<Representation id="1" bandwidth="130405" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
16+
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
17+
<BaseURL>http://example.com/audio_es_2c_128k_aac.mp4</BaseURL>
18+
<SegmentBase indexRange="786-1009" timescale="48000">
19+
<Initialization range="0-785"/>
20+
</SegmentBase>
21+
</Representation>
22+
</AdaptationSet>
23+
24+
</Period>
25+
</MPD>

0 commit comments

Comments
 (0)