diff --git a/src/errors.js b/src/errors.js index 04f6b649..e6845627 100644 --- a/src/errors.js +++ b/src/errors.js @@ -2,5 +2,7 @@ export default { INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', DASH_INVALID_XML: 'DASH_INVALID_XML', - UNSUPPORTED_SEGMENTATION_TYPE: 'UNSUPPORTED_SEGMENTATION_TYPE' + NO_BASE_URL: 'NO_BASE_URL', + MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', + SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED' }; diff --git a/src/inheritAttributes.js b/src/inheritAttributes.js index 9f160c79..285427f6 100644 --- a/src/inheritAttributes.js +++ b/src/inheritAttributes.js @@ -2,7 +2,7 @@ import { flatten } from './utils/list'; import { shallowMerge, getAttributes } from './utils/object'; import { parseDuration } from './utils/time'; import { findChildren, getContent } from './utils/xml'; -import resolveUrl from './resolveUrl'; +import resolveUrl from './utils/resolveUrl'; import errors from './errors'; /** @@ -50,17 +50,48 @@ export const buildBaseUrls = (referenceUrls, baseUrlElements) => { */ export const getSegmentInformation = (adaptationSet) => { const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; - const segmentTimeline = - segmentTemplate && findChildren(segmentTemplate, 'SegmentTimeline')[0]; const segmentList = findChildren(adaptationSet, 'SegmentList')[0]; + const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL') + .map(s => shallowMerge({ tag: 'SegmentURL' }, getAttributes(s))); const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; + const segmentTimelineParentNode = segmentList || segmentTemplate; + const segmentTimeline = segmentTimelineParentNode && + findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; + const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; + const segmentInitialization = segmentInitializationParentNode && + findChildren(segmentInitializationParentNode, 'Initialization')[0]; + + // SegmentTemplate is handled slightly differently, since it can have both + // @initialization and an node. @initialization can be templated, + // while the node can have a url and range specified. If the has + // both @initialization and an 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 = + (segmentInitialization && getAttributes(segmentInitialization)); + } else if (template && template.initialization) { + // If it is @initialization we convert it to an object since this is the format that + // later functions will rely on for the initialization segment. This is only valid + // for + template.initialization = { sourceURL: template.initialization }; + } return { - template: segmentTemplate && getAttributes(segmentTemplate), + template, timeline: segmentTimeline && - findChildren(segmentTimeline, 'S').map(s => getAttributes(s)), - list: segmentList && getAttributes(segmentList), - base: segmentBase && getAttributes(segmentBase) + findChildren(segmentTimeline, 'S').map(s => getAttributes(s)), + list: segmentList && shallowMerge( + getAttributes(segmentList), + { + segmentUrls, + initialization: getAttributes(segmentInitialization) + }), + base: segmentBase && shallowMerge( + getAttributes(segmentBase), { + initialization: getAttributes(segmentInitialization) + }) }; }; diff --git a/src/segment/segmentBase.js b/src/segment/segmentBase.js new file mode 100644 index 00000000..556b79cf --- /dev/null +++ b/src/segment/segmentBase.js @@ -0,0 +1,64 @@ +import errors from '../errors'; +import urlTypeConverter from './urlType'; +import { parseByDuration } from './timeParser'; + +/** + * Translates SegmentBase into a set of segments. + * (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each + * node should be translated into segment. + * + * @param {Object} attributes + * Object containing all inherited attributes from parent elements with attribute + * names as keys + * @return {Object.} list of segments + */ +export const segmentsFromBase = (attributes) => { + const { + baseUrl, + initialization = {}, + sourceDuration, + timescale = 1, + startNumber = 1, + periodIndex = 0, + indexRange = '', + duration + } = attributes; + + // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) + if (!baseUrl) { + throw new Error(errors.NO_BASE_URL); + } + + 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 + // (since SegmentBase is only for one total segment) + if (duration) { + const parsedDuration = parseInt(duration, 10); + const start = parseInt(startNumber, 10); + const segmentTimeInfo = parseByDuration(start, + periodIndex, + parsedTimescale, + parsedDuration, + sourceDuration); + + if (segmentTimeInfo.length >= 1) { + segment.duration = segmentTimeInfo[0].duration; + segment.timeline = segmentTimeInfo[0].timeline; + } + } else if (sourceDuration) { + segment.duration = (sourceDuration / parsedTimescale); + segment.timeline = 0; + } + + return [segment]; +}; diff --git a/src/segment/segmentList.js b/src/segment/segmentList.js new file mode 100644 index 00000000..f892c25a --- /dev/null +++ b/src/segment/segmentList.js @@ -0,0 +1,103 @@ +import { parseByDuration, parseByTimeline } from './timeParser'; +import urlTypeConverter from './urlType'; +import errors from '../errors'; + +/** + * Converts a (of type URLType from the DASH spec 5.3.9.2 Table 14) + * to an object that matches the output of a segment in videojs/mpd-parser + * + * @param {Object} attributes + * Object containing all inherited attributes from parent elements with attribute + * names as keys + * @param {Object} segmentUrl + * node to translate into a segment object + * @return {Object} translated segment object + */ +const SegmentURLToSegmentObject = (attributes, segmentUrl) => { + const { baseUrl, initialization = {} } = attributes; + + const initSegment = urlTypeConverter({ + baseUrl, + source: initialization.sourceURL, + range: initialization.range + }); + + const segment = urlTypeConverter({ + baseUrl, + source: segmentUrl.media, + range: segmentUrl.mediaRange + }); + + segment.map = initSegment; + + return segment; +}; + +/** + * Generates a list of segments using information provided by the SegmentList element + * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each + * node should be translated into segment. + * + * @param {Object} attributes + * Object containing all inherited attributes from parent elements with attribute + * names as keys + * @param {Object[]|undefined} segmentTimeline + * List of objects representing the attributes of each S element contained within + * the SegmentTimeline element + * @return {Object.} list of segments + */ +export const segmentsFromList = (attributes, segmentTimeline) => { + const { + timescale = 1, + duration, + segmentUrls = [], + periodIndex = 0, + startNumber = 1 + } = attributes; + + // Per spec (5.3.9.2.1) no way to determine segment duration OR + // if both SegmentTimeline and @duration are defined, it is outside of spec. + if ((!duration && !segmentTimeline) || + (duration && segmentTimeline)) { + throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); + } + + const parsedTimescale = parseInt(timescale, 10); + const start = parseInt(startNumber, 10); + const segmentUrlMap = segmentUrls.map(segmentUrlObject => + SegmentURLToSegmentObject(attributes, segmentUrlObject)); + let segmentTimeInfo; + + if (duration) { + const parsedDuration = parseInt(duration, 10); + + segmentTimeInfo = parseByDuration(start, + periodIndex, + parsedTimescale, + parsedDuration, + attributes.sourceDuration); + } + + if (segmentTimeline) { + segmentTimeInfo = parseByTimeline(start, + periodIndex, + parsedTimescale, + segmentTimeline, + attributes.sourceDuration); + } + + const segments = segmentTimeInfo.map((segmentTime, index) => { + if (segmentUrlMap[index]) { + const segment = segmentUrlMap[index]; + + segment.timeline = segmentTime.timeline; + segment.duration = segmentTime.duration; + return segment; + } + // Since we're mapping we should get rid of any blank segments (in case + // the given SegmentTimeline is handling for more elements than we have + // SegmentURLs for). + }).filter(segment => segment); + + return segments; +}; diff --git a/src/segmentTemplate.js b/src/segment/segmentTemplate.js similarity index 54% rename from src/segmentTemplate.js rename to src/segment/segmentTemplate.js index 9c793b57..c22559b1 100644 --- a/src/segmentTemplate.js +++ b/src/segment/segmentTemplate.js @@ -1,5 +1,6 @@ -import { range } from './utils/list'; -import resolveUrl from './resolveUrl'; +import resolveUrl from '../utils/resolveUrl'; +import urlTypeToSegment from './urlType'; +import { parseByDuration, parseByTimeline } from './timeParser'; const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; @@ -90,128 +91,6 @@ export const identifierReplacement = (values) => (match, identifier, format, wid export const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values)); -/** - * Uses information provided by SegmentTemplate@duration attribute to determine segment - * timing and duration - * - * @param {number} start - * The start number for the first segment of this period - * @param {number} timeline - * The timeline (period index) for the first segment of this period - * @param {number} timescale - * The timescale for the timestamps contained within the media content - * @param {number} duration - * Duration of each segment - * @param {number} sourceDuration - * Duration of the entire Media Presentation - * @return {{number: number, duration: number, time: number, timeline: number}[]} - * List of Objects with segment timing and duration info - */ -export const parseByDuration = (start, timeline, timescale, duration, sourceDuration) => { - const count = Math.ceil(sourceDuration / (duration / timescale)); - - return range(start, start + count).map((number, index) => { - const segment = { number, duration: duration / timescale, timeline }; - - if (index === count - 1) { - // final segment may be less than duration - segment.duration = sourceDuration - (segment.duration * index); - } - - segment.time = (segment.number - start) * duration; - - return segment; - }); -}; - -/** - * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment - * timing and duration - * - * @param {number} start - * The start number for the first segment of this period - * @param {number} timeline - * The timeline (period index) for the first segment of this period - * @param {number} timescale - * The timescale for the timestamps contained within the media content - * @param {Object[]} segmentTimeline - * List of objects representing the attributes of each S element contained within - * @param {number} sourceDuration - * Duration of the entire Media Presentation - * @return {{number: number, duration: number, time: number, timeline: number}[]} - * List of Objects with segment timing and duration info - */ -export const parseByTimeline = -(start, timeline, timescale, segmentTimeline, sourceDuration) => { - const segments = []; - let time = -1; - - for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { - const S = segmentTimeline[sIndex]; - const duration = parseInt(S.d, 10); - const repeat = parseInt(S.r || 0, 10); - const segmentTime = parseInt(S.t || 0, 10); - - if (time < 0) { - // first segment - time = segmentTime; - } - - if (segmentTime && segmentTime > time) { - // discontinuity - - // TODO: How to handle this type of discontinuity - // timeline++ here would treat it like HLS discontuity and content would - // get appended without gap - // E.G. - // - // - // - // - // would have $Time$ values of [0, 1, 2, 5] - // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) - // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) - // does the value of sourceDuration consider this when calculating arbitrary - // negative @r repeat value? - // E.G. Same elements as above with this added at the end - // - // with a sourceDuration of 10 - // Would the 2 gaps be included in the time duration calculations resulting in - // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments - // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? - - time = segmentTime; - } - - let count; - - if (repeat < 0) { - const nextS = sIndex + 1; - - if (nextS === segmentTimeline.length) { - // last segment - // TODO: This may be incorrect depending on conclusion of TODO above - count = ((sourceDuration * timescale) - time) / duration; - } else { - count = (parseInt(segmentTimeline[nextS].t, 10) - time) / duration; - } - } else { - count = repeat + 1; - } - - const end = start + segments.length + count; - let number = start + segments.length; - - while (number < end) { - segments.push({ number, duration: duration / timescale, time, timeline }); - time += duration; - number++; - } - } - - return segments; -}; - /** * Generates a list of objects containing timing and duration information about each * segment needed to generate segment uris and the complete segment object @@ -273,7 +152,15 @@ export const segmentsFromTemplate = (attributes, segmentTimeline) => { RepresentationID: attributes.id, Bandwidth: parseInt(attributes.bandwidth || 0, 10) }; - const mapUri = constructTemplateUrl(attributes.initialization || '', templateValues); + + const { initialization = { sourceURL: '', range: '' } } = attributes; + + const mapSegment = urlTypeToSegment({ + baseUrl: attributes.baseUrl, + source: constructTemplateUrl(initialization.sourceURL, templateValues), + range: initialization.range + }); + const segments = parseTemplateInfo(attributes, segmentTimeline); return segments.map(segment => { @@ -287,10 +174,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 }; }); }; diff --git a/src/segment/timeParser.js b/src/segment/timeParser.js new file mode 100644 index 00000000..5f8a1402 --- /dev/null +++ b/src/segment/timeParser.js @@ -0,0 +1,105 @@ +import { range } from '../utils/list'; +/** + * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment + * timing and duration + * + * @param {number} start + * The start number for the first segment of this period + * @param {number} timeline + * The timeline (period index) for the first segment of this period + * @param {number} timescale + * The timescale for the timestamps contained within the media content + * @param {Object[]} segmentTimeline + * List of objects representing the attributes of each S element contained within + * @param {number} sourceDuration + * Duration of the entire Media Presentation + * @return {{number: number, duration: number, time: number, timeline: number}[]} + * List of Objects with segment timing and duration info + */ +export const parseByTimeline = +(start, timeline, timescale, segmentTimeline, sourceDuration) => { + const segments = []; + let time = -1; + + for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { + const S = segmentTimeline[sIndex]; + const duration = parseInt(S.d, 10); + const repeat = parseInt(S.r || 0, 10); + const segmentTime = parseInt(S.t || 0, 10); + + if (time < 0) { + // first segment + time = segmentTime; + } + + if (segmentTime && segmentTime > time) { + // discontinuity + + // TODO: How to handle this type of discontinuity + // timeline++ here would treat it like HLS discontuity and content would + // get appended without gap + // E.G. + // + // + // + // + // would have $Time$ values of [0, 1, 2, 5] + // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) + // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) + // does the value of sourceDuration consider this when calculating arbitrary + // negative @r repeat value? + // E.G. Same elements as above with this added at the end + // + // with a sourceDuration of 10 + // Would the 2 gaps be included in the time duration calculations resulting in + // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments + // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? + + time = segmentTime; + } + + let count; + + if (repeat < 0) { + const nextS = sIndex + 1; + + if (nextS === segmentTimeline.length) { + // last segment + // TODO: This may be incorrect depending on conclusion of TODO above + count = ((sourceDuration * timescale) - time) / duration; + } else { + count = (parseInt(segmentTimeline[nextS].t, 10) - time) / duration; + } + } else { + count = repeat + 1; + } + + const end = start + segments.length + count; + let number = start + segments.length; + + while (number < end) { + segments.push({ number, duration: duration / timescale, time, timeline }); + time += duration; + number++; + } + } + + return segments; +}; + +export const parseByDuration = (start, timeline, timescale, duration, sourceDuration) => { + const count = Math.ceil(sourceDuration / (duration / timescale)); + + return range(start, start + count).map((number, index) => { + const segment = { number, duration: duration / timescale, timeline }; + + if (index === count - 1) { + // final segment may be less than duration + segment.duration = sourceDuration - (segment.duration * index); + } + + segment.time = (segment.number - start) * duration; + + return segment; + }); +}; diff --git a/src/segment/urlType.js b/src/segment/urlType.js new file mode 100644 index 00000000..83df358c --- /dev/null +++ b/src/segment/urlType.js @@ -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 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 (range) { + 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; diff --git a/src/toPlaylists.js b/src/toPlaylists.js index d8698c11..6d5a95d8 100644 --- a/src/toPlaylists.js +++ b/src/toPlaylists.js @@ -1,12 +1,7 @@ import { shallowMerge } from './utils/object'; -import errors from './errors'; -import { segmentsFromTemplate } from './segmentTemplate'; - -// TODO -export const segmentsFromBase = x => [{ uri: '' }]; - -// TODO -export const segmentsFromList = x => [{ uri: '' }]; +import { segmentsFromTemplate } from './segment/segmentTemplate'; +import { segmentsFromList } from './segment/segmentList'; +import { segmentsFromBase } from './segment/segmentBase'; export const generateSegments = (segmentInfo, attributes) => { if (segmentInfo.template) { @@ -16,18 +11,14 @@ export const generateSegments = (segmentInfo, attributes) => { ); } - // TODO if (segmentInfo.base) { - throw new Error(errors.UNSUPPORTED_SEGMENTATION_TYPE); - - // return segmentsFromBase(attributes); + return segmentsFromBase(shallowMerge(segmentInfo.base, attributes)); } - // TODO if (segmentInfo.list) { - throw new Error(errors.UNSUPPORTED_SEGMENTATION_TYPE); - - // return segmentsFromList(attributes); + return segmentsFromList( + shallowMerge(segmentInfo.list, attributes), segmentInfo.timeline + ); } }; diff --git a/src/resolveUrl.js b/src/utils/resolveUrl.js similarity index 100% rename from src/resolveUrl.js rename to src/utils/resolveUrl.js diff --git a/test/index.test.js b/test/index.test.js index 0dd30c55..a90d9d5b 100755 --- a/test/index.test.js +++ b/test/index.test.js @@ -3,10 +3,17 @@ import QUnit from 'qunit'; // manifests import maatVttSegmentTemplate from './manifests/maat_vtt_segmentTemplate.mpd'; +import segmentBaseTemplate from './manifests/segmentBase.mpd'; +import segmentListTemplate from './manifests/segmentList.mpd'; import { parsedManifest as maatVttSegmentTemplateManifest } from './manifests/maat_vtt_segmentTemplate.js'; - +import { + parsedManifest as segmentBaseManifest +} from './manifests/segmentBase.js'; +import { + parsedManifest as segmentListManifest +} from './manifests/segmentList.js'; QUnit.module('mpd-parser'); QUnit.test('has VERSION', function(assert) { @@ -21,6 +28,14 @@ QUnit.test('has parse', function(assert) { name: 'maat_vtt_segmentTemplate', input: maatVttSegmentTemplate, expected: maatVttSegmentTemplateManifest +}, { + name: 'segmentBase', + input: segmentBaseTemplate, + expected: segmentBaseManifest +}, { + name: 'segmentList', + input: segmentListTemplate, + expected: segmentListManifest }].forEach(({ name, input, expected }) => { QUnit.test(`${name} test manifest`, function(assert) { const actual = parse(input); diff --git a/test/inheritAttributes.test.js b/test/inheritAttributes.test.js index 888b3a87..aa04067f 100644 --- a/test/inheritAttributes.test.js +++ b/test/inheritAttributes.test.js @@ -106,13 +106,18 @@ QUnit.test('gets SegmentList attributes', function(assert) { const adaptationSet = { childNodes: [{ tagName: 'SegmentList', - attributes: [{ name: 'duration', value: '10' }] + attributes: [{ name: 'duration', value: '10' }], + childNodes: [] }] }; const expected = { template: void 0, timeline: void 0, - list: { duration: '10' }, + list: { + duration: '10', + segmentUrls: [], + initialization: {} + }, base: void 0 }; @@ -124,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, diff --git a/test/manifests/segmentBase.js b/test/manifests/segmentBase.js new file mode 100644 index 00000000..b99f6130 --- /dev/null +++ b/test/manifests/segmentBase.js @@ -0,0 +1,46 @@ +export const parsedManifest = { + allowCache: true, + discontinuityStarts: [], + duration: 6, + endList: true, + mediaGroups: { + 'AUDIO': {}, + 'CLOSED-CAPTIONS': {}, + 'SUBTITLES': {}, + 'VIDEO': {} + }, + playlists: [ + { + attributes: { + 'AUDIO': 'audio', + 'BANDWIDTH': 449000, + 'CODECS': 'avc1.420015', + 'NAME': '482', + 'PROGRAM-ID': 1, + 'RESOLUTION': { + height: 270, + width: 482 + }, + 'SUBTITLES': 'subs' + }, + endList: true, + resolvedUri: '', + segments: [ + { + duration: 6, + timeline: 0, + map: { + uri: '', + resolvedUri: 'https://www.example.com/1080p.ts' + }, + resolvedUri: 'https://www.example.com/1080p.ts', + uri: 'https://www.example.com/1080p.ts' + } + ], + timeline: 0, + uri: '' + } + ], + segments: [], + uri: '' +}; diff --git a/test/manifests/segmentBase.mpd b/test/manifests/segmentBase.mpd new file mode 100644 index 00000000..609202a5 --- /dev/null +++ b/test/manifests/segmentBase.mpd @@ -0,0 +1,14 @@ + + + https://www.example.com/base + + + + + 1080p.ts + + + + + + diff --git a/test/manifests/segmentList.js b/test/manifests/segmentList.js new file mode 100644 index 00000000..787f162b --- /dev/null +++ b/test/manifests/segmentList.js @@ -0,0 +1,216 @@ +export const parsedManifest = { + allowCache: true, + discontinuityStarts: [], + duration: 6, + endList: true, + mediaGroups: { + 'AUDIO': {}, + 'CLOSED-CAPTIONS': {}, + 'SUBTITLES': {}, + 'VIDEO': {} + }, + playlists: [ + { + attributes: { + 'AUDIO': 'audio', + 'BANDWIDTH': 449000, + 'CODECS': 'avc1.420015', + 'NAME': '482', + 'PROGRAM-ID': 1, + 'RESOLUTION': { + height: 270, + width: 482 + }, + 'SUBTITLES': 'subs' + }, + endList: true, + resolvedUri: '', + segments: [ + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-1.ts', + timeline: 0, + uri: 'low/segment-1.ts' + }, + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-2.ts', + timeline: 0, + uri: 'low/segment-2.ts' + }, + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-3.ts', + timeline: 0, + uri: 'low/segment-3.ts' + }, + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-4.ts', + timeline: 0, + uri: 'low/segment-4.ts' + }, + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-5.ts', + timeline: 0, + uri: 'low/segment-5.ts' + }, + { + duration: 1, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/low/segment-6.ts', + timeline: 0, + uri: 'low/segment-6.ts' + } + ], + timeline: 0, + uri: '' + }, + { + attributes: { + 'AUDIO': 'audio', + 'BANDWIDTH': 3971000, + 'CODECS': 'avc1.420015', + 'NAME': '720', + 'PROGRAM-ID': 1, + 'RESOLUTION': { + height: 404, + width: 720 + }, + 'SUBTITLES': 'subs' + }, + endList: true, + resolvedUri: '', + segments: [ + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-1.ts', + timeline: 0, + uri: 'high/segment-1.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-2.ts', + timeline: 0, + uri: 'high/segment-2.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-3.ts', + timeline: 0, + uri: 'high/segment-3.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-4.ts', + timeline: 0, + uri: 'high/segment-4.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-5.ts', + timeline: 0, + uri: 'high/segment-5.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-6.ts', + timeline: 0, + uri: 'high/segment-6.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-7.ts', + timeline: 0, + uri: 'high/segment-7.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-8.ts', + timeline: 0, + uri: 'high/segment-8.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-9.ts', + timeline: 0, + uri: 'high/segment-9.ts' + }, + { + duration: 60, + map: { + uri: '', + resolvedUri: 'https://www.example.com/base' + }, + resolvedUri: 'https://www.example.com/high/segment-10.ts', + timeline: 0, + uri: 'high/segment-10.ts' + } + ], + timeline: 0, + uri: '' + } + ], + segments: [], + uri: '' +}; diff --git a/test/manifests/segmentList.mpd b/test/manifests/segmentList.mpd new file mode 100644 index 00000000..0977c5a4 --- /dev/null +++ b/test/manifests/segmentList.mpd @@ -0,0 +1,45 @@ + + + https://www.example.com/base + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/segment/segmentBase.test.js b/test/segment/segmentBase.test.js new file mode 100644 index 00000000..5050f844 --- /dev/null +++ b/test/segment/segmentBase.test.js @@ -0,0 +1,135 @@ +import QUnit from 'qunit'; +import { + segmentsFromBase +} from '../../src/segment/segmentBase'; +import errors from '../../src/errors'; + +QUnit.module('segmentBase - segmentsFromBase'); + +QUnit.test('sets segment to baseUrl', function(assert) { + const inputAttributes = { + baseUrl: 'http://www.example.com/i.fmp4', + initialization: { sourceURL: 'http://www.example.com/init.fmp4' } + }; + + assert.deepEqual(segmentsFromBase(inputAttributes), [{ + map: { + resolvedUri: 'http://www.example.com/init.fmp4', + uri: 'http://www.example.com/init.fmp4' + }, + resolvedUri: 'http://www.example.com/i.fmp4', + uri: 'http://www.example.com/i.fmp4' + }]); +}); + +QUnit.test('sets duration based on sourceDuration', function(assert) { + const inputAttributes = { + baseUrl: 'http://www.example.com/i.fmp4', + initialization: { sourceURL: 'http://www.example.com/init.fmp4' }, + sourceDuration: 10 + }; + + assert.deepEqual(segmentsFromBase(inputAttributes), [{ + duration: 10, + timeline: 0, + map: { + resolvedUri: 'http://www.example.com/init.fmp4', + uri: 'http://www.example.com/init.fmp4' + }, + resolvedUri: 'http://www.example.com/i.fmp4', + uri: 'http://www.example.com/i.fmp4' + }]); +}); + +QUnit.test('sets duration based on sourceDuration and @timescale', function(assert) { + const inputAttributes = { + baseUrl: 'http://www.example.com/i.fmp4', + initialization: { sourceURL: 'http://www.example.com/init.fmp4' }, + sourceDuration: 10, + timescale: 2 + }; + + assert.deepEqual(segmentsFromBase(inputAttributes), [{ + duration: 5, + timeline: 0, + map: { + resolvedUri: 'http://www.example.com/init.fmp4', + uri: 'http://www.example.com/init.fmp4' + }, + resolvedUri: 'http://www.example.com/i.fmp4', + uri: 'http://www.example.com/i.fmp4' + }]); +}); + +QUnit.test('sets duration based on @duration', function(assert) { + const inputAttributes = { + duration: 10, + sourceDuration: 20, + baseUrl: 'http://www.example.com/i.fmp4', + initialization: { sourceURL: 'http://www.example.com/init.fmp4' } + }; + + assert.deepEqual(segmentsFromBase(inputAttributes), [{ + duration: 10, + timeline: 0, + map: { + resolvedUri: 'http://www.example.com/init.fmp4', + uri: 'http://www.example.com/init.fmp4' + }, + resolvedUri: 'http://www.example.com/i.fmp4', + uri: 'http://www.example.com/i.fmp4' + }]); +}); + +QUnit.test('sets duration based on @duration and @timescale', 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' } + }; + + assert.deepEqual(segmentsFromBase(inputAttributes), [{ + duration: 2, + timeline: 0, + map: { + resolvedUri: 'http://www.example.com/init.fmp4', + uri: 'http://www.example.com/init.fmp4' + }, + resolvedUri: 'http://www.example.com/i.fmp4', + uri: 'http://www.example.com/i.fmp4' + }]); +}); + +QUnit.test('translates ranges in 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)); +}); diff --git a/test/segment/segmentList.test.js b/test/segment/segmentList.test.js new file mode 100644 index 00000000..af2128ca --- /dev/null +++ b/test/segment/segmentList.test.js @@ -0,0 +1,515 @@ +import QUnit from 'qunit'; +import { + segmentsFromList +} from '../../src/segment/segmentList'; +import errors from '../../src/errors'; + +QUnit.module('segmentList - segmentsFromList'); + +QUnit.test('uses segmentTimeline to set segments', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }, { + media: '3.fmp4' + }, { + media: '4.fmp4' + }, { + media: '5.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + periodIndex: 0, + startNumber: 1, + baseUrl: 'http://example.com/' + }; + + const inputTimeline = [{ + t: 1000, + d: 1000, + r: 4 + }]; + + assert.deepEqual(segmentsFromList(inputAttributes, inputTimeline), [{ + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/3.fmp4', + timeline: 0, + uri: '3.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/4.fmp4', + timeline: 0, + uri: '4.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/5.fmp4', + timeline: 0, + uri: '5.fmp4' + }]); +}); + +QUnit.test('truncates if segmentTimeline does not apply for all segments', + function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }, { + media: '3.fmp4' + }, { + media: '4.fmp4' + }, { + media: '5.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + periodIndex: 0, + startNumber: 1, + baseUrl: 'http://example.com/' + }; + + const inputTimeline = [{ + t: 1000, + d: 1000, + r: 1 + }]; + + assert.deepEqual(segmentsFromList(inputAttributes, inputTimeline), [{ + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }]); + }); + +QUnit.test('if segment timeline is too long does not add extra blank segments', + function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }, { + media: '3.fmp4' + }, { + media: '4.fmp4' + }, { + media: '5.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + periodIndex: 0, + startNumber: 1, + baseUrl: 'http://example.com/' + }; + + const inputTimeline = [{ + t: 1000, + d: 1000, + r: 10 + }]; + + assert.deepEqual(segmentsFromList(inputAttributes, inputTimeline), [{ + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/3.fmp4', + timeline: 0, + uri: '3.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/4.fmp4', + timeline: 0, + uri: '4.fmp4' + }, { + duration: 1000, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/5.fmp4', + timeline: 0, + uri: '5.fmp4' + }]); + }); + +QUnit.test('uses duration to set segments', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }, { + media: '3.fmp4' + }, { + media: '4.fmp4' + }, { + media: '5.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + duration: 10, + periodIndex: 0, + startNumber: 1, + sourceDuration: 50, + baseUrl: 'http://example.com/' + }; + + assert.deepEqual(segmentsFromList(inputAttributes), [{ + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }, { + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/3.fmp4', + timeline: 0, + uri: '3.fmp4' + }, { + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/4.fmp4', + timeline: 0, + uri: '4.fmp4' + }, { + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/5.fmp4', + timeline: 0, + uri: '5.fmp4' + }]); +}); + +QUnit.test('uses timescale to set segment duration', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }, { + media: '3.fmp4' + }, { + media: '4.fmp4' + }, { + media: '5.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + duration: 10, + timescale: 2, + periodIndex: 0, + startNumber: 1, + sourceDuration: 25, + baseUrl: 'http://example.com/' + }; + + assert.deepEqual(segmentsFromList(inputAttributes), [{ + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }, { + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/3.fmp4', + timeline: 0, + uri: '3.fmp4' + }, { + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/4.fmp4', + timeline: 0, + uri: '4.fmp4' + }, { + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/5.fmp4', + timeline: 0, + uri: '5.fmp4' + }]); +}); + +QUnit.test('timescale sets duration of last segment correctly', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + duration: 10, + timescale: 1, + periodIndex: 0, + startNumber: 1, + sourceDuration: 15, + baseUrl: 'http://example.com/' + }; + + assert.deepEqual(segmentsFromList(inputAttributes), [{ + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 5, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }]); +}); + +QUnit.test('segmentUrl translates ranges correctly', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4', + mediaRange: '0-200' + }, { + media: '1.fmp4', + mediaRange: '201-400' + }], + initialization: { sourceURL: 'init.fmp4' }, + duration: 10, + timescale: 1, + periodIndex: 0, + startNumber: 1, + sourceDuration: 20, + baseUrl: 'http://example.com/' + }; + + assert.deepEqual(segmentsFromList(inputAttributes), [{ + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + byterange: { + length: 200, + offset: 0 + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 10, + byterange: { + length: 199, + offset: 201 + }, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }]); +}); + +QUnit.test('throws error if more than 1 segment and no duration or timeline', + function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }], + duration: 10, + initialization: { sourceURL: 'init.fmp4' }, + timescale: 1, + periodIndex: 0, + startNumber: 1, + sourceDuration: 20, + baseUrl: 'http://example.com/' + }; + + const inputTimeline = [{ + t: 1000, + d: 1000, + r: 4 + }]; + + assert.throws(() => segmentsFromList(inputAttributes, inputTimeline), + new Error(errors.SEGMENT_TIME_UNSPECIFIED)); + }); + +QUnit.test('throws error if timeline and duration are both defined', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }], + initialization: { sourceURL: 'init.fmp4' }, + timescale: 1, + periodIndex: 0, + startNumber: 1, + sourceDuration: 20, + baseUrl: 'http://example.com/' + }; + + assert.throws(() => segmentsFromList(inputAttributes), + new Error(errors.SEGMENT_TIME_UNSPECIFIED)); +}); + +QUnit.test('translates ranges in node', function(assert) { + const inputAttributes = { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '1.fmp4' + }], + initialization: { sourceURL: 'init.fmp4', range: '121-125' }, + duration: 10, + timescale: 1, + periodIndex: 0, + startNumber: 1, + sourceDuration: 20, + baseUrl: 'http://example.com/' + }; + + assert.deepEqual(segmentsFromList(inputAttributes), [{ + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4', + byterange: { + length: 4, + offset: 121 + } + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 10, + map: { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4', + byterange: { + length: 4, + offset: 121 + } + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }]); +}); diff --git a/test/segmentTemplate.test.js b/test/segment/segmentTemplate.test.js similarity index 89% rename from test/segmentTemplate.test.js rename to test/segment/segmentTemplate.test.js index d75e96af..a6df52aa 100644 --- a/test/segmentTemplate.test.js +++ b/test/segment/segmentTemplate.test.js @@ -3,7 +3,7 @@ import { constructTemplateUrl, parseTemplateInfo, segmentsFromTemplate -} from '../src/segmentTemplate'; +} from '../../src/segment/segmentTemplate'; QUnit.module('segmentTemplate - constructTemplateUrl'); @@ -698,7 +698,9 @@ QUnit.test('constructs simple segment list and resolves uris', function(assert) timescale: '1000', bandwidth: '100', id: 'Rep1', - initialization: '$RepresentationID$/$Bandwidth$/init.mp4', + initialization: { + sourceURL: '$RepresentationID$/$Bandwidth$/init.mp4' + }, media: '$RepresentationID$/$Bandwidth$/$Number%03d$-$Time%05d$.mp4', periodIndex: 1, baseUrl: 'https://example.com/' @@ -740,3 +742,67 @@ QUnit.test('constructs simple segment list and resolves uris', function(assert) 'creates segments from template'); }); +QUnit.test('constructs simple segment list and with node', function(assert) { + const attributes = { + startNumber: '0', + duration: '6000', + sourceDuration: 16, + timescale: '1000', + bandwidth: '100', + id: 'Rep1', + initialization: { + sourceURL: 'init.mp4', + range: '121-125' + }, + media: '$RepresentationID$/$Bandwidth$/$Number%03d$-$Time%05d$.mp4', + periodIndex: 1, + baseUrl: 'https://example.com/' + }; + const segments = [ + { + duration: 6, + map: { + resolvedUri: 'https://example.com/init.mp4', + uri: 'init.mp4', + byterange: { + length: 4, + offset: 121 + } + }, + resolvedUri: 'https://example.com/Rep1/100/000-00000.mp4', + timeline: 1, + uri: 'Rep1/100/000-00000.mp4' + }, + { + duration: 6, + map: { + resolvedUri: 'https://example.com/init.mp4', + uri: 'init.mp4', + byterange: { + length: 4, + offset: 121 + } + }, + resolvedUri: 'https://example.com/Rep1/100/001-06000.mp4', + timeline: 1, + uri: 'Rep1/100/001-06000.mp4' + }, + { + duration: 4, + map: { + resolvedUri: 'https://example.com/init.mp4', + uri: 'init.mp4', + byterange: { + length: 4, + offset: 121 + } + }, + resolvedUri: 'https://example.com/Rep1/100/002-12000.mp4', + timeline: 1, + uri: 'Rep1/100/002-12000.mp4' + } + ]; + + assert.deepEqual(segmentsFromTemplate(attributes, void 0), segments, + 'creates segments from template'); +}); diff --git a/test/segment/urlType.test.js b/test/segment/urlType.test.js new file mode 100644 index 00000000..d3a92ecb --- /dev/null +++ b/test/segment/urlType.test.js @@ -0,0 +1,50 @@ +import QUnit from 'qunit'; +import urlTypeConverter from '../../src/segment/urlType'; + +QUnit.module('urlType - urlTypeConverter'); + +QUnit.test('returns correct object if given baseUrl only', function(assert) { + assert.deepEqual(urlTypeConverter({ baseUrl: 'http://example.com' }), { + resolvedUri: 'http://example.com', + uri: '' + }); +}); + +QUnit.test('returns correct object if given baseUrl and source', function(assert) { + assert.deepEqual(urlTypeConverter({ + baseUrl: 'http://example.com', + source: 'init.fmp4' + }), { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4' + }); +}); + +QUnit.test('returns correct object if given baseUrl, source and range', function(assert) { + assert.deepEqual(urlTypeConverter({ + baseUrl: 'http://example.com', + source: 'init.fmp4', + range: '101-105' + }), { + resolvedUri: 'http://example.com/init.fmp4', + uri: 'init.fmp4', + byterange: { + offset: 101, + length: 4 + } + }); +}); + +QUnit.test('returns correct object if given baseUrl and range', function(assert) { + assert.deepEqual(urlTypeConverter({ + baseUrl: 'http://example.com', + range: '101-105' + }), { + resolvedUri: 'http://example.com', + uri: '', + byterange: { + offset: 101, + length: 4 + } + }); +}); diff --git a/test/toPlaylists.test.js b/test/toPlaylists.test.js index 1f3726df..5e30f410 100644 --- a/test/toPlaylists.test.js +++ b/test/toPlaylists.test.js @@ -1,7 +1,6 @@ import { toPlaylists } from '../src/toPlaylists'; -import errors from '../src/errors'; import QUnit from 'qunit'; QUnit.module('toPlaylists'); @@ -37,24 +36,71 @@ QUnit.test('pretty simple', function(assert) { QUnit.test('segment base', function(assert) { const representations = [{ - attributes: {}, + attributes: { baseUrl: 'http://example.com/' }, segmentInfo: { base: true } }]; - assert.throws(() => toPlaylists(representations), - new RegExp(errors.UNSUPPORTED_SEGMENTATION_TYPE)); + const playlists = [{ + attributes: { baseUrl: 'http://example.com/' }, + segments: [{ + map: { + resolvedUri: 'http://example.com/', + uri: '' + }, + resolvedUri: 'http://example.com/', + uri: 'http://example.com/' + }] + }]; + + assert.deepEqual(toPlaylists(representations), playlists); }); QUnit.test('segment list', function(assert) { const representations = [{ - attributes: {}, + attributes: { + baseUrl: 'http://example.com/', + duration: 10, + sourceDuration: 11 + }, segmentInfo: { - list: true + list: { + segmentUrls: [{ + media: '1.fmp4' + }, { + media: '2.fmp4' + }] + } } }]; - assert.throws(() => toPlaylists(representations), - new RegExp(errors.UNSUPPORTED_SEGMENTATION_TYPE)); + const playlists = [{ + attributes: { + baseUrl: 'http://example.com/', + duration: 10, + sourceDuration: 11 + }, + segments: [{ + duration: 10, + map: { + resolvedUri: 'http://example.com/', + uri: '' + }, + resolvedUri: 'http://example.com/1.fmp4', + timeline: 0, + uri: '1.fmp4' + }, { + duration: 1, + map: { + resolvedUri: 'http://example.com/', + uri: '' + }, + resolvedUri: 'http://example.com/2.fmp4', + timeline: 0, + uri: '2.fmp4' + }] + }]; + + assert.deepEqual(toPlaylists(representations), playlists); });