@l5i/dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
341 lines (303 loc) • 12.8 kB
JavaScript
/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import Debug from '../core/Debug';
import DashJSError from './vo/DashJSError';
import EventBus from '../core/EventBus';
import Events from '../core/events/Events';
import FactoryMaker from '../core/FactoryMaker';
import TextController from './text/TextController';
import Errors from '../core/errors/Errors';
/**
* @class SourceBufferSink
* @implements FragmentSink
*/
function SourceBufferSink(mediaSource, mediaInfo, onAppendedCallback, oldBuffer) {
const context = this.context;
const eventBus = EventBus(context).getInstance();
let instance,
logger,
buffer,
isAppendingInProgress;
let callbacks = [];
let appendQueue = [];
let onAppended = onAppendedCallback;
let intervalId;
function setup() {
logger = Debug(context).getInstance().getLogger(instance);
isAppendingInProgress = false;
const codec = mediaInfo.codec;
try {
// Safari claims to support anything starting 'application/mp4'.
// it definitely doesn't understand 'application/mp4;codecs="stpp"'
// - currently no browser does, so check for it and use our own
// implementation. The same is true for codecs="wvtt".
if (codec.match(/application\/mp4;\s*codecs="(stpp|wvtt).*"/i)) {
throw new Error('not really supported');
}
buffer = oldBuffer ? oldBuffer : mediaSource.addSourceBuffer(codec);
const CHECK_INTERVAL = 50;
// use updateend event if possible
if (typeof buffer.addEventListener === 'function') {
try {
buffer.addEventListener('updateend', updateEndHandler, false);
buffer.addEventListener('error', errHandler, false);
buffer.addEventListener('abort', errHandler, false);
} catch (err) {
// use setInterval to periodically check if updating has been completed
intervalId = setInterval(checkIsUpdateEnded, CHECK_INTERVAL);
}
} else {
// use setInterval to periodically check if updating has been completed
intervalId = setInterval(checkIsUpdateEnded, CHECK_INTERVAL);
}
} catch (ex) {
// Note that in the following, the quotes are open to allow for extra text after stpp and wvtt
if ((mediaInfo.isText) || (codec.indexOf('codecs="stpp') !== -1) || (codec.indexOf('codecs="wvtt') !== -1)) {
const textController = TextController(context).getInstance();
buffer = textController.getTextSourceBuffer();
} else {
throw ex;
}
}
}
function reset(keepBuffer) {
if (buffer) {
if (typeof buffer.removeEventListener === 'function') {
buffer.removeEventListener('updateend', updateEndHandler, false);
buffer.removeEventListener('error', errHandler, false);
buffer.removeEventListener('abort', errHandler, false);
}
clearInterval(intervalId);
if (!keepBuffer) {
try {
if (!buffer.getClassName || buffer.getClassName() !== 'TextSourceBuffer') {
mediaSource.removeSourceBuffer(buffer);
}
} catch (e) {
logger.error('Failed to remove source buffer from media source.');
}
buffer = null;
}
isAppendingInProgress = false;
}
appendQueue = [];
onAppended = null;
}
function getBuffer() {
return buffer;
}
function getAllBufferRanges() {
try {
return buffer.buffered;
} catch (e) {
logger.error('getAllBufferRanges exception: ' + e.message);
return null;
}
}
function append(chunk) {
if (!chunk) {
onAppended({
chunk: chunk,
error: new DashJSError(Errors.APPEND_ERROR_CODE, Errors.APPEND_ERROR_MESSAGE)
});
return;
}
appendQueue.push(chunk);
if (!isAppendingInProgress) {
waitForUpdateEnd(buffer, appendNextInQueue.bind(this));
}
}
function updateTimestampOffset(MSETimeOffset) {
if (buffer.timestampOffset !== MSETimeOffset && !isNaN(MSETimeOffset)) {
waitForUpdateEnd(buffer, () => {
buffer.timestampOffset = MSETimeOffset;
});
}
}
function remove(start, end, forceRemoval) {
const sourceBufferSink = this;
// make sure that the given time range is correct. Otherwise we will get InvalidAccessError
waitForUpdateEnd(buffer, function () {
try {
if ((start >= 0) && (end > start) && (forceRemoval || mediaSource.readyState !== 'ended')) {
buffer.remove(start, end);
}
// updating is in progress, we should wait for it to complete before signaling that this operation is done
waitForUpdateEnd(buffer, function () {
eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, {
buffer: sourceBufferSink,
from: start,
to: end,
unintended: false
});
});
} catch (err) {
eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, {
buffer: sourceBufferSink,
from: start,
to: end,
unintended: false,
error: new DashJSError(err.code, err.message)
});
}
});
}
function appendNextInQueue() {
const sourceBufferSink = this;
if (appendQueue.length > 0) {
isAppendingInProgress = true;
const nextChunk = appendQueue[0];
appendQueue.splice(0,1);
let oldRanges = [];
const afterSuccess = function () {
// Safari sometimes drops a portion of a buffer after appending. Handle these situations here
const newRanges = getAllBufferRanges();
checkBufferGapsAfterAppend(sourceBufferSink, oldRanges, newRanges, nextChunk);
if (appendQueue.length > 0) {
appendNextInQueue.call(this);
} else {
isAppendingInProgress = false;
if (onAppended) {
onAppended({
chunk: nextChunk
});
}
}
};
try {
if (nextChunk.bytes.length === 0) {
afterSuccess.call(this);
} else {
oldRanges = getAllBufferRanges();
if (buffer.appendBuffer) {
buffer.appendBuffer(nextChunk.bytes);
} else {
buffer.append(nextChunk.bytes, nextChunk);
}
// updating is in progress, we should wait for it to complete before signaling that this operation is done
waitForUpdateEnd(buffer, afterSuccess.bind(this));
}
} catch (err) {
logger.fatal('SourceBuffer append failed "' + err + '"');
if (appendQueue.length > 0) {
appendNextInQueue();
} else {
isAppendingInProgress = false;
}
if (onAppended) {
onAppended({
chunk: nextChunk,
error: new DashJSError(err.code, err.message)
});
}
}
}
}
function checkBufferGapsAfterAppend(buffer, oldRanges, newRanges, chunk) {
if (oldRanges && oldRanges.length > 0 && oldRanges.length < newRanges.length &&
isChunkAlignedWithRange(oldRanges, chunk)) {
// A split in the range was created while appending
eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, {
buffer: buffer,
from: newRanges.end(newRanges.length - 2),
to: newRanges.start(newRanges.length - 1),
unintended: true
});
}
}
function isChunkAlignedWithRange(oldRanges, chunk) {
for (let i = 0; i < oldRanges.length; i++ ) {
const start = Math.round(oldRanges.start(i));
const end = Math.round(oldRanges.end(i));
if (end === chunk.start || start === chunk.end || (chunk.start >= start && chunk.end <= end) ) {
return true;
}
}
return false;
}
function abort() {
try {
if (mediaSource.readyState === 'open') {
buffer.abort();
} else if (buffer.setTextTrack && mediaSource.readyState === 'ended') {
buffer.abort(); //The cues need to be removed from the TextSourceBuffer via a call to abort()
}
} catch (ex) {
logger.error('SourceBuffer append abort failed: "' + ex + '"');
}
appendQueue = [];
}
function executeCallback() {
if (callbacks.length > 0) {
const cb = callbacks.shift();
if (buffer.updating) {
waitForUpdateEnd(buffer, cb);
} else {
cb();
// Try to execute next callback if still not updating
executeCallback();
}
}
}
function checkIsUpdateEnded() {
// if updating is still in progress do nothing and wait for the next check again.
if (buffer.updating) return;
// updating is completed, now we can stop checking and resolve the promise
executeCallback();
}
function updateEndHandler() {
if (buffer.updating) return;
executeCallback();
}
function errHandler() {
logger.error('SourceBufferSink error', mediaInfo.type);
}
function waitForUpdateEnd(buffer, callback) {
callbacks.push(callback);
if (!buffer.updating) {
executeCallback();
}
}
instance = {
getAllBufferRanges: getAllBufferRanges,
getBuffer: getBuffer,
append: append,
remove: remove,
abort: abort,
reset: reset,
updateTimestampOffset: updateTimestampOffset
};
setup();
return instance;
}
SourceBufferSink.__dashjs_factory_name = 'SourceBufferSink';
const factory = FactoryMaker.getClassFactory(SourceBufferSink);
export default factory;