Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

use last segment duration + 2*targetDuration for safe live point instead of 3 segments #1271

Merged
merged 13 commits into from
Oct 19, 2017
4 changes: 2 additions & 2 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,12 +820,12 @@ export class MasterPlaylistController extends videojs.EventTarget {

if (!buffered.length) {
// return true if the playhead reached the absolute end of the playlist
return absolutePlaylistEnd - currentTime <= 0.1;
return absolutePlaylistEnd - currentTime <= Ranges.SAFE_TIME_DELTA;
}
let bufferedEnd = buffered.end(buffered.length - 1);

// return true if there is too little buffer left and buffer has reached absolute
// end of playlist.
// end of playlist
return bufferedEnd - currentTime <= Ranges.SAFE_TIME_DELTA &&
absolutePlaylistEnd - bufferedEnd <= Ranges.SAFE_TIME_DELTA;
}
Expand Down
41 changes: 27 additions & 14 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@ export const resolveMediaGroupUris = (master) => {
});
};

/**
* Calculates the time to wait before refreshing a live playlist
*
* @param {Object} media
* The current media
* @param {Boolean} update
* True if there were any updates from the last refresh, false otherwise
* @return {Number}
* The time in ms to wait before refreshing the live playlist
*/
export const refreshDelay = (media, update) => {
const lastSegment = media.segments[media.segments.length - 1];
let delay;

if (update && lastSegment && lastSegment.duration) {
delay = lastSegment.duration * 1000;
} else {
// if the playlist is unchanged since the last reload or last segment duration
// cannot be determined, try again after half the target duration
delay = (media.targetDuration || 10) * 500;
}
return delay;
};

/**
* Load a playlist from a remote location
*
Expand Down Expand Up @@ -242,21 +266,10 @@ export default class PlaylistLoader extends EventTarget {

// refresh live playlists after a target duration passes
if (!this.media().endList) {
const lastSegment = this.media_.segments[this.media_.segments.length - 1];
let refreshDelay;

if (update && lastSegment && lastSegment.duration) {
refreshDelay = lastSegment.duration * 1000;
} else {
// if the playlist is unchanged since the last reload or last segment duration
// cannot be determined, try again after half the target duration
refreshDelay = (this.targetDuration || 10) * 500;
}

window.clearTimeout(this.mediaUpdateTimeout);
this.mediaUpdateTimeout = window.setTimeout(() => {
this.trigger('mediaupdatetimeout');
}, refreshDelay);
}, refreshDelay(this.media(), !!update));
}

this.trigger('loadedplaylist');
Expand Down Expand Up @@ -458,9 +471,9 @@ export default class PlaylistLoader extends EventTarget {
const media = this.media();

if (isFinalRendition) {
const refreshDelay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000;
const delay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000;

this.mediaUpdateTimeout = window.setTimeout(() => this.load(), refreshDelay);
this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/ranges.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const TIME_FUDGE_FACTOR = 1 / 30;
// aligned audio and video, which can cause values to be slightly off from what you would
// expect. This value is what we consider to be safe to use in such comparisons to account
// for these scenarios.
const SAFE_TIME_DELTA = 0.1;
const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;

/**
* Clamps a value to within a range
Expand Down
70 changes: 43 additions & 27 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,46 @@ export const illegalMediaSwitch = (loaderType, startingMedia, newSegmentMedia) =
return null;
};

/**
* Calculates a time value that is safe to remove from the back buffer without interupting
* playback.
*
* @param {TimeRange} seekable
* The current seekable range
* @param {Number} currentTime
* The current time of the player
* @param {Number} targetDuration
* The target duration of the current playlist
* @return {Number}
* Time that is safe to remove from the back buffer without interupting playback
*/
export const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => {
// Don't allow removing from the buffer within target duration of current time
// to avoid the possibility of removing the GOP currently being played which could
// cause playback stalls.
const safeRemoveToTimeLimit = currentTime - targetDuration;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line and comment might be better just as part of the Math.min return. We can remove the variable since it just obfuscates the meaning (since it is just a subtraction of two numbers).


// Chrome has a hard limit of 150MB of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should probably go in the trimBackBuffer_ function, since we aren't doing any trimming here

// buffer and a very conservative "garbage collector"
// We manually clear out the old buffer to ensure
// we don't trigger the QuotaExceeded error
// on the source buffer during subsequent appends

let removeToTime;

if (seekable.length &&
seekable.start(0) > 0 &&
seekable.start(0) < currentTime) {
// If we have a seekable range use that as the limit for what can be removed safely
removeToTime = seekable.start(0);
} else {
// otherwise remove anything older than 30 seconds before the current play head
removeToTime = currentTime - 30;
}

return Math.min(removeToTime, safeRemoveToTimeLimit);
};

