Browse Source

feat: Detect excessive unproductive segment downloads and exclude accordingly (#829)

This change adds two new events to `SegmentLoader`. The first `appendsdone` is fired whenever an append is complete on any main, audio, or subtitle loader. The second `playlistupdate` is fired whenever a playlist is changed on the segment loader. 

Using these new events `PlaybackWatcher` watches for the `SegmentLoaders` `buffered_()` function to return a different buffered time range after each append. If it finds that we are not changing the buffered time range after three appends it will exclude the playlist or disable and remove the text track causing the issue.

Finally `PlaybackWatcher`  will reset the buffered change counter whenever `playlistupdate` is fired on the `SegmentLoader` or when `seeking`/`seeked` fire on the `Tech`.
pull/842/head
Brandon Casey 5 years ago
committed by GitHub
parent
commit
4e1d2f3866
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 59
      src/master-playlist-controller.js
  2. 113
      src/playback-watcher.js
  3. 41
      src/ranges.js
  4. 2
      src/segment-loader.js
  5. 12
      src/videojs-http-streaming.js
  6. 47
      test/master-playlist-controller.test.js
  7. 270
      test/playback-watcher.test.js
  8. 35
      test/ranges.test.js
  9. 39
      test/videojs-http-streaming.test.js

59
src/master-playlist-controller.js

@ -143,6 +143,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
timeout: null
};
this.on('error', this.pauseLoading);
this.mediaTypes_ = createMediaTypes();
this.mediaSource = new window.MediaSource();
@ -881,10 +883,11 @@ export class MasterPlaylistController extends videojs.EventTarget {
const enabledPlaylists = playlists.filter(isEnabled);
const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === currentPlaylist;
if (playlists.length === 1) {
// Never blacklisting this playlist because it's the only playlist
videojs.log.warn('Problem encountered with the current ' +
'playlist. Trying again since it is the only playlist.');
// Don't blacklist the only playlist unless it was blacklisted
// forever
if (playlists.length === 1 && blacklistDuration !== Infinity) {
videojs.log.warn(`Problem encountered with playlist ${currentPlaylist.id}. ` +
'Trying again since it is the only playlist.');
this.tech_.trigger('retryplaylist');
return this.masterPlaylistLoader_.load(isFinalRendition);
@ -895,17 +898,30 @@ export class MasterPlaylistController extends videojs.EventTarget {
// it, instead of erring the player or retrying this playlist, clear out the current
// blacklist. This allows other playlists to be attempted in case any have been
// fixed.
videojs.log.warn('Removing all playlists from the blacklist because the last ' +
'rendition is about to be blacklisted.');
let reincluded = false;
playlists.forEach((playlist) => {
if (playlist.excludeUntil !== Infinity) {
// skip current playlist which is about to be blacklisted
if (playlist === currentPlaylist) {
return;
}
const excludeUntil = playlist.excludeUntil;
// a playlist cannot be reincluded if it wasn't excluded to begin with.
if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) {
reincluded = true;
delete playlist.excludeUntil;
}
});
// Technically we are retrying a playlist, in that we are simply retrying a previous
// playlist. This is needed for users relying on the retryplaylist event to catch a
// case where the player might be stuck and looping through "dead" playlists.
this.tech_.trigger('retryplaylist');
if (reincluded) {
videojs.log.warn('Removing other playlists from the exclusion list because the last ' +
'rendition is about to be excluded.');
// Technically we are retrying a playlist, in that we are simply retrying a previous
// playlist. This is needed for users relying on the retryplaylist event to catch a
// case where the player might be stuck and looping through "dead" playlists.
this.tech_.trigger('retryplaylist');
}
}
// Blacklist this playlist
@ -915,19 +931,26 @@ export class MasterPlaylistController extends videojs.EventTarget {
// Select a new playlist
const nextPlaylist = this.selectPlaylist();
if (!nextPlaylist) {
this.error = 'Playback cannot continue. No available working or supported playlists.';
this.trigger('error');
return;
}
const logFn = error.internal ? this.logger_ : videojs.log.warn;
const errorMessage = error.message ? (' ' + error.message) : '';
logFn(`${(error.internal ? 'Internal problem' : 'Problem')} encountered with the current playlist.` +
`${errorMessage} Switching to another playlist.`);
logFn(`${(error.internal ? 'Internal problem' : 'Problem')} encountered with playlist ${currentPlaylist.id}.` +
`${errorMessage} Switching to playlist ${nextPlaylist.id}.`);
return this.masterPlaylistLoader_.media(nextPlaylist, isFinalRendition);
}
/**
* Pause all segment loaders
* Pause all segment/playlist loaders
*/
pauseLoading() {
// pause all segment loaders
this.mainSegmentLoader_.pause();
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
this.audioSegmentLoader_.pause();
@ -935,6 +958,14 @@ export class MasterPlaylistController extends videojs.EventTarget {
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
this.subtitleSegmentLoader_.pause();
}
// pause all playlist loaders
this.masterPlaylistLoader_.pause();
Object.keys(this.mediaTypes_).forEach((type) => {
if (this.mediaTypes_[type].activePlaylistLoader) {
this.mediaTypes_[type].activePlaylistLoader.pause();
}
});
}
/**

113
src/playback-watcher.js

@ -11,6 +11,7 @@
import window from 'global/window';
import * as Ranges from './ranges';
import logger from './util/logger';
import videojs from 'video.js';
// Set of events that reset the playback-watcher time check logic and clear the timeout
const timerCancelEvents = [
@ -71,6 +72,7 @@ export default class PlaybackWatcher {
* @param {Object} options an object that includes the tech and settings
*/
constructor(options) {
this.masterPlaylistController_ = options.masterPlaylistController;
this.tech_ = options.tech;
this.seekable = options.seekable;
this.seekTo = options.seekTo;
@ -90,6 +92,29 @@ export default class PlaybackWatcher {
const cancelTimerHandler = () => this.cancelTimer_();
const fixesBadSeeksHandler = () => this.fixesBadSeeks_();
const mpc = this.masterPlaylistController_;
const loaderTypes = ['main', 'subtitle', 'audio'];
const loaderChecks = {};
loaderTypes.forEach((type) => {
loaderChecks[type] = {
reset: () => this.resetSegmentDownloads_(type),
updateend: () => this.checkSegmentDownloads_(type)
};
mpc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend);
// If a rendition switch happens during a playback stall where the buffer
// isn't changing we want to reset. We cannot assume that the new rendition
// will also be stalled, until after new appends.
mpc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset);
// Playback stalls should not be detected right after seeking.
// This prevents one segment playlists (single vtt or single segment content)
// from being detected as stalling. As the buffer will not change in those cases, since
// the buffer is the entire video duration.
this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset);
});
this.tech_.on('seekablechanged', fixesBadSeeksHandler);
this.tech_.on('waiting', waitingHandler);
this.tech_.on(timerCancelEvents, cancelTimerHandler);
@ -102,6 +127,12 @@ export default class PlaybackWatcher {
this.tech_.off('waiting', waitingHandler);
this.tech_.off(timerCancelEvents, cancelTimerHandler);
this.tech_.off('canplay', canPlayHandler);
loaderTypes.forEach((type) => {
mpc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend);
mpc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset);
this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset);
});
if (this.checkCurrentTimeTimeout_) {
window.clearTimeout(this.checkCurrentTimeTimeout_);
}
@ -126,6 +157,86 @@ export default class PlaybackWatcher {
window.setTimeout(this.monitorCurrentTime_.bind(this), 250);
}
/**
* Reset stalled download stats for a specific type of loader
*
* @param {string} type
* The segment loader type to check.
*
* @listens SegmentLoader#playlistupdate
* @listens Tech#seeking
* @listens Tech#seeked
*/
resetSegmentDownloads_(type) {
const loader = this.masterPlaylistController_[`${type}SegmentLoader_`];
if (this[`${type}StalledDownloads_`] > 0) {
this.logger_(`resetting stalled downloads for ${type} loader`);
}
this[`${type}StalledDownloads_`] = 0;
this[`${type}Buffered_`] = loader.buffered_();
}
/**
* Checks on every segment `appendsdone` to see
* if segment appends are making progress. If they are not
* and we are still downloading bytes. We blacklist the playlist.
*
* @param {string} type
* The segment loader type to check.
*
* @listens SegmentLoader#appendsdone
*/
checkSegmentDownloads_(type) {
const mpc = this.masterPlaylistController_;
const loader = mpc[`${type}SegmentLoader_`];
const buffered = loader.buffered_();
const isBufferedDifferent = Ranges.isRangeDifferent(this[`${type}Buffered_`], buffered);
this[`${type}Buffered_`] = buffered;
// if another watcher is going to fix the issue or
// the buffered value for this loader changed
// appends are working
if (isBufferedDifferent) {
this.resetSegmentDownloads_(type);
return;
}
this[`${type}StalledDownloads_`]++;
this.logger_(`found stalled download #${this[`${type}StalledDownloads_`]} for ${type} loader`);
// We will technically get past this on the fourth bad append
// rather than the third. As the first will almost always cause
// buffered to change which means that StalledDownloads_ will
// not be incremented
if (this[`${type}StalledDownloads_`] < 3) {
return;
}
this.logger_(`${type} loader download exclusion`);
this.resetSegmentDownloads_(type);
this.tech_.trigger({type: 'usage', name: `vhs-${type}-download-exclusion`});
if (type === 'subtitle') {
// TODO: Is there anything else that we can do here?
// removing the track and disabling could have accesiblity implications.
const track = loader.track();
const label = track.label || track.language || 'Unknown';
videojs.log.warn(`Text track "${label}" is not working correctly. It will be disabled and excluded.`);
track.mode = 'disabled';
this.tech_.textTracks().removeTrack(track);
return;
}
// TODO: should we exclude audio tracks rather than main tracks
// when type is audio?
mpc.blacklistCurrentPlaylist({
message: `Excessive ${type} segment downloading detected.`
}, Infinity);
}
/**
* The purpose of this function is to emulate the "waiting" event on
* browsers that do not emit it when they are waiting for more
@ -352,6 +463,8 @@ export default class PlaybackWatcher {
this.logger_(`Stopped at ${currentTime}, setting timer for ${difference}, seeking ` +
`to ${nextRange.start(0)}`);
this.cancelTimer_();
this.timer_ = setTimeout(
this.skipTheGap_.bind(this),
difference * 1000,

41
src/ranges.js

@ -396,3 +396,44 @@ export const timeRangesToArray = (timeRanges) => {
return timeRangesList;
};
/**
* Determines if two time range objects are different.
*
* @param {TimeRange} a
* the first time range object to check
*
* @param {TimeRange} b
* the second time range object to check
*
* @return {Boolean}
* Whether the time range objects differ
*/
export const isRangeDifferent = function(a, b) {
// same object
if (a === b) {
return false;
}
// one or the other is undefined
if (!a && b || (!b && a)) {
return true;
}
// length is different
if (a.length !== b.length) {
return true;
}
// see if any start/end pair is different
for (let i = 0; i < a.length; i++) {
if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) {
return true;
}
}
// if the length and every pair is the same
// this is the same time range
return false;
};

