Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse <SegmentList> and <SegmentBase> #18

Merged
merged 15 commits into from
Feb 5, 2018
41 changes: 29 additions & 12 deletions src/inheritAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,41 @@ export const buildBaseUrls = (referenceUrls, baseUrlElements) => {
export const getSegmentInformation = (adaptationSet) => {
const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL');
const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL')
.map(s => shallowMerge({ tag: 'SegmentURL' }, getAttributes(s)));
const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
const segmentTimelineNode = segmentList ? segmentList : segmentTemplate;
const segmentTimelineNode = segmentList || segmentTemplate;
const segmentTimeline =
segmentTimelineNode && findChildren(segmentTimelineNode, 'SegmentTimeline')[0];
const segmentInitializationNode = segmentList || segmentBase || segmentTemplate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variable names are a bit misleading. segmentTimelineNode and segmentInitializationNode are actually the parent nodes of those nodes

const segmentInitialization = segmentInitializationNode &&
findChildren(segmentInitializationNode, 'Initialization')[0];

// SegmentTemplate is handled slightly differently, since it can have both @initialization
// and an <Initialization> node. @initialization can be templated, while the node can have a
// url and range specified. If the <SegmentTemplate> has both @initialization and
// an <Initialization> subelement we opt to override with the node,
// as this interaction is not defined in the spec.
const template = segmentTemplate && getAttributes(segmentTemplate);

if (template && segmentInitialization) {
template.initialization = getAttributes(segmentInitialization);
Copy link
Contributor

@mjneil mjneil Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good if we can account for the string vs object here and have template.initialization always be an object.

if (template) {
  template.initialization = (segmentInitialization && getAttributes(segmentIntitialization) || { sourceURL: template.initialization || '' };
}

This way in segmentsFromTemplate you do not need the extra code path for the string

}

return {
template: segmentTemplate && getAttributes(segmentTemplate),
template,
timeline: segmentTimeline &&
findChildren(segmentTimeline, 'S').map(s => getAttributes(s)),
list: segmentList &&
shallowMerge(getAttributes(segmentList),
{
segmentUrls: segmentUrls &&
segmentUrls.map(s =>
shallowMerge({ tag: 'SegmentURL' }, getAttributes(s)))
}),
base: segmentBase && getAttributes(segmentBase)
findChildren(segmentTimeline, 'S').map(s => getAttributes(s)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this line was added by me, including the indentation, but do you mind fixing the indentation here?

Copy link
Contributor Author

@OshinKaramian OshinKaramian Jan 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder why the linter didn't catch this one, I think it gave me a warning or error for the indentation under list: and base:.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I'm surprised I added it myself, and didn't catch it myself or with the linter. Its not even lined up with anything ha

list: segmentList && shallowMerge(
getAttributes(segmentList),
{
segmentUrls,
initialization: getAttributes(segmentInitialization)
}),
base: segmentBase && shallowMerge(
getAttributes(segmentBase), {
initialization: getAttributes(segmentInitialization)
})
};
};

Expand Down
22 changes: 12 additions & 10 deletions src/segment/segmentBase.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import resolveUrl from '../utils/resolveUrl';
import errors from '../errors';
import urlTypeConverter from './urlType';
import { parseByDuration } from './timeParser';

/**
Expand All @@ -15,11 +15,12 @@ import { parseByDuration } from './timeParser';
export const segmentsFromBase = (attributes) => {
const {
baseUrl,
initialization = '',
initialization = {},
sourceDuration,
timescale = 1,
startNumber = 1,
periodIndex = 0,
indexRange = '',
duration
} = attributes;

Expand All @@ -28,14 +29,15 @@ export const segmentsFromBase = (attributes) => {
throw new Error(errors.NO_BASE_URL);
}

const segment = {
map: {
uri: initialization,
resolvedUri: resolveUrl(attributes.baseUrl || '', initialization)
},
resolvedUri: resolveUrl(attributes.baseUrl || '', ''),
uri: attributes.baseUrl
};
const initSegment = urlTypeConverter({
baseUrl,
source: initialization.sourceURL,
range: initialization.range
});
const segment = urlTypeConverter({ baseUrl, source: baseUrl, range: indexRange });

segment.map = initSegment;

const parsedTimescale = parseInt(timescale, 10);

// If there is a duration, use it, otherwise use the given duration of the source
Expand Down
34 changes: 13 additions & 21 deletions src/segment/segmentList.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import resolveUrl from '../utils/resolveUrl';
import { parseByDuration, parseByTimeline } from './timeParser';
import urlTypeConverter from './urlType';
import errors from '../errors';

/**
Expand All @@ -14,29 +14,21 @@ import errors from '../errors';
* @return {Object} translated segment object
*/
const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
const initUri = attributes.initialization || '';
const { baseUrl, initialization = {} } = attributes;

const segment = {
map: {
uri: initUri,
resolvedUri: resolveUrl(attributes.baseUrl || '', initUri)
},
resolvedUri: resolveUrl(attributes.baseUrl || '', segmentUrl.media),
uri: segmentUrl.media
};
const initSegment = urlTypeConverter({
baseUrl,
source: initialization.sourceURL,
range: initialization.range
});

// Follows byte-range-spec per RFC2616
// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
if (segmentUrl.mediaRange) {
const ranges = segmentUrl.mediaRange.split('-');
const startRange = parseInt(ranges[0], 10);
const endRange = parseInt(ranges[1], 10);
const segment = urlTypeConverter({
baseUrl,
source: segmentUrl.media,
range: segmentUrl.mediaRange
});

segment.byterange = {
length: endRange - startRange,
offset: startRange
};
}
segment.map = initSegment;

return segment;
};
Expand Down
27 changes: 22 additions & 5 deletions src/segment/segmentTemplate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import resolveUrl from '../utils/resolveUrl';
import urlTypeToSegment from './urlType';
import { parseByDuration, parseByTimeline } from './timeParser';

const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
Expand Down Expand Up @@ -151,7 +152,26 @@ export const segmentsFromTemplate = (attributes, segmentTimeline) => {
RepresentationID: attributes.id,
Bandwidth: parseInt(attributes.bandwidth || 0, 10)
};
const mapUri = constructTemplateUrl(attributes.initialization || '', templateValues);

let mapSegment = { uri: '', resolvedUri: resolveUrl(attributes.baseUrl || '', '') };

if (attributes.initialization && typeof attributes.initialization === 'string') {
const mapUri = constructTemplateUrl(attributes.initialization || '', templateValues);

mapSegment = {
uri: mapUri,
resolvedUri: resolveUrl(attributes.baseUrl || '', mapUri)
};
}

if (attributes.initialization && typeof attributes.initialization === 'object') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what I picked up off the spec, <Initialization> doesn't get templated, but @initialization does, which is why there is a difference here.

mapSegment = urlTypeToSegment({
baseUrl: attributes.baseUrl,
source: attributes.initialization.sourceURL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the changes proposed in my other comment to handle the string to object conversion in getSegmentInformation, I think you can safely do

source: constructTemplateUrl(attributes.initialization.sourceURL, templateValues)

In the case that the <Initialization> node took precedence, there shouldn't be any template identifiers, so constructTemplateUrl here should just return the sourceURL unchanged

range: attributes.initialization.range
});
}

const segments = parseTemplateInfo(attributes, segmentTimeline);

return segments.map(segment => {
Expand All @@ -165,10 +185,7 @@ export const segmentsFromTemplate = (attributes, segmentTimeline) => {
timeline: segment.timeline,
duration: segment.duration,
resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
map: {
uri: mapUri,
resolvedUri: resolveUrl(attributes.baseUrl || '', mapUri)
}
map: mapSegment
};
});
};
47 changes: 47 additions & 0 deletions src/segment/urlType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import resolveUrl from '../utils/resolveUrl';

