UNPKG

hjplayer

Version:

hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;

919 lines (805 loc) 29.4 kB
import Logger from '../Utils/Logger'; import SpeedChecker from '../Utils/networkSpeedChecker'; import LoaderStatus from '../Loaders/LoaderStatus'; import Errors from '../Errors/index'; import Events from '../Events/index'; import FetchStreamLoader from '../Loaders/FetchStreamLoader'; import MozChunkedLoader from '../Loaders/XHRMozChunkedLoader'; import MSStreamLoader from '../Loaders/XHRMsStreamLoader'; import RangeLoader from '../Loaders/XHRRangeLoader'; import WebSocketLoader from '../Loaders/WebSocketLoader'; import FragmentLoader from '../Loaders/FragmentLoader'; import RangeSeekHandler from '../Utils/range-seek-handler'; import ParamSeekHandler from '../Utils/param-seek-handler'; import { RuntimeException, IllegalStateException, InvalidArgumentException } from '../Utils/Exception'; import MediaConfig from '../Interfaces/MediaConfig'; import SeekRange from '../Interfaces/SeekRange'; import ErrorData from '../Interfaces/ErrorData'; import HJPlayerConfig from '../Interfaces/HJPlayerConfig'; import TSManifest from '../Interfaces/TSManifest'; import { TSExtraData } from '../Interfaces/TSExtraData'; class IOController { /** * 文件标签 */ Tag: string /** * 媒体文件设置 */ private _mediaConfig: MediaConfig /** * 用户设置 */ private _config: HJPlayerConfig /** * 存储的buffer初始大小 */ private _stashInitialSize: number /** * 缓冲池已用大小 */ private _stashUsed: number /** * 存储的buffer尺寸 */ private _stashSize: number /** * 是否允许 LoaderIO 建立缓冲池 */ private _enableStash: boolean /** * 缓冲池的尺寸 */ private _bufferSize: number /** * LoaderIO的缓冲池 */ private _stashBuffer: ArrayBuffer /** * 缓冲池缓冲的Buffer数据流在媒体文件中的位置 */ private _stashByteStart: number /** * 加载器实例 */ private _loader: | FetchStreamLoader | MozChunkedLoader | MSStreamLoader | RangeLoader | WebSocketLoader | FragmentLoader | null // 加载媒体文件的加载器实例 /** * 加载媒体文件的加载器定义类 */ private _loaderClass: | FetchStreamLoader | MozChunkedLoader | MSStreamLoader | RangeLoader | WebSocketLoader | FragmentLoader | any /** * 视频 seek 处理函数, 可根据后台服务自己去定义 */ private _seekHandler: ParamSeekHandler | RangeSeekHandler | any /** * 媒体设置里的URL是否为 WebSocket 地址 */ private _isWebSocketURL: boolean /** * 加载文件体积 */ private _refTotalLength: number | null /** * 总体文件大小 */ private _totalLength: number | null /** * 全部请求的标志 */ private _fullRequestFlag: boolean /** * 请求的范围 from xxx, to xxx */ private _currentRange: SeekRange | null /** * HTTP 301/302跳转后的地址 */ private _redirectedURL: string | null /** * 计算后的网速标准值 */ private _speedNormalized: number | null /** * 网速标准表, 用于把 networkSpeedChecker 估算后得到的网速经计算后得到标准值 _speedNormalized */ private _speedNormalizeList: Array<number> /** * 是否过早的遇到 EOF */ private _isEarlyEofReconnecting: boolean /** * 是否暂停 */ private _paused: boolean /** * 恢复下载时的续传点 */ private _resumeFrom: number // 恢复下载时的续传点 /** * 数据透传到IOController的处理函数 */ private _onDataArrival: Function | null /** * 当 seek 结束时上报的函数 */ private _onSeeked: Function | null /** * 发生错误的处理函数 */ private _onError: Function | null /** * 加载完成时透传的函数 */ private _onComplete: Function | null /** * 遇到 HTTP 301/302 网址跳转时处理的函数 */ private _onRedirect: Function | null /** * 当从 EarlyEof 恢复时透传的函数 */ private _onRecoveredEarlyEof: Function | null /** * 当M3U8文档被解析之后向上提交 */ private _onManifestParsed: Function | null /** * 额外的数据, 在flv.js中用于传输segment的索引值 */ private _extraData: number | null; /** * hls 解析的Levels */ private _tsExtraData: TSExtraData | undefined /** * 网速检查器 */ private _speedChecker: SpeedChecker constructor(dataSource: MediaConfig, config: HJPlayerConfig, extraData: number) { this.Tag = 'IOController'; this._config = config; this._extraData = extraData; this._stashInitialSize = 1024 * 384; // default initial size: 384KB if(config.stashInitialSize !== undefined && config.stashInitialSize > 0) { // apply from config this._stashInitialSize = config.stashInitialSize * 1024; } 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._mediaConfig = dataSource; this._isWebSocketURL = /wss?:\/\/(.+?)/.test(dataSource.url); this._refTotalLength = dataSource.fileSize ? dataSource.fileSize : null; this._totalLength = this._refTotalLength; this._tsExtraData = undefined; this._fullRequestFlag = false; this._currentRange = null; this._redirectedURL = null; this._speedNormalized = 0; this._speedChecker = new SpeedChecker(); this._speedNormalizeList = [64, 128, 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._onManifestParsed = null; this._selectSeekHandler(); this._selectLoader(); this._createLoader(); } destroy() { if(this._loader!.isWorking()) { this._loader!.abort(); } this._loader!.destroy(); this._loader = null; this._loaderClass = null; delete this._mediaConfig; delete this._stashBuffer; this._stashByteStart = 0; this._bufferSize = 0; this._stashSize = 0; this._stashUsed = 0; this._currentRange = null; delete this._speedChecker; this._isEarlyEofReconnecting = false; this._onDataArrival = null; this._onSeeked = null; this._onError = null; this._onComplete = null; this._onRedirect = null; this._onRecoveredEarlyEof = null; this._onManifestParsed = null; this._extraData = null; } isWorking() { return this._loader && this._loader.isWorking() && !this._paused; } isPaused() { return this._paused; } get status() { return this._loader!.status; } get extraData() { return this._extraData; } set extraData(data) { this._extraData = data; } // prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number get onDataArrival() { return this._onDataArrival; } set onDataArrival(callback) { this._onDataArrival = callback; } get onSeeked() { return this._onSeeked; } set onSeeked(callback) { this._onSeeked = callback; } // prototype: function onError(type: number, info: {code: number, msg: string}): void get onError() { return this._onError; } set onError(callback) { this._onError = callback; } get onComplete() { return this._onComplete; } set onComplete(callback) { this._onComplete = callback; } get onRedirect() { return this._onRedirect; } set onRedirect(callback) { this._onRedirect = callback; } get onRecoveredEarlyEof() { return this._onRecoveredEarlyEof; } set onRecoveredEarlyEof(callback) { this._onRecoveredEarlyEof = callback; } get onManifestParsed() { return this._onManifestParsed; } set onManifestParsed(callback) { this._onManifestParsed = callback; } get currentURL() { return this._mediaConfig.url; } get hasRedirect() { return this._redirectedURL != null || this._mediaConfig.redirectedURL !== undefined; } get currentRedirectedURL() { return this._redirectedURL || this._mediaConfig.redirectedURL; } // in KB/s get currentSpeed() { if(this._loader instanceof RangeLoader) { // SpeedSampler is inaccuracy if loader is RangeLoader return this._loader.currentSpeed; } return this._speedChecker.lastSecondKBps; } get loaderType() { return this._loader!.type; } _selectSeekHandler() { const config = this._config; if(config.seekType === 'range') { this._seekHandler = new RangeSeekHandler(this._config.rangeLoadZeroStart); } else if(config.seekType === 'param') { const paramStart = config.seekParamStart || 'bstart'; const 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: ${config.seekType}`); } } _selectLoader() { if(this._mediaConfig.type === 'flv') { 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!" ); } } else if(this._mediaConfig.type === 'm3u8') { this._loaderClass = FragmentLoader; } } _createLoader() { this._loader = new this._loaderClass(this._seekHandler, this._config); this._bindLoaderEvents(); } _bindLoaderEvents() { if(!this._loader) return; 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); if(this._mediaConfig.type === 'm3u8') { this._loader.on(Events.MANIFEST_PARSED, (data: TSManifest) => { this._onManifestParsed && this._onManifestParsed(data); }); } } /** * 从选择的点开始加载 * @param optionalFrom 开始加载的点 */ open(optionalFrom?: number) { this._currentRange = { from: 0, to: -1 }; if(optionalFrom) { this._currentRange.from = optionalFrom; } this._speedChecker.reset(); if(!optionalFrom) { this._fullRequestFlag = true; } this._loader && this._loader.startLoad(this._mediaConfig, { ...this._currentRange }); } abort() { this._loader!.abort(); if(this._paused) { this._paused = false; this._resumeFrom = 0; } } pause() { if(this.isWorking()) { this._loader!.abort(); if(this._currentRange) { 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; } } resume() { if(this._paused) { this._paused = false; const bytes = this._resumeFrom; this._resumeFrom = 0; this._internalSeek(bytes, true); } } seek(bytes: number) { this._paused = false; this._stashUsed = 0; this._stashByteStart = 0; this._internalSeek(bytes, true); } /** * hls流处理seek, 查找相应的ts文件, 然后下载 */ tsSeek(milliseconds: number) { this._loader instanceof FragmentLoader && this._loader.seek(milliseconds); } /** * 加载下一个片段 */ loadNextFrag() { this._loader instanceof FragmentLoader && this._loader.loadNextFrag(); } /** * 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 */ _internalSeek(bytes: number, dropUnconsumed?: boolean) { if(this._loader!.isWorking()) { this._loader!.abort(); } if(this._mediaConfig.type === 'flv') { this._flushStashBuffer(dropUnconsumed); // dispatch & flush stash buffer before seek this._loader!.destroy(); this._loader = null; const requestRange = { from: bytes, to: -1 }; this._currentRange = { from: requestRange.from, to: -1 }; this._stashSize = this._stashInitialSize; this._createLoader(); this._loader!.startLoad(this._mediaConfig, requestRange); } else { if(this._loader instanceof FragmentLoader) { this._loader.resume(); this._loader.loadNextFrag(); } } this._speedChecker.reset(); if(this._onSeeked) { this._onSeeked(); } } /** * 扩冲数据池 * @param expectedBytes 期望的数据池大小 */ _expandBuffer(expectedBytes: number) { let bufferNewSize = this._stashSize; while(bufferNewSize + 1024 * 1024 * 1 < expectedBytes) { bufferNewSize *= 2; } bufferNewSize += 1024 * 1024 * 1; // bufferSize = stashSize + 1MB if(bufferNewSize === this._bufferSize) { return; } const newBuffer = new ArrayBuffer(bufferNewSize); if(this._stashUsed > 0) { // copy existing data into new buffer const stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed); const stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize); stashNewArray.set(stashOldArray, 0); } this._stashBuffer = newBuffer; this._bufferSize = bufferNewSize; } /** * 标准化网速值, 使之等于标准1M, 2M, 4M 10M等带宽的网速最大值 * @param input speedChecker返回的网速值 */ _normalizeSpeed(input: number) { const list = this._speedNormalizeList; const last = list.length - 1; let mid = 0; let lbound = 0; let 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]; } if(list[mid] < input) { lbound = mid + 1; } else { ubound = mid - 1; } } } _adjustStashSize(normalized: number) { let stashSizeKB = 0; if(this._config.isLive) { // live stream: always use single normalized speed for size of stashSizeKB stashSizeKB = normalized; } 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; } const bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1; // stashSize + 1MB if(this._bufferSize < bufferSize) { this._expandBuffer(bufferSize); } this._stashSize = stashSizeKB * 1024; } /** * 向上发送数据块, 并返回已经解析处理的数据的长度 * @param chunks 发送的数据内容 * @param byteStart 数据内容在总的数据中的索引值 * @param extraData 额外的数据 */ _dispatchChunks(chunks: ArrayBuffer, byteStart: number, extraData?: TSExtraData): number { this._currentRange && (this._currentRange.to = byteStart + chunks.byteLength - 1); if(this._onDataArrival) { return this._onDataArrival(chunks, byteStart, extraData); } return 0; } /** * 发生 301/302 地址跳转后的处理函数 * @param redirectedURL 跳转的地址 */ _onURLRedirect(redirectedURL: string) { this._redirectedURL = redirectedURL; if(this._onRedirect) { this._onRedirect(redirectedURL); } } /** * 当在response header 中获知请求文件大小后处理的函数 * @param contentLength 在请求头中获知文件的大小 */ _onContentLengthKnown(contentLength: number) { if(contentLength && this._fullRequestFlag) { this._totalLength = contentLength; this._fullRequestFlag = false; } } /** * 收到loader发送过来的数据块的处理函数 * @param chunk loader接受到的数据块 * @param byteStart 该数据块在已经收数据流里索引 * @param receivedLength 收到的长度 * @param extraData 额外数据 */ _onLoaderChunkArrival( chunk: ArrayBuffer, byteStart: number, receivedLength: number, extraData?: TSExtraData ) { if(!this._onDataArrival) { throw new IllegalStateException( 'IOController: No existing consumer (onDataArrival) callback!' ); } if(this._paused) { return; } // 当收到数据流时如果处于 EarlyEOF 状态中, 而取消该状态, 通知外部已恢复 if(this._isEarlyEofReconnecting) { this._isEarlyEofReconnecting = false; if(this._onRecoveredEarlyEof) { this._onRecoveredEarlyEof(); } } this._speedChecker.addBytes(new Uint8Array(chunk).length); this._tsExtraData = extraData; // adjust stash buffer size according to network speed dynamically const KBps = this._speedChecker.lastSecondKBps; if(KBps !== 0) { const normalized = this._normalizeSpeed(KBps); if(this._speedNormalized !== normalized) { this._speedNormalized = Number(normalized); this._adjustStashSize(normalized as number); } } if(this._mediaConfig.type === 'm3u8') { this._dispatchChunks(chunk, this._stashByteStart, this._tsExtraData); return; } if(!this._enableStash) { // disable stash if(this._stashUsed === 0) { // dispatch chunk directly to consumer; // check ret value (consumed bytes) and stash unconsumed to stashBuffer const consumed = this._dispatchChunks(chunk, byteStart, extraData); if(consumed < chunk.byteLength) { // unconsumed data remain. const remain = chunk.byteLength - consumed; if(remain > this._bufferSize) { this._expandBuffer(remain); } const 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); } const stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize); stashArray.set(new Uint8Array(chunk), this._stashUsed); this._stashUsed += chunk.byteLength; const consumed = this._dispatchChunks( this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart, extraData ); if(consumed < this._stashUsed && consumed > 0) { // unconsumed data remain const 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 const 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 let 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) const buffer = this._stashBuffer.slice(0, this._stashUsed); const consumed = this._dispatchChunks(buffer, this._stashByteStart, extraData); if(consumed < buffer.byteLength) { if(consumed > 0) { const 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 const consumed = this._dispatchChunks(chunk, byteStart, extraData); if(consumed < chunk.byteLength) { const 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; } } } } } /** * 清空存储的buffer; * @param dropUnconsumed 是否丢弃未处理的数据 */ _flushStashBuffer(dropUnconsumed?: boolean) { if(this._stashUsed > 0) { const buffer = this._stashBuffer.slice(0, this._stashUsed); const consumed = this._dispatchChunks(buffer, this._stashByteStart, this._tsExtraData); const remain = buffer.byteLength - consumed; if(consumed < buffer.byteLength) { if(dropUnconsumed) { Logger.warn( this.Tag, `${remain} bytes unconsumed data remain when flush buffer, dropped` ); } else { if(consumed > 0) { const stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize); const 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; } _onLoaderComplete() { // Force-flush stash buffer, and drop unconsumed data this._flushStashBuffer(true); if(this._onComplete) { this._onComplete(this._extraData); } } _onLoaderError(type: string, data: ErrorData) { Logger.error(this.Tag, `Loader error, code = ${data.code}, msg = ${data.reason}`); this._flushStashBuffer(false); if(this._isEarlyEofReconnecting) { // Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer this._isEarlyEofReconnecting = false; type = Errors.UNRECOVERABLE_EARLY_EOF; } switch(type) { case Errors.EARLY_EOF: { if(!this._config.isLive) { // Do internal http reconnect if not live stream if(this._totalLength && this._currentRange) { const nextFrom = this._currentRange.to + 1; if(nextFrom < this._totalLength) { Logger.warn(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 = Errors.UNRECOVERABLE_EARLY_EOF; break; } case Errors.UNRECOVERABLE_EARLY_EOF: case Errors.CONNECTING_TIMEOUT: case Errors.HTTP_STATUS_CODE_INVALID: case Errors.EXCEPTION: break; default: break; } if(this._onError) { this._onError(type, data); } else { throw new RuntimeException(`IOException: ${data.reason}`); } } } export default IOController;