diff --git a/src/parse-stream.js b/src/parse-stream.js index 5836cab..ee63cc9 100644 --- a/src/parse-stream.js +++ b/src/parse-stream.js @@ -5,6 +5,23 @@ import Stream from '@videojs/vhs-utils/es/stream.js'; const TAB = String.fromCharCode(0x09); +const parseByterange = function(byterangeString) { + // optionally match and capture 0+ digits before `@` + // optionally match and capture 0+ digits after `@` + const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || ''); + const result = {}; + + if (match[1]) { + result.length = parseInt(match[1], 10); + } + + if (match[2]) { + result.offset = parseInt(match[2], 10); + } + + return result; +}; + /** * "forgiving" attribute list psuedo-grammar: * attributes -> keyvalue (',' keyvalue)* @@ -222,18 +239,12 @@ export default class ParseStream extends Stream { this.trigger('data', event); return; } - match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(newLine); + match = (/^#EXT-X-BYTERANGE:?(.*)?$/).exec(newLine); if (match) { - event = { + event = Object.assign(parseByterange(match[1]), { type: 'tag', tagType: 'byterange' - }; - if (match[1]) { - event.length = parseInt(match[1], 10); - } - if (match[2]) { - event.offset = parseInt(match[2], 10); - } + }); this.trigger('data', event); return; } @@ -263,15 +274,7 @@ export default class ParseStream extends Stream { event.uri = attributes.URI; } if (attributes.BYTERANGE) { - const [length, offset] = attributes.BYTERANGE.split('@'); - - event.byterange = {}; - if (length) { - event.byterange.length = parseInt(length, 10); - } - if (offset) { - event.byterange.offset = parseInt(offset, 10); - } + event.byterange = parseByterange(attributes.BYTERANGE); } } @@ -472,15 +475,7 @@ export default class ParseStream extends Stream { }); if (event.attributes.hasOwnProperty('BYTERANGE')) { - const [length, offset] = event.attributes.BYTERANGE.split('@'); - - event.byterange = {}; - if (length) { - event.byterange.length = parseInt(length, 10); - } - if (offset) { - event.byterange.offset = parseInt(offset, 10); - } + event.attributes.byterange = parseByterange(event.attributes.BYTERANGE); } this.trigger('data', event); @@ -535,6 +530,11 @@ export default class ParseStream extends Stream { ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function(key) { if (event.attributes.hasOwnProperty(key)) { event.attributes[key] = parseInt(event.attributes[key], 10); + + const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset'; + + event.attributes.byterange = event.attributes.byterange || {}; + event.attributes.byterange[subkey] = event.attributes[key]; } }); diff --git a/src/parser.js b/src/parser.js index 2960a0c..221ee6e 100644 --- a/src/parser.js +++ b/src/parser.js @@ -92,6 +92,7 @@ export default class Parser extends Stream { let currentMap; // if specified, the active decryption key let key; + let hasParts = false; const noop = function() {}; const defaultMediaGroups = { 'AUDIO': {}, @@ -115,6 +116,29 @@ export default class Parser extends Stream { // to provide the offset, in which case it defaults to the next byte after the // previous segment let lastByterangeEnd = 0; + // keep track of the last seen part's byte range end. + let lastPartByterangeEnd = 0; + + this.on('end', () => { + // only add preloadSegment if we don't yet have a uri for it. + // and we actually have parts/preloadHints + if (currentUri.uri || (!currentUri.parts && !currentUri.preloadHints)) { + return; + } + if (!currentUri.map && currentMap) { + currentUri.map = currentMap; + } + + if (!currentUri.key && key) { + currentUri.key = key; + } + + if (!currentUri.timeline && typeof currentTimeline === 'number') { + currentUri.timeline = currentTimeline; + } + + this.manifest.preloadSegment = currentUri; + }); // update the manifest with the m3u8 entry from the parse stream this.parseStream.on('data', function(entry) { @@ -441,8 +465,22 @@ export default class Parser extends Stream { } }, 'part'() { - this.manifest.parts = this.manifest.parts || []; - this.manifest.parts.push(entry.attributes); + hasParts = true; + // parts are always specifed before a segment + const segmentIndex = this.manifest.segments.length; + + currentUri.parts = currentUri.parts || []; + currentUri.parts.push(entry.attributes); + + if (entry.attributes.byterange) { + const byterange = entry.attributes.byterange; + + if (!byterange.hasOwnProperty('offset')) { + byterange.offset = lastPartByterangeEnd; + } + lastPartByterangeEnd = byterange.offset + byterange.length; + } + const missingAttributes = []; ['URI', 'DURATION'].forEach(function(k) { @@ -452,10 +490,10 @@ export default class Parser extends Stream { }); if (missingAttributes.length) { - const index = this.manifest.parts.length - 1; + const partIndex = currentUri.parts.length - 1; this.trigger('warn', { - message: `#EXT-X-PART #${index} lacks required attribute(s): ${missingAttributes.join(', ')}` + message: `#EXT-X-PART #${partIndex} for segment #${segmentIndex} lacks required attribute(s): ${missingAttributes.join(', ')}` }); } @@ -489,8 +527,25 @@ export default class Parser extends Stream { } }, 'preload-hint'() { - this.manifest.preloadHints = this.manifest.preloadHints || []; - this.manifest.preloadHints.push(entry.attributes); + // parts are always specifed before a segment + const segmentIndex = this.manifest.segments.length; + + currentUri.preloadHints = currentUri.preloadHints || []; + currentUri.preloadHints.push(entry.attributes); + + if (entry.attributes.byterange) { + const byterange = entry.attributes.byterange; + + if (!byterange.hasOwnProperty('offset')) { + byterange.offset = 0; + if (entry.attributes.TYPE && entry.attributes.TYPE === 'PART') { + // use last segment byterange end if we are the first part. + // otherwise use lastPartByterangeEnd + byterange.offset = lastPartByterangeEnd; + lastPartByterangeEnd = byterange.offset + byterange.length; + } + } + } const missingAttributes = []; @@ -499,14 +554,28 @@ export default class Parser extends Stream { missingAttributes.push(k); } }); + const index = currentUri.preloadHints.length - 1; if (missingAttributes.length) { - const index = this.manifest.preloadHints.length - 1; this.trigger('warn', { - message: `#EXT-X-PRELOAD-HINT #${index} lacks required attribute(s): ${missingAttributes.join(', ')}` + message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} lacks required attribute(s): ${missingAttributes.join(', ')}` }); } + + if (entry.attributes.TYPE) { + // search through all preload hints except for the current one for + // a duplicate type. + for (let i = 0; i < currentUri.preloadHints.length - 1; i++) { + const hint = currentUri.preloadHints[i]; + + if (hint.TYPE && hint.TYPE === entry.attributes.TYPE) { + this.trigger('warn', { + message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${entry.attributes.TYPE} as preload hint #${i}` + }); + } + } + } }, 'rendition-report'() { this.manifest.renditionReports = this.manifest.renditionReports || []; @@ -521,7 +590,7 @@ export default class Parser extends Stream { } }); - if (this.manifest.parts && !entry.attributes['LAST-PART']) { + if (hasParts && !entry.attributes['LAST-PART']) { missingAttributes.push('LAST-PART'); } @@ -565,6 +634,9 @@ export default class Parser extends Stream { currentUri.map = currentMap; } + // reset the last byterange end as it needs to be 0 between parts + lastPartByterangeEnd = 0; + // prepare for the next URI currentUri = {}; }, @@ -603,6 +675,8 @@ export default class Parser extends Stream { end() { // flush any buffered input this.lineStream.push('\n'); + + this.trigger('end'); } /** * Add an additional parser for non-standard tags diff --git a/test/fixtures/integration/llhls-byte-range.js b/test/fixtures/integration/llhls-byte-range.js new file mode 100644 index 0000000..b36edb2 --- /dev/null +++ b/test/fixtures/integration/llhls-byte-range.js @@ -0,0 +1,264 @@ +module.exports = { + allowCache: true, + discontinuitySequence: 0, + discontinuityStarts: [], + mediaSequence: 0, + playlistType: 'VOD', + preloadSegment: { + preloadHints: [ + { + 'BYTERANGE-LENGTH': 2000, + 'TYPE': 'PART', + 'URI': 'filePart273.1.mp4', + 'byterange': { + length: 2000, + offset: 0 + } + }, + { + 'BYTERANGE-LENGTH': 5000, + 'BYTERANGE-START': 8355216, + 'TYPE': 'MAP', + 'URI': 'file-init.mp4', + 'byterange': { + length: 5000, + offset: 8355216 + } + }, + { + 'BYTERANGE-LENGTH': 5000, + 'TYPE': 'FOO', + 'URI': 'foo.mp4', + 'byterange': { + length: 5000, + offset: 0 + } + } + ], + timeline: 0 + }, + segments: [ + { + byterange: { + length: 587500, + offset: 0 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 587500, + offset: 522828 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 713084, + offset: 1110328 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video2.ts' + }, + { + byterange: { + length: 476580, + offset: 1823412 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 535612, + offset: 2299992 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 207176, + offset: 2835604 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 455900, + offset: 3042780 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 657248, + offset: 3498680 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 571708, + offset: 4155928 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 485040, + offset: 4727636 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 709136, + offset: 5212676 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 730004, + offset: 5921812 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 456276, + offset: 6651816 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 468684, + offset: 7108092 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 444996, + offset: 7576776 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 331444, + offset: 8021772 + }, + duration: 10, + parts: [ + { + BYTERANGE: '45553', + DURATION: 0.33334, + URI: 'hls_450k_video.part.ts', + byterange: { + length: 45553, + offset: 0 + } + }, + { + BYTERANGE: '28823@7622329', + DURATION: 0.33334, + URI: 'hls_450k_video.part.ts', + byterange: { + length: 28823, + offset: 7622329 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.part.ts', + byterange: { + length: 22444, + offset: 7651152 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.part.ts', + byterange: { + length: 22444, + offset: 7673596 + } + } + ], + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 44556, + offset: 8353216 + }, + duration: 1.4167, + parts: [ + { + BYTERANGE: '45553@8021772', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 45553, + offset: 8021772 + } + }, + { + BYTERANGE: '28823', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 28823, + offset: 8067325 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 22444, + offset: 8096148 + } + } + ], + timeline: 0, + uri: 'hls_450k_video.ts' + } + ], + targetDuration: 10, + version: 3 +}; diff --git a/test/fixtures/integration/llhls-byte-range.m3u8 b/test/fixtures/integration/llhls-byte-range.m3u8 new file mode 100644 index 0000000..15b18f7 --- /dev/null +++ b/test/fixtures/integration/llhls-byte-range.m3u8 @@ -0,0 +1,66 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXTINF:10, +#EXT-X-BYTERANGE:587500@ +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:587500@522828 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:713084 +hls_450k_video2.ts +#EXTINF:10, +#EXT-X-BYTERANGE:476580@1823412 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:535612@2299992 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:207176@2835604 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:455900@3042780 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:657248@3498680 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:571708@4155928 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:485040@4727636 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:709136@5212676 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:730004@5921812 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:456276@6651816 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:468684@7108092 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:444996@7576776 +hls_450k_video.ts +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.part.ts",BYTERANGE=45553 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.part.ts",BYTERANGE=28823@7622329 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.part.ts",BYTERANGE=22444 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.part.ts",BYTERANGE=22444 +#EXTINF:10, +#EXT-X-BYTERANGE:331444@8021772 +hls_450k_video.ts +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=45553@8021772 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=28823 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=22444 +#EXTINF:1.4167, +#EXT-X-BYTERANGE:44556@8353216 +hls_450k_video.ts +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.1.mp4",BYTERANGE-LENGTH=2000 +#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="file-init.mp4",BYTERANGE-LENGTH=5000,BYTERANGE-START=8355216 +#EXT-X-PRELOAD-HINT:TYPE=FOO,URI="foo.mp4",BYTERANGE-LENGTH=5000 diff --git a/test/fixtures/integration/llhls-delta-byte-range.js b/test/fixtures/integration/llhls-delta-byte-range.js new file mode 100644 index 0000000..c01e3aa --- /dev/null +++ b/test/fixtures/integration/llhls-delta-byte-range.js @@ -0,0 +1,161 @@ +module.exports = { + allowCache: true, + discontinuitySequence: 0, + discontinuityStarts: [], + mediaSequence: 0, + playlistType: 'VOD', + preloadSegment: { + parts: [ + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 22444, + offset: 0 + } + } + ], + preloadHints: [ + { + 'BYTERANGE-LENGTH': 2000, + 'TYPE': 'PART', + 'URI': 'filePart273.1.mp4', + 'byterange': { + length: 2000, + offset: 22444 + } + }, + { + 'BYTERANGE-LENGTH': 5000, + 'BYTERANGE-START': 8377660, + 'TYPE': 'MAP', + 'URI': 'file-init.mp4', + 'byterange': { + length: 5000, + offset: 8377660 + } + }, + { + 'BYTERANGE-LENGTH': 5000, + 'TYPE': 'FOO', + 'URI': 'foo.mp4', + 'byterange': { + length: 5000, + offset: 0 + } + } + ], + timeline: 0 + }, + segments: [ + { + byterange: { + length: 468684, + offset: 7108092 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 444996, + offset: 7576776 + }, + duration: 10, + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 331444, + offset: 8021772 + }, + duration: 10, + parts: [ + { + BYTERANGE: '45553', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 45553, + offset: 0 + } + }, + { + BYTERANGE: '28823@7622329', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 28823, + offset: 7622329 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 22444, + offset: 7651152 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 22444, + offset: 7673596 + } + } + ], + timeline: 0, + uri: 'hls_450k_video.ts' + }, + { + byterange: { + length: 44556, + offset: 8353216 + }, + duration: 1.4167, + parts: [ + { + BYTERANGE: '45553@8021772', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 45553, + offset: 8021772 + } + }, + { + BYTERANGE: '28823', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 28823, + offset: 8067325 + } + }, + { + BYTERANGE: '22444', + DURATION: 0.33334, + URI: 'hls_450k_video.ts', + byterange: { + length: 22444, + offset: 8096148 + } + } + ], + timeline: 0, + uri: 'hls_450k_video.ts' + } + ], + skip: { + 'SKIPPED-SEGMENTS': 3 + }, + targetDuration: 10, + version: 3 +}; diff --git a/test/fixtures/integration/llhls-delta-byte-range.m3u8 b/test/fixtures/integration/llhls-delta-byte-range.m3u8 new file mode 100644 index 0000000..d884ccc --- /dev/null +++ b/test/fixtures/integration/llhls-delta-byte-range.m3u8 @@ -0,0 +1,30 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXTINF:10, +#EXT-X-SKIP:SKIPPED-SEGMENTS=3 +#EXTINF:10, +#EXT-X-BYTERANGE:468684@7108092 +hls_450k_video.ts +#EXTINF:10, +#EXT-X-BYTERANGE:444996@7576776 +hls_450k_video.ts +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=45553 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=28823@7622329 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=22444 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=22444 +#EXTINF:10, +#EXT-X-BYTERANGE:331444@8021772 +hls_450k_video.ts +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=45553@8021772 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=28823 +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=22444 +#EXTINF:1.4167, +#EXT-X-BYTERANGE:44556@8353216 +hls_450k_video.ts +#EXT-X-PART:DURATION=0.33334,URI="hls_450k_video.ts",BYTERANGE=22444 +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.1.mp4",BYTERANGE-LENGTH=2000 +#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="file-init.mp4",BYTERANGE-LENGTH=5000,BYTERANGE-START=8377660 +#EXT-X-PRELOAD-HINT:TYPE=FOO,URI="foo.mp4",BYTERANGE-LENGTH=5000 diff --git a/test/fixtures/integration/llhls.js b/test/fixtures/integration/llhls.js index bddf0f6..8e82c32 100644 --- a/test/fixtures/integration/llhls.js +++ b/test/fixtures/integration/llhls.js @@ -5,10 +5,29 @@ module.exports = { discontinuitySequence: 0, discontinuityStarts: [], mediaSequence: 266, - preloadHints: [ - {TYPE: 'PART', URI: 'filePart273.3.mp4'}, - {'TYPE': 'PART', 'URI': 'filePart273.4.mp4', 'BYTERANGE-LENGTH': 10, 'BYTERANGE-START': 0} - ], + preloadSegment: { + map: {uri: 'init.mp4'}, + parts: [ + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart273.0.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart273.1.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart273.2.mp4' + } + ], + preloadHints: [ + {TYPE: 'PART', URI: 'filePart273.3.mp4'}, + {TYPE: 'MAP', URI: 'file-init.mp4'} + ], + timeline: 0 + }, renditionReports: [ {'LAST-MSN': 273, 'LAST-PART': 2, 'URI': '../1M/waitForMSN.php'}, {'LAST-MSN': 273, 'LAST-PART': 1, 'URI': '../4M/waitForMSN.php'} @@ -17,122 +36,6 @@ module.exports = { 'PART-TARGET': 0.33334 }, partTargetDuration: 0.33334, - parts: [ - { - DURATION: 0.33334, - URI: 'filePart271.0.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.1.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.2.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.3.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart271.4.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.5.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.6.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.7.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart271.8.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.9.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.10.mp4' - }, - { - BYTERANGE: '587500@522828', - DURATION: 0.33334, - URI: 'filePart271.11.mp4' - }, - { - DURATION: 0.33334, - GAP: true, - URI: 'filePart272.a.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.b.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.c.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.d.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.e.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart272.f.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.g.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.h.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.i.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.j.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.k.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.l.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart273.0.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart273.1.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart273.2.mp4' - } - ], segments: [ { dateTimeObject: new Date('2019-02-14T02:13:36.106Z'), @@ -182,7 +85,59 @@ module.exports = { uri: 'init.mp4' }, timeline: 0, - uri: 'fileSequence271.mp4' + uri: 'fileSequence271.mp4', + parts: [ + { + DURATION: 0.33334, + URI: 'filePart271.0.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.1.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.2.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.3.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart271.4.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.5.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.6.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.7.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart271.8.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.9.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.10.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.11.mp4' + } + ] }, { dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), @@ -192,7 +147,59 @@ module.exports = { uri: 'init.mp4' }, timeline: 0, - uri: 'fileSequence272.mp4' + uri: 'fileSequence272.mp4', + parts: [ + { + DURATION: 0.33334, + GAP: true, + URI: 'filePart272.a.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.b.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.c.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.d.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.e.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart272.f.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.g.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.h.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.i.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.j.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.k.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.l.mp4' + } + ] } ], serverControl: { diff --git a/test/fixtures/integration/llhls.m3u8 b/test/fixtures/integration/llhls.m3u8 index 2c9cc0d..40fe352 100644 --- a/test/fixtures/integration/llhls.m3u8 +++ b/test/fixtures/integration/llhls.m3u8 @@ -28,7 +28,7 @@ fileSequence270.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" -#EXT-X-PART:BYTERANGE=587500@522828,DURATION=0.33334,URI="filePart271.11.mp4" +#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z @@ -50,7 +50,7 @@ fileSequence272.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4" -#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4",BYTERANGE-START=0,BYTERANGE-LENGTH=10 +#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="file-init.mp4" #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2 #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1 diff --git a/test/fixtures/integration/llhlsDelta.js b/test/fixtures/integration/llhlsDelta.js index 3cd2d3a..888cf1e 100644 --- a/test/fixtures/integration/llhlsDelta.js +++ b/test/fixtures/integration/llhlsDelta.js @@ -5,10 +5,32 @@ module.exports = { discontinuitySequence: 0, discontinuityStarts: [], mediaSequence: 266, - preloadHints: [ - {TYPE: 'PART', URI: 'filePart273.4.mp4'}, - {TYPE: 'PART', URI: 'filePart273.5.mp4'} - ], + preloadSegment: { + timeline: 0, + preloadHints: [ + {TYPE: 'PART', URI: 'filePart273.4.mp4'}, + {TYPE: 'MAP', URI: 'file-init.mp4'} + ], + parts: [ + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart273.0.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart273.1.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart273.2.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart273.3.mp4' + } + ] + }, renditionReports: [ {'LAST-MSN': 273, 'LAST-PART': 3, 'URI': '../1M/waitForMSN.php'}, {'LAST-MSN': 273, 'LAST-PART': 3, 'URI': '../4M/waitForMSN.php'} @@ -17,126 +39,6 @@ module.exports = { 'PART-TARGET': 0.33334 }, partTargetDuration: 0.33334, - parts: [ - { - DURATION: 0.33334, - URI: 'filePart271.0.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.1.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.2.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.3.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart271.4.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.5.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.6.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.7.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart271.8.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.9.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart271.10.mp4' - }, - { - BYTERANGE: '587500@522828', - DURATION: 0.33334, - URI: 'filePart271.11.mp4' - }, - { - DURATION: 0.33334, - GAP: true, - URI: 'filePart272.a.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.b.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.c.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.d.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.e.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart272.f.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.g.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.h.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.i.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.j.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.k.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart272.l.mp4' - }, - { - DURATION: 0.33334, - INDEPENDENT: true, - URI: 'filePart273.0.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart273.1.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart273.2.mp4' - }, - { - DURATION: 0.33334, - URI: 'filePart273.3.mp4' - } - ], segments: [ { duration: 4.00008, @@ -151,14 +53,118 @@ module.exports = { { duration: 4.00008, timeline: 0, - uri: 'fileSequence271.mp4' + uri: 'fileSequence271.mp4', + parts: [ + { + DURATION: 0.33334, + URI: 'filePart271.0.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.1.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.2.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.3.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart271.4.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.5.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.6.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.7.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart271.8.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.9.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.10.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart271.11.mp4' + } + ] }, { dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), dateTimeString: '2019-02-14T02:14:00.106Z', duration: 4.00008, timeline: 0, - uri: 'fileSequence272.mp4' + uri: 'fileSequence272.mp4', + parts: [ + { + DURATION: 0.33334, + GAP: true, + URI: 'filePart272.a.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.b.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.c.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.d.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.e.mp4' + }, + { + DURATION: 0.33334, + INDEPENDENT: true, + URI: 'filePart272.f.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.g.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.h.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.i.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.j.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.k.mp4' + }, + { + DURATION: 0.33334, + URI: 'filePart272.l.mp4' + } + ] } ], skip: { diff --git a/test/fixtures/integration/llhlsDelta.m3u8 b/test/fixtures/integration/llhlsDelta.m3u8 index a95ee5d..2aedcdb 100644 --- a/test/fixtures/integration/llhlsDelta.m3u8 +++ b/test/fixtures/integration/llhlsDelta.m3u8 @@ -21,7 +21,7 @@ fileSequence270.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" -#EXT-X-PART:BYTERANGE=587500@522828,DURATION=0.33334,URI="filePart271.11.mp4" +#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z @@ -44,7 +44,7 @@ fileSequence272.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart273.3.mp4" #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4" -#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.5.mp4" +#EXT-X-PRELOAD-HINT:TYPE=MAP,URI="file-init.mp4" #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 diff --git a/test/parser.test.js b/test/parser.test.js index ee4a990..3977785 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -6,6 +6,7 @@ import {Parser} from '../src'; QUnit.module('m3u8s', function(hooks) { hooks.beforeEach(function() { this.parser = new Parser(); + QUnit.dump.maxDepth = 8; }); QUnit.module('general'); @@ -492,9 +493,9 @@ QUnit.module('m3u8s', function(hooks) { this.parser.end(); const warnings = [ - '#EXT-X-PART #0 lacks required attribute(s): URI', - '#EXT-X-PART #1 lacks required attribute(s): DURATION', - '#EXT-X-PART #2 lacks required attribute(s): URI, DURATION' + '#EXT-X-PART #0 for segment #0 lacks required attribute(s): URI', + '#EXT-X-PART #1 for segment #0 lacks required attribute(s): DURATION', + '#EXT-X-PART #2 for segment #0 lacks required attribute(s): URI, DURATION' ]; assert.deepEqual( @@ -526,9 +527,40 @@ QUnit.module('m3u8s', function(hooks) { this.parser.end(); const warnings = [ - '#EXT-X-PRELOAD-HINT #0 lacks required attribute(s): URI', - '#EXT-X-PRELOAD-HINT #1 lacks required attribute(s): TYPE', - '#EXT-X-PRELOAD-HINT #2 lacks required attribute(s): TYPE, URI' + '#EXT-X-PRELOAD-HINT #0 for segment #0 lacks required attribute(s): URI', + '#EXT-X-PRELOAD-HINT #1 for segment #0 lacks required attribute(s): TYPE', + '#EXT-X-PRELOAD-HINT #2 for segment #0 lacks required attribute(s): TYPE, URI' + ]; + + assert.deepEqual( + this.warnings, + warnings, + 'warnings as expected' + ); + + assert.deepEqual( + this.infos, + [], + 'info as expected' + ); + }); + + QUnit.test('warns when we get #EXT-X-PRELOAD-HINT with the same TYPE', function(assert) { + this.parser.push([ + '#EXT-X-VERSION:3', + '#EXT-X-MEDIA-SEQUENCE:0', + '#EXT-X-DISCONTINUITY-SEQUENCE:0', + '#EXT-X-TARGETDURATION:10', + '#EXT-X-PRELOAD-HINT:TYPE=foo,URI=foo1', + '#EXT-X-PRELOAD-HINT:TYPE=foo,URI=foo2', + '#EXTINF:10,', + 'media-00001.ts', + '#EXT-X-ENDLIST' + ].join('\n')); + this.parser.end(); + + const warnings = [ + '#EXT-X-PRELOAD-HINT #1 for segment #0 has the same TYPE foo as preload hint #0' ]; assert.deepEqual(