Skip to content

Commit

Permalink
feat: add sidx information to segment base playlists (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldayananda authored and gkatsev committed Apr 11, 2019
1 parent fcbabc3 commit 1176109
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 34 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mpd-parser",
"version": "0.7.0",
"version": "0.8.0-0",
"description": "mpd parser",
"main": "dist/mpd-parser.cjs.js",
"module": "dist/mpd-parser.es.js",
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { parseUTCTimingScheme } from './parseUTCTimingScheme';

export const VERSION = version;

export const parse = (manifestString, options) =>
toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)));
export const parse = (manifestString, options = {}) =>
toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)), options.sidxMapping);

/**
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
Expand Down
69 changes: 68 additions & 1 deletion src/segment/segmentBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const segmentsFromBase = (attributes) => {
source: initialization.sourceURL,
range: initialization.range
});
const segment = urlTypeConverter({ baseUrl, source: baseUrl, range: indexRange });

const segment = urlTypeConverter({ baseUrl, source: baseUrl, indexRange });

segment.map = initSegment;

Expand All @@ -55,3 +56,69 @@ export const segmentsFromBase = (attributes) => {

return [segment];
};

/**
* Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
* according to the sidx information given.
*
* playlist.sidx has metadadata about the sidx where-as the sidx param
* is the parsed sidx box itself.
*
* @param {Object} playlist the playlist to update the sidx information for
* @param {Object} sidx the parsed sidx box
* @return {Object} the playlist object with the updated sidx information
*/
export const addSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
// Retain init segment information
const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
// Retain source duration from initial master manifest parsing
const sourceDuration = playlist.sidx.duration;
// Retain source timeline
const timeline = playlist.timeline || 0;
const sidxByteRange = playlist.sidx.byterange;
const sidxEnd = sidxByteRange.offset + sidxByteRange.length;
// Retain timescale of the parsed sidx
const timescale = sidx.timescale;
// referenceType 1 refers to other sidx boxes
const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
const segments = [];

// firstOffset is the offset from the end of the sidx box
let startIndex = sidxEnd + sidx.firstOffset;

for (let i = 0; i < mediaReferences.length; i++) {
const reference = sidx.references[i];
// size of the referenced (sub)segment
const size = reference.referencedSize;
// duration of the referenced (sub)segment, in the timescale
// this will be converted to seconds when generating segments
const duration = reference.subsegmentDuration;
// should be an inclusive range
const endIndex = startIndex + size - 1;
const indexRange = `${startIndex}-${endIndex}`;

const attributes = {
baseUrl,
timescale,
timeline,
// this is used in parseByDuration
periodIndex: timeline,
duration,
sourceDuration,
indexRange
};

const segment = segmentsFromBase(attributes)[0];

if (initSegment) {
segment.map = initSegment;
}

segments.push(segment);
startIndex += size;
}

playlist.segments = segments;

return playlist;
};
21 changes: 15 additions & 6 deletions src/segment/urlType.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,35 @@ import resolveUrl from '../utils/resolveUrl';
* @return {SingleUri} full segment information transformed into a format similar
* to m3u8-parser
*/
export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '' }) => {
const init = {
export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '', indexRange = '' }) => {
const segment = {
uri: source,
resolvedUri: resolveUrl(baseUrl || '', source)
};

if (range) {
const ranges = range.split('-');
if (range || indexRange) {
const rangeStr = range ? range : indexRange;
const ranges = rangeStr.split('-');
const startRange = parseInt(ranges[0], 10);
const endRange = parseInt(ranges[1], 10);

// byterange should be inclusive according to
// RFC 2616, Clause 14.35.1
init.byterange = {
segment.byterange = {
length: endRange - startRange + 1,
offset: startRange
};
}

return init;
return segment;
};

export const byteRangeToString = (byterange) => {
// `endRange` is one less than `offset + length` because the HTTP range
// header uses inclusive ranges
const endRange = byterange.offset + byterange.length - 1;

return `${byterange.offset}-${endRange}`;
};

export default urlTypeToSegment;
83 changes: 69 additions & 14 deletions src/toM3u8.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { values } from './utils/object';
import { findIndexes } from './utils/list';
import { addSegmentsToPlaylist } from './segment/segmentBase';
import { byteRangeToString } from './segment/urlType';

const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
Expand All @@ -11,7 +13,9 @@ const mergeDiscontiguousPlaylists = playlists => {
// Periods after first
if (acc[name]) {
// first segment of subsequent periods signal a discontinuity
playlist.segments[0].discontinuity = true;
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);

// bubble up contentProtection, this assumes all DRM content
Expand All @@ -36,7 +40,26 @@ const mergeDiscontiguousPlaylists = playlists => {
});
};

export const formatAudioPlaylist = ({ attributes, segments }) => {
const addSegmentInfoFromSidx = (playlists, sidxMapping = {}) => {
if (!Object.keys(sidxMapping).length) {
return playlists;
}

for (const i in playlists) {
const playlist = playlists[i];
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);
}
}

return playlists;
};

