Skip to content

Commit

Permalink
Refactor live backbuffer eviction to fit more closely to design goals (
Browse files Browse the repository at this point in the history
  • Loading branch information
johnBartos authored Oct 31, 2018
1 parent 3e6651d commit 1671c91
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 102 deletions.
2 changes: 1 addition & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ If you want to have a native Live UI in environments like iOS Safari, Safari, An

(default: `Infinity`)

Sets the maximum length of the buffer, in seconds, to keep durin a live stream. Any video buffered past this time will be evicted. `Infinity` means no restriction on back buffer length; `0` keeps the minimum amount. The minimum amount is equal to the target duration of a segment to ensure that current playback is not interrupted.
Sets the maximum length of the buffer, in seconds, to keep during a live stream. Any video buffered past this time will be evicted. `Infinity` means no restriction on back buffer length; `0` keeps the minimum amount. The minimum amount is equal to the target duration of a segment to ensure that current playback is not interrupted.

### `enableWorker`

Expand Down
80 changes: 30 additions & 50 deletions src/controller/buffer-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ class BufferController extends EventHandler {
this._msDuration = null;
// the value that we want to set mediaSource.duration to
this._levelDuration = null;
/**
* the target duration of the current playlist
*
* @default 10
*/
// the target duration of the current media playlist
this._levelTargetDuration = 10;
// current stream state: true - for live broadcast, false - for VoD content
this._live = null;
Expand Down Expand Up @@ -242,7 +238,7 @@ class BufferController extends EventHandler {

// appending goes first
if (pending === 0) {
this.clearLiveBackBuffer();
this.flushLiveBackBuffer();
}
}

Expand Down Expand Up @@ -363,7 +359,7 @@ class BufferController extends EventHandler {
return;
}
}
logger.log('all media data available, signal endOfStream() to MediaSource and stop loading fragment');
logger.log('all media data are available, signal endOfStream() to MediaSource and stop loading fragment');
// Notify the media element that it now has all of the media data
try {
mediaSource.endOfStream();
Expand All @@ -380,44 +376,39 @@ class BufferController extends EventHandler {
this.doFlush();
}

clearLiveBackBuffer () {
flushLiveBackBuffer () {
// clear back buffer for live only
if (!this._live) {
return;
}

const { liveBackBufferLength } = this.hls.config;

if (isFinite(liveBackBufferLength) === false || liveBackBufferLength < 0) {
const liveBackBufferLength = this.hls.config.liveBackBufferLength;
if (!isFinite(liveBackBufferLength) || liveBackBufferLength < 0) {
return;
}

try {
const currentTime = this.media.currentTime;
const sourceBuffer = this.sourceBuffer;
const bufferTypes = Object.keys(sourceBuffer);
const targetBackBufferPosition = currentTime - Math.max(liveBackBufferLength, this._levelTargetDuration);

for (let index = bufferTypes.length - 1; index >= 0; index--) {
const bufferType = bufferTypes[index], buffered = sourceBuffer[bufferType].buffered;

// when target buffer start exceeds actual buffer start
if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) {
// remove buffer up until current time minus minimum back buffer length (removing buffer too close to current
// time will lead to playback freezing)
// credits for level target duration - https://github.com/videojs/http-streaming/blob/3132933b6aa99ddefab29c10447624efd6fd6e52/src/segment-loader.js#L91
this.removeBufferRange(bufferType, sourceBuffer[bufferType], 0, targetBackBufferPosition);
}
const currentTime = this.media.currentTime;
const sourceBuffer = this.sourceBuffer;
const bufferTypes = Object.keys(sourceBuffer);
const targetBackBufferPosition = currentTime - Math.max(liveBackBufferLength, this._levelTargetDuration);

for (let index = bufferTypes.length - 1; index >= 0; index--) {
const bufferType = bufferTypes[index], buffered = sourceBuffer[bufferType].buffered;

// when target buffer start exceeds actual buffer start
if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) {
// remove buffer up until current time minus minimum back buffer length (removing buffer too close to current
// time will lead to playback freezing)
// credits for level target duration - https://github.com/videojs/http-streaming/blob/3132933b6aa99ddefab29c10447624efd6fd6e52/src/segment-loader.js#L91
this.removeBufferRange(bufferType, sourceBuffer[bufferType], 0, targetBackBufferPosition);
}
} catch (error) {
logger.warn('clearLiveBackBuffer failed', error);
}
}

onLevelUpdated ({ details }) {
if (details.fragments.length > 0) {
this._levelDuration = details.totalduration + details.fragments[0].start;
this._levelTargetDuration = details.targetDuration || 10;
this._levelTargetDuration = details.averagetargetduration || details.targetduration || 10;
this._live = details.live;
this.updateMediaElementDuration();
}
Expand Down Expand Up @@ -505,7 +496,7 @@ class BufferController extends EventHandler {
}

doAppending () {
let hls = this.hls, sourceBuffer = this.sourceBuffer, segments = this.segments;
let { hls, segments, sourceBuffer } = this;
if (Object.keys(sourceBuffer).length) {
if (this.media.error) {
this.segments = [];
Expand Down Expand Up @@ -557,7 +548,7 @@ class BufferController extends EventHandler {
*/
if (this.appendError > hls.config.appendErrorMaxRetry) {
logger.log(`fail ${hls.config.appendErrorMaxRetry} times to append segment in sourceBuffer`);
segments = [];
this.segments = [];
event.fatal = true;
hls.trigger(Event.ERROR, event);
} else {
Expand All @@ -583,7 +574,8 @@ class BufferController extends EventHandler {
as sourceBuffer.remove() is asynchronous, flushBuffer will be retriggered on sourceBuffer update end
*/
flushBuffer (startOffset, endOffset, typeIn) {
let sb, sourceBuffer = this.sourceBuffer;
let sb;
const sourceBuffer = this.sourceBuffer;
if (Object.keys(sourceBuffer).length) {
logger.log(`flushBuffer,pos/start/end: ${this.media.currentTime.toFixed(3)}/${startOffset}/${endOffset}`);
// safeguard to avoid infinite looping : don't try to flush more than the nb of appended segments
Expand All @@ -604,9 +596,6 @@ class BufferController extends EventHandler {
return false;
}
} else {
// logger.log('abort ' + type + ' append in progress');
// this will abort any appending in progress
// sb.abort();
logger.warn('cannot flush, sb updating in progress');
return false;
}
Expand All @@ -632,20 +621,11 @@ class BufferController extends EventHandler {
*/
removeBufferRange (type, sb, startOffset, endOffset) {
try {
let i, length, bufStart, bufEnd, removeStart, removeEnd;

for (i = 0, length = sb.buffered.length; i < length; i++) {
bufStart = sb.buffered.start(i);
bufEnd = sb.buffered.end(i);

// workaround firefox not able to properly flush multiple buffered range.
if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1 && endOffset === Number.POSITIVE_INFINITY) {
removeStart = startOffset;
removeEnd = endOffset;
} else {
removeStart = Math.max(bufStart, startOffset);
removeEnd = Math.min(bufEnd, endOffset);
}
for (let i = 0; i < sb.buffered.length; i++) {
let bufStart = sb.buffered.start(i);
let bufEnd = sb.buffered.end(i);
let removeStart = Math.max(bufStart, startOffset);
let removeEnd = Math.min(bufEnd, endOffset);

/* sometimes sourcebuffer.remove() does not flush
the exact expected time range.
Expand Down
142 changes: 91 additions & 51 deletions tests/unit/controller/buffer-controller.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,120 @@
import assert from 'assert';
import { stub } from 'sinon';
import sinon from 'sinon';
import Hls from '../../../src/hls';
import BufferController from '../../../src/controller/buffer-controller';

describe('BufferController tests', function () {
let hls;
let bufferController;
let flushSpy;
let removeStub;
const sandbox = sinon.sandbox.create();

beforeEach(function () {
hls = new Hls({});

bufferController = new BufferController(hls);
flushSpy = sandbox.spy(bufferController, 'flushLiveBackBuffer');
removeStub = sandbox.stub(bufferController, 'removeBufferRange');
});

describe('Live back buffer enforcement', () => {
it('should trigger clean back buffer when there are no pending appends', () => {
bufferController.parent = {};
bufferController.segments = [{ parent: bufferController.parent }];

let clearStub = stub(bufferController, 'clearLiveBackBuffer');
stub(bufferController, 'doAppending');

bufferController.onSBUpdateEnd();

assert(clearStub.notCalled, 'clear live back buffer was called');

bufferController.segments = [];
bufferController.onSBUpdateEnd();
afterEach(function () {
sandbox.restore();
});

assert(clearStub.calledOnce, 'clear live back buffer was not called once');
});
describe('Live back buffer enforcement', function () {
let mockMedia;
let mockSourceBuffer;
let bufStart;

it('should trigger buffer removal with valid range for live', () => {
bufferController.media = { currentTime: 360 };
hls.config.liveBackBufferLength = 60;
bufferController.sourceBuffer = {
beforeEach(function () {
bufStart = 0;
bufferController._levelTargetDuration = 10;
bufferController.media = mockMedia = {
currentTime: 0
};
bufferController.sourceBuffer = mockSourceBuffer = {
video: {
buffered: {
start: stub().withArgs(0).returns(120),
start () {
return bufStart;
},
length: 1
}
}
};
bufferController._live = true;
hls.config.liveBackBufferLength = 10;
});

let removeBufferRangeStub = stub(bufferController, 'removeBufferRange');
it('exits early if not live', function () {
bufferController.flushLiveBackBuffer();
assert(removeStub.notCalled);
});

bufferController._live = false;
bufferController.clearLiveBackBuffer();
assert(removeBufferRangeStub.notCalled, 'remove buffer range was called for non-live');
it('exits early if liveBackBufferLength is not a finite number, or is less than 0', function () {
hls.config.liveBackBufferLength = 'foo';
bufferController.flushLiveBackBuffer();

bufferController._live = true;
bufferController.clearLiveBackBuffer();
assert(removeBufferRangeStub.calledOnce, 'remove buffer range was not called once');

assert(
removeBufferRangeStub.calledWith(
'video',
bufferController.sourceBuffer.video,
0,
bufferController.media.currentTime - hls.config.liveBackBufferLength
),
'remove buffer range was not requested with valid data from liveBackBufferLength'
);
hls.config.liveBackBufferLength = -1;
bufferController.flushLiveBackBuffer();

assert(removeStub.notCalled);
});

it('does not flush if nothing is buffered', function () {
delete mockSourceBuffer.buffered;
bufferController.flushLiveBackBuffer();

mockSourceBuffer = null;
bufferController.flushLiveBackBuffer();

assert(removeStub.notCalled);
});

it('does not flush if no buffered range intersects with back buffer limit', function () {
bufStart = 5;
mockMedia.currentTime = 10;
bufferController.flushLiveBackBuffer();
assert(removeStub.notCalled);
});

it('does not flush if the liveBackBufferLength is Infinity', function () {
hls.config.liveBackBufferLength = Infinity;
mockMedia.currentTime = 15;
bufferController.flushLiveBackBuffer();
assert(removeStub.notCalled);
});

it('flushes up to the back buffer limit if the buffer intersects with that point', function () {
mockMedia.currentTime = 15;
bufferController.flushLiveBackBuffer();
assert(removeStub.calledOnce);
assert(!bufferController.flushBufferCounter, 'Should reset the flushBufferCounter');
assert(removeStub.calledWith('video', mockSourceBuffer.video, 0, 5));
});

it('flushes to a max of one targetDuration from currentTime, regardless of liveBackBufferLength', function () {
mockMedia.currentTime = 15;
bufferController._levelTargetDuration = 5;
hls.config.liveBackBufferLength = 0;
bufferController._levelTargetDuration = 10;
bufferController.clearLiveBackBuffer();
assert(
removeBufferRangeStub.calledWith(
'video',
bufferController.sourceBuffer.video,
0,
bufferController.media.currentTime - bufferController._levelTargetDuration
),
'remove buffer range was not requested with valid data from _levelTargetDuration'
);
bufferController.flushLiveBackBuffer();
assert(removeStub.calledWith('video', mockSourceBuffer.video, 0, 10));
});

it('should trigger clean back buffer when there are no pending appends', function () {
bufferController.parent = {};
bufferController.segments = [{ parent: bufferController.parent }];

sandbox.stub(bufferController, 'doAppending');

bufferController.onSBUpdateEnd();

assert(flushSpy.notCalled, 'clear live back buffer was called');

bufferController.segments = [];
bufferController.onSBUpdateEnd();

assert(flushSpy.calledOnce, 'clear live back buffer was not called once');
});
});
});

0 comments on commit 1671c91

Please sign in to comment.