/**
* @typedef {Object} SingleUri
* @property {string} uri - relative location of segment
* @property {string} resolvedUri - resolved location of segment
* @property {Object} byterange - Object containing information on how to make byte range
* requests following byte-range-spec per RFC2616.
* @property {String} byterange.length - length of range request
* @property {String} byterange.offset - byte offset of range request
*
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
*/

/**
* Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
* that conforms to how m3u8-parser is structured
*
* @see https://github.com/videojs/m3u8-parser
*
* @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
* @param {string} source - source url for segment
* @param {string} range - optional range used for range calls, follows
* @return {SingleUri} full segment information transformed into a format similar
* to m3u8-parser
*/
export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '' }) => {
const init = {
uri: source,
resolvedUri: resolveUrl(baseUrl || '', source)
};

if (source && range) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think range should be parsed even if source is not available. According to the spec for @sourceURL,

If not present, then any BaseURL element is mapped to the @sourceURL attribute and the range attribute shall be present.

The BaseURL mapping is already handled by the resolvedUri above, but this check would prevent parsing of the range that "shall be present"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I made an assumption that both were required but the existence of baseUrl makes that a poor assumption.

const ranges = range.split('-');
const startRange = parseInt(ranges[0], 10);
const endRange = parseInt(ranges[1], 10);

init.byterange = {
length: endRange - startRange,
offset: startRange
};
}