export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
const playlist = {
attributes: {
NAME: attributes.id,
Expand All @@ -57,6 +80,10 @@ export const formatAudioPlaylist = ({ attributes, segments }) => {
playlist.contentProtection = attributes.contentProtection;
}

if (sidx) {
playlist.sidx = sidx;
}

return playlist;
};

Expand Down Expand Up @@ -89,16 +116,20 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
};
};

export const organizeAudioPlaylists = playlists => {
return playlists.reduce((a, playlist) => {
export const organizeAudioPlaylists = (playlists, sidxMapping = {}) => {
let mainPlaylist;

const formattedPlaylists = playlists.reduce((a, playlist) => {
const role = playlist.attributes.role &&
playlist.attributes.role.value || 'main';
playlist.attributes.role.value || '';
const language = playlist.attributes.lang || '';

let label = 'main';

if (language) {
label = `${playlist.attributes.lang} (${role})`;
const roleLabel = role ? ` (${role})` : '';

label = `${playlist.attributes.lang}${roleLabel}`;
}

// skip if we already have the highest quality audio for a language
Expand All @@ -112,15 +143,32 @@ export const organizeAudioPlaylists = playlists => {
language,
autoselect: true,
default: role === 'main',
playlists: [formatAudioPlaylist(playlist)],
playlists: addSegmentInfoFromSidx(
[formatAudioPlaylist(playlist)],
sidxMapping
),
uri: ''
};

if (typeof mainPlaylist === 'undefined' && role === 'main') {
mainPlaylist = playlist;
mainPlaylist.default = true;
}

return a;
}, {});

// if no playlists have role "main", mark the first as main
if (!mainPlaylist) {
const firstLabel = Object.keys(formattedPlaylists)[0];

formattedPlaylists[firstLabel].default = true;
}

return formattedPlaylists;
};

export const organizeVttPlaylists = playlists => {
export const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
return playlists.reduce((a, playlist) => {
const label = playlist.attributes.lang || 'text';

Expand All @@ -133,15 +181,18 @@ export const organizeVttPlaylists = playlists => {
language: label,
default: false,
autoselect: false,
playlists: [formatVttPlaylist(playlist)],
playlists: addSegmentInfoFromSidx(
[formatVttPlaylist(playlist)],
sidxMapping
),
uri: ''
};

return a;
}, {});
};

export const formatVideoPlaylist = ({ attributes, segments }) => {
export const formatVideoPlaylist = ({ attributes, segments, sidx }) => {
const playlist = {
attributes: {
NAME: attributes.id,
Expand All @@ -168,10 +219,14 @@ export const formatVideoPlaylist = ({ attributes, segments }) => {
playlist.contentProtection = attributes.contentProtection;
}

if (sidx) {
playlist.sidx = sidx;
}

return playlist;
};

export const toM3u8 = dashPlaylists => {
export const toM3u8 = (dashPlaylists, sidxMapping = {}) => {
if (!dashPlaylists.length) {
return {};
}
Expand Down Expand Up @@ -208,16 +263,16 @@ export const toM3u8 = dashPlaylists => {
},
uri: '',
duration,
playlists: videoPlaylists,
playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping),
minimumUpdatePeriod: minimumUpdatePeriod * 1000
};

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

if (vttPlaylists.length) {
master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists);
master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists, sidxMapping);
}

return master;
Expand Down
20 changes: 15 additions & 5 deletions src/toPlaylists.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ export const generateSegments = ({ attributes, segmentInfo }) => {
segmentAttributes = merge(attributes, segmentInfo.list);
}

const segmentsInfo = {
attributes
};

if (!segmentsFn) {
return { attributes };
return segmentsInfo;
}

const segments = segmentsFn(segmentAttributes, segmentInfo.timeline);
Expand All @@ -41,10 +45,16 @@ export const generateSegments = ({ attributes, segmentInfo }) => {
segmentAttributes.duration = 0;
}

return {
attributes: segmentAttributes,
segments
};
segmentsInfo.attributes = segmentAttributes;
segmentsInfo.segments = segments;

// This is a sidx box without actual segment information
if (segmentInfo.base && segmentAttributes.indexRange) {
segmentsInfo.sidx = segments[0];
segmentsInfo.segments = [];
}

return segmentsInfo;
};

export const toPlaylists = (representations) => representations.map(generateSegments);
4 changes: 2 additions & 2 deletions test/manifests/maat_vtt_segmentTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ export const parsedManifest = {
}],
uri: ''
},
['es (main)']: {
['es']: {
autoselect: true,
default: true,
default: false,
language: 'es',
playlists: [{
attributes: {
Expand Down
Loading

0 comments on commit 1176109

Please sign in to comment.