Skip to content

Commit

Permalink
fix: support multiple audio/subtitle playlists and export generateSid…
Browse files Browse the repository at this point in the history
…xKey (#123)
  • Loading branch information
brandonocasey authored Mar 26, 2021
1 parent f0da2cc commit f7105d8
Show file tree
Hide file tree
Showing 12 changed files with 3,754 additions and 2,485 deletions.
9 changes: 4 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { version } from '../package.json';
import { toM3u8 } from './toM3u8';
import { toM3u8, generateSidxKey } from './toM3u8';
import { toPlaylists } from './toPlaylists';
import { inheritAttributes } from './inheritAttributes';
import { stringToMpdXml } from './stringToMpdXml';
import { parseUTCTimingScheme } from './parseUTCTimingScheme';
import {addSegmentsToPlaylist} from './segment/segmentBase.js';
import {addSidxSegmentsToPlaylist} from './segment/segmentBase.js';

const VERSION = version;

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

const addSidxSegmentsToPlaylist = addSegmentsToPlaylist;

export {
VERSION,
parse,
Expand All @@ -36,5 +34,6 @@ export {
inheritAttributes,
toPlaylists,
toM3u8,
addSidxSegmentsToPlaylist
addSidxSegmentsToPlaylist,
generateSidxKey
};
2 changes: 1 addition & 1 deletion src/segment/segmentBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const segmentsFromBase = (attributes) => {
* @param {Object} sidx the parsed sidx box
* @return {Object} the playlist object with the updated sidx information
*/
export const addSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
// Retain init segment information
const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
// Retain source duration from initial master manifest parsing
Expand Down
110 changes: 55 additions & 55 deletions src/toM3u8.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { values } from './utils/object';
import { findIndexes } from './utils/list';
import { addSegmentsToPlaylist } from './segment/segmentBase';
import { addSidxSegmentsToPlaylist as addSidxSegmentsToPlaylist_ } from './segment/segmentBase';
import { byteRangeToString } from './segment/urlType';

export const generateSidxKey = (sidx) => sidx &&
sidx.uri + '-' + byteRangeToString(sidx.byterange);

const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
Expand Down Expand Up @@ -40,31 +43,30 @@ const mergeDiscontiguousPlaylists = playlists => {
});
};

const addSegmentInfoFromSidx = (playlists, sidxMapping = {}) => {
export const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
const sidxKey = generateSidxKey(playlist.sidx);
const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;

if (sidxMatch) {
addSidxSegmentsToPlaylist_(playlist, sidxMatch, playlist.sidx.resolvedUri);
}

return playlist;
};

export const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
if (!Object.keys(sidxMapping).length) {
return playlists;
}

for (const i in playlists) {
const playlist = playlists[i];

if (!playlist.sidx) {
continue;
}

const sidxKey = playlist.sidx.uri + '-' +
byteRangeToString(playlist.sidx.byterange);
const sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;

if (playlist.sidx && sidxMatch) {
addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri);
}
playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
}

return playlists;
};

export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
export const formatAudioPlaylist = ({ attributes, segments, sidx }, isAudioOnly) => {
const playlist = {
attributes: {
NAME: attributes.id,
Expand All @@ -89,6 +91,11 @@ export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
playlist.sidx = sidx;
}

if (isAudioOnly) {
playlist.attributes.AUDIO = 'audio';
playlist.attributes.SUBTITLES = 'subs';
}

return playlist;
};

Expand Down Expand Up @@ -127,7 +134,7 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
};
};

export const organizeAudioPlaylists = (playlists, sidxMapping = {}) => {
export const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
let mainPlaylist;

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

// skip if we already have the highest quality audio for a language
if (a[label] &&
a[label].playlists[0].attributes.BANDWIDTH >
playlist.attributes.bandwidth) {
return a;
if (!a[label]) {
a[label] = {
language,
autoselect: true,
default: role === 'main',
playlists: [],
uri: ''
};
}

a[label] = {
language,
autoselect: true,
default: role === 'main',
playlists: addSegmentInfoFromSidx(
[formatAudioPlaylist(playlist)],
sidxMapping
),
uri: ''
};
const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);

a[label].playlists.push(formatted);

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

// skip if we already have subtitles
if (a[label]) {
return a;
if (!a[label]) {
a[label] = {
language: label,
default: false,
autoselect: false,
playlists: [],
uri: ''
};
}

a[label] = {
language: label,
default: false,
autoselect: false,
playlists: addSegmentInfoFromSidx(
[formatVttPlaylist(playlist)],
sidxMapping
),
uri: ''
};
a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));

return a;
}, {});
Expand Down Expand Up @@ -237,6 +235,13 @@ export const formatVideoPlaylist = ({ attributes, segments, sidx }) => {
return playlist;
};