2
src/segment-loader.js

@ -833,6 +833,7 @@ export default class SegmentLoader extends videojs.EventTarget {
}
if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
this.trigger('playlistupdate');
if (this.mediaIndex !== null || this.handlePartialData_) {
// we must "resync" the segment loader when we switch renditions and
// the segment loader is already synced to the previous rendition
@ -2470,6 +2471,7 @@ export default class SegmentLoader extends videojs.EventTarget {
* @private
*/
handleAppendsDone_() {
this.trigger('appendsdone');
if (!this.pendingSegment_) {
this.state = 'READY';
// TODO should this move into this.checkForAbort to speed up requests post abort in

12
src/videojs-http-streaming.js

@ -511,13 +511,21 @@ class HlsHandler extends Component {
this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
this.playbackWatcher_ = new PlaybackWatcher(videojs.mergeOptions(this.options_, {
seekable: () => this.seekable(),
media: () => this.masterPlaylistController_.media()
media: () => this.masterPlaylistController_.media(),
masterPlaylistController: this.masterPlaylistController_
}));
this.masterPlaylistController_.on('error', () => {
const player = videojs.players[this.tech_.options_.playerId];
let error = this.masterPlaylistController_.error;
player.error(this.masterPlaylistController_.error);
if (typeof error === 'object' && !error.code) {
error.code = 3;
} else if (typeof error === 'string') {
error = {message: error, code: 3};
}
player.error(error);
});
// `this` in selectPlaylist should be the HlsHandler for backwards

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

@ -1840,9 +1840,9 @@ QUnit.test('blacklists playlist on earlyabort', function(assert) {
assert.equal(warnings.length, 1, 'one warning logged');
assert.equal(
warnings[0],
'Problem encountered with the current playlist. ' +
`Problem encountered with playlist ${currentMedia.id}. ` +
'Aborted early because there isn\'t enough bandwidth to complete the ' +
'request without rebuffering. Switching to another playlist.',
`request without rebuffering. Switching to playlist ${mediaChanges[0].id}.`,
'warning message is correct'
);
@ -4354,3 +4354,46 @@ QUnit.test('disposes timeline change controller on dispose', function(assert) {
assert.equal(disposes, 1, 'disposed timeline change controller');
});
QUnit.test('on error all segment and playlist loaders are paused', function(assert) {
const paused = {
audioSegment: false,
subtitleSegment: false,
mainSegment: false,
masterPlaylist: false
};
Object.keys(this.masterPlaylistController.mediaTypes_).forEach((type) => {
const key = `${type.toLowerCase()}Playlist`;
paused[key] = false;
this.masterPlaylistController.mediaTypes_[type].activePlaylistLoader = {
pause() {
paused[key] = true;
}
};
});
this.masterPlaylistController.audioSegmentLoader_.pause = () => {
paused.audioSegment = true;
};
this.masterPlaylistController.subtitleSegmentLoader_.pause = () => {
paused.subtitleSegment = true;
};
this.masterPlaylistController.mainSegmentLoader_.pause = () => {
paused.mainSegment = true;
};
this.masterPlaylistController.masterPlaylistLoader_.pause = () => {
paused.masterPlaylist = true;
};
this.masterPlaylistController.trigger('error');
Object.keys(paused).forEach(function(name) {
assert.ok(paused[name], `${name} was paused on error`);
});
});

270
test/playback-watcher.test.js

@ -13,6 +13,7 @@ import {
} from '../src/playback-watcher';
// needed for plugin registration
import '../src/videojs-http-streaming';
import sinon from 'sinon';
let monitorCurrentTime_;
@ -742,6 +743,270 @@ QUnit.test('jumps to buffered content if seeking just before', function(assert)
assert.equal(seeks[1], 11.1, 'seeked to seekable range');
});
const loaderTypes = ['audio', 'main', 'subtitle'];
QUnit.module('PlaybackWatcher download detection', {
beforeEach(assert) {
this.env = useFakeEnvironment(assert);
this.requests = this.env.requests;
this.mse = useFakeMediaSource();
this.clock = this.env.clock;
this.old = {};
this.respondToPlaylists_ = () => {
const regex = (/\.(m3u8|mpd)/i);
for (let i = 0; i < this.requests.length; i++) {
const r = this.requests[i];
if (regex.test(r.uri)) {
this.requests.splice(i, 1);
standardXHRResponse(r);
i--;
}
}
};
this.setup = function(src = {src: 'media.m3u8', type: 'application/vnd.apple.mpegurl'}) {
// setup a player
this.player = createPlayer({html5: {
hls: {
overrideNative: true
}
}});
this.player.src(src);
// start playback normally
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.player.tech_.trigger('playing');
this.clock.tick(1);
this.respondToPlaylists_();
this.usageEvents = {};
this.mpcErrors = 0;
this.playbackWatcher = this.player.vhs.playbackWatcher_;
this.mpc = this.player.vhs.masterPlaylistController_;
this.mpc.on('error', () => this.mpcErrors++);
this.player.tech_.on('usage', (event) => {
const name = event.name;
this.usageEvents[name] = this.usageEvents[name] || 0;
this.usageEvents[name]++;
});
this.setBuffered = (val) => {
this.player.buffered = () => val;
loaderTypes.forEach((type) => {
this.mpc[`${type}SegmentLoader_`].buffered_ = () => val;
});
};
};
},
afterEach() {
this.env.restore();
this.mse.restore();
this.player.dispose();
}
});
loaderTypes.forEach(function(type) {
QUnit.test(`detects ${type} appends without buffer changes and excludes`, function(assert) {
this.setup();
const loader = this.mpc[`${type}SegmentLoader_`];
const track = {label: 'foobar', mode: 'showing'};
if (type === 'subtitle') {
loader.track = () => track;
sinon.stub(this.player.tech_.textTracks(), 'removeTrack');
}
this.setBuffered(videojs.createTimeRanges([[0, 30]]));
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '1st append 0 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '2nd append 1 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 2, '3rd append 2 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '4th append 0 stalled downloads');
const expectedUsage = {};
expectedUsage[`vhs-${type}-download-exclusion`] = 1;
if (type !== 'subtitle') {
expectedUsage['hls-rendition-blacklisted'] = 1;
}
assert.deepEqual(this.usageEvents, expectedUsage, 'usage as expected');
if (type !== 'subtitle') {
const message = 'Playback cannot continue. No available working or supported playlists.';
assert.equal(this.mpcErrors, 1, 'one mpc error');
assert.equal(this.mpc.error, message, 'mpc error set');
assert.equal(this.player.error().message, message, 'player error set');
assert.equal(this.env.log.error.callCount, 1, 'player error logged');
assert.equal(this.env.log.error.args[0][1], message, 'error message as expected');
this.env.log.error.resetHistory();
} else {
const message = 'Text track "foobar" is not working correctly. It will be disabled and excluded.';
assert.equal(this.mpcErrors, 0, 'no mpc error set');
assert.notOk(this.player.error(), 'no player error set');
assert.equal(this.player.textTracks().removeTrack.callCount, 1, 'text track remove called');
assert.equal(this.player.textTracks().removeTrack.args[0][0], track, 'text track remove called with expected');
assert.equal(track.mode, 'disabled', 'mode set to disabled now');
assert.equal(this.env.log.warn.callCount, 1, 'warning logged');
assert.equal(this.env.log.warn.args[0][0], message, 'warning message as expected');
this.env.log.warn.resetHistory();
}
});
if (type !== 'subtitle') {
QUnit.test(`detects ${type} appends without buffer changes and excludes many playlists`, function(assert) {
this.setup({src: 'multipleAudioGroupsCombinedMain.m3u8', type: 'application/vnd.apple.mpegurl'});
const loader = this.mpc[`${type}SegmentLoader_`];
const playlists = this.mpc.master().playlists;
const excludeAndVerify = () => {
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '1st append 1 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 2, '2nd append 1 stalled downloads');
const oldPlaylist = this.mpc.media();
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '3rd append 0 stalled downloads');
const expectedUsage = {};
expectedUsage[`vhs-${type}-download-exclusion`] = 1;
expectedUsage['hls-rendition-blacklisted'] = 1;
assert.deepEqual(this.usageEvents, expectedUsage, 'usage as expected');
this.usageEvents = {};
this.respondToPlaylists_();
const otherPlaylistsLeft = this.mpc.master().playlists.some((p) => p.excludeUntil !== Infinity);
if (otherPlaylistsLeft) {
const message = `Problem encountered with playlist ${oldPlaylist.id}.` +
` Excessive ${type} segment downloading detected.` +
` Switching to playlist ${this.mpc.media().id}.`;
assert.equal(this.mpcErrors, 0, 'no mpc error');
assert.notOk(this.mpc.error, 'no mpc error set');
assert.notOk(this.player.error(), 'player error not set');
assert.equal(this.env.log.warn.callCount, 1, 'player warning logged');
assert.equal(this.env.log.warn.args[0][0], message, 'warning message as expected');
this.env.log.warn.resetHistory();
} else {
const message = 'Playback cannot continue. No available working or supported playlists.';
assert.equal(this.mpcErrors, 1, 'one mpc error');
assert.equal(this.mpc.error, message, 'mpc error set');
assert.equal(this.player.error().message, message, 'player error set');
assert.equal(this.env.log.error.callCount, 1, 'player error logged');
assert.equal(this.env.log.error.args[0][1], message, 'error message as expected');
this.env.log.error.resetHistory();
}
};
this.setBuffered(videojs.createTimeRanges([[0, 30]]));
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, 'initial append 0 stalled downloads');
let i = playlists.length;
// exclude all playlists and verify
while (i--) {
excludeAndVerify();
}
});
}
QUnit.test(`resets ${type} exclusion on playlistupdate, tech seeking, tech seeked`, function(assert) {
this.setup();
const loader = this.mpc[`${type}SegmentLoader_`];
this.setBuffered(videojs.createTimeRanges([[0, 30]]));
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '1st append 0 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '2nd append 1 stalled downloads');
loader.trigger('playlistupdate');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '0 stalled downloads after playlistupdate');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '1st append 1 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 2, '2nd append 2 stalled downloads');
this.player.tech_.trigger('seeking');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '0 stalled downloads after seeking');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '1st append 1 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 2, '2nd append 2 stalled downloads');
this.player.tech_.trigger('seeked');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '0 stalled downloads after seeked');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '1st append 1 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 2, '2nd append 2 stalled downloads');
assert.deepEqual(this.usageEvents, {}, 'no usage events');
});
QUnit.test(`Resets ${type} exclusion on buffered change`, function(assert) {
this.setup();
const loader = this.mpc[`${type}SegmentLoader_`];
this.setBuffered(videojs.createTimeRanges([[0, 30]]));
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '1st append 0 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '2nd append 1 stalled downloads');
this.setBuffered(videojs.createTimeRanges([[0, 31]]));
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 0, '1st append 0 stalled downloads');
loader.trigger('appendsdone');
assert.equal(this.playbackWatcher[`${type}StalledDownloads_`], 1, '2nd append 1 stalled downloads');
});
});
QUnit.module('PlaybackWatcher isolated functions', {
beforeEach() {
monitorCurrentTime_ = PlaybackWatcher.prototype.monitorCurrentTime_;
@ -754,6 +1019,11 @@ QUnit.module('PlaybackWatcher isolated functions', {
options_: {
playerId: 'mock-player-id'
}
},
masterPlaylistController: {
mainSegmentLoader_: Object.assign(new videojs.EventTarget(), {buffered_: () => videojs.createTimeRanges()}),
audioSegmentLoader_: Object.assign(new videojs.EventTarget(), {buffered_: () => videojs.createTimeRanges()}),
subtitleSegmentLoader_: Object.assign(new videojs.EventTarget(), {buffered_: () => videojs.createTimeRanges()})
}
});
},

35
test/ranges.test.js

@ -47,6 +47,41 @@ QUnit.test('finds overlapping time ranges when off by SAFE_TIME_DELTA', function
assert.equal(range.end(0), 12, 'inside the second buffered region');
});
QUnit.test('detects when time ranges differ', function(assert) {
const isDifferent = Ranges.isRangeDifferent;
const same = createTimeRanges([[0, 1]]);
assert.notOk(isDifferent(same, same), 'same object is not different');
assert.notOk(
isDifferent(null, null),
'null and null are not different'
);
assert.ok(
isDifferent(createTimeRanges([[0, 1], [1, 2]]), null),
'valid and null are different'
);
assert.ok(
isDifferent(createTimeRanges([[0, 1]]), createTimeRanges([[0, 1], [1, 2]])),
'objects with different length are different'
);
assert.ok(
isDifferent(createTimeRanges([[1, 2], [1, 2]]), createTimeRanges([[0, 1], [1, 2]])),
'objects with different values are different'
);
assert.ok(
isDifferent(null, createTimeRanges([[0, 1], [1, 2]])),
'null object and valid are different'
);
assert.ok(
isDifferent(createTimeRanges([[0, 1], [1, 2]]), null),
'valid and null are different'
);
});
QUnit.module('Buffer Inpsection');
QUnit.test('detects time range end-point changed by updates', function(assert) {

39
test/videojs-http-streaming.test.js

@ -1576,8 +1576,8 @@ QUnit.test('blacklists fmp4 playlists by browser support', function(assert) {
assert.strictEqual(playlists[1].excludeUntil, Infinity, 'blacklisted second playlist');
assert.strictEqual(typeof playlists[2].excludeUntil, 'undefined', 'did not blacklist second playlist');
assert.deepEqual(debugLogs, [
'Internal problem encountered with the current playlist. browser does not support codec(s): "hvc1". Switching to another playlist.',
'Internal problem encountered with the current playlist. browser does not support codec(s): "ac-3". Switching to another playlist.'
`Internal problem encountered with playlist ${playlists[0].id}. browser does not support codec(s): "hvc1". Switching to playlist ${playlists[1].id}.`,
`Internal problem encountered with playlist ${playlists[1].id}. browser does not support codec(s): "ac-3". Switching to playlist ${playlists[2].id}.`
], 'debug log as expected');
window.MediaSource.isTypeSupported = oldIsTypeSupported;
@ -1644,8 +1644,8 @@ QUnit.test('blacklists ts playlists by muxer support', function(assert) {
assert.strictEqual(playlists[1].excludeUntil, Infinity, 'blacklisted second playlist');
assert.strictEqual(typeof playlists[2].excludeUntil, 'undefined', 'did not blacklist second playlist');
assert.deepEqual(debugLogs, [
'Internal problem encountered with the current playlist. muxer does not support codec(s): "hvc1". Switching to another playlist.',
'Internal problem encountered with the current playlist. muxer does not support codec(s): "ac-3". Switching to another playlist.'
`Internal problem encountered with playlist ${playlists[0].id}. muxer does not support codec(s): "hvc1". Switching to playlist ${playlists[1].id}.`,
`Internal problem encountered with playlist ${playlists[1].id}. muxer does not support codec(s): "ac-3". Switching to playlist ${playlists[2].id}.`
], 'debug log as expected');
});
@ -1736,6 +1736,7 @@ QUnit.test('unsupported playlist should not be re-included when excluding last p
Infinity,
'blacklisted invalid audio codec'
);
const requri = this.requests[0].uri;
this.requests.shift().respond(400);
@ -1746,10 +1747,10 @@ QUnit.test('unsupported playlist should not be re-included when excluding last p
'audio codec still blacklisted'
);
assert.equal(this.env.log.warn.calls, 2, 'warning logged for blacklist');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Removing all playlists from the blacklist because the last rendition is about to be blacklisted.',
this.env.log.warn.args[0][0],
`Problem encountered with playlist ${master.playlists[0].id}. HLS request errored at URL: ${requri} Switching to playlist 0-media.m3u8.`,
'log generic error message'
);
});
@ -1843,7 +1844,7 @@ QUnit.test('playlist 404 should blacklist media', function(assert) {
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Problem encountered with the current playlist. HLS playlist request error at URL: media.m3u8. Switching to another playlist.',
`Problem encountered with playlist ${media.id}. HLS playlist request error at URL: media.m3u8. Switching to playlist 1-media1.m3u8.`,
'log generic error message'
);
assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
@ -1869,13 +1870,13 @@ QUnit.test('playlist 404 should blacklist media', function(assert) {
assert.equal(this.env.log.warn.calls, 2, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[1],
'Removing all playlists from the blacklist because the last rendition is about to be blacklisted.',
'Removing other playlists from the exclusion list because the last rendition is about to be excluded.',
'log generic error message'
);
assert.equal(
this.env.log.warn.args[2],
'Problem encountered with the current playlist. HLS playlist request error at URL: media1.m3u8. ' +
'Switching to another playlist.',
`Problem encountered with playlist ${media.id}. HLS playlist request error at URL: media1.m3u8. ` +
'Switching to playlist 0-media.m3u8.',
'log generic error message'
);
assert.equal(retryplaylist, 1, 'fired a retryplaylist event');
@ -1963,15 +1964,17 @@ QUnit.test('blacklists playlist if it has stopped being updated', function(asser
'16.ts\n'
);
const media = this.player.tech_.hls.playlists.media();
assert.ok(
this.player.tech_.hls.playlists.media().excludeUntil > 0,
media.excludeUntil > 0,
'playlist blacklisted for some time'
);
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Problem encountered with the current playlist. ' +
'Playlist no longer updating. Switching to another playlist.',
`Problem encountered with playlist ${media.id}. ` +
'Playlist no longer updating. Switching to playlist 0-media.m3u8.',
'log specific error message for not updated playlist'
);
assert.equal(playliststuck, 1, 'there is one stuck playlist');
@ -2000,7 +2003,7 @@ QUnit.test('never blacklist the playlist if it is the only playlist', function(a
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Problem encountered with the current playlist. ' +
`Problem encountered with playlist ${media.id}. ` +
'Trying again since it is the only playlist.',
'log specific error message for the only playlist'
);
@ -2034,7 +2037,7 @@ QUnit.test(
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Problem encountered with the current playlist. ' +
`Problem encountered with playlist ${media.id}. ` +
'Trying again since it is the only playlist.',
'log specific error message for the onlyplaylist'
);
@ -2550,9 +2553,9 @@ QUnit.test('playlist blacklisting duration is set through options', function(ass
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(
this.env.log.warn.args[0],
'Problem encountered with the current playlist. ' +
`Problem encountered with playlist ${media.id}. ` +
'HLS playlist request error at URL: media.m3u8. ' +
'Switching to another playlist.',
'Switching to playlist 1-media1.m3u8.',
'log generic error message'
);

Loading…
Cancel
Save