@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
645 lines • 26.1 kB
JavaScript
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Log from '../utils/logger.js';
import SpeedSampler from './speed-sampler.js';
import { LoaderStatus, LoaderErrors } from './loader.js';
import FetchStreamLoader from './fetch-stream-loader.js';
import MozChunkedLoader from './xhr-moz-chunked-loader.js';
import MSStreamLoader from './xhr-msstream-loader.js';
import RangeLoader from './xhr-range-loader.js';
import WebSocketLoader from './websocket-loader.js';
import RangeSeekHandler from './range-seek-handler.js';
import ParamSeekHandler from './param-seek-handler.js';
import { RuntimeException, IllegalStateException, InvalidArgumentException } from '../utils/exception.js';
/**
* DataSource: {
* url: string,
* filesize: number,
* cors: boolean,
* withCredentials: boolean
* }
*
*/
// Manage IO Loaders
var IOController = /** @class */ (function () {
function IOController(dataSource, config, extraData) {
this.TAG = 'IOController';
this._config = config;
this._extraData = extraData;
this._stashInitialSize = 64 * 1024; // default initial size: 64KB
if (config.stashInitialSize != undefined && config.stashInitialSize > 0) {
// apply from config
this._stashInitialSize = config.stashInitialSize;
}
this._stashUsed = 0;
this._stashSize = this._stashInitialSize;
this._bufferSize = 1024 * 1024 * 3; // initial size: 3MB
this._stashBuffer = new ArrayBuffer(this._bufferSize);
this._stashByteStart = 0;
this._enableStash = true;
if (config.enableStashBuffer === false) {
this._enableStash = false;
}
this._loader = null;
this._loaderClass = null;
this._seekHandler = null;
this._dataSource = dataSource;
this._isWebSocketURL = /wss?:\/\/(.+?)/.test(dataSource.url);
this._refTotalLength = dataSource.filesize ? dataSource.filesize : null;
this._totalLength = this._refTotalLength;
this._fullRequestFlag = false;
this._currentRange = null;
this._redirectedURL = null;
this._speedNormalized = 0;
this._speedSampler = new SpeedSampler();
this._speedNormalizeList = [32, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
this._isEarlyEofReconnecting = false;
this._paused = false;
this._resumeFrom = 0;
this._onDataArrival = null;
this._onSeeked = null;
this._onError = null;
this._onComplete = null;
this._onRedirect = null;
this._onRecoveredEarlyEof = null;
this._onInformation = null;
this._selectSeekHandler();
this._selectLoader();
this._createLoader();
}
IOController.prototype.destroy = function () {
if (this._loader.isWorking()) {
this._loader.abort();
}
this._loader.destroy();
this._loader = null;
this._loaderClass = null;
this._dataSource = null;
this._stashBuffer = null;
this._stashUsed = this._stashSize = this._bufferSize = this._stashByteStart = 0;
this._currentRange = null;
this._speedSampler = null;
this._isEarlyEofReconnecting = false;
this._onDataArrival = null;
this._onSeeked = null;
this._onError = null;
this._onComplete = null;
this._onRedirect = null;
this._onRecoveredEarlyEof = null;
this._onInformation = null;
this._extraData = null;
};
IOController.prototype.isWorking = function () {
return this._loader && this._loader.isWorking() && !this._paused;
};
IOController.prototype.isPaused = function () {
return this._paused;
};
Object.defineProperty(IOController.prototype, "status", {
get: function () {
return this._loader.status;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "extraData", {
get: function () {
return this._extraData;
},
set: function (data) {
this._extraData = data;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onDataArrival", {
// prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number
get: function () {
return this._onDataArrival;
},
set: function (callback) {
this._onDataArrival = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onSeeked", {
get: function () {
return this._onSeeked;
},
set: function (callback) {
this._onSeeked = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onError", {
// prototype: function onError(type: number, info: {code: number, msg: string}): void
get: function () {
return this._onError;
},
set: function (callback) {
this._onError = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onComplete", {
get: function () {
return this._onComplete;
},
set: function (callback) {
this._onComplete = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onInformation", {
get: function () {
return this._onInformation;
},
set: function (callback) {
this._onInformation = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onRedirect", {
get: function () {
return this._onRedirect;
},
set: function (callback) {
this._onRedirect = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "onRecoveredEarlyEof", {
get: function () {
return this._onRecoveredEarlyEof;
},
set: function (callback) {
this._onRecoveredEarlyEof = callback;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "currentURL", {
get: function () {
return this._dataSource.url;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "hasRedirect", {
get: function () {
return (this._redirectedURL != null || this._dataSource.redirectedURL != undefined);
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "currentRedirectedURL", {
get: function () {
return this._redirectedURL || this._dataSource.redirectedURL;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "currentSpeed", {
// in KB/s
get: function () {
if (this._loaderClass === RangeLoader) {
// SpeedSampler is inaccuracy if loader is RangeLoader
return this._loader.currentSpeed;
}
return this._speedSampler.lastSecondKBps;
},
enumerable: false,
configurable: true
});
Object.defineProperty(IOController.prototype, "loaderType", {
get: function () {
return this._loader.type;
},
enumerable: false,
configurable: true
});
IOController.prototype._selectSeekHandler = function () {
var config = this._config;
if (config.seekType === 'range') {
this._seekHandler = new RangeSeekHandler(this._config.rangeLoadZeroStart);
}
else if (config.seekType === 'param') {
var paramStart = config.seekParamStart || 'bstart';
var paramEnd = config.seekParamEnd || 'bend';
this._seekHandler = new ParamSeekHandler(paramStart, paramEnd);
}
else if (config.seekType === 'custom') {
if (typeof config.customSeekHandler !== 'function') {
throw new InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!');
}
this._seekHandler = new config.customSeekHandler();
}
else {
throw new InvalidArgumentException("Invalid seekType in config: ".concat(config.seekType));
}
};
IOController.prototype._selectLoader = function () {
if (this._config.customLoader != null) {
this._loaderClass = this._config.customLoader;
}
else if (this._isWebSocketURL) {
this._loaderClass = WebSocketLoader;
}
else if (FetchStreamLoader.isSupported()) {
this._loaderClass = FetchStreamLoader;
}
else if (MozChunkedLoader.isSupported()) {
this._loaderClass = MozChunkedLoader;
}
else if (RangeLoader.isSupported()) {
this._loaderClass = RangeLoader;
}
else {
throw new RuntimeException('Your browser doesn\'t support xhr with arraybuffer responseType!');
}
};
IOController.prototype._createLoader = function () {
this._loader = new this._loaderClass(this._seekHandler, this._config);
if (this._loader.needStashBuffer === false) {
this._enableStash = false;
}
this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this);
this._loader.onURLRedirect = this._onURLRedirect.bind(this);
this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
this._loader.onComplete = this._onLoaderComplete.bind(this);
this._loader.onError = this._onLoaderError.bind(this);
this._loader.onInformation = this._onLoaderInformation.bind(this);
};
IOController.prototype.open = function (optionalFrom) {
this._currentRange = { from: 0, to: -1 };
if (optionalFrom) {
this._currentRange.from = optionalFrom;
}
this._speedSampler.reset();
if (!optionalFrom) {
this._fullRequestFlag = true;
}
this._loader.open(this._dataSource, Object.assign({}, this._currentRange));
};
IOController.prototype.abort = function () {
this._loader.abort();
if (this._paused) {
this._paused = false;
this._resumeFrom = 0;
}
};
IOController.prototype.pause = function () {
if (this.isWorking()) {
this._loader.abort();
if (this._stashUsed !== 0) {
this._resumeFrom = this._stashByteStart;
this._currentRange.to = this._stashByteStart - 1;
}
else {
this._resumeFrom = this._currentRange.to + 1;
}
this._stashUsed = 0;
this._stashByteStart = 0;
this._paused = true;
}
};
IOController.prototype.resume = function () {
if (this._paused) {
this._paused = false;
var bytes = this._resumeFrom;
this._resumeFrom = 0;
this._internalSeek(bytes, true);
}
};
IOController.prototype.seek = function (bytes) {
this._paused = false;
this._stashUsed = 0;
this._stashByteStart = 0;
this._internalSeek(bytes, true);
};
/**
* When seeking request is from media seeking, unconsumed stash data should be dropped
* However, stash data shouldn't be dropped if seeking requested from http reconnection
*
* @dropUnconsumed: Ignore and discard all unconsumed data in stash buffer
*/
IOController.prototype._internalSeek = function (bytes, dropUnconsumed) {
if (this._loader.isWorking()) {
this._loader.abort();
}
// dispatch & flush stash buffer before seek
this._flushStashBuffer(dropUnconsumed);
this._loader.destroy();
this._loader = null;
var requestRange = { from: bytes, to: -1 };
this._currentRange = { from: requestRange.from, to: -1 };
this._speedSampler.reset();
this._stashSize = this._stashInitialSize;
this._createLoader();
this._loader.open(this._dataSource, requestRange);
if (this._onSeeked) {
this._onSeeked();
}
};
IOController.prototype.updateUrl = function (url) {
if (!url || typeof url !== 'string' || url.length === 0) {
throw new InvalidArgumentException('Url must be a non-empty string!');
}
this._dataSource.url = url;
// TODO: replace with new url
};
IOController.prototype._expandBuffer = function (expectedBytes) {
var bufferNewSize = this._stashSize;
while (bufferNewSize + 1024 * 1024 * 1 < expectedBytes) {
bufferNewSize *= 2;
}
bufferNewSize += 1024 * 1024 * 1; // bufferSize = stashSize + 1MB
if (bufferNewSize === this._bufferSize) {
return;
}
var newBuffer = new ArrayBuffer(bufferNewSize);
if (this._stashUsed > 0) { // copy existing data into new buffer
var stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed);
var stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize);
stashNewArray.set(stashOldArray, 0);
}
this._stashBuffer = newBuffer;
this._bufferSize = bufferNewSize;
};
IOController.prototype._normalizeSpeed = function (input) {
var list = this._speedNormalizeList;
var last = list.length - 1;
var mid = 0;
var lbound = 0;
var ubound = last;
if (input < list[0]) {
return list[0];
}
// binary search
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (input >= list[mid] && input < list[mid + 1])) {
return list[mid];
}
else if (list[mid] < input) {
lbound = mid + 1;
}
else {
ubound = mid - 1;
}
}
};
IOController.prototype._adjustStashSize = function (normalized) {
var stashSizeKB = 0;
if (this._config.isLive) {
// live stream: always use 1/8 normalized speed for size of stashSizeKB
stashSizeKB = normalized / 8;
}
else {
if (normalized < 512) {
stashSizeKB = normalized;
}
else if (normalized >= 512 && normalized <= 1024) {
stashSizeKB = Math.floor(normalized * 1.5);
}
else {
stashSizeKB = normalized * 2;
}
}
if (stashSizeKB > 8192) {
stashSizeKB = 8192;
}
var bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1; // stashSize + 1MB
if (this._bufferSize < bufferSize) {
this._expandBuffer(bufferSize);
}
this._stashSize = stashSizeKB * 1024;
};
IOController.prototype._dispatchChunks = function (chunks, byteStart) {
this._currentRange.to = byteStart + chunks.byteLength - 1;
return this._onDataArrival(chunks, byteStart);
};
IOController.prototype._onURLRedirect = function (redirectedURL) {
this._redirectedURL = redirectedURL;
if (this._onRedirect) {
this._onRedirect(redirectedURL);
}
};
IOController.prototype._onContentLengthKnown = function (contentLength) {
if (contentLength && this._fullRequestFlag) {
this._totalLength = contentLength;
this._fullRequestFlag = false;
}
};
IOController.prototype._onLoaderChunkArrival = function (chunk, byteStart, receivedLength) {
if (!this._onDataArrival) {
throw new IllegalStateException('IOController: No existing consumer (onDataArrival) callback!');
}
if (this._paused) {
return;
}
if (this._isEarlyEofReconnecting) {
// Auto-reconnect for EarlyEof succeed, notify to upper-layer by callback
this._isEarlyEofReconnecting = false;
if (this._onRecoveredEarlyEof) {
this._onRecoveredEarlyEof();
}
}
this._speedSampler.addBytes(chunk.byteLength);
// adjust stash buffer size according to network speed dynamically
var KBps = this._speedSampler.lastSecondKBps;
if (KBps !== 0) {
var normalized = this._normalizeSpeed(KBps);
if (this._speedNormalized !== normalized) {
this._speedNormalized = normalized;
this._adjustStashSize(normalized);
}
}
if (!this._enableStash) { // disable stash
if (this._stashUsed === 0) {
// dispatch chunk directly to consumer;
// check ret value (consumed bytes) and stash unconsumed to stashBuffer
var consumed = this._dispatchChunks(chunk, byteStart);
if (consumed < chunk.byteLength) { // unconsumed data remain.
var remain = chunk.byteLength - consumed;
if (remain > this._bufferSize) {
this._expandBuffer(remain);
}
var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
stashArray.set(new Uint8Array(chunk, consumed), 0);
this._stashUsed += remain;
this._stashByteStart = byteStart + consumed;
}
}
else {
// else: Merge chunk into stashBuffer, and dispatch stashBuffer to consumer.
if (this._stashUsed + chunk.byteLength > this._bufferSize) {
this._expandBuffer(this._stashUsed + chunk.byteLength);
}
var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
var consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart);
if (consumed < this._stashUsed && consumed > 0) { // unconsumed data remain
var remainArray = new Uint8Array(this._stashBuffer, consumed);
stashArray.set(remainArray, 0);
}
this._stashUsed -= consumed;
this._stashByteStart += consumed;
}
}
else { // enable stash
if (this._stashUsed === 0 && this._stashByteStart === 0) { // seeked? or init chunk?
// This is the first chunk after seek action
this._stashByteStart = byteStart;
}
if (this._stashUsed + chunk.byteLength <= this._stashSize) {
// just stash
var stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
}
else { // stashUsed + chunkSize > stashSize, size limit exceeded
var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
if (this._stashUsed > 0) { // There're stash datas in buffer
// dispatch the whole stashBuffer, and stash remain data
// then append chunk to stashBuffer (stash)
var buffer = this._stashBuffer.slice(0, this._stashUsed);
var consumed = this._dispatchChunks(buffer, this._stashByteStart);
if (consumed < buffer.byteLength) {
if (consumed > 0) {
var remainArray = new Uint8Array(buffer, consumed);
stashArray.set(remainArray, 0);
this._stashUsed = remainArray.byteLength;
this._stashByteStart += consumed;
}
}
else {
this._stashUsed = 0;
this._stashByteStart += consumed;
}
if (this._stashUsed + chunk.byteLength > this._bufferSize) {
this._expandBuffer(this._stashUsed + chunk.byteLength);
stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
}
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
}
else { // stash buffer empty, but chunkSize > stashSize (oh, holy shit)
// dispatch chunk directly and stash remain data
var consumed = this._dispatchChunks(chunk, byteStart);
if (consumed < chunk.byteLength) {
var remain = chunk.byteLength - consumed;
if (remain > this._bufferSize) {
this._expandBuffer(remain);
stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
}
stashArray.set(new Uint8Array(chunk, consumed), 0);
this._stashUsed += remain;
this._stashByteStart = byteStart + consumed;
}
}
}
}
};
IOController.prototype._flushStashBuffer = function (dropUnconsumed) {
if (this._stashUsed > 0) {
var buffer = this._stashBuffer.slice(0, this._stashUsed);
var consumed = this._dispatchChunks(buffer, this._stashByteStart);
var remain = buffer.byteLength - consumed;
if (consumed < buffer.byteLength) {
if (dropUnconsumed) {
Log.w(this.TAG, "".concat(remain, " bytes unconsumed data remain when flush buffer, dropped"));
}
else {
if (consumed > 0) {
var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
var remainArray = new Uint8Array(buffer, consumed);
stashArray.set(remainArray, 0);
this._stashUsed = remainArray.byteLength;
this._stashByteStart += consumed;
}
return 0;
}
}
this._stashUsed = 0;
this._stashByteStart = 0;
return remain;
}
return 0;
};
IOController.prototype._onLoaderComplete = function (from, to) {
// Force-flush stash buffer, and drop unconsumed data
this._flushStashBuffer(true);
if (this._onComplete) {
this._onComplete(this._extraData);
}
};
IOController.prototype._onLoaderError = function (type, data) {
// Log.e(this.TAG, `Loader error, code = ${data.code}, msg = ${data.msg}`);
this._flushStashBuffer(false);
if (this._isEarlyEofReconnecting) {
// Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer
this._isEarlyEofReconnecting = false;
type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;
}
switch (type) {
case LoaderErrors.EARLY_EOF: {
if (!this._config.isLive) {
// Do internal http reconnect if not live stream
if (this._totalLength) {
var nextFrom = this._currentRange.to + 1;
if (nextFrom < this._totalLength) {
Log.w(this.TAG, 'Connection lost, trying reconnect...');
this._isEarlyEofReconnecting = true;
this._internalSeek(nextFrom, false);
}
return;
}
// else: We don't know totalLength, throw UnrecoverableEarlyEof
}
// live stream: throw UnrecoverableEarlyEof error to upper-layer
type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;
break;
}
case LoaderErrors.UNRECOVERABLE_EARLY_EOF:
case LoaderErrors.CONNECTING_TIMEOUT:
case LoaderErrors.HTTP_STATUS_CODE_INVALID:
case LoaderErrors.EXCEPTION:
break;
}
if (this._onError) {
this._onError(type, data);
}
else {
throw new RuntimeException('IOException: ' + data.msg);
}
};
IOController.prototype._onLoaderInformation = function (type, data) {
this._onInformation(type, data);
};
return IOController;
}());
export default IOController;
//# sourceMappingURL=io-controller.js.map