return init;
};

export default urlTypeToSegment;
8 changes: 5 additions & 3 deletions test/inheritAttributes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ QUnit.test('gets SegmentList attributes', function(assert) {
timeline: void 0,
list: {
duration: '10',
segmentUrls: []
segmentUrls: [],
initialization: {}
},
base: void 0
};
Expand All @@ -128,14 +129,15 @@ QUnit.test('gets SegmentBase attributes', function(assert) {
const adaptationSet = {
childNodes: [{
tagName: 'SegmentBase',
attributes: [{ name: 'duration', value: '10' }]
attributes: [{ name: 'duration', value: '10' }],
childNodes: []
}]
};
const expected = {
template: void 0,
timeline: void 0,
list: void 0,
base: { duration: '10' }
base: { duration: '10', initialization: {} }
};

assert.deepEqual(getSegmentInformation(adaptationSet), expected,
Expand Down
38 changes: 33 additions & 5 deletions test/segment/segmentBase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ QUnit.module('segmentBase - segmentsFromBase');
QUnit.test('sets segment to baseUrl', function(assert) {
const inputAttributes = {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: 'http://www.example.com/init.fmp4'
initialization: { sourceURL: 'http://www.example.com/init.fmp4' }
};

assert.deepEqual(segmentsFromBase(inputAttributes), [{
Expand All @@ -25,7 +25,7 @@ QUnit.test('sets segment to baseUrl', function(assert) {
QUnit.test('sets duration based on sourceDuration', function(assert) {
const inputAttributes = {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: 'http://www.example.com/init.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
sourceDuration: 10
};

Expand All @@ -44,7 +44,7 @@ QUnit.test('sets duration based on sourceDuration', function(assert) {
QUnit.test('sets duration based on sourceDuration and @timescale', function(assert) {
const inputAttributes = {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: 'http://www.example.com/init.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
sourceDuration: 10,
timescale: 2
};
Expand All @@ -66,7 +66,7 @@ QUnit.test('sets duration based on @duration', function(assert) {
duration: 10,
sourceDuration: 20,
baseUrl: 'http://www.example.com/i.fmp4',
initialization: 'http://www.example.com/init.fmp4'
initialization: { sourceURL: 'http://www.example.com/init.fmp4' }
};

assert.deepEqual(segmentsFromBase(inputAttributes), [{
Expand All @@ -87,7 +87,7 @@ QUnit.test('sets duration based on @duration and @timescale', function(assert) {
sourceDuration: 20,
timescale: 5,
baseUrl: 'http://www.example.com/i.fmp4',
initialization: 'http://www.example.com/init.fmp4'
initialization: { sourceURL: 'http://www.example.com/init.fmp4' }
};

assert.deepEqual(segmentsFromBase(inputAttributes), [{
Expand All @@ -102,6 +102,34 @@ QUnit.test('sets duration based on @duration and @timescale', function(assert) {
}]);
});

QUnit.test('translates ranges in <Initialization> node', function(assert) {
const inputAttributes = {
duration: 10,
sourceDuration: 20,
timescale: 5,
baseUrl: 'http://www.example.com/i.fmp4',
initialization: {
sourceURL: 'http://www.example.com/init.fmp4',
range: '121-125'
}
};

assert.deepEqual(segmentsFromBase(inputAttributes), [{
duration: 2,
timeline: 0,
map: {
resolvedUri: 'http://www.example.com/init.fmp4',
uri: 'http://www.example.com/init.fmp4',
byterange: {
length: 4,
offset: 121
}
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4'
}]);
});

QUnit.test('errors if no baseUrl exists', function(assert) {
assert.throws(() => segmentsFromBase({}), new Error(errors.NO_BASE_URL));
});
Loading