const videoOnly = ({ attributes }) =>
attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
const audioOnly = ({ attributes }) =>
attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
const vttOnly = ({ attributes }) =>
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';

export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
if (!dashPlaylists.length) {
return {};
Expand All @@ -250,13 +255,6 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
minimumUpdatePeriod
} = dashPlaylists[0].attributes;

const videoOnly = ({ attributes }) =>
attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
const audioOnly = ({ attributes }) =>
attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
const vttOnly = ({ attributes }) =>
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';

const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
const vttPlaylists = dashPlaylists.filter(vttOnly);
Expand All @@ -274,7 +272,7 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
},
uri: '',
duration,
playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping)
playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
};

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

const isAudioOnly = master.playlists.length === 0;

if (audioPlaylists.length) {
master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping);
master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly);
}

if (vttPlaylists.length) {
Expand Down
9 changes: 9 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import locationsTemplate from './manifests/locations.mpd';
import multiperiod from './manifests/multiperiod.mpd';
import webmsegments from './manifests/webmsegments.mpd';
import multiperiodDynamic from './manifests/multiperiod-dynamic.mpd';
import audioOnly from './manifests/audio-only.mpd';
import {
parsedManifest as maatVttSegmentTemplateManifest
} from './manifests/maat_vtt_segmentTemplate.js';
Expand Down Expand Up @@ -42,6 +43,10 @@ import {
parsedManifest as vttCodecsManifest
} from './manifests/vtt_codecs.js';

import {
parsedManifest as audioOnlyManifest
} from './manifests/audio-only.js';

QUnit.module('mpd-parser');

QUnit.test('has VERSION', function(assert) {
Expand Down Expand Up @@ -88,6 +93,10 @@ QUnit.test('has parse', function(assert) {
name: 'vtt_codecs',
input: vttCodecsTemplate,
expected: vttCodecsManifest
}, {
name: 'audio-only',
input: audioOnly,
expected: audioOnlyManifest
}].forEach(({ name, input, expected }) => {
QUnit.test(`${name} test manifest`, function(assert) {
const actual = parse(input);
Expand Down
106 changes: 106 additions & 0 deletions test/manifests/audio-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export const parsedManifest = {
allowCache: true,
discontinuityStarts: [],
segments: [],
endList: true,
mediaGroups: {
'AUDIO': {
audio: {
en: {
language: 'en',
autoselect: true,
default: true,
playlists: [
{
attributes: {
'NAME': '0',
'BANDWIDTH': 130803,
'CODECS': 'mp4a.40.2',
'PROGRAM-ID': 1,
'AUDIO': 'audio',
'SUBTITLES': 'subs'
},
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
targetDuration: 60,
segments: [],
mediaSequence: 1,
sidx: {
uri: 'http://example.com/audio_en_2c_128k_aac.mp4',
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
byterange: {
length: 224,
offset: 786
},
map: {
uri: '',
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
byterange: {
length: 786,
offset: 0
}
},
duration: 60,
timeline: 0,
number: 0
}
}
],
uri: ''
},
es: {
language: 'es',
autoselect: true,
default: false,
playlists: [
{
attributes: {
'NAME': '1',
'BANDWIDTH': 130405,
'CODECS': 'mp4a.40.2',
'PROGRAM-ID': 1,
'AUDIO': 'audio',
'SUBTITLES': 'subs'
},
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
targetDuration: 60,
segments: [],
mediaSequence: 1,
sidx: {
uri: 'http://example.com/audio_es_2c_128k_aac.mp4',
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
byterange: {
length: 224,
offset: 786
},
map: {
uri: '',
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
byterange: {
length: 786,
offset: 0
}
},
duration: 60,
timeline: 0,
number: 0
}
}
],
uri: ''
}
}
},
'VIDEO': {},
'CLOSED-CAPTIONS': {},
'SUBTITLES': {}
},
uri: '',
duration: 60,
playlists: []
};
25 changes: 25 additions & 0 deletions test/manifests/audio-only.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/shaka-packager version v2.4.1-c731217-release-->
<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">
<Period id="0">
<AdaptationSet id="0" contentType="audio" lang="en" subsegmentAlignment="true">
<Representation id="0" bandwidth="130803" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>http://example.com/audio_en_2c_128k_aac.mp4</BaseURL>
<SegmentBase indexRange="786-1009" timescale="48000">
<Initialization range="0-785"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" lang="es" subsegmentAlignment="true">
<Representation id="1" bandwidth="130405" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>http://example.com/audio_es_2c_128k_aac.mp4</BaseURL>
<SegmentBase indexRange="786-1009" timescale="48000">
<Initialization range="0-785"/>
</SegmentBase>
</Representation>
</AdaptationSet>

</Period>
</MPD>
Loading

0 comments on commit f7105d8

Please sign in to comment.