1551d9c4 by David LaPalomento

Merge pull request #432 from dmlap/drain-race

Don't append segments that have already been appended
2 parents eb5c0c61 2270048a
......@@ -9,21 +9,8 @@
<!-- video.js -->
<script src="node_modules/video.js/dist/video.js"></script>
<!-- transmuxing -->
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/stream.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/flv-tag.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/exp-golomb.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-extradata.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-stream.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/aac-stream.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/metadata-stream.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/segment-parser.js"></script>
<!-- Media Sources plugin -->
<script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script>
videojs.MediaSource.webWorkerURI = 'node_modules/videojs-contrib-media-sources/src/transmuxer_worker.js';
</script>
<script src="node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="src/videojs-hls.js"></script>
......
......@@ -44,11 +44,11 @@
"karma-sauce-launcher": "~0.1.8",
"qunitjs": "^1.18.0",
"sinon": "1.10.2",
"video.js": "^5.1.0"
"video.js": "^5.2.1"
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "^2.0.0",
"videojs-contrib-media-sources": "^2.4.0",
"videojs-swf": "^5.0.0"
}
}
......
......@@ -23,7 +23,8 @@ keyFailed = function(key) {
return key.retries && key.retries >= 2;
};
videojs.Hls = videojs.extend(Component, {
videojs.Hls = {};
videojs.HlsHandler = videojs.extend(Component, {
constructor: function(tech, options) {
var self = this, _player;
......@@ -110,7 +111,7 @@ videojs.HlsSourceHandler = function(mode) {
tech.trigger('loadstart');
}, 1);
}
tech.hls = new videojs.Hls(tech, {
tech.hls = new videojs.HlsHandler(tech, {
source: source,
mode: mode
});
......@@ -129,7 +130,7 @@ videojs.getComponent('Flash').registerSourceHandler(videojs.HlsSourceHandler('fl
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
videojs.Hls.prototype.src = function(src) {
videojs.HlsHandler.prototype.src = function(src) {
var oldMediaPlaylist;
// do nothing if the src is falsey
......@@ -208,7 +209,7 @@ videojs.Hls.prototype.src = function(src) {
this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
};
videojs.Hls.prototype.handleSourceOpen = function() {
videojs.HlsHandler.prototype.handleSourceOpen = function() {
// Only attempt to create the source buffer if none already exist.
// handleSourceOpen is also called when we are "re-opening" a source buffer
// after `endOfStream` has been called (in response to a seek for instance)
......@@ -284,7 +285,7 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
return result;
};
videojs.Hls.prototype.setupSourceBuffer_ = function() {
videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
// wait until a media playlist is available and the Media Source is
......@@ -375,7 +376,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
* Seek to the latest media position if this is a live video and the
* player and video are loaded and initialized.
*/
videojs.Hls.prototype.setupFirstPlay = function() {
videojs.HlsHandler.prototype.setupFirstPlay = function() {
var seekable, media;
media = this.playlists.media();
......@@ -405,7 +406,7 @@ videojs.Hls.prototype.setupFirstPlay = function() {
/**
* Begin playing the video.
*/
videojs.Hls.prototype.play = function() {
videojs.HlsHandler.prototype.play = function() {
this.loadingState_ = 'segments';
if (this.tech_.ended()) {
......@@ -425,7 +426,7 @@ videojs.Hls.prototype.play = function() {
}
};
videojs.Hls.prototype.setCurrentTime = function(currentTime) {
videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
var
buffered = this.findCurrentBuffered_();
......@@ -461,7 +462,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
};
videojs.Hls.prototype.duration = function() {
videojs.HlsHandler.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.Playlist.duration(playlists.media());
......@@ -469,7 +470,7 @@ videojs.Hls.prototype.duration = function() {
return 0;
};
videojs.Hls.prototype.seekable = function() {
videojs.HlsHandler.prototype.seekable = function() {
var media;
if (!this.playlists) {
......@@ -486,31 +487,32 @@ videojs.Hls.prototype.seekable = function() {
/**
* Update the player duration
*/
videojs.Hls.prototype.updateDuration = function(playlist) {
videojs.HlsHandler.prototype.updateDuration = function(playlist) {
var oldDuration = this.mediaSource.duration,
newDuration = videojs.Hls.Playlist.duration(playlist),
setDuration = function() {
this.mediaSource.duration = newDuration;
// update seekable
if (seekable.length !== 0 && newDuration === Infinity) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
this.tech_.trigger('durationchange');
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this),
seekable = this.seekable();
// TODO: Move to videojs-contrib-media-sources
if (seekable.length && newDuration === Infinity) {
if (isNaN(oldDuration)) {
oldDuration = 0;
}
newDuration = Math.max(oldDuration,
seekable.end(0) + playlist.targetDuration * 3);
}
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
// update the duration
if (this.mediaSource.readyState !== 'open') {
this.mediaSource.addEventListener('sourceopen', setDuration);
} else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
this.mediaSource.duration = newDuration;
// update seekable
if (seekable.length !== 0 && newDuration === Infinity) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
this.tech_.trigger('durationchange');
}
}
......@@ -521,7 +523,7 @@ videojs.Hls.prototype.updateDuration = function(playlist) {
* source. After this function is called, the tech should be in a
* state suitable for switching to a different video.
*/
videojs.Hls.prototype.resetSrc_ = function() {
videojs.HlsHandler.prototype.resetSrc_ = function() {
this.cancelSegmentXhr();
this.cancelKeyXhr();
......@@ -530,7 +532,7 @@ videojs.Hls.prototype.resetSrc_ = function() {
}
};
videojs.Hls.prototype.cancelKeyXhr = function() {
videojs.HlsHandler.prototype.cancelKeyXhr = function() {
if (this.keyXhr_) {
this.keyXhr_.onreadystatechange = null;
this.keyXhr_.abort();
......@@ -538,7 +540,7 @@ videojs.Hls.prototype.cancelKeyXhr = function() {
}
};
videojs.Hls.prototype.cancelSegmentXhr = function() {
videojs.HlsHandler.prototype.cancelSegmentXhr = function() {
if (this.segmentXhr_) {
// Prevent error handler from running.
this.segmentXhr_.onreadystatechange = null;
......@@ -552,7 +554,7 @@ videojs.Hls.prototype.cancelSegmentXhr = function() {
/**
* Abort all outstanding work and cleanup.
*/
videojs.Hls.prototype.dispose = function() {
videojs.HlsHandler.prototype.dispose = function() {
this.stopCheckingBuffer_();
if (this.playlists) {
......@@ -569,7 +571,7 @@ videojs.Hls.prototype.dispose = function() {
* @return the highest bitrate playlist less than the currently detected
* bandwidth, accounting for some amount of bandwidth variance
*/
videojs.Hls.prototype.selectPlaylist = function () {
videojs.HlsHandler.prototype.selectPlaylist = function () {
var
effectiveBitrate,
sortedPlaylists = this.playlists.master.playlists.slice(),
......@@ -662,7 +664,7 @@ videojs.Hls.prototype.selectPlaylist = function () {
/**
* Periodically request new segments and append video data.
*/
videojs.Hls.prototype.checkBuffer_ = function() {
videojs.HlsHandler.prototype.checkBuffer_ = function() {
// calling this method directly resets any outstanding buffer checks
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
......@@ -681,7 +683,7 @@ videojs.Hls.prototype.checkBuffer_ = function() {
* Setup a periodic task to request new segments if necessary and
* append bytes into the SourceBuffer.
*/
videojs.Hls.prototype.startCheckingBuffer_ = function() {
videojs.HlsHandler.prototype.startCheckingBuffer_ = function() {
// if the player ever stalls, check if there is video data available
// to append immediately
this.tech_.on('waiting', (this.drainBuffer).bind(this));
......@@ -693,7 +695,7 @@ videojs.Hls.prototype.startCheckingBuffer_ = function() {
* Stop the periodic task requesting new segments and feeding the
* SourceBuffer.
*/
videojs.Hls.prototype.stopCheckingBuffer_ = function() {
videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
......@@ -705,7 +707,7 @@ videojs.Hls.prototype.stopCheckingBuffer_ = function() {
* Attempts to find the buffered TimeRange where playback is currently
* happening. Returns a new TimeRange with one or zero ranges.
*/
videojs.Hls.prototype.findCurrentBuffered_ = function() {
videojs.HlsHandler.prototype.findCurrentBuffered_ = function() {
var
ranges,
i,
......@@ -743,7 +745,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() {
* @param seekToTime (optional) {number} the offset into the downloaded segment
* to seek to, in seconds
*/
videojs.Hls.prototype.fillBuffer = function(mediaIndex) {
videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
var
tech = this.tech_,
currentTime = tech.currentTime(),
......@@ -840,7 +842,7 @@ videojs.Hls.prototype.fillBuffer = function(mediaIndex) {
this.loadSegment(segmentInfo);
};
videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
var playListUrl;
// resolve the segment URL relative to the playlist
if (this.playlists.media().uri === this.source_.src) {
......@@ -859,7 +861,7 @@ videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
* * `bytesReceived` - amount of bytes downloaded
* `bandwidth` is the only required property.
*/
videojs.Hls.prototype.setBandwidth = function(xhr) {
videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
// calculate the download bandwidth
this.segmentXhrTime = xhr.roundTripTime;
this.bandwidth = xhr.bandwidth;
......@@ -868,7 +870,7 @@ videojs.Hls.prototype.setBandwidth = function(xhr) {
this.tech_.trigger('bandwidthupdate');
};
videojs.Hls.prototype.loadSegment = function(segmentInfo) {
videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
var
self = this,
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
......@@ -927,7 +929,7 @@ videojs.Hls.prototype.loadSegment = function(segmentInfo) {
});
};
videojs.Hls.prototype.drainBuffer = function(event) {
videojs.HlsHandler.prototype.drainBuffer = function(event) {
var
segmentInfo,
mediaIndex,
......@@ -948,6 +950,12 @@ videojs.Hls.prototype.drainBuffer = function(event) {
return;
}
// the pending segment has already been appended and we're waiting
// for updateend to fire
if (this.pendingSegment_.buffered) {
return;
}
// we can't append more data if the source buffer is busy processing
// what we've already sent
if (this.sourceBuffer.updating) {
......@@ -1034,7 +1042,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
/**
* Attempt to retrieve the key for a particular media segment.
*/
videojs.Hls.prototype.fetchKey_ = function(segment) {
videojs.HlsHandler.prototype.fetchKey_ = function(segment) {
var key, self, settings, receiveKey;
// if there is a pending XHR or no segments, don't do anything
......
......@@ -9,23 +9,9 @@
<!-- video.js -->
<script src="../../node_modules/video.js/dist/video.js"></script>
<!-- transmuxing -->
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/stream.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/mp4-generator.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/transmuxer.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/flv-tag.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/exp-golomb.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-extradata.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-stream.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/aac-stream.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/metadata-stream.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/segment-parser.js"></script>
<!-- Media Sources plugin -->
<script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script>
videojs.MediaSource.webWorkerURI = '../../node_modules/videojs-contrib-media-sources/src/transmuxer_worker.js';
</script>
<script src="../../node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="../../src/videojs-hls.js"></script>
......
......@@ -134,11 +134,6 @@ var
type: 'sourceopen',
swfId: player.tech_.el().id
});
// endOfStream triggers an exception if flash isn't available
player.tech_.hls.mediaSource.endOfStream = function(error) {
this.error_ = error;
};
},
standardXHRResponse = function(request) {
if (!request.url) {
......@@ -170,6 +165,11 @@ var
// a no-op MediaSource implementation to allow synchronous testing
MockMediaSource = videojs.extend(videojs.EventTarget, {
constructor: function() {},
duration: NaN,
seekable: videojs.createTimeRange(),
addSeekableRange_: function(start, end) {
this.seekable = videojs.createTimeRange(start, end);
},
addSourceBuffer: function() {
return new (videojs.extend(videojs.EventTarget, {
constructor: function() {},
......@@ -179,7 +179,10 @@ var
remove: function() {}
}))();
},
endOfStream: function() {}
// endOfStream triggers an exception if flash isn't available
endOfStream: function(error) {
this.error_ = error;
}
}),
// do a shallow copy of the properties of source onto the target object
......@@ -1180,16 +1183,24 @@ test('only makes one segment request at a time', function() {
});
test('only appends one segment at a time', function() {
var appends = 0;
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.pop()); // media.m3u8
player.tech_.hls.sourceBuffer.appendBuffer = function() {
appends++;
};
standardXHRResponse(requests.pop()); // segment 0
player.tech_.hls.checkBuffer_();
equal(requests.length, 0, 'did not request while updating');
player.tech_.hls.checkBuffer_();
equal(appends, 1, 'appended once');
});
test('waits to download new segments until the media playlist is stable', function() {
......@@ -1377,7 +1388,7 @@ test('seeking in an empty playlist is a non-erroring noop', function() {
equal(requests.length, requestsLength, 'made no additional requests');
});
test('tech\'s duration reports Infinity for live playlists', function() {
test('sets seekable and duration for live playlists', function() {
player.src({
src: 'http://example.com/manifest/missingEndlist.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1386,13 +1397,19 @@ test('tech\'s duration reports Infinity for live playlists', function() {
standardXHRResponse(requests[0]);
strictEqual(player.tech_.duration(),
Infinity,
'duration on the tech is infinity');
equal(player.tech_.hls.mediaSource.seekable.length,
1,
'set one seekable range');
equal(player.tech_.hls.mediaSource.seekable.start(0),
player.tech_.hls.seekable().start(0),
'set seekable start');
equal(player.tech_.hls.mediaSource.seekable.end(0),
player.tech_.hls.seekable().end(0),
'set seekable end');
notEqual(player.tech_.hls.mediaSource.duration,
strictEqual(player.tech_.hls.mediaSource.duration,
Infinity,
'duration on the mediaSource is not infinity');
'duration on the mediaSource is infinity');
});
test('live playlist starts three target durations before live', function() {
......@@ -1518,6 +1535,7 @@ test('reloads out-of-date live playlists when switching variants', function() {
});
test('if withCredentials global option is used, withCredentials is set on the XHR object', function() {
var hlsOptions = videojs.options.hls;
player.dispose();
videojs.options.hls = {
withCredentials: true
......@@ -1530,6 +1548,7 @@ test('if withCredentials global option is used, withCredentials is set on the XH
openMediaSource(player);
ok(requests[0].withCredentials,
'with credentials should be set to true if that option is passed in');
videojs.options.hls = hlsOptions;
});
test('if withCredentials src option is used, withCredentials is set on the XHR object', function() {
......@@ -1901,10 +1920,10 @@ test('the source handler supports HLS mime types', function() {
ok(!(videojs.HlsSourceHandler(techName).canHandleSource({
type: 'video/mp4'
}) instanceof videojs.Hls), 'does not support mp4');
}) instanceof videojs.HlsHandler), 'does not support mp4');
ok(!(videojs.HlsSourceHandler(techName).canHandleSource({
type: 'video/x-flv'
}) instanceof videojs.Hls), 'does not support flv');
}) instanceof videojs.HlsHandler), 'does not support flv');
});
});
......