diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index fe9dd16e7d..f65d21b774 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1216,6 +1216,54 @@ shaka.hls.HlsParser.prototype.createSegments_ = }; +/** + * Try to fetch a partial segment, and fall back to a full segment if we have + * to. + * + * @param {!shaka.media.SegmentReference} segmentRef + * @return {!Promise.} + * @throws {shaka.util.Error} + * @private + */ +shaka.hls.HlsParser.prototype.fetchPartialSegment_ = function(segmentRef) { + var networkingEngine = this.playerInterface_.networkingEngine; + var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + var request = shaka.net.NetworkingEngine.makeRequest( + segmentRef.getUris(), this.config_.retryParameters); + + // Try to avoid fetching the entire segment, which can be quite large. + var partialSegmentHeaders = {}; + var startByte = segmentRef.startByte; + var partialEndByte = startByte + shaka.hls.HlsParser.PARTIAL_SEGMENT_SIZE_; + partialSegmentHeaders['Range'] = 'bytes=' + startByte + '-' + partialEndByte; + + // Prepare a fallback to the entire segment. + var fullSegmentHeaders = {}; + if ((startByte != 0) || (segmentRef.endByte != null)) { + var range = 'bytes=' + startByte + '-'; + if (segmentRef.endByte != null) range += segmentRef.endByte; + + fullSegmentHeaders['Range'] = range; + } + + // Try a partial request first. + request.headers = partialSegmentHeaders; + return networkingEngine.request(requestType, request).catch(function(error) { + // The partial request may fail for a number of reasons. + // Some servers do not support Range requests, and others do not support the + // OPTIONS request which must be made before any cross-origin Range request. + // Since this fallback is expensive, warn the app developer. + shaka.log.alwaysWarn('Unable to fetch a partial HLS segment! ' + + 'Falling back to a full segment request, ' + + 'which is expensive! Your server should support ' + + 'Range requests and CORS preflights.', + request.uris[0]); + request.headers = fullSegmentHeaders; + return networkingEngine.request(requestType, request); + }); +}; + + /** * Gets start time of a segment from the existing manifest (if possible) or by * downloading it and parsing it otherwise. @@ -1249,37 +1297,25 @@ shaka.hls.HlsParser.prototype.getStartTime_ = shaka.log.debug('Unable to find segment start time in previous manifest!'); } - // TODO: try fetching part of the segment, to speed up start time deduction shaka.log.v1('Fetching segment to find start time'); - var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - var request = shaka.net.NetworkingEngine.makeRequest( - segmentRef.getUris(), this.config_.retryParameters); - - if ((segmentRef.startByte != 0) || (segmentRef.endByte != null)) { - var range = 'bytes=' + segmentRef.startByte + '-'; - if (segmentRef.endByte != null) range += segmentRef.endByte; - request.headers['Range'] = range; - } - - return this.playerInterface_.networkingEngine.request(requestType, request) - .then(function(response) { - if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') { - return this.getStartTimeFromMp4Segment_(response.data); - } else if (mimeType == 'video/mp2t') { - return this.getStartTimeFromTsSegment_(response.data); - } else if (mimeType == 'application/mp4' || - mimeType.indexOf('text/') == 0) { - return this.getStartTimeFromTextSegment_( - mimeType, codecs, response.data); - } else { - // TODO: Parse WebM? - // TODO: Parse raw AAC? - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME); - } - }.bind(this)); + return this.fetchPartialSegment_(segmentRef).then(function(response) { + if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') { + return this.getStartTimeFromMp4Segment_(response.data); + } else if (mimeType == 'video/mp2t') { + return this.getStartTimeFromTsSegment_(response.data); + } else if (mimeType == 'application/mp4' || + mimeType.indexOf('text/') == 0) { + return this.getStartTimeFromTextSegment_( + mimeType, codecs, response.data); + } else { + // TODO: Parse WebM? + // TODO: Parse raw AAC? + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME); + } + }.bind(this)); }; @@ -1307,6 +1343,7 @@ shaka.hls.HlsParser.prototype.getStartTimeFromMp4Segment_ = function(data) { box.reader.readUint64(); startTime = baseTime / shaka.hls.HlsParser.TS_TIMESCALE_; parsed = true; + box.parser.stop(); }).parse(data); if (!parsed) { @@ -1930,6 +1967,18 @@ shaka.hls.HlsParser.TS_TIMESCALE_ = 90000; // TODO: Consider extracting this from the MP4 instead of assuming a // TS-compatible timescale in fMP4 HLS content. + +/** + * The amount of data from the start of a segment we will try to fetch when we + * need to know the segment start time. This allows us to avoid fetching the + * entire segment in many cases. + * + * @const {number} + * @private + */ +shaka.hls.HlsParser.PARTIAL_SEGMENT_SIZE_ = 1024; + + shaka.media.ManifestParser.registerParserByExtension( 'm3u8', shaka.hls.HlsParser); shaka.media.ManifestParser.registerParserByMime( diff --git a/lib/media/mp4_segment_index_parser.js b/lib/media/mp4_segment_index_parser.js index a2258c4add..d2a9023abb 100644 --- a/lib/media/mp4_segment_index_parser.js +++ b/lib/media/mp4_segment_index_parser.js @@ -161,5 +161,6 @@ shaka.media.Mp4SegmentIndexParser.parseSIDX_ = function( startByte += referenceSize; } + box.parser.stop(); return references; }; diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 8930e7e623..6159ae49da 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -1657,6 +1657,8 @@ shaka.media.StreamingEngine.prototype.parseEMSG_ = function( var event = new shaka.util.FakeEvent('emsg', {'detail': emsg}); this.playerInterface_.onEvent(event); } + + box.parser.stop(); }; diff --git a/lib/text/mp4_ttml_parser.js b/lib/text/mp4_ttml_parser.js index 27470a3785..d8e4658cd4 100644 --- a/lib/text/mp4_ttml_parser.js +++ b/lib/text/mp4_ttml_parser.js @@ -53,6 +53,7 @@ shaka.text.Mp4TtmlParser.prototype.parseInit = function(data) { .fullBox('stsd', Mp4Parser.sampleDescription) .box('stpp', function(box) { sawSTPP = true; + box.parser.stop(); }).parse(data); if (!sawSTPP) { diff --git a/lib/util/mp4_parser.js b/lib/util/mp4_parser.js index 478b806ed5..9342c8ae7f 100644 --- a/lib/util/mp4_parser.js +++ b/lib/util/mp4_parser.js @@ -29,17 +29,14 @@ goog.require('shaka.util.DataViewReader'); * @export */ shaka.util.Mp4Parser = function() { - /** - * @type {!Object.} - * @private - */ + /** @private {!Object.} */ this.headers_ = []; - /** - * @type {!Object.} - * @private - */ + /** @private {!Object.} */ this.boxDefinitions_ = []; + + /** @private {boolean} */ + this.done_ = false; }; @@ -126,6 +123,17 @@ shaka.util.Mp4Parser.prototype.fullBox = function(type, definition) { }; +/** + * Stop parsing. Useful for extracting information from partial segments and + * avoiding an out-of-bounds error once you find what you are looking for. + * + * @export + */ +shaka.util.Mp4Parser.prototype.stop = function() { + this.done_ = true; +}; + + /** * Parse the given data using the added callbacks. * @@ -138,7 +146,8 @@ shaka.util.Mp4Parser.prototype.parse = function(data) { new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength), shaka.util.DataViewReader.Endianness.BIG_ENDIAN); - while (reader.hasMoreData()) { + this.done_ = false; + while (reader.hasMoreData() && !this.done_) { this.parseNext(0, reader); } }; @@ -215,7 +224,7 @@ shaka.util.Mp4Parser.prototype.parseNext = function(absStart, reader) { * @export */ shaka.util.Mp4Parser.children = function(box) { - while (box.reader.hasMoreData()) { + while (box.reader.hasMoreData() && !box.parser.done_) { box.parser.parseNext(box.start, box.reader); } }; diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index fc8cbefc99..1142062d57 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -515,6 +515,9 @@ describe('HlsParser live', function() { }); it('gets start time of segments with byte range', function(done) { + // Nit: this value is an implementation detail of the fix for #1106 + var partialEndByte = expectedStartByte + 1024; + fakeNetEngine.setResponseMap({ 'test:/master': toUTF8(master), 'test:/video': toUTF8(mediaWithByteRange), @@ -528,7 +531,7 @@ describe('HlsParser live', function() { segmentDataStartTime + 2 /* end */, '' /* baseUri */, expectedStartByte, - expectedEndByte); + expectedEndByte); // Complete segment reference parser.start('test:/master', playerInterface).then(function(manifest) { var video = manifest.periods[0].variants[0].video; @@ -539,7 +542,7 @@ describe('HlsParser live', function() { fakeNetEngine.expectRangeRequest( 'test:/main.mp4', expectedStartByte, - expectedEndByte); + partialEndByte); // Partial segment request }).catch(fail).then(done); shaka.polyfill.Promise.flush(); diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 4a7c915e9a..4dda16c174 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -1305,6 +1305,8 @@ describe('HlsParser', function() { // constructing references. Here it is covered incidentally. var expectedStartByte = 616; var expectedEndByte = 121705; + // Nit: this value is an implementation detail of the fix for #1106 + var partialEndByte = expectedStartByte + 1024; beforeEach(function() { // TODO: use StreamGenerator? @@ -1361,7 +1363,7 @@ describe('HlsParser', function() { fakeNetEngine.expectRangeRequest( 'test:/main.mp4', expectedStartByte, - expectedEndByte); + partialEndByte); // In VOD content, we set the presentationTimeOffset to align the // content to presentation time 0. @@ -1387,15 +1389,21 @@ describe('HlsParser', function() { expectedStartByte, expectedEndByte); - parser.start('test:/master', playerInterface) - .then(function(manifest) { - var video = manifest.periods[0].variants[0].video; - ManifestParser.verifySegmentIndex(video, [ref]); - // In VOD content, we set the presentationTimeOffset to align the - // content to presentation time 0. - expect(video.presentationTimeOffset) - .toEqual(segmentDataStartTime); - }).catch(fail).then(done); + parser.start('test:/master', playerInterface).then(function(manifest) { + var video = manifest.periods[0].variants[0].video; + ManifestParser.verifySegmentIndex(video, [ref]); + + // Make sure the segment data was fetched with the correct byte + // range. + fakeNetEngine.expectRangeRequest( + 'test:/main.ts', + expectedStartByte, + partialEndByte); + + // In VOD content, we set the presentationTimeOffset to align the + // content to presentation time 0. + expect(video.presentationTimeOffset).toEqual(segmentDataStartTime); + }).catch(fail).then(done); }); it('sets duration with respect to presentation offset', function(done) {