Skip to content

Commit

Permalink
Make partial segment requests in HLS parser
Browse files Browse the repository at this point in the history
This speeds up HLS stream startup significantly for servers that
permit this kind of request, and falls back to full segments for
servers that do not.

Closes #1106

Change-Id: I96bc7f0df0fb84b75f3a3fe43476ba0ba5fc2264
  • Loading branch information
joeyparrish committed Nov 7, 2017
1 parent bde4c45 commit a93455a
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 52 deletions.
109 changes: 79 additions & 30 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<shakaExtern.Response>}
* @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.
Expand Down Expand Up @@ -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));
};


Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions lib/media/mp4_segment_index_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,6 @@ shaka.media.Mp4SegmentIndexParser.parseSIDX_ = function(
startByte += referenceSize;
}

box.parser.stop();
return references;
};
2 changes: 2 additions & 0 deletions lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};


Expand Down
1 change: 1 addition & 0 deletions lib/text/mp4_ttml_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 19 additions & 10 deletions lib/util/mp4_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,14 @@ goog.require('shaka.util.DataViewReader');
* @export
*/
shaka.util.Mp4Parser = function() {
/**
* @type {!Object.<number, shaka.util.Mp4Parser.BoxType_>}
* @private
*/
/** @private {!Object.<number, shaka.util.Mp4Parser.BoxType_>} */
this.headers_ = [];

/**
* @type {!Object.<number, !shaka.util.Mp4Parser.CallbackType>}
* @private
*/
/** @private {!Object.<number, !shaka.util.Mp4Parser.CallbackType>} */
this.boxDefinitions_ = [];

/** @private {boolean} */
this.done_ = false;
};


Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
}
};
Expand Down Expand Up @@ -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);
}
};
Expand Down
7 changes: 5 additions & 2 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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;
Expand All @@ -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();
Expand Down
28 changes: 18 additions & 10 deletions test/hls/hls_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand Down

0 comments on commit a93455a

Please sign in to comment.