diff --git a/src/parser.js b/src/parser.js index b5d39f0..417e8d8 100644 --- a/src/parser.js +++ b/src/parser.js @@ -62,6 +62,10 @@ export default class Parser extends Stream { discontinuityStarts: [], segments: [] }; + // keep track of the last seen segment's byte range end, as segments are not required + // to provide the offset, in which case it defaults to the next byte after the + // previous segment + let lastByterangeEnd = 0; // update the manifest with the m3u8 entry from the parse stream this.parseStream.on('data', function(entry) { @@ -89,16 +93,24 @@ export default class Parser extends Stream { byterange.length = entry.length; if (!('offset' in entry)) { - this.trigger('info', { - message: 'defaulting offset to zero' - }); - entry.offset = 0; + /* + * From the latest spec (as of this writing): + * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2 + * + * Same text since EXT-X-BYTERANGE's introduction in draft 7: + * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1) + * + * "If o [offset] is not present, the sub-range begins at the next byte + * following the sub-range of the previous media segment." + */ + entry.offset = lastByterangeEnd; } } if ('offset' in entry) { currentUri.byterange = byterange; byterange.offset = entry.offset; } + lastByterangeEnd = byterange.offset + byterange.length; }, endlist() { this.manifest.endList = true; diff --git a/test/fixtures/m3u8/byteRange.json b/test/fixtures/m3u8/byteRange.json index b2d4c0c..47c7582 100644 --- a/test/fixtures/m3u8/byteRange.json +++ b/test/fixtures/m3u8/byteRange.json @@ -20,7 +20,7 @@ { "byterange": { "length": 713084, - "offset": 0 + "offset": 1110328 }, "duration": 10, "timeline": 0, diff --git a/test/m3u8.test.js b/test/m3u8.test.js index a377c7c..d4d7d04 100644 --- a/test/m3u8.test.js +++ b/test/m3u8.test.js @@ -1334,6 +1334,62 @@ QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if KEYFORMAT assert.notOk(parser.manifest.contentProtection, 'contentProtection not added'); }); +QUnit.test('byterange offset defaults to next byte', function(assert) { + const parser = new Parser(); + + const manifest = [ + '#EXTM3U', + '#EXTINF:5,', + '#EXT-X-BYTERANGE:10@5', + 'segment.ts', + '#EXTINF:5,', + '#EXT-X-BYTERANGE:20', + 'segment.ts', + '#EXTINF:5,', + '#EXT-X-BYTERANGE:30', + 'segment.ts', + '#EXTINF:5,', + 'segment2.ts', + '#EXT-X-BYTERANGE:15@100', + 'segment.ts', + '#EXT-X-BYTERANGE:17', + 'segment.ts', + '#EXT-X-ENDLIST' + ].join('\n'); + + parser.push(manifest); + + assert.deepEqual( + parser.manifest.segments[0].byterange, + { length: 10, offset: 5 }, + 'first segment has correct byterange' + ); + assert.deepEqual( + parser.manifest.segments[1].byterange, + { length: 20, offset: 15 }, + 'second segment has correct byterange' + ); + assert.deepEqual( + parser.manifest.segments[2].byterange, + { length: 30, offset: 35 }, + 'third segment has correct byterange' + ); + assert.notOk(parser.manifest.segments[3].byterange, 'fourth segment has no byterange'); + assert.deepEqual( + parser.manifest.segments[4].byterange, + { length: 15, offset: 100 }, + 'fifth segment has correct byterange' + ); + // not tested is a segment with no offset coming after a segment that isn't a sub range, + // as the spec requires that a byterange without an offset must follow a segment that + // is a sub range of the same media resource + assert.deepEqual( + parser.manifest.segments[5].byterange, + { length: 17, offset: 115 }, + 'sixth segment has correct byterange' + ); +}); + QUnit.module('m3u8s'); QUnit.test('parses static manifests as expected', function(assert) {