Browse Source

Fix any possible fillBuffer_ race conditions by debouncing all fillBuffers_ (#959)

- Convert all calls to fillBuffer_ to calls to monitorBuffer_
- Rename monitorBuffer_ to monitorBufferTick_ which becomes the 500ms buffer check timer loop
- Make monitorBuffer_ schedule an immediate timer for monitorBufferTick_
pull/6/head
Jon-Carlos Rivera 9 years ago
committed by Matthew Neil
parent
commit
409313c5b9
  1. 33
      src/segment-loader.js
  2. 112
      test/master-playlist-controller.test.js
  3. 74
      test/segment-loader.test.js
  4. 1
      test/test-helpers.js
  5. 287
      test/videojs-contrib-hls.test.js

33
src/segment-loader.js

@ -203,7 +203,7 @@ export default class SegmentLoader extends videojs.EventTarget {
// next segment
if (!this.paused()) {
this.state = 'READY';
this.fillBuffer_();
this.monitorBuffer_();
}
}
@ -252,7 +252,6 @@ export default class SegmentLoader extends videojs.EventTarget {
}
this.state = 'READY';
this.fillBuffer_();
}
/**
@ -348,13 +347,26 @@ export default class SegmentLoader extends videojs.EventTarget {
}
}
/**
* (re-)schedule monitorBufferTick_ to run as soon as possible
*
* @private
*/
monitorBuffer_() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
}
this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1);
}
/**
* As long as the SegmentLoader is in the READY state, periodically
* invoke fillBuffer_().
*
* @private
*/
monitorBuffer_() {
monitorBufferTick_() {
if (this.state === 'READY') {
this.fillBuffer_();
}
@ -363,7 +375,7 @@ export default class SegmentLoader extends videojs.EventTarget {
window.clearTimeout(this.checkBufferTimeout_);
}
this.checkBufferTimeout_ = window.setTimeout(this.monitorBuffer_.bind(this),
this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this),
CHECK_BUFFER_DELAY);
}
@ -543,12 +555,15 @@ export default class SegmentLoader extends videojs.EventTarget {
this.state = 'READY';
this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_);
this.resetEverything();
return this.fillBuffer_();
return this.monitorBuffer_();
}
/**
* fill the buffer with segements unless the
* sourceBuffers are currently updating
* fill the buffer with segements unless the sourceBuffers are
* currently updating
*
* Note: this function should only ever be called by monitorBuffer_
* and never directly
*
* @private
*/
@ -980,7 +995,7 @@ export default class SegmentLoader extends videojs.EventTarget {
if (!this.pendingSegment_) {
this.state = 'READY';
if (!this.paused()) {
this.fillBuffer_();
this.monitorBuffer_();
}
return;
}
@ -1015,7 +1030,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.trigger('progress');
if (!this.paused()) {
this.fillBuffer_();
this.monitorBuffer_();
}
}

112
test/master-playlist-controller.test.js

