Skip to content

Commit

Permalink
Merge branch 'main' into poc-ll-hls
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey authored Mar 19, 2021
2 parents c4b24d3 + 87947fc commit 74c785f
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 36 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
<a name="2.6.4"></a>
## [2.6.4](https://github.com/videojs/http-streaming/compare/v2.6.3...v2.6.4) (2021-03-12)

### Bug Fixes

* Monitor playback for stalls due to gaps in the beginning of stream when a new source is loaded ([#1087](https://github.com/videojs/http-streaming/issues/1087)) ([64a1f35](https://github.com/videojs/http-streaming/commit/64a1f35))
* retry appends on QUOTA_EXCEEDED_ERR ([#1093](https://github.com/videojs/http-streaming/issues/1093)) ([008aeaf](https://github.com/videojs/http-streaming/commit/008aeaf))

### Chores

* Get test coverage working again with mock/sync worker ([#1094](https://github.com/videojs/http-streaming/issues/1094)) ([035e8c0](https://github.com/videojs/http-streaming/commit/035e8c0))
* pin CI to ubuntu 18.04 ([#1091](https://github.com/videojs/http-streaming/issues/1091)) ([01ca182](https://github.com/videojs/http-streaming/commit/01ca182))

<a name="2.6.3"></a>
## [2.6.3](https://github.com/videojs/http-streaming/compare/v2.6.2...v2.6.3) (2021-03-05)

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@videojs/http-streaming",
"version": "2.6.3",
"version": "2.6.4",
"description": "Play back HLS and DASH with Video.js, even where it's not natively supported",
"main": "dist/videojs-http-streaming.cjs.js",
"module": "dist/videojs-http-streaming.es.js",
Expand Down
1 change: 0 additions & 1 deletion scripts/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ let syncWorker;
const options = {
input: 'src/videojs-http-streaming.js',
distName: 'videojs-http-streaming',
checkWatch: false,
excludeCoverage(defaults) {
defaults.push(/^rollup-plugin-worker-factory/);
defaults.push(/^create-test-data!/);
Expand Down
2 changes: 2 additions & 0 deletions src/error-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// https://www.w3.org/TR/WebIDL-1/#quotaexceedederror
export const QUOTA_EXCEEDED_ERR = 22;
175 changes: 149 additions & 26 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import {
} from './util/text-tracks';
import { gopsSafeToAlignWith, removeGopBuffer, updateGopBuffer } from './util/gops';
import shallowEqual from './util/shallow-equal.js';
import { QUOTA_EXCEEDED_ERR } from './error-codes';
import { timeRangesToArray } from './ranges';

// In the event of a quota exceeded error, keep at least one second of back buffer. This
// number was arbitrarily chosen and may be updated in the future, but seemed reasonable
// as a start to prevent any potential issues with removing content too close to the
// playhead.
const MIN_BACK_BUFFER = 1;

// in ms
const CHECK_BUFFER_DELAY = 500;
Expand Down Expand Up @@ -509,6 +517,8 @@ export default class SegmentLoader extends videojs.EventTarget {
id3: [],
caption: []
};
this.waitingOnRemove_ = false;
this.quotaExceededErrorRetryTimeout_ = null;

// Fragmented mp4 playback
this.activeInitSegmentId_ = null;
Expand Down Expand Up @@ -692,6 +702,9 @@ export default class SegmentLoader extends videojs.EventTarget {
this.metadataQueue_.id3 = [];
this.metadataQueue_.caption = [];
this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
this.waitingOnRemove_ = false;
window.clearTimeout(this.quotaExceededErrorRetryTimeout_);
this.quotaExceededErrorRetryTimeout_ = null;
}

checkForAbort_(requestId) {
Expand Down Expand Up @@ -1122,9 +1135,10 @@ export default class SegmentLoader extends videojs.EventTarget {
* @param {number} start - the start time of the region to remove from the buffer
* @param {number} end - the end time of the region to remove from the buffer
* @param {Function} [done] - an optional callback to be executed when the remove
* @param {boolean} force - force all remove operations to happen
* operation is complete
*/
remove(start, end, done = () => {}) {
remove(start, end, done = () => {}, force = false) {
// clamp end to duration if we need to remove everything.
// This is due to a browser bug that causes issues if we remove to Infinity.
// videojs/videojs-contrib-hls#1225
Expand All @@ -1147,7 +1161,7 @@ export default class SegmentLoader extends videojs.EventTarget {
}
};

if (!this.audioDisabled_) {
if (force || !this.audioDisabled_) {
removesRemaining++;
this.sourceUpdater_.removeAudio(start, end, removeFinished);
}
Expand All @@ -1160,7 +1174,7 @@ export default class SegmentLoader extends videojs.EventTarget {
// the event that we're switching between renditions and from video to audio only
// (when we add support for that), we may need to clear the video contents despite
// what the new media will contain.
if (this.loaderType_ === 'main') {
if (force || this.loaderType_ === 'main') {
this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_);
removesRemaining++;
this.sourceUpdater_.removeVideo(start, end, removeFinished);
Expand Down Expand Up @@ -1893,6 +1907,12 @@ export default class SegmentLoader extends videojs.EventTarget {
return false;
}

// If content needs to be removed or the loader is waiting on an append reattempt,
// then no additional content should be appended until the prior append is resolved.
if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) {
return false;
}

const segmentInfo = this.pendingSegment_;

// no segment to append any data for or
Expand Down Expand Up @@ -2095,34 +2115,137 @@ export default class SegmentLoader extends videojs.EventTarget {
return null;
}

appendToSourceBuffer_({ segmentInfo, type, initSegment, data }) {
const segments = [data];
let byteLength = data.byteLength;
handleQuotaExceededError_({segmentInfo, type, bytes}, error) {
const audioBuffered = this.sourceUpdater_.audioBuffered();
const videoBuffered = this.sourceUpdater_.videoBuffered();

if (initSegment) {
// if the media initialization segment is changing, append it before the content
// segment
segments.unshift(initSegment);
byteLength += initSegment.byteLength;
// For now we're ignoring any notion of gaps in the buffer, but they, in theory,
// should be cleared out during the buffer removals. However, log in case it helps
// debug.
if (audioBuffered.length > 1) {
this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' +
timeRangesToArray(audioBuffered).join(', '));
}
if (videoBuffered.length > 1) {
this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' +
timeRangesToArray(videoBuffered).join(', '));
}

// Technically we should be OK appending the init segment separately, however, we
// haven't yet tested that, and prepending is how we have always done things.
const bytes = concatSegments({
bytes: byteLength,
segments
});
const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0;
const audioBufferEnd = audioBuffered.length ?
audioBuffered.end(audioBuffered.length - 1) : 0;
const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0;
const videoBufferEnd = videoBuffered.length ?
videoBuffered.end(videoBuffered.length - 1) : 0;

if (
(audioBufferEnd - audioBufferStart) <= MIN_BACK_BUFFER &&
(videoBufferEnd - videoBufferStart) <= MIN_BACK_BUFFER
) {
// Can't remove enough buffer to make room for new segment (or the browser doesn't
// allow for appends of segments this size). In the future, it may be possible to
// split up the segment and append in pieces, but for now, error out this playlist
// in an attempt to switch to a more manageable rendition.
this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' +
'buffer, triggering an error. ' +
`Appended byte length: ${bytes.byteLength}, ` +
`audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` +
`video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
this.error({
message: 'Quota exceeded error with append of a single segment of content',
// To prevent any possible repeated downloads for content we can't actually
// append, blacklist forever.
blacklistDuration: Infinity
});
this.trigger('error');
return;
}

// To try to resolve the quota exceeded error, clear back buffer and retry. This means
// that the segment-loader should block on future events until this one is handled, so
// that it doesn't keep moving onto further segments. Adding the call to the call
// queue will prevent further appends until waitingOnRemove_ and
// quotaExceededErrorRetryTimeout_ are cleared.
//
// Note that this will only block the current loader. In the case of demuxed content,
// the other load may keep filling as fast as possible. In practice, this should be
// OK, as it is a rare case when either audio has a high enough bitrate to fill up a
// source buffer, or video fills without enough room for audio to append (and without
// the availability of clearing out seconds of back buffer to make room for audio).
// But it might still be good to handle this case in the future as a TODO.
this.waitingOnRemove_ = true;
this.callQueue_.push(this.appendToSourceBuffer_.bind(this, {segmentInfo, type, bytes}));

const currentTime = this.currentTime_();
// Try to remove as much audio and video as possible to make room for new content
// before retrying.
const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER;

this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`);
this.remove(0, timeToRemoveUntil, () => {

this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`);
this.waitingOnRemove_ = false;
// wait the length of time alotted in the back buffer to prevent wasted
// attempts (since we can't clear less than the minimum)
this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => {
this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue');
this.quotaExceededErrorRetryTimeout_ = null;
this.processCallQueue_();
}, MIN_BACK_BUFFER * 1000);
}, true);
}

this.sourceUpdater_.appendBuffer({segmentInfo, type, bytes}, (error) => {
if (error) {
this.error(`${type} append of ${bytes.length}b failed for segment #${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`);
// If an append errors, we can't recover.
// (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
// Trigger a special error so that it can be handled separately from normal,
// recoverable errors.
this.trigger('appenderror');
handleAppendError_({segmentInfo, type, bytes}, error) {
// if there's no error, nothing to do
if (!error) {
return;
}

if (error.code === QUOTA_EXCEEDED_ERR) {
this.handleQuotaExceededError_({segmentInfo, type, bytes});
// A quota exceeded error should be recoverable with a future re-append, so no need
// to trigger an append error.
return;
}

this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error);
this.error(`${type} append of ${bytes.length}b failed for segment ` +
`#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`);

// If an append errors, we often can't recover.
// (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
//
// Trigger a special error so that it can be handled separately from normal,
// recoverable errors.
this.trigger('appenderror');
}

appendToSourceBuffer_({ segmentInfo, type, initSegment, data, bytes }) {
// If this is a re-append, bytes were already created and don't need to be recreated
if (!bytes) {
const segments = [data];
let byteLength = data.byteLength;

if (initSegment) {
// if the media initialization segment is changing, append it before the content
// segment
segments.unshift(initSegment);
byteLength += initSegment.byteLength;
}
});

// Technically we should be OK appending the init segment separately, however, we
// haven't yet tested that, and prepending is how we have always done things.
bytes = concatSegments({
bytes: byteLength,
segments
});
}

this.sourceUpdater_.appendBuffer(
{segmentInfo, type, bytes},
this.handleAppendError_.bind(this, {segmentInfo, type, bytes})
);
}

handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) {
Expand Down
39 changes: 32 additions & 7 deletions src/source-updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { bufferIntersection } from './ranges.js';
import {getMimeForCodec} from '@videojs/vhs-utils/es/codecs.js';
import window from 'global/window';
import toTitleCase from './util/to-title-case.js';
import { QUOTA_EXCEEDED_ERR } from './error-codes';

const bufferTypes = [
'video',
Expand Down Expand Up @@ -101,16 +102,22 @@ const shiftQueue = (type, sourceUpdater) => {
}

sourceUpdater.queue.splice(queueIndex, 1);
// Keep a record that this source buffer type is in use.
//
// The queue pending operation must be set before the action is performed in the event
// that the action results in a synchronous event that is acted upon. For instance, if
// an exception is thrown that can be handled, it's possible that new actions will be
// appended to an empty queue and immediately executed, but would not have the correct
// pending information if this property was set after the action was performed.
sourceUpdater.queuePending[type] = queueEntry;
queueEntry.action(type, sourceUpdater);

if (!queueEntry.doneFn) {
// synchronous operation, process next entry
sourceUpdater.queuePending[type] = null;
shiftQueue(type, sourceUpdater);
return;
}

// asynchronous operation, so keep a record that this source buffer type is in use
sourceUpdater.queuePending[type] = queueEntry;
};

const cleanupBuffer = (type, sourceUpdater) => {
Expand All @@ -132,7 +139,7 @@ const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuff
Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1;

const actions = {
appendBuffer: (bytes, segmentInfo) => (type, sourceUpdater) => {
appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => {
const sourceBuffer = sourceUpdater[`${type}Buffer`];

// can't do anything if the media source / source buffer is null
Expand All @@ -143,7 +150,15 @@ const actions = {

sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`);

sourceBuffer.appendBuffer(bytes);
try {
sourceBuffer.appendBuffer(bytes);
} catch (e) {
sourceUpdater.logger_(`Error with code ${e.code} ` +
(e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') +
`when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`);
sourceUpdater.queuePending[type] = null;
onError(e);
}
},
remove: (start, end) => (type, sourceUpdater) => {
const sourceBuffer = sourceUpdater[`${type}Buffer`];
Expand All @@ -155,7 +170,11 @@ const actions = {
}

sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`);
sourceBuffer.remove(start, end);
try {
sourceBuffer.remove(start, end);
} catch (e) {
sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`);
}
},
timestampOffset: (offset) => (type, sourceUpdater) => {
const sourceBuffer = sourceUpdater[`${type}Buffer`];
Expand Down Expand Up @@ -546,10 +565,16 @@ export default class SourceUpdater extends videojs.EventTarget {
return;
}

// In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will
// not be fired. This means that the queue will be blocked until the next action
// taken by the segment-loader. Provide a mechanism for segment-loader to handle
// these errors by calling the doneFn with the specific error.
const onError = doneFn;

pushQueue({
type,
sourceUpdater: this,
action: actions.appendBuffer(bytes, segmentInfo || {mediaIndex: -1}),
action: actions.appendBuffer(bytes, segmentInfo || {mediaIndex: -1}, onError),
doneFn,
name: 'appendBuffer'
});
Expand Down
Loading

0 comments on commit 74c785f

Please sign in to comment.