77aaebc4 by David LaPalomento

Merge pull request #91 from videojs/discontinuity

Support for EXT-X-DISCONTINUITY
2 parents 18928e94 25232346
......@@ -298,6 +298,9 @@ hls.FlvTag = function(type, extraData) {
this.bytes[ 9] = 0;
this.bytes[10] = 0;
// Sometimes we're at the end of the view and have one slot to write a
// uint32, so, prepareWrite of count 4, since, view is uint8
prepareWrite(this, 4);
this.view.setUint32(this.length, this.length);
this.length += 4;
this.position += 4;
......
......@@ -302,6 +302,7 @@
h264Frame.endNalUnit();
this.tags.push(h264Frame);
}
h264Frame = null;
......@@ -427,7 +428,9 @@
// We did not find any start codes. Try again next packet
state = 1;
h264Frame.writeBytes(data, start, length);
if (h264Frame) {
h264Frame.writeBytes(data, start, length);
}
return;
case 3:
// The next byte is the first byte of a NAL Unit
......
......@@ -273,6 +273,14 @@
});
return;
}
match = (/^#EXT-X-DISCONTINUITY/).exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'discontinuity'
});
return;
}
// unknown tag type
this.trigger('data', {
......@@ -399,6 +407,9 @@
currentUri.attributes = mergeOptions(currentUri.attributes,
entry.attributes);
},
'discontinuity': function() {
currentUri.discontinuity = true;
},
'targetduration': function() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
......
......@@ -7,6 +7,7 @@
*/
(function(window, videojs, document, undefined) {
'use strict';
var
......@@ -145,22 +146,42 @@ var
},
/**
* Calculate the duration of a playlist from a given start index to a given
* end index.
* @param playlist {object} a media playlist object
* @param startIndex {number} an inclusive lower boundary for the playlist.
* Defaults to 0.
* @param endIndex {number} an exclusive upper boundary for the playlist.
* Defaults to playlist length.
* @return {number} the duration between the start index and end index.
*/
duration = function(playlist, startIndex, endIndex) {
var dur = 0,
segment,
i;
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
i = endIndex - 1;
for (; i >= startIndex; i--) {
segment = playlist.segments[i];
dur += segment.duration || playlist.targetDuration || 0;
}
return dur;
},
/**
* Calculate the total duration for a playlist based on segment metadata.
* @param playlist {object} a media playlist object
* @return {number} the currently known duration, in seconds
*/
totalDuration = function(playlist) {
var
duration = 0,
segment,
i;
if (!playlist) {
return 0;
}
i = (playlist.segments || []).length;
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
......@@ -171,11 +192,7 @@ var
return window.Infinity;
}
while (i--) {
segment = playlist.segments[i];
duration += segment.duration || playlist.targetDuration || 0;
}
return duration;
return duration(playlist);
},
resolveUrl,
......@@ -184,10 +201,12 @@ var
var
segmentParser = new videojs.Hls.SegmentParser(),
settings = videojs.util.mergeOptions({}, player.options().hls),
segmentBuffer = [],
lastSeekedTime,
segmentXhr,
fillBuffer,
drainBuffer,
updateDuration;
......@@ -197,6 +216,7 @@ var
}
return this.el().vjs_getProperty('currentTime');
};
player.hls.setCurrentTime = function(currentTime) {
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
......@@ -219,6 +239,9 @@ var
segmentXhr.abort();
}
// clear out any buffered segments
segmentBuffer = [];
// begin filling the buffer at the new position
fillBuffer(currentTime * 1000);
};
......@@ -367,6 +390,8 @@ var
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(error, url) {
var tags;
// the segment request is no longer outstanding
segmentXhr = null;
......@@ -395,45 +420,101 @@ var
segmentParser.parseSegmentBinaryData(new Uint8Array(this.response));
segmentParser.flushTags();
// if we're refilling the buffer after a seek, scan through the muxed
// FLV tags until we find the one that is closest to the desired
// playback time
if (typeof offset === 'number') {
(function() {
var tag = segmentParser.getTags()[0];
for (; tag.pts < offset; tag = segmentParser.getTags()[0]) {
segmentParser.getNextTag();
}
// tell the SWF where we will be seeking to
player.hls.el().vjs_setProperty('currentTime', tag.pts * 0.001);
lastSeekedTime = null;
})();
}
// package up all the work to append the segment
// if the segment is the start of a timestamp discontinuity,
// we have to wait until the sourcebuffer is empty before
// aborting the source buffer processing
tags = [];
while (segmentParser.tagsAvailable()) {
// queue up the bytes to be appended to the SourceBuffer
// the queue gives control back to the browser between tags
// so that large segments don't cause a "hiccup" in playback
player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
player);
tags.push(segmentParser.getNextTag());
}
segmentBuffer.push({
mediaIndex: player.hls.mediaIndex,
playlist: player.hls.playlists.media(),
offset: offset,
tags: tags
});
drainBuffer();
player.hls.mediaIndex++;
if (player.hls.mediaIndex === player.hls.playlists.media().segments.length) {
mediaSource.endOfStream();
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
player.hls.playlists.media(player.hls.selectPlaylist());
});
};
drainBuffer = function(event) {
var
i = 0,
mediaIndex,
playlist,
offset,
tags,
segment,
ptsTime,
segmentOffset;
if (!segmentBuffer.length) {
return;
}
mediaIndex = segmentBuffer[0].mediaIndex;
playlist = segmentBuffer[0].playlist;
offset = segmentBuffer[0].offset;
tags = segmentBuffer[0].tags;
segment = playlist.segments[mediaIndex];
event = event || {};
segmentOffset = duration(playlist, 0, mediaIndex) * 1000;
// abort() clears any data queued in the source buffer so wait
// until it empties before calling it when a discontinuity is
// next in the buffer
if (segment.discontinuity) {
if (event.type !== 'waiting') {
return;
}
player.hls.sourceBuffer.abort();
// tell the SWF where playback is continuing in the stitched timeline
player.hls.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
}
// if we're refilling the buffer after a seek, scan through the muxed
// FLV tags until we find the one that is closest to the desired
// playback time
if (typeof offset === 'number') {
ptsTime = offset - segmentOffset + tags[0].pts;
while (tags[i].pts < ptsTime) {
i++;
}
// tell the SWF where we will be seeking to
player.hls.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
tags = tags.slice(i);
lastSeekedTime = null;
}
for (i = 0; i < tags.length; i++) {
// queue up the bytes to be appended to the SourceBuffer
// the queue gives control back to the browser between tags
// so that large segments don't cause a "hiccup" in playback
player.hls.sourceBuffer.appendBuffer(tags[i].bytes, player);
}
// we're done processing this segment
segmentBuffer.shift();
if (mediaIndex === playlist.segments.length) {
mediaSource.endOfStream();
}
};
// load the MediaSource into the player
mediaSource.addEventListener('sourceopen', function() {
// construct the video data buffer and set the appropriate MIME type
......@@ -450,9 +531,12 @@ var
player.hls.playlists.on('loadedmetadata', function() {
oldMediaPlaylist = player.hls.playlists.media();
// periodicaly check if the buffer needs to be refilled
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
fillBuffer();
player.on('timeupdate', fillBuffer);
player.on('timeupdate', drainBuffer);
player.on('waiting', drainBuffer);
player.trigger('loadedmetadata');
});
......
{
"allowCache": true,
"mediaSequence": 0,
"segments": [
{
"duration": 10,
"uri": "001.ts"
},
{
"duration": 19,
"uri": "002.ts"
},
{
"discontinuity": true,
"duration": 10,
"uri": "003.ts"
},
{
"duration": 11,
"uri": "004.ts"
},
{
"discontinuity": true,
"duration": 10,
"uri": "005.ts"
},
{
"duration": 10,
"uri": "006.ts"
},
{
"duration": 10,
"uri": "007.ts"
},
{
"discontinuity": true,
"duration": 10,
"uri": "008.ts"
},
{
"duration": 16,
"uri": "009.ts"
}
],
"targetDuration": 19,
"endList": true
}
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:19
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10,0
001.ts
#EXTINF:19,0
002.ts
#EXT-X-DISCONTINUITY
#EXTINF:10,0
003.ts
#EXTINF:11,0
004.ts
#EXT-X-DISCONTINUITY
#EXTINF:10,0
005.ts
#EXTINF:10,0
006.ts
#EXTINF:10,0
007.ts
#EXT-X-DISCONTINUITY
#EXTINF:10,0
008.ts
#EXTINF:16,0
009.ts
#EXT-X-ENDLIST
......@@ -124,6 +124,8 @@
0x12: 'metadata'
};
videojs.log = console.log.bind(console);
original.addEventListener('change', function() {
var reader = new FileReader();
reader.addEventListener('loadend', function() {
......
......@@ -840,8 +840,11 @@ test('calls abort() on the SourceBuffer before seeking', function() {
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
// seek to 7s
// drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream
// adding a tag with a PTS of zero looks like a stream with no discontinuities
tags.push({ pts: 0, bytes: 0 });
tags.push({ pts: 7000, bytes: 7 });
// seek to 7s
player.currentTime(7);
standardXHRResponse(requests[2]);
......@@ -1047,6 +1050,106 @@ test('does not break if the playlist has no segments', function() {
strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
});
test('waits until the buffer is empty before appending bytes at a discontinuity', function() {
var aborts = 0, setTime, currentTime, bufferEnd;
player.src({
src: 'disc.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
player.currentTime = function() { return currentTime; };
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.hls.sourceBuffer.abort = function() {
aborts++;
};
player.hls.el().vjs_setProperty = function(name, value) {
if (name === 'currentTime') {
return setTime = value;
}
};
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,0\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
'2.ts\n');
standardXHRResponse(requests.pop());
// play to 6s to trigger the next segment request
currentTime = 6;
bufferEnd = 10;
player.trigger('timeupdate');
strictEqual(aborts, 0, 'no aborts before the buffer empties');
standardXHRResponse(requests.pop());
strictEqual(aborts, 0, 'no aborts before the buffer empties');
// pretend the buffer has emptied
player.trigger('waiting');
strictEqual(aborts, 1, 'aborted before appending the new segment');
strictEqual(setTime, 10, 'updated the time after crossing the discontinuity');
});
test('clears the segment buffer on seek', function() {
var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
src: 'disc.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
oldCurrentTime = player.currentTime;
player.currentTime = function(time) {
if (time !== undefined) {
return oldCurrentTime.call(player, time);
}
return currentTime;
};
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.hls.sourceBuffer.abort = function() {
aborts++;
};
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,0\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
'2.ts\n');
standardXHRResponse(requests.pop());
// play to 6s to trigger the next segment request
currentTime = 6;
bufferEnd = 10;
player.trigger('timeupdate');
standardXHRResponse(requests.pop());
// seek back to the beginning
player.currentTime(0);
tags.push({ pts: 0, bytes: 0 });
standardXHRResponse(requests.pop());
strictEqual(aborts, 1, 'aborted once for the seek');
// the source buffer empties. is 2.ts still in the segment buffer?
player.trigger('waiting');
strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
});
test('disposes the playlist loader', function() {
var disposes = 0, player, loaderDispose;
player = createPlayer();
......