@ -35,6 +35,16 @@ QUnit.module('MasterPlaylistController', {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
// Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
// we have to use clock.tick to get the expected side effects of
// SegmentLoader#handleUpdateEnd_
this.clock.tick(1);
};
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
},
afterEach() {
@ -63,9 +73,9 @@ QUnit.test('throws error when given an empty URL', function(assert) {
QUnit.test('obeys none preload option', function(assert) {
this.player.preload('none');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
@ -78,9 +88,9 @@ QUnit.test('obeys none preload option', function(assert) {
QUnit.test('obeys auto preload option', function(assert) {
this.player.preload('auto');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
@ -93,9 +103,9 @@ QUnit.test('obeys auto preload option', function(assert) {
QUnit.test('obeys metadata preload option', function(assert) {
this.player.preload('metadata');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
@ -109,9 +119,9 @@ QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
let resyncs = 0;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mediaSource.trigger('sourceopen');
let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
@ -137,9 +147,9 @@ QUnit.test('does not resync the segmentLoader when no fast quality change occurs
let resyncs = 0;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mediaSource.trigger('sourceopen');
let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
@ -172,7 +182,7 @@ QUnit.test('if buffered, will request second segment byte range', function(asser
openMediaSource(this.player, this.clock);
// playlist
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
return videojs.createTimeRanges([[0, 20]]);
@ -180,7 +190,7 @@ QUnit.test('if buffered, will request second segment byte range', function(asser
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
this.masterPlaylistController.mainSegmentLoader_.fetchAtBuffer_ = true;
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(10 * 1000);
@ -198,11 +208,11 @@ QUnit.test('re-initializes the combined playlist loader when switching sources',
function(assert) {
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// segment
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// change the source
this.player.src({
src: 'manifest/master.m3u8',
@ -237,9 +247,9 @@ QUnit.test('updates the combined segment loader on live playlist refreshes', fun
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
updates.push(update);
@ -259,9 +269,9 @@ function(assert) {
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.on('progress', function() {
progressCount++;
@ -269,7 +279,7 @@ function(assert) {
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
assert.equal(progressCount, 1, 'fired a progress event');
@ -287,13 +297,13 @@ QUnit.test('updates the enabled track when switching audio groups', function(ass
this.requests.shift().respond(200, null,
manifests.multipleAudioGroupsCombinedMain);
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// init segment
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// video segment
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// audio media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// ignore audio segment requests
this.requests.length = 0;
@ -327,7 +337,7 @@ QUnit.test('blacklists switching from video+audio playlists to audio only', func
'#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
'media1.m3u8\n');
// media1
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1],
@ -354,7 +364,7 @@ QUnit.test('blacklists switching from audio-only playlists to video+audio', func
'media1.m3u8\n');
// media1
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
'selected audio only');
@ -384,7 +394,7 @@ QUnit.test('blacklists switching from video-only playlists to video+audio', func
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
'selected video only');
@ -415,7 +425,7 @@ function(assert) {
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
'selected HE-AAC stream');
@ -435,9 +445,9 @@ QUnit.test('blacklists the current playlist when audio changes in Firefox 48 & b
videojs.Hls.supportsAudioInfoChange_ = () => false;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
let media = this.masterPlaylistController.media();
@ -468,9 +478,9 @@ QUnit.test('updates the combined segment loader on media changes', function(asse
this.masterPlaylistController.mainSegmentLoader_.bandwidth = 1;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
updates.push(update);
@ -481,10 +491,10 @@ QUnit.test('updates the combined segment loader on media changes', function(asse
// downloading the new segment will update bandwidth and cause a
// playlist change
// segment 0
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.ok(updates.length > 0, 'updated the segment list');
// verify stats
@ -505,9 +515,9 @@ QUnit.test('selects a playlist after main/combined segment downloads', function(
this.masterPlaylistController.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// "downloaded" a segment
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
@ -528,9 +538,9 @@ QUnit.test('updates the duration after switching playlists', function(assert) {
this.masterPlaylistController.bandwidth = 1e20;
// master
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
this.masterPlaylistController.selectPlaylist = () => {
selectedPlaylist = true;
@ -544,10 +554,10 @@ QUnit.test('updates the duration after switching playlists', function(assert) {
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment 0
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[2]);
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
// media1
standardXHRResponse(this.requests[3]);
this.standardXHRResponse(this.requests[3]);
assert.ok(selectedPlaylist, 'selected playlist');
assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
'updates the duration');
@ -566,22 +576,22 @@ QUnit.test('playlist selection uses systemBandwidth', function(assert) {
this.player.height(900);
// master
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
assert.ok(/media3\.m3u8/i.test(this.requests[1].url), 'Selected the highest rendition');
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment 0
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[2]);
// 20ms have passed to upload 1kb that gives us a throughput of 1024 / 20 * 8 * 1000 = 409600
this.clock.tick(20);
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
// systemBandwidth is 1 / (1 / 8192000 + 1 / 409600) = ~390095
// media1
standardXHRResponse(this.requests[3]);
this.standardXHRResponse(this.requests[3]);
assert.ok(/media\.m3u8/i.test(this.requests[3].url), 'Selected the rendition < 390095');
assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
@ -600,9 +610,9 @@ function(assert) {
this.masterPlaylistController.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
assert.equal(this.masterPlaylistController.requestOptions_.timeout,
this.masterPlaylistController.masterPlaylistLoader_.targetDuration * 1.5 *
@ -618,9 +628,9 @@ function(assert) {
// Downloading segment should cause media change and timeout removal
// segment 0
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[2]);
// Download new segment after media change
standardXHRResponse(this.requests[3]);
this.standardXHRResponse(this.requests[3]);
assert.ok(this.masterPlaylistController
.masterPlaylistLoader_.isLowestEnabledRendition_(), 'On lowest rendition');
@ -777,12 +787,12 @@ QUnit.test('calls to update cues on new media', function(assert) {
this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(callCount, 0, 'no call to update cues on master');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(callCount, 1, 'calls to update cues on first media');
@ -809,7 +819,7 @@ QUnit.test('calls to update cues on media when no master', function(assert) {
this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(callCount, 1, 'calls to update cues on first media');

74
test/segment-loader.test.js

@ -77,12 +77,14 @@ QUnit.test('fails without required initialization options', function(assert) {
QUnit.test('load waits until a playlist and mime type are specified to proceed',
function(assert) {
loader.load();
assert.equal(loader.state, 'INIT', 'waiting in init');
assert.equal(loader.paused(), false, 'not paused');
loader.playlist(playlistWithDuration(10));
assert.equal(this.requests.length, 0, 'have not made a request yet');
loader.mimeType(this.mimeType);
this.clock.tick(1);
assert.equal(this.requests.length, 1, 'made a request');
assert.equal(loader.state, 'WAITING', 'transitioned states');
@ -97,6 +99,7 @@ QUnit.test('calling mime type and load begins buffering', function(assert) {
loader.mimeType(this.mimeType);
assert.equal(loader.state, 'INIT', 'still in the init state');
loader.load();
this.clock.tick(1);
assert.equal(loader.state, 'WAITING', 'moves to the ready state');
assert.ok(!loader.paused(), 'loading is not paused');
@ -107,6 +110,8 @@ QUnit.test('calling load is idempotent', function(assert) {
loader.playlist(playlistWithDuration(20));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.state, 'WAITING', 'moves to the ready state');
assert.equal(this.requests.length, 1, 'made one request');
@ -136,6 +141,7 @@ QUnit.test('calling load should unpause', function(assert) {
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.paused(), false, 'loading unpauses');
loader.pause();
@ -168,6 +174,8 @@ QUnit.test('regularly checks the buffer while unpaused', function(assert) {
loader.playlist(playlistWithDuration(90));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
sourceBuffer = mediaSource.sourceBuffers[0];
// fill the buffer
@ -197,6 +205,7 @@ QUnit.test('does not check the buffer while paused', function(assert) {
loader.playlist(playlistWithDuration(90));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
sourceBuffer = mediaSource.sourceBuffers[0];
loader.pause();
@ -218,6 +227,7 @@ QUnit.test('calculates bandwidth after downloading a segment', function(assert)
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// some time passes and a response is received
this.clock.tick(100);
@ -238,6 +248,7 @@ QUnit.test('segment request timeouts reset bandwidth', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// a lot of time passes so the request times out
this.requests[0].timedout = true;
@ -282,6 +293,7 @@ QUnit.test('appending a segment triggers progress', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// some time passes and a response is received
this.requests[0].response = new Uint8Array(10).buffer;
@ -299,6 +311,7 @@ QUnit.test('only requests one segment at a time', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// a bunch of time passes without recieving a response
this.clock.tick(20 * 1000);
@ -309,6 +322,7 @@ QUnit.test('only appends one segment at a time', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// some time passes and a segment is received
this.clock.tick(100);
@ -337,10 +351,11 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
loader.playlist(playlist);
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
sourceBuffer = mediaSource.sourceBuffers[0];
// buffer some content and switch playlists on progress
this.clock.tick(1);
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
loader.on('progress', function f() {
@ -350,9 +365,11 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
playlist.uri = 'alternate.m3u8';
playlist.endList = false;
loader.playlist(playlist);
});
this.clock.tick(1);
}.bind(this));
sourceBuffer.buffered = videojs.createTimeRanges([[0, 5]]);
sourceBuffer.trigger('updateend');
this.clock.tick(1);
// the next segment doesn't increase the buffer at all
assert.equal(this.requests[0].url, '0.ts', 'requested the same segment');
@ -360,13 +377,14 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
sourceBuffer.trigger('updateend');
this.clock.tick(1);
// so the loader should try the next segment
assert.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
// verify stats
assert.equal(loader.mediaBytesTransferred, 20, '20 bytes');
assert.equal(loader.mediaTransferDuration, 2, '2 ms (clocks above)');
assert.equal(loader.mediaTransferDuration, 1, '1 ms (clocks above)');
assert.equal(loader.mediaRequests, 2, '2 requests');
});
@ -464,6 +482,7 @@ QUnit.test('downloads init segments if specified', function(assert) {
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
let sourceBuffer = mediaSource.sourceBuffers[0];
assert.equal(this.requests.length, 2, 'made requests');
@ -486,6 +505,7 @@ QUnit.test('downloads init segments if specified', function(assert) {
// append the segment
sourceBuffer.buffered = videojs.createTimeRanges([[0, 10]]);
sourceBuffer.trigger('updateend');
this.clock.tick(1);
assert.equal(this.requests.length, 1, 'made a request');
assert.equal(this.requests[0].url, '1.ts',
@ -513,6 +533,8 @@ QUnit.test('detects init segment changes and downloads it', function(assert) {
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
let sourceBuffer = mediaSource.sourceBuffers[0];
assert.equal(this.requests.length, 2, 'made requests');
@ -537,6 +559,7 @@ QUnit.test('detects init segment changes and downloads it', function(assert) {
// append the segment
sourceBuffer.buffered = videojs.createTimeRanges([[0, 10]]);
sourceBuffer.trigger('updateend');
this.clock.tick(1);
assert.equal(this.requests.length, 2, 'made requests');
assert.equal(this.requests[0].url, 'init0.mp4', 'requested the init segment');
@ -550,11 +573,15 @@ QUnit.test('cancels outstanding requests on abort', function(assert) {
loader.playlist(playlistWithDuration(20));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.xhr_.segmentXhr.onreadystatechange = function() {
throw new Error('onreadystatechange should not be called');
};
loader.abort();
this.clock.tick(1);
assert.ok(this.requests[0].aborted, 'aborted the first request');
assert.equal(this.requests.length, 2, 'started a new request');
assert.equal(loader.state, 'WAITING', 'back to the waiting state');
@ -564,11 +591,14 @@ QUnit.test('abort does not cancel segment processing in progress', function(asse
loader.playlist(playlistWithDuration(20));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
loader.abort();
this.clock.tick(1);
assert.equal(loader.state, 'APPENDING', 'still appending');
// verify stats
@ -584,12 +614,14 @@ QUnit.test('sets the timestampOffset on timeline change', function(assert) {
loader.playlist(playlist);
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// segment 0
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(1);
// segment 1, discontinuity
this.requests[0].response = new Uint8Array(10).buffer;
@ -611,11 +643,14 @@ QUnit.test('tracks segment end times as they are buffered', function(assert) {
loader.playlist(playlist);
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(1);
assert.equal(playlist.segments[0].end, 9.5, 'updated duration');
// verify stats
@ -629,6 +664,8 @@ QUnit.test('segment 404s should trigger an error', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.on('error', function(error) {
errors.push(error);
});
@ -647,6 +684,8 @@ QUnit.test('segment 5xx status codes trigger an error', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.on('error', function(error) {
errors.push(error);
});
@ -665,6 +704,8 @@ QUnit.test('fires ended at the end of a playlist', function(assert) {
loader.playlist(playlistWithDuration(10));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.mediaSource_ = {
readyState: 'open',
sourceBuffers: mediaSource.sourceBuffers,
@ -678,6 +719,8 @@ QUnit.test('fires ended at the end of a playlist', function(assert) {
this.requests.shift().respond(200, null, '');
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(1);
assert.equal(endOfStreams, 1, 'triggered ended');
// verify stats
@ -694,6 +737,8 @@ QUnit.test('live playlists do not trigger ended', function(assert) {
loader.playlist(playlist);
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.mediaSource_ = {
readyState: 'open',
sourceBuffers: mediaSource.sourceBuffers,
@ -706,6 +751,8 @@ QUnit.test('live playlists do not trigger ended', function(assert) {
this.requests.shift().respond(200, null, '');
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(1);
assert.equal(endOfStreams, 0, 'did not trigger ended');
// verify stats
@ -717,6 +764,8 @@ QUnit.test('remains ready if there are no segments', function(assert) {
loader.playlist(playlistWithDuration(0));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.state, 'READY', 'in the ready state');
});
@ -724,6 +773,7 @@ QUnit.test('dispose cleans up outstanding work', function(assert) {
loader.playlist(playlistWithDuration(20));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.dispose();
assert.ok(this.requests[0].aborted, 'aborted segment request');
@ -747,6 +797,8 @@ QUnit.test('calling load with an encrypted segment requests key and segment', fu
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.state, 'WAITING', 'moves to the ready state');
assert.ok(!loader.paused(), 'loading is not paused');
assert.equal(this.requests.length, 2, 'requested a segment and key');
@ -758,12 +810,16 @@ QUnit.test('cancels outstanding key request on abort', function(assert) {
loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.xhr_.keyXhr.onreadystatechange = function() {
throw new Error('onreadystatechange should not be called');
};
assert.equal(this.requests.length, 2, 'requested a segment and key');
loader.abort();
this.clock.tick(1);
assert.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
assert.ok(this.requests[0].aborted, 'aborted the first key request');
assert.equal(this.requests.length, 4, 'started a new request');
@ -774,6 +830,7 @@ QUnit.test('dispose cleans up key requests for encrypted segments', function(ass
loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.dispose();
assert.equal(this.requests.length, 2, 'requested a segment and key');
@ -788,6 +845,8 @@ QUnit.test('key 404s should trigger an error', function(assert) {
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.on('error', function(error) {
errors.push(error);
});
@ -808,6 +867,8 @@ QUnit.test('key 5xx status codes trigger an error', function(assert) {
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
loader.on('error', function(error) {
errors.push(error);
});
@ -831,6 +892,7 @@ QUnit.test('the key is saved to the segment in the correct format', function(ass
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// stop processing so we can examine segment info
loader.processResponse_ = function() {};
@ -866,6 +928,7 @@ function(assert) {
loader.playlist(playlistWithDuration(10, {isEncrypted: true, mediaSequence: 5}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
// stop processing so we can examine segment info
loader.processResponse_ = function() {};
@ -899,6 +962,7 @@ QUnit.test('segment with key has decrypted bytes appended during processing', fu
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
segmentRequest = this.requests.pop();
segmentRequest.response = new Uint8Array(8).buffer;
@ -929,6 +993,7 @@ QUnit.test('calling load with an encrypted segment waits for both key and segmen
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.state, 'WAITING', 'moves to waiting state');
assert.equal(this.requests.length, 2, 'requested a segment and key');
@ -954,6 +1019,7 @@ QUnit.test('key request timeouts reset bandwidth', function(assert) {
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
assert.equal(this.requests[1].url, '0.ts', 'requested the first segment');
@ -994,6 +1060,7 @@ function(assert) {
loader.playlist(playlist);
loader.mimeType(this.mimeType);
loader.load();
this.clock.tick(1);
assert.equal(loader.pendingSegment_.uri, '0.ts', 'retrieving first segment');
assert.equal(loader.state, 'WAITING', 'waiting for response');
@ -1009,6 +1076,7 @@ function(assert) {
// finish append
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(1);
assert.equal(loader.pendingSegment_.uri, '1.ts', 'retrieving second segment');
assert.equal(loader.state, 'WAITING', 'waiting for response');

1
test/test-helpers.js

@ -263,6 +263,7 @@ export const openMediaSource = function(player, clock) {
type: 'sourceopen',
swfId: player.tech_.el().id
});
clock.tick(1);
};
export const standardXHRResponse = function(request, data) {

287
test/videojs-contrib-hls.test.js

@ -96,6 +96,15 @@ QUnit.module('HLS', {
// save and restore browser detection for the Firefox-specific tests
this.old.IS_FIREFOX = videojs.browser.IS_FIREFOX;
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
// Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
// we have to use clock.tick to get the expected side effects of
// SegmentLoader#handleUpdateEnd_
this.clock.tick(1);
};
// setup a player
this.player = createPlayer();
},
@ -145,7 +154,7 @@ QUnit.test('starts playing if autoplay is specified', function(assert) {
// make sure play() is called *after* the media source opens
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
assert.ok(!this.player.paused(), 'not paused');
});
@ -157,8 +166,8 @@ QUnit.test('stats are reset on each new source', function(assert) {
// make sure play() is called *after* the media source opens
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'stat is set');
this.player.src({
@ -177,7 +186,7 @@ QUnit.test('XHR requests first byte range on play', function(assert) {
this.clock.tick(1);
this.player.tech_.trigger('play');
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
assert.equal(this.requests[1].headers.Range, 'bytes=0-522827');
});
@ -188,10 +197,10 @@ QUnit.test('Seeking requests correct byte range', function(assert) {
});
this.player.tech_.trigger('play');
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.clock.tick(1);
this.player.currentTime(41);
this.clock.tick(1);
this.clock.tick(2);
assert.equal(this.requests[2].headers.Range, 'bytes=2299992-2835603');
});
@ -208,7 +217,7 @@ QUnit.test('autoplay seeks to the live point after playlist load', function(asse
});
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.clock.tick(1);
assert.notEqual(currentTime, 0, 'seeked on autoplay');
@ -227,7 +236,7 @@ QUnit.test('autoplay seeks to the live point after media source open', function(
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.clock.tick(1);
@ -242,7 +251,7 @@ QUnit.test('duration is set when the source opens after the playlist is loaded',
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
assert.equal(this.player.tech_.hls.mediaSource.duration,
@ -269,7 +278,7 @@ QUnit.test('codecs are passed to the source buffer', function(assert) {
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:CODECS="avc1.dd00dd, mp4a.40.f"\n' +
'media.m3u8\n');
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(codecs.length, 1, 'created a source buffer');
assert.equal(codecs[0], 'video/mp2t; codecs="avc1.dd00dd, mp4a.40.f"', 'specified the codecs');
});
@ -296,7 +305,7 @@ QUnit.test('creates a PlaylistLoader on init', function(assert) {
openMediaSource(this.player, this.clock);
assert.equal(this.requests[0].aborted, true, 'aborted previous src');
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
assert.ok(this.player.tech_.hls.playlists.master,
'set the master playlist');
assert.ok(this.player.tech_.hls.playlists.media(),
@ -320,7 +329,7 @@ QUnit.test('sets the duration if one is available on the playlist', function(ass
events++;
});
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
assert.equal(this.player.tech_.hls.mediaSource.duration,
40,
'set the duration');
@ -340,7 +349,7 @@ QUnit.test('estimates individual segment durations if needed', function(assert)
changes++;
});
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
assert.strictEqual(this.player.tech_.hls.mediaSource.duration,
this.player.tech_.hls.playlists.media().segments.length * 10,
'duration is updated');
@ -383,8 +392,8 @@ QUnit.test('starts downloading a segment on loadedmetadata', function(assert) {
};
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[1]);
assert.strictEqual(this.requests[1].url,
absoluteUrl('manifest/media-00001.ts'),
'the first segment is requested');
@ -409,8 +418,8 @@ QUnit.test('re-initializes the handler for each source', function(assert) {
openMediaSource(this.player, this.clock);
firstPlaylists = this.player.tech_.hls.playlists;
firstMSE = this.player.tech_.hls.mediaSource;
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
masterPlaylistController.mainSegmentLoader_.sourceUpdater_.sourceBuffer_.abort = () => {
aborts++;
@ -453,9 +462,9 @@ QUnit.test('downloads media playlists after loading the master', function(assert
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 20e10;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[2]);
assert.strictEqual(this.requests[0].url,
'manifest/master.m3u8',
@ -522,9 +531,9 @@ QUnit.test('upshifts if the initial bandwidth hint is high', function(assert) {
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 10e20;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[2]);
assert.strictEqual(
this.requests[0].url,
@ -555,9 +564,9 @@ QUnit.test('downshifts if the initial bandwidth hint is low', function(assert) {
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 100;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[2]);
assert.strictEqual(this.requests[0].url,
'manifest/master.m3u8',
@ -596,9 +605,9 @@ QUnit.test('buffer checks are noops when only the master is ready', function(ass
});
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// ignore any outstanding segment requests
this.requests.length = 0;
@ -614,7 +623,7 @@ QUnit.test('buffer checks are noops when only the master is ready', function(ass
// force media1 to be requested
this.player.tech_.hls.bandwidth = 1;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.clock.tick(10 * 1000);
assert.strictEqual(1, this.requests.length, 'one request was made');
@ -634,7 +643,7 @@ QUnit.test('selects a playlist below the current bandwidth', function(assert) {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// the default playlist has a really high bitrate
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
@ -682,7 +691,7 @@ QUnit.test('raises the minimum bitrate for a stream proportionially', function(a
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// the default playlist's bandwidth + 10% is assert.equal to the current bandwidth
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
@ -709,7 +718,7 @@ QUnit.test('uses the lowest bitrate if no other is suitable', function(assert) {
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// the lowest bitrate playlist is much greater than 1b/s
this.player.tech_.hls.bandwidth = 1;
@ -733,7 +742,7 @@ QUnit.test('selects the correct rendition by tech dimensions', function(assert)
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
hls = this.player.tech_.hls;
@ -808,7 +817,7 @@ QUnit.test('selects the highest bitrate playlist when the player dimensions are
'#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.hls.bandwidth = 1e10;
this.player.width(1024);
@ -842,7 +851,7 @@ QUnit.test('filters playlists that are currently excluded', function(assert) {
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// exclude the current playlist
this.player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
@ -882,7 +891,7 @@ QUnit.test('does not blacklist compatible H.264 codec strings', function(assert)
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
master = this.player.tech_.hls.playlists.master;
assert.strictEqual(typeof master.playlists[0].excludeUntil,
'undefined',
@ -915,7 +924,7 @@ QUnit.test('does not blacklist compatible AAC codec strings', function(assert) {
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
master = this.player.tech_.hls.playlists.master;
assert.strictEqual(typeof master.playlists[0].excludeUntil,
'undefined',
@ -934,7 +943,7 @@ QUnit.test('cancels outstanding XHRs when seeking', function(assert) {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.player.tech_.hls.media = {
segments: [{
uri: '0.ts',
@ -947,7 +956,7 @@ QUnit.test('cancels outstanding XHRs when seeking', function(assert) {
// attempt to seek while the download is in progress
this.player.currentTime(7);
this.clock.tick(1);
this.clock.tick(2);
assert.ok(this.requests[1].aborted, 'XHR aborted');
assert.strictEqual(this.requests.length, 3, 'opened new XHR');
@ -959,7 +968,7 @@ QUnit.test('does not abort segment loading for in-buffer seeking', function(asse
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.buffered = function() {
return videojs.createTimeRange(0, 20);
};
@ -991,9 +1000,9 @@ QUnit.test('segment 404 should trigger blacklisting of media', function(assert)
this.player.tech_.hls.bandwidth = 20000;
// master
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
media = this.player.tech_.hls.playlists.media_;
@ -1075,7 +1084,7 @@ QUnit.test('fire loadedmetadata once we successfully load a playlist', function(
count += 1;
});
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(count, 0,
'loadedMedia not triggered before requesting playlist');
// media
@ -1085,7 +1094,7 @@ QUnit.test('fire loadedmetadata once we successfully load a playlist', function(
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(count, 1,
'loadedMedia triggered after successful recovery from 404');
@ -1100,7 +1109,7 @@ QUnit.test('sets seekable and duration for live playlists', function(assert) {
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
assert.equal(this.player.tech_.hls.mediaSource.seekable.length,
1,
@ -1124,7 +1133,7 @@ QUnit.test('live playlist starts with correct currentTime value', function(asser
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.player.tech_.hls.playlists.trigger('loadedmetadata');
@ -1148,7 +1157,7 @@ QUnit.test('estimates seekable ranges for live streams that have been paused for
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.hls.playlists.media().mediaSequence = 172;
this.player.tech_.hls.playlists.media().syncInfo = {
mediaSequence: 130,
@ -1347,7 +1356,7 @@ QUnit.test('can seek before the source buffer opens', function(assert) {
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.triggerReady();
this.player.currentTime(1);
@ -1363,15 +1372,15 @@ QUnit.test('resets the switching algorithm if a request times out', function(ass
this.player.tech_.hls.bandwidth = 1e20;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media.m3u8
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// simulate a segment timeout
this.requests[0].timedout = true;
// segment
this.requests.shift().abort();
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.strictEqual(this.player.tech_.hls.playlists.media(),
this.player.tech_.hls.playlists.master.playlists[1],
@ -1423,8 +1432,8 @@ QUnit.test('remove event handlers on dispose', function(assert) {
});
openMediaSource(player, this.clock);
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[1]);
player.dispose();
@ -1517,7 +1526,7 @@ QUnit.test('re-emits mediachange events', function(assert) {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.hls.playlists.trigger('mediachange');
assert.strictEqual(mediaChanges, 1, 'fired mediachange');
@ -1568,12 +1577,15 @@ QUnit.test('calling play() at the end of a video replays', function(assert) {
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(this.requests.shift());
this.clock.tick(1);
this.standardXHRResponse(this.requests.shift());
this.player.tech_.ended = function() {
return true;
};
this.player.tech_.trigger('play');
this.clock.tick(1);
assert.equal(seekTime, 0, 'seeked to the beginning');
// verify stats
@ -1592,6 +1604,8 @@ QUnit.test('keys are resolved relative to the master playlist', function(assert)
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
'playlist/playlist.m3u8\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-TARGETDURATION:15\n' +
@ -1599,6 +1613,8 @@ QUnit.test('keys are resolved relative to the master playlist', function(assert)
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
assert.equal(this.requests.length, 2, 'requested the key');
assert.equal(this.requests[0].url,
absoluteUrl('video/playlist/keys/key.php'),
@ -1621,6 +1637,8 @@ QUnit.test('keys are resolved relative to their containing playlist', function(a
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
assert.equal(this.requests.length, 2, 'requested a key');
assert.equal(this.requests[0].url,
absoluteUrl('video/keys/key.php'),
@ -1644,11 +1662,13 @@ QUnit.test('seeking should abort an outstanding key request and create a new one
'#EXTINF:9,\n' +
'http://media.example.com/fileSequence2.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
// segment 1
standardXHRResponse(this.requests.pop());
this.standardXHRResponse(this.requests.pop());
this.player.currentTime(11);
this.clock.tick(1);
this.clock.tick(2);
assert.ok(this.requests[0].aborted, 'the key XHR should be aborted');
// aborted key 1
this.requests.shift();
@ -1682,13 +1702,16 @@ QUnit.test('switching playlists with an outstanding key request aborts request a
});
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.clock.tick(1);
// master playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media playlist
this.requests.shift().respond(200, null, media);
this.clock.tick(1);
// first segment of the original media playlist
standardXHRResponse(this.requests.pop());
this.standardXHRResponse(this.requests.pop());
assert.equal(this.requests.length, 1, 'key request only one outstanding');
keyXhr = this.requests.shift();
@ -1696,6 +1719,7 @@ QUnit.test('switching playlists with an outstanding key request aborts request a
this.player.tech_.hls.playlists.trigger('mediachanging');
this.player.tech_.hls.playlists.trigger('mediachange');
this.clock.tick(1);
assert.ok(keyXhr.aborted, 'key request aborted');
assert.equal(this.requests.length, 2, 'loaded key and segment');
@ -1719,9 +1743,9 @@ QUnit.test('does not download segments if preload option set to none', function(
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.clock.tick(10 * 1000);
this.requests = this.requests.filter(function(request) {
@ -1747,9 +1771,9 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func
});
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.player.tech_.hls.selectPlaylist();
assert.ok(true, 'should not throw');
@ -1773,6 +1797,8 @@ QUnit.test('resolves relative key URLs against the playlist', function(assert) {
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
assert.equal(this.requests[0].url,
'https://example.com/key.php?r=52',
'resolves the key URL');
@ -1789,9 +1815,9 @@ QUnit.test('adds 1 default audio track if we have not parsed any and the playlis
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
@ -1817,9 +1843,9 @@ QUnit.test('adds 1 default audio track if in flash mode', function(assert) {
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
@ -1838,9 +1864,9 @@ QUnit.test('adds audio tracks if we have parsed some from a playlist', function(
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
let vjsAudioTracks = this.player.audioTracks();
assert.equal(vjsAudioTracks.length, 3, '3 active vjs tracks');
@ -1871,9 +1897,9 @@ QUnit.test('when audioinfo changes on an independent audio track in Firefox 48 &
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(audioTracks.length, 3, 'three audio track after load');
let defaultTrack = mpc.activeAudioGroup().filter((track) => {
@ -1910,8 +1936,8 @@ QUnit.test('audioinfo changes with one track, blacklist playlist on Firefox 48 &
assert.equal(audioTracks.length, 0, 'zero audio tracks at load time');
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(audioTracks.length, 1, 'one audio track after load');
let mpc = this.player.tech_.hls.masterPlaylistController_;
@ -1948,14 +1974,14 @@ QUnit.test('changing audioinfo for muxed audio blacklists the current playlist i
let mpc = hls.masterPlaylistController_;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// video media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// video segments
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// audio media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// ignore audio requests
this.requests.length = 0;
assert.equal(audioTracks.length, 3, 'three audio track after load');
@ -2019,7 +2045,7 @@ QUnit.test('cleans up the buffer when loading live segments', function(assert) {
};
this.player.tech_.hls.bandwidth = 20e10;
this.player.tech_.triggerReady();
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.player.tech_.hls.playlists.trigger('loadedmetadata');
this.player.tech_.trigger('canplay');
@ -2027,9 +2053,10 @@ QUnit.test('cleans up the buffer when loading live segments', function(assert) {
return false;
};
this.player.tech_.trigger('play');
this.clock.tick(1);
// request first playable segment
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8',
'master playlist requested');
@ -2075,7 +2102,7 @@ QUnit.test('cleans up the buffer based on currentTime when loading a live segmen
};
this.player.tech_.hls.bandwidth = 20e10;
this.player.tech_.triggerReady();
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.player.tech_.hls.playlists.trigger('loadedmetadata');
this.player.tech_.trigger('canplay');
@ -2084,13 +2111,14 @@ QUnit.test('cleans up the buffer based on currentTime when loading a live segmen
};
this.player.tech_.trigger('play');
this.clock.tick(1);
// Change seekable so that it starts *after* the currentTime which was set
// based on the previous seekable range (the end of 80)
seekable = videojs.createTimeRanges([[100, 120]]);
// request first playable segment
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
assert.equal(removes.length, 1, 'remove called');
@ -2129,10 +2157,10 @@ QUnit.test('cleans up the buffer when loading VOD segments', function(assert) {
this.player.width(640);
this.player.height(360);
this.player.tech_.hls.bandwidth = 20e10;
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
this.player.currentTime(120);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[2]);
assert.strictEqual(this.requests[0].url, 'manifest/master.m3u8',
'master playlist requested');
@ -2154,9 +2182,9 @@ QUnit.test('when mediaGroup changes enabled track should not change', function(a
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// video media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
let hls = this.player.tech_.hls;
let mpc = hls.masterPlaylistController_;
let audioTracks = this.player.audioTracks();
@ -2170,13 +2198,10 @@ QUnit.test('when mediaGroup changes enabled track should not change', function(a
this.requests.length = 0;
// force mpc to select a playlist from a new media group
mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
// TODO extra segment requests!!!
this.requests.shift();
this.requests.shift();
this.clock.tick(1);
// video media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
audioTracks = this.player.audioTracks();
@ -2198,6 +2223,7 @@ QUnit.test('when mediaGroup changes enabled track should not change', function(a
// swap back to the old media group
// this playlist is already loaded so no new requests are made
mpc.masterPlaylistLoader_.media(mpc.master().playlists[3]);
this.clock.tick(1);
assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
audioTracks = this.player.audioTracks();
@ -2222,9 +2248,9 @@ QUnit.test('Allows specifying the beforeRequest function on the player', functio
beforeRequestCalled = true;
};
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.ok(beforeRequestCalled, 'beforeRequest was called');
@ -2245,7 +2271,7 @@ QUnit.test('Allows specifying the beforeRequest function globally', function(ass
});
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.ok(beforeRequestCalled, 'beforeRequest was called');
@ -2273,11 +2299,11 @@ QUnit.test('Allows overriding the global beforeRequest function', function(asser
beforeLocalRequestCalled++;
};
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// ts
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
'for the media playlist and media');
@ -2341,9 +2367,9 @@ QUnit.test('populates quality levels list when available', function(assert) {
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(addCount, 4, 'four levels added from master');
assert.equal(changeCount, 1, 'selected initial quality level');
@ -2356,6 +2382,16 @@ QUnit.module('HLS Integration', {
this.mse = useFakeMediaSource();
this.tech = new (videojs.getTech('Html5'))({});
this.clock = this.env.clock;
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
// Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
// we have to use clock.tick to get the expected side effects of
// SegmentLoader#handleUpdateEnd_
this.clock.tick(1);
};
videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
},
afterEach() {
@ -2375,9 +2411,9 @@ QUnit.test('does not error when MediaSource is not defined', function(assert) {
hls.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.ok(true, 'did not throw an exception');
});
@ -2390,9 +2426,9 @@ QUnit.test('aborts all in-flight work when disposed', function(assert) {
hls.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
hls.dispose();
assert.ok(this.requests[0].aborted, 'aborted the old segment request');
@ -2411,12 +2447,12 @@ QUnit.test('stats are reset on dispose', function(assert) {
hls.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
assert.equal(hls.stats.mediaBytesTransferred, 1024, 'stat is set');
hls.dispose();
@ -2467,19 +2503,19 @@ QUnit.test('downloads additional playlists if required', function(assert) {
hls.mediaSource.trigger('sourceopen');
hls.bandwidth = 1;
// master
standardXHRResponse(this.requests[0]);
this.standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
this.standardXHRResponse(this.requests[1]);
originalPlaylist = hls.playlists.media();
// the playlist selection is revisited after a new segment is downloaded
this.requests[2].bandwidth = 3000000;
// segment
standardXHRResponse(this.requests[2]);
this.standardXHRResponse(this.requests[2]);
hls.mediaSource.sourceBuffers[0].trigger('updateend');
// new media
standardXHRResponse(this.requests[3]);
this.standardXHRResponse(this.requests[3]);
assert.ok((/manifest\/media\d+.m3u8$/).test(this.requests[3].url),
'made a playlist request');
@ -2506,16 +2542,16 @@ QUnit.test('waits to download new segments until the media playlist is stable',
// make sure we stay on the lowest variant
hls.bandwidth = 1;
// master
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// media1
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// source buffer created after media source is open and first media playlist is selected
sourceBuffer = hls.mediaSource.sourceBuffers[0];
// segment 0
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
// no time has elapsed, so bandwidth is really high and we'll switch
// playlists
sourceBuffer.trigger('updateend');
@ -2525,7 +2561,7 @@ QUnit.test('waits to download new segments until the media playlist is stable',
assert.equal(this.requests.length, 1, 'delays segment fetching');
// another media playlist
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
this.clock.tick(10 * 1000);
assert.equal(this.requests.length, 1, 'resumes segment fetching');
@ -2622,6 +2658,17 @@ QUnit.module('HLS - Encryption', {
this.requests = this.env.requests;
this.mse = useFakeMediaSource();
this.tech = new (videojs.getTech('Html5'))({});
this.clock = this.env.clock;
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
// Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
// we have to use clock.tick to get the expected side effects of
// SegmentLoader#handleUpdateEnd_
this.clock.tick(1);
};
videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
},
afterEach() {
@ -2648,15 +2695,17 @@ QUnit.test('blacklists playlist if key requests fail', function(assert) {
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence53-A.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
// segment 1
if (/key\.php/i.test(this.requests[0].url)) {
standardXHRResponse(this.requests.pop());
this.standardXHRResponse(this.requests.pop());
} else {
standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift());
}
// fail key
this.requests.shift().respond(404);
assert.ok(hls.playlists.media().excludeUntil > 0,
'playlist blacklisted');
assert.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
@ -2680,9 +2729,10 @@ QUnit.test('treats invalid keys as a key request failure and blacklists playlist
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence52-B.ts\n' +
'#EXT-X-ENDLIST\n');
this.clock.tick(1);
// segment request
standardXHRResponse(this.requests.pop());
this.standardXHRResponse(this.requests.pop());
assert.equal(this.requests[0].url,
'https://priv.example.com/key.php?r=52',
@ -2690,6 +2740,7 @@ QUnit.test('treats invalid keys as a key request failure and blacklists playlist
// keys *should* be 16 bytes long -- this one is too small
this.requests[0].response = new Uint8Array(1).buffer;
this.requests.shift().respond(200, null, '');
this.clock.tick(1);
// blacklist this playlist
assert.ok(hls.playlists.media().excludeUntil > 0,

Loading…
Cancel
Save