From 6cf57fd8539de27626c06be5c28e64a40499652e Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Wed, 8 Nov 2017 16:07:33 -0800 Subject: [PATCH] Do not assume same timescale in manifest and media According to the DASH spec, the timescale in the manifest need not match the timescale in the media. Therefore, we should be applying scaled presentationTimeOffsets in segment index parsers, since the two scales might differ. Issue #1098 Change-Id: Ic191d1bba399b30a656ab5060d7bb226e659b79a --- lib/dash/segment_base.js | 16 +++---- lib/media/mp4_segment_index_parser.js | 19 +++++---- lib/media/webm_segment_index_parser.js | 34 ++++++++------- test/dash/dash_parser_segment_base_unit.js | 45 ++++++++++++++++++++ test/media/mp4_segment_index_parser_unit.js | 20 +++++++++ test/media/webm_segment_index_parser_unit.js | 20 +++++++++ 6 files changed, 122 insertions(+), 32 deletions(-) diff --git a/lib/dash/segment_base.js b/lib/dash/segment_base.js index e354a98a63..5bbf09868a 100644 --- a/lib/dash/segment_base.js +++ b/lib/dash/segment_base.js @@ -105,7 +105,7 @@ shaka.dash.SegmentBase.createStream = function(context, requestInitSegment) { var init = SegmentBase.createInitSegment(context, SegmentBase.fromInheritance_); var index = SegmentBase.createSegmentIndex_( - context, requestInitSegment, init, unscaledPresentationTimeOffset); + context, requestInitSegment, init, scaledPresentationTimeOffset); return { createSegmentIndex: index.createSegmentIndex, @@ -127,12 +127,12 @@ shaka.dash.SegmentBase.createStream = function(context, requestInitSegment) { * @param {number} startByte * @param {?number} endByte * @param {string} containerType - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @return {shaka.dash.DashParser.SegmentIndexFunctions} */ shaka.dash.SegmentBase.createSegmentIndexFromUris = function( context, requestInitSegment, init, uris, - startByte, endByte, containerType, unscaledPresentationTimeOffset) { + startByte, endByte, containerType, scaledPresentationTimeOffset) { var presentationTimeline = context.presentationTimeline; var fitLast = !context.dynamic || !context.periodInfo.isLastPeriod; var periodStartTime = context.periodInfo.start; @@ -157,12 +157,12 @@ shaka.dash.SegmentBase.createSegmentIndexFromUris = function( if (containerType == 'mp4') { references = shaka.media.Mp4SegmentIndexParser( - indexData, startByte, uris, unscaledPresentationTimeOffset); + indexData, startByte, uris, scaledPresentationTimeOffset); } else { goog.asserts.assert(initData, 'WebM requires init data'); var parser = new shaka.media.WebmSegmentIndexParser(); references = parser.parse(indexData, initData, uris, - unscaledPresentationTimeOffset); + scaledPresentationTimeOffset); } presentationTimeline.notifySegments(periodStartTime, references); @@ -210,13 +210,13 @@ shaka.dash.SegmentBase.fromInheritance_ = function(frame) { * @param {shaka.dash.DashParser.Context} context * @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment * @param {shaka.media.InitSegmentReference} init - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @return {shaka.dash.DashParser.SegmentIndexFunctions} * @throws shaka.util.Error When there is a parsing error. * @private */ shaka.dash.SegmentBase.createSegmentIndex_ = function( - context, requestInitSegment, init, unscaledPresentationTimeOffset) { + context, requestInitSegment, init, scaledPresentationTimeOffset) { var MpdUtils = shaka.dash.MpdUtils; var SegmentBase = shaka.dash.SegmentBase; var XmlUtils = shaka.util.XmlUtils; @@ -280,5 +280,5 @@ shaka.dash.SegmentBase.createSegmentIndex_ = function( return shaka.dash.SegmentBase.createSegmentIndexFromUris( context, requestInitSegment, init, indexUris, indexRange.start, - indexRange.end, containerType, unscaledPresentationTimeOffset); + indexRange.end, containerType, scaledPresentationTimeOffset); }; diff --git a/lib/media/mp4_segment_index_parser.js b/lib/media/mp4_segment_index_parser.js index a2258c4add..f675a727c8 100644 --- a/lib/media/mp4_segment_index_parser.js +++ b/lib/media/mp4_segment_index_parser.js @@ -31,12 +31,12 @@ goog.require('shaka.util.Mp4Parser'); * the MP4 container. * @param {!Array.} uris The possible locations of the MP4 file that * contains the segments. - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @return {!Array.} * @throws {shaka.util.Error} */ shaka.media.Mp4SegmentIndexParser = function( - sidxData, sidxOffset, uris, unscaledPresentationTimeOffset) { + sidxData, sidxOffset, uris, scaledPresentationTimeOffset) { var Mp4SegmentIndexParser = shaka.media.Mp4SegmentIndexParser; @@ -46,7 +46,7 @@ shaka.media.Mp4SegmentIndexParser = function( .fullBox('sidx', function(box) { references = Mp4SegmentIndexParser.parseSIDX_( sidxOffset, - unscaledPresentationTimeOffset, + scaledPresentationTimeOffset, uris, box); }); @@ -71,7 +71,7 @@ shaka.media.Mp4SegmentIndexParser = function( * Parse a SIDX box from the given reader. * * @param {number} sidxOffset - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @param {!Array.} uris The possible locations of the MP4 file that * contains the segments. * @param {!shaka.util.Mp4Parser.ParsedBox} box @@ -80,7 +80,7 @@ shaka.media.Mp4SegmentIndexParser = function( */ shaka.media.Mp4SegmentIndexParser.parseSIDX_ = function( sidxOffset, - unscaledPresentationTimeOffset, + scaledPresentationTimeOffset, uris, box) { @@ -122,8 +122,7 @@ shaka.media.Mp4SegmentIndexParser.parseSIDX_ = function( var referenceCount = box.reader.readUint16(); // Substract the presentation time offset - var unscaledStartTime = - earliestPresentationTime - unscaledPresentationTimeOffset; + var unscaledStartTime = earliestPresentationTime; var startByte = sidxOffset + box.size + firstOffset; for (var i = 0; i < referenceCount; i++) { @@ -151,8 +150,10 @@ shaka.media.Mp4SegmentIndexParser.parseSIDX_ = function( references.push( new shaka.media.SegmentReference( references.length, - unscaledStartTime / timescale, - (unscaledStartTime + subsegmentDuration) / timescale, + (unscaledStartTime / timescale) - + scaledPresentationTimeOffset, + ((unscaledStartTime + subsegmentDuration) / timescale) - + scaledPresentationTimeOffset, function() { return uris; }, startByte, startByte + referenceSize - 1)); diff --git a/lib/media/webm_segment_index_parser.js b/lib/media/webm_segment_index_parser.js index 4a3ac0a1ba..375d11fe08 100644 --- a/lib/media/webm_segment_index_parser.js +++ b/lib/media/webm_segment_index_parser.js @@ -81,7 +81,7 @@ shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1; * @param {!ArrayBuffer} initData The WebM container's headers. * @param {!Array.} uris The possible locations of the WebM file that * contains the segments. - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @return {!Array.} * @throws {shaka.util.Error} @@ -89,7 +89,7 @@ shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1; * @see http://www.webmproject.org/docs/container/ */ shaka.media.WebmSegmentIndexParser.prototype.parse = function( - cuesData, initData, uris, unscaledPresentationTimeOffset) { + cuesData, initData, uris, scaledPresentationTimeOffset) { var tuple = this.parseWebmContainer_(initData); var parser = new shaka.util.EbmlParser(new DataView(cuesData)); var cuesElement = parser.parseElement(); @@ -103,7 +103,7 @@ shaka.media.WebmSegmentIndexParser.prototype.parse = function( return this.parseCues_( cuesElement, tuple.segmentOffset, tuple.timecodeScale, tuple.duration, - uris, unscaledPresentationTimeOffset); + uris, scaledPresentationTimeOffset); }; @@ -242,21 +242,21 @@ shaka.media.WebmSegmentIndexParser.prototype.parseInfo_ = function( * @param {number} timecodeScale * @param {number} duration * @param {!Array.} uris - * @param {number} unscaledPresentationTimeOffset + * @param {number} scaledPresentationTimeOffset * @return {!Array.} * @throws {shaka.util.Error} * @private */ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( cuesElement, segmentOffset, timecodeScale, duration, uris, - unscaledPresentationTimeOffset) { + scaledPresentationTimeOffset) { var references = []; var getUris = function() { return uris; }; var parser = cuesElement.createParser(); - var lastTime = -1; - var lastOffset = -1; + var lastTime = null; + var lastOffset = null; while (parser.hasMoreData()) { var elem = parser.parseElement(); @@ -270,17 +270,17 @@ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( } // Substract presentation time offset from unscaled time - var currentTime = timecodeScale * - (tuple.unscaledTime - unscaledPresentationTimeOffset); + var currentTime = timecodeScale * tuple.unscaledTime; var currentOffset = segmentOffset + tuple.relativeOffset; - if (lastTime >= 0) { - goog.asserts.assert(lastOffset >= 0, 'last offset cannot be 0'); + if (lastTime != null) { + goog.asserts.assert(lastOffset != null, 'last offset cannot be null'); references.push( new shaka.media.SegmentReference( references.length, - lastTime, currentTime, + lastTime - scaledPresentationTimeOffset, + currentTime - scaledPresentationTimeOffset, getUris, lastOffset, currentOffset - 1)); } @@ -289,12 +289,16 @@ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( lastOffset = currentOffset; } - if (lastTime >= 0) { - goog.asserts.assert(lastOffset >= 0, 'last offset cannot be 0'); + if (lastTime != null) { + goog.asserts.assert(lastOffset != null, 'last offset cannot be null'); references.push( new shaka.media.SegmentReference( - references.length, lastTime, duration, getUris, lastOffset, null)); + references.length, + lastTime - scaledPresentationTimeOffset, + duration - scaledPresentationTimeOffset, + getUris, + lastOffset, null)); } return references; diff --git a/test/dash/dash_parser_segment_base_unit.js b/test/dash/dash_parser_segment_base_unit.js index 8b71584620..e04b291252 100644 --- a/test/dash/dash_parser_segment_base_unit.js +++ b/test/dash/dash_parser_segment_base_unit.js @@ -25,6 +25,16 @@ describe('DashParser SegmentBase', function() { var parser; /** @type {shakaExtern.ManifestParser.PlayerInterface} */ var playerInterface; + /** @const {string} */ + var indexSegmentUri = '/base/test/test/assets/index-segment.mp4'; + /** @type {ArrayBuffer} */ + var indexSegment; + + beforeAll(function(done) { + shaka.test.Util.fetch(indexSegmentUri).then(function(data) { + indexSegment = data; + }).catch(fail).then(done); + }); beforeEach(function() { fakeNetEngine = new shaka.test.FakeNetworkingEngine(); @@ -252,6 +262,41 @@ describe('DashParser SegmentBase', function() { .then(done); }); + it('does not assume the same timescale as media', function(done) { + var source = [ + '', + ' ', + ' ', + ' ', + ' http://example.com/index.mp4', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + fakeNetEngine.setResponseMap({ + 'dummy://foo': shaka.util.StringUtils.toUTF8(source), + 'http://example.com/index.mp4': indexSegment + }); + + var video; + parser.start('dummy://foo', playerInterface) + .then(function(manifest) { + video = manifest.periods[0].variants[0].video; + return video.createSegmentIndex(); // real data, should succeed + }) + .then(function() { + var reference = video.getSegmentReference(0); + expect(reference.startTime).toEqual(-2); + expect(reference.endTime).toEqual(10); + }) + .catch(fail) + .then(done); + }); + describe('fails for', function() { it('unsupported container', function(done) { var source = [ diff --git a/test/media/mp4_segment_index_parser_unit.js b/test/media/mp4_segment_index_parser_unit.js index 2a5ce78c31..b10c9b13f8 100644 --- a/test/media/mp4_segment_index_parser_unit.js +++ b/test/media/mp4_segment_index_parser_unit.js @@ -68,4 +68,24 @@ describe('Mp4SegmentIndexParser', function() { expect(result[i].endByte).toBe(references[i].endByte); } }); + + it('takes a scaled presentationTimeOffset in seconds', function() { + var result = shaka.media.Mp4SegmentIndexParser(indexSegment, 0, [], 2); + var references = + [ + {startTime: -2, endTime: 10}, + {startTime: 10, endTime: 22}, + {startTime: 22, endTime: 34}, + {startTime: 34, endTime: 46}, + {startTime: 46, endTime: 58} + ]; + + expect(result).toBeTruthy(); + expect(result.length).toBe(references.length); + for (var i = 0; i < result.length; i++) { + expect(result[i].position).toBe(i); + expect(result[i].startTime).toBe(references[i].startTime); + expect(result[i].endTime).toBe(references[i].endTime); + } + }); }); diff --git a/test/media/webm_segment_index_parser_unit.js b/test/media/webm_segment_index_parser_unit.js index 6f3da487c2..058c808048 100644 --- a/test/media/webm_segment_index_parser_unit.js +++ b/test/media/webm_segment_index_parser_unit.js @@ -82,4 +82,24 @@ describe('WebmSegmentIndexParser', function() { expect(result[i].endByte).toBe(references[i].endByte); } }); + + it('takes a scaled presentationTimeOffset in seconds', function() { + var result = parser.parse(indexSegment, initSegment, [], 2); + var references = + [ + {startTime: -2, endTime: 10}, + {startTime: 10, endTime: 22}, + {startTime: 22, endTime: 34}, + {startTime: 34, endTime: 46}, + {startTime: 46, endTime: 58} + ]; + + expect(result).toBeTruthy(); + expect(result.length).toBe(references.length); + for (var i = 0; i < result.length; i++) { + expect(result[i].position).toBe(i); + expect(result[i].startTime).toBe(references[i].startTime); + expect(result[i].endTime).toBe(references[i].endTime); + } + }); });