a369f3a0 by Jon-Carlos Rivera

Merge pull request #367 from videojs/add-loadstart-5

Add loadstart 5
2 parents c3592df3 096757e7
......@@ -30,7 +30,7 @@
"grunt-github-releaser": "^0.1.17",
"grunt-karma": "~0.6.2",
"grunt-open": "0.2.3",
"grunt-protractor-runner": "git+https://github.com/forbesjo/grunt-protractor-runner.git#webdriverManagerUpdate",
"grunt-protractor-runner": "forbesjo/grunt-protractor-runner.git#webdriverManagerUpdate",
"grunt-shell": "0.6.1",
"grunt-version": "^1.0.0",
"karma": "~0.10.0",
......@@ -48,7 +48,7 @@
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "git+ssh://git@github.com:videojs/videojs-contrib-media-sources.git#mse-mp2t-polyfill",
"videojs-contrib-media-sources": "videojs/videojs-contrib-media-sources.git#mse-mp2t-polyfill",
"videojs-swf": "5.0.0-rc0"
}
}
......
......@@ -131,6 +131,12 @@ videojs.Hls.prototype.src = function(src) {
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
// We need to trigger this asynchronously to give others the chance
// to bind to the event when a source is set at player creation
setTimeout(function() {
this.tech_.trigger('loadstart');
}.bind(this), 1);
// The index of the next segment to be downloaded in the current
// media playlist. When the current media playlist is live with
// expiring segments, it may be a different value from the media
......@@ -210,6 +216,14 @@ videojs.Hls.prototype.src = function(src) {
}.bind(this));
this.playlists.on('error', function() {
// close the media source with the appropriate error type
if (this.playlists.error.code === 2) {
this.mediaSource.endOfStream('network');
} else if (this.playlists.error.code === 4) {
this.mediaSource.endOfStream('decode');
}
// if this error is unrecognized, pass it along to the tech
this.tech_.error(this.playlists.error);
}.bind(this));
......@@ -425,7 +439,7 @@ videojs.Hls.prototype.play = function() {
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.tech_.duration() === Infinity) {
if (this.duration() === Infinity) {
if (this.tech_.currentTime() < this.tech_.seekable().start(0)) {
this.tech_.setCurrentTime(this.tech_.seekable().start(0));
}
......@@ -903,11 +917,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
return;
}
segmentInfo = segmentBuffer[0];
mediaIndex = segmentInfo.mediaIndex;
playlist = segmentInfo.playlist;
offset = segmentInfo.offset;
......@@ -970,31 +980,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
this.addCuesForMetadata_(segmentInfo);
//this.updateDuration(this.playlists.media());
// // 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') {
// if (tags.length) {
// // determine the offset within this segment we're seeking to
// segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
// segmentOffset += videojs.Hls.Playlist.duration(playlist,
// playlist.mediaSequence,
// playlist.mediaSequence + mediaIndex);
// segmentOffset = offset - (segmentOffset * 1000);
// ptsTime = segmentOffset + tags[0].pts;
// while (tags[i + 1] && tags[i].pts < ptsTime) {
// i++;
// }
// // tell the SWF the media position of the first tag we'll be delivering
// this.tech_.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
// tags = tags.slice(i);
// }
// }
// // when we're crossing a discontinuity, inject metadata to indicate
// // that the decoder should be reset appropriately
// if (segment.discontinuity && tags.length) {
......
......@@ -6,9 +6,6 @@ if (process.env.SAUCE_USERNAME) {
config.multiCapabilities = [{
browserName: 'chrome',
platform: 'Windows 8.1'
}, {
browserName: 'firefox',
platform: 'Windows 8.1'
}].map(function(caps) {
caps.name = process.env.TRAVIS_BUILD_NUMBER + process.env.TRAVIS_BRANCH;
caps.build = process.env.TRAVIS_BUILD_NUMBER;
......
......@@ -60,7 +60,7 @@ module.exports = function(config) {
customLaunchers: customLaunchers,
// Start these browsers
browsers: ['chrome_sl', 'firefox_sl'], //Object.keys(customLaunchers),
browsers: ['chrome_sl'], //Object.keys(customLaunchers),
// List of files / patterns to load in the browser
// Add any new src files to this list.
......
......@@ -104,7 +104,9 @@ var
});
// endOfStream triggers an exception if flash isn't available
player.tech.hls.mediaSource.endOfStream = function() {};
player.tech.hls.mediaSource.endOfStream = function(error) {
this.error_ = error;
};
},
standardXHRResponse = function(request) {
if (!request.url) {
......@@ -196,7 +198,7 @@ var
abort: function() {},
buffered: videojs.createTimeRange(),
appendBuffer: function() {}
}));
}))();
},
}),
......@@ -447,7 +449,7 @@ test('re-initializes the playlist loader when switching sources', function() {
});
test('sets the duration if one is available on the playlist', function() {
var calls = 0, events = 0, duration = 0;
var events = 0;
player.on('durationchange', function() {
events++;
});
......@@ -462,7 +464,7 @@ test('sets the duration if one is available on the playlist', function() {
equal(events, 1, 'durationchange is fired');
});
test('calculates the duration if needed', function() {
QUnit.skip('calculates the duration if needed', function() {
var changes = 0;
player.src({
src: 'http://example.com/manifest/missingExtinf.m3u8',
......@@ -583,11 +585,7 @@ test('re-initializes the handler for each source', function() {
notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
});
QUnit.skip('triggers an error when a master playlist request errors', function() {
var errors = 0;
player.on('error', function() {
errors++;
});
test('triggers an error when a master playlist request errors', function() {
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -595,9 +593,7 @@ QUnit.skip('triggers an error when a master playlist request errors', function()
openMediaSource(player);
requests.pop().respond(500);
ok(player.error(), 'an error is triggered');
strictEqual(1, errors, 'fired one error');
strictEqual(2, player.error().code, 'a network error is triggered');
equal(player.tech.hls.mediaSource.error_, 'network', 'a network error is triggered');
});
test('downloads media playlists after loading the master', function() {
......@@ -1773,11 +1769,7 @@ test('does not modify the media index for in-buffer seeking', function() {
equal(requests.length, 1, 'did not abort the outstanding request');
});
QUnit.skip('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
var errorTriggered = false;
player.on('error', function() {
errorTriggered = true;
});
test('playlist 404 should end stream with a network error', function() {
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1785,13 +1777,7 @@ QUnit.skip('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
openMediaSource(player);
requests.pop().respond(404);
equal(errorTriggered,
true,
'Missing Playlist error event should trigger');
equal(player.error().code,
2,
'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
ok(player.error().message, 'included an error message');
equal(player.tech.hls.mediaSource.error_, 'network', 'set a network error');
});
test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
......@@ -1982,6 +1968,9 @@ test('resets the time to a seekable position when resuming a live stream ' +
seekTarget = time;
}
};
player.tech.played = function() {
return videojs.createTimeRange(120, 170);
};
player.tech.trigger('playing');
player.tech.trigger('play');
......@@ -1989,35 +1978,6 @@ test('resets the time to a seekable position when resuming a live stream ' +
player.tech.trigger('seeked');
});
test('clamps seeks to the seekable window', function() {
var seekTarget;
player.src({
src: 'live0.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n');
// mock out a seekable window
player.tech.hls.seekable = function() {
return videojs.createTimeRange(160, 170);
};
player.tech.hls.fillBuffer = function(time) {
if (time !== undefined) {
seekTarget = time;
}
};
player.currentTime(180);
equal(seekTarget * 0.001, player.seekable().end(0), 'forward seeks are clamped');
player.currentTime(45);
equal(seekTarget * 0.001, player.seekable().start(0), 'backward seeks are clamped');
});
test('mediaIndex is zero before the first segment loads', function() {
window.manifests['first-seg-load'] =
'#EXTM3U\n' +
......@@ -2045,7 +2005,8 @@ test('mediaIndex returns correctly at playlist boundaries', function() {
strictEqual(player.tech.hls.mediaIndex, 0, 'mediaIndex is zero at first segment');
// seek to end
player.currentTime(40);
player.tech.setCurrentTime(40);
clock.tick(1);
strictEqual(player.tech.hls.mediaIndex, 3, 'mediaIndex is 3 at last segment');
});
......@@ -2148,47 +2109,8 @@ test('does not break if the playlist has no segments', function() {
strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
});
test('calls vjs_discontinuity() before appending bytes at a discontinuity', function() {
var discontinuities = 0, tags = [], bufferEnd;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
src: 'discontinuity.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech.trigger('play');
player.tech.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.tech.el().vjs_discontinuity = function() {
discontinuities++;
};
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
player.tech.el().currentTime = 6;
bufferEnd = 10;
player.tech.hls.checkBuffer_();
strictEqual(discontinuities, 0, 'no discontinuities before the segment is received');
tags.push({ pts: 0, bytes: new Uint8Array(1) });
standardXHRResponse(requests.pop());
strictEqual(discontinuities, 1, 'signals a discontinuity');
});
test('clears the segment buffer on seek', function() {
var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
var currentTime, bufferEnd, oldCurrentTime;
player.src({
src: 'discontinuity.m3u8',
......@@ -2205,37 +2127,30 @@ test('clears the segment buffer on seek', function() {
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.tech.hls.sourceBuffer.abort = function() {
aborts++;
};
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
'#EXTINF:10,0\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
'2.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop());
standardXHRResponse(requests.pop()); // 1.ts
// play to 6s to trigger the next segment request
currentTime = 6;
bufferEnd = 10;
player.tech.hls.checkBuffer_();
clock.tick(6000);
standardXHRResponse(requests.pop());
standardXHRResponse(requests.pop()); // 2.ts
equal(player.tech.hls.segmentBuffer_.length, 2, 'started fetching segments');
// seek back to the beginning
player.currentTime(0);
tags.push({ pts: 0, bytes: new Uint8Array(1) });
clock.tick(1);
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');
equal(player.tech.hls.segmentBuffer_.length, 0, 'cleared the segment buffer');
});
test('can seek before the source buffer opens', function() {
......@@ -2252,29 +2167,16 @@ test('can seek before the source buffer opens', function() {
equal(player.currentTime(), 1, 'seeked');
});
test('continues playing after seek to discontinuity', function() {
var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
test('sets the timestampOffset after seeking to discontinuity', function() {
var bufferEnd;
player.src({
src: 'discontinuity.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
oldCurrentTime = player.currentTime;
player.currentTime = function(time) {
if (time !== undefined) {
return oldCurrentTime.call(player, time);
}
return currentTime;
};
player.buffered = function() {
player.tech.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.tech.hls.sourceBuffer.abort = function() {
aborts++;
};
requests.pop().respond(200, null,
'#EXTM3U\n' +
......@@ -2286,27 +2188,51 @@ test('continues playing after seek to discontinuity', function() {
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop()); // 1.ts
currentTime = 1;
bufferEnd = 10;
// seek to a discontinuity
player.tech.setCurrentTime(10);
bufferEnd = 9.9;
clock.tick(1);
standardXHRResponse(requests.pop()); // 1.ts
player.tech.hls.checkBuffer_();
standardXHRResponse(requests.pop()); // 2.ts, again
equal(player.tech.hls.sourceBuffer.timestampOffset,
10,
'set the timestamp offset');
});
standardXHRResponse(requests.pop()); // 2.ts
QUnit.skip('tracks segment end times as they are buffered', function() {
var bufferEnd = 0;
player.src({
src: 'media.m3u8',
type: 'application/x-mpegURL'
});
openMediaSource(player);
// seek to the discontinuity
player.currentTime(10);
tags.push({ pts: 0, bytes: new Uint8Array(1) });
tags.push({ pts: 11 * 1000, bytes: new Uint8Array(1) });
standardXHRResponse(requests.pop()); // 1.ts, again
strictEqual(aborts, 1, 'aborted once for the seek');
// as new segments are downloaded, the buffer end is updated
player.tech.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
// 0.ts is shorter than advertised
standardXHRResponse(requests.shift());
equal(player.tech.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
// 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');
bufferEnd = 9.5;
player.tech.hls.sourceBuffer.trigger('update');
player.tech.hls.sourceBuffer.trigger('updateend');
equal(player.tech.duration(), 10 + 9.5, 'updated duration');
equal(player.tech.hls.appendingSegmentInfo_, null, 'cleared the appending segment');
});
test('seeking does not fail when targeted between segments', function() {
var tags = [], currentTime, segmentUrl;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
QUnit.skip('seeking does not fail when targeted between segments', function() {
var currentTime, segmentUrl;
player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -2326,22 +2252,19 @@ test('seeking does not fail when targeted between segments', function() {
};
standardXHRResponse(requests.shift()); // media
tags.push({ pts: 100, bytes: new Uint8Array(1) },
{ pts: 9 * 1000 + 100, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 0
player.tech.hls.checkBuffer_();
tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
{ pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
segmentUrl = requests[0].url;
standardXHRResponse(requests.shift()); // segment 1
// seek to a time that is greater than the last tag in segment 0 but
// less than the first in segment 1
player.currentTime(9.4);
// FIXME: it's not possible to seek here without timestamp-based
// segment durations
player.tech.setCurrentTime(9.4);
clock.tick(1);
equal(requests[0].url, segmentUrl, 'requested the later segment');
tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
{ pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 1
player.tech.trigger('seeked');
equal(player.currentTime(), 9.5, 'seeked to the later time');
......@@ -2986,7 +2909,7 @@ test('treats invalid keys as a key request failure', function() {
equal(bytes.length, 0, 'did not append bytes');
// second segment request
requests[0].response = new Uint8Array([1, 2])
requests[0].response = new Uint8Array([1, 2]);
requests.shift().respond(200, null, '');
equal(bytes.length, 1, 'appended bytes');
......