/**
* An object that manages segment loading and appending.
*
Expand Down Expand Up @@ -906,33 +946,9 @@ export default class SegmentLoader extends videojs.EventTarget {
* @param {Object} segmentInfo - the current segment
*/
trimBackBuffer_(segmentInfo) {
const seekable = this.seekable_();
const currentTime = this.currentTime_();
const targetDuration = this.playlist_.targetDuration || 10;

// Don't allow removing from the buffer within target duration of current time
// to avoid the possibility of removing the GOP currently being played which could
// cause playback stalls.
const safeRemoveToTimeLimit = currentTime - targetDuration;
let removeToTime = 0;

// Chrome has a hard limit of 150MB of
// buffer and a very conservative "garbage collector"
// We manually clear out the old buffer to ensure
// we don't trigger the QuotaExceeded error
// on the source buffer during subsequent appends

// If we have a seekable range use that as the limit for what can be removed safely
// otherwise remove anything older than 30 seconds before the current play head
if (seekable.length &&
seekable.start(0) > 0 &&
seekable.start(0) < currentTime) {
removeToTime = seekable.start(0);
} else {
removeToTime = currentTime - 30;
}

removeToTime = Math.min(removeToTime, safeRemoveToTimeLimit);
const removeToTime = safeBackBufferTrimTime(this.seekable_(),
this.currentTime_(),
this.playlist_.targetDuration || 10);

if (removeToTime > 0) {
this.remove(0, removeToTime);
Expand Down
20 changes: 19 additions & 1 deletion test/playlist-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
updateSegments,
updateMaster,
setupMediaPlaylists,
resolveMediaGroupUris
resolveMediaGroupUris,
refreshDelay
} from '../src/playlist-loader';
import xhrFactory from '../src/xhr';
import { useFakeEnvironment } from './test-helpers';
Expand Down Expand Up @@ -773,6 +774,23 @@ QUnit.test('resolveMediaGroupUris resolves media group URIs', function(assert) {
}, 'resolved URIs of certain media groups');
});

QUnit.test('uses last segment duration for refresh delay', function(assert) {
const media = { targetDuration: 7, segments: [] };

assert.equal(refreshDelay(media, true), 3500,
'used half targetDuration when no segments');

media.segments = [ { duration: 6}, { duration: 4 }, { } ];
assert.equal(refreshDelay(media, true), 3500,
'used half targetDuration when last segment duration cannot be determined');

media.segments = [ { duration: 6}, { duration: 4}, { duration: 5 } ];
assert.equal(refreshDelay(media, true), 5000, 'used last segment duration for delay');

assert.equal(refreshDelay(media, false), 3500,
'used half targetDuration when update is false');
});

QUnit.test('throws if the playlist url is empty or undefined', function(assert) {
assert.throws(function() {
PlaylistLoader();
Expand Down
3 changes: 2 additions & 1 deletion test/playlist.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ function(assert) {
// least 6s from end. Adding segment durations starting from the end to get
// that 6s target
9 - (2 + 2 + 1 + 2),
'allows seeking no further than three times target duration from the end');
'allows seeking no further than the start of the segment 2 target' +
'durations back from the beginning of the last segment');
assert.equal(playlistEnd, 9, 'playlist end at the last segment');
});

Expand Down
24 changes: 23 additions & 1 deletion test/segment-loader.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import QUnit from 'qunit';
import {
default as SegmentLoader,
illegalMediaSwitch
illegalMediaSwitch,
safeBackBufferTrimTime
} from '../src/segment-loader';
import videojs from 'video.js';
import mp4probe from 'mux.js/lib/mp4/probe';
Expand Down Expand Up @@ -107,6 +108,27 @@ QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert)
'error when video only to audio only');
});

QUnit.test('safeBackBufferTrimTime determines correct safe removeToTime',
function(assert) {
let seekable = videojs.createTimeRanges([[75, 120]]);
let targetDuration = 10;
let currentTime = 70;

assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 40,
'uses 30s before current time if currentTime is before seekable start');

currentTime = 110;

assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 75,
'uses seekable start if currentTime is after seekable start');

currentTime = 80;

assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 70,
'uses target duration before currentTime if currentTime is after seekable but' +
'within target duration');
});

QUnit.module('SegmentLoader', function(hooks) {
hooks.beforeEach(LoaderCommonHooks.beforeEach);
hooks.afterEach(LoaderCommonHooks.afterEach);
Expand Down
68 changes: 0 additions & 68 deletions test/videojs-contrib-hls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2629,74 +2629,6 @@ QUnit.test('cleans up the buffer based on currentTime when loading a live segmen
assert.deepEqual(removes[0], [0, 80 - 30], 'remove called with the right range');
});

QUnit.test('cleans up the buffer based on currentTime when loading a live segment if' +
'seekable start is within a target duration of currentTime', function(assert) {
const removes = [];
let seekable = videojs.createTimeRanges([[0, 80]]);

this.player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.masterPlaylistController_.seekable = function() {
return seekable;
};

// This is so we do not track first call to remove during segment loader init
this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
this.resetLoader();
};

this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
let buffer = new (videojs.extend(videojs.EventTarget, {
constructor() {},
abort() {},
buffered: videojs.createTimeRange(),
appendBuffer() {},
remove(start, end) {
removes.push([start, end]);
}
}))();

this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
return buffer;

};
this.player.tech_.hls.bandwidth = 20e10;
this.player.tech_.triggerReady();
this.standardXHRResponse(this.requests[0]);
this.player.tech_.hls.playlists.trigger('loadedmetadata');
this.player.tech_.trigger('canplay');

this.player.tech_.paused = function() {
return false;
};

this.player.tech_.trigger('play');
this.clock.tick(1);

// request first playable segment
this.standardXHRResponse(this.requests[1]);

this.clock.tick(1);
this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');

// Change seekable so that it starts *within a target duration before* the currentTime
// which was set based on the previous seekable range (the end of 80)
// target duration is 10 seconds for this source
seekable = videojs.createTimeRanges([[75, 120]]);

this.clock.tick(1);

// request second playable segment
this.standardXHRResponse(this.requests[2]);

assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
assert.equal(removes.length, 1, 'remove called');
assert.deepEqual(removes[0], [0, 80 - 10], 'remove called with the right range');
});

QUnit.test('cleans up the buffer when loading VOD segments', function(assert) {
let removes = [];

Expand Down