UNPKG

flv.js

Version:
307 lines (262 loc) 10.3 kB
/* * 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 {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js'; import {RuntimeException} from '../utils/exception.js'; /* Notice: ms-stream may cause IE/Edge browser crash if seek too frequently!!! * The browser may crash in wininet.dll. Disable for now. * * For IE11/Edge browser by microsoft which supports `xhr.responseType = 'ms-stream'` * Notice that ms-stream API sucks. The buffer is always expanding along with downloading. * * We need to abort the xhr if buffer size exceeded limit size (e.g. 16 MiB), then do reconnect. * in order to release previous ArrayBuffer to avoid memory leak * * Otherwise, the ArrayBuffer will increase to a terrible size that equals final file size. */ class MSStreamLoader extends BaseLoader { static isSupported() { try { if (typeof self.MSStream === 'undefined' || typeof self.MSStreamReader === 'undefined') { return false; } let xhr = new XMLHttpRequest(); xhr.open('GET', 'https://example.com', true); xhr.responseType = 'ms-stream'; return (xhr.responseType === 'ms-stream'); } catch (e) { Log.w('MSStreamLoader', e.message); return false; } } constructor(seekHandler, config) { super('xhr-msstream-loader'); this.TAG = 'MSStreamLoader'; this._seekHandler = seekHandler; this._config = config; this._needStash = true; this._xhr = null; this._reader = null; // MSStreamReader this._totalRange = null; this._currentRange = null; this._currentRequestURL = null; this._currentRedirectedURL = null; this._contentLength = null; this._receivedLength = 0; this._bufferLimit = 16 * 1024 * 1024; // 16MB this._lastTimeBufferSize = 0; this._isReconnecting = false; } destroy() { if (this.isWorking()) { this.abort(); } if (this._reader) { this._reader.onprogress = null; this._reader.onload = null; this._reader.onerror = null; this._reader = null; } if (this._xhr) { this._xhr.onreadystatechange = null; this._xhr = null; } super.destroy(); } open(dataSource, range) { this._internalOpen(dataSource, range, false); } _internalOpen(dataSource, range, isSubrange) { this._dataSource = dataSource; if (!isSubrange) { this._totalRange = range; } else { this._currentRange = range; } let sourceURL = dataSource.url; if (this._config.reuseRedirectedURL) { if (this._currentRedirectedURL != undefined) { sourceURL = this._currentRedirectedURL; } else if (dataSource.redirectedURL != undefined) { sourceURL = dataSource.redirectedURL; } } let seekConfig = this._seekHandler.getConfig(sourceURL, range); this._currentRequestURL = seekConfig.url; let reader = this._reader = new self.MSStreamReader(); reader.onprogress = this._msrOnProgress.bind(this); reader.onload = this._msrOnLoad.bind(this); reader.onerror = this._msrOnError.bind(this); let xhr = this._xhr = new XMLHttpRequest(); xhr.open('GET', seekConfig.url, true); xhr.responseType = 'ms-stream'; xhr.onreadystatechange = this._xhrOnReadyStateChange.bind(this); xhr.onerror = this._xhrOnError.bind(this); if (dataSource.withCredentials) { xhr.withCredentials = true; } if (typeof seekConfig.headers === 'object') { let headers = seekConfig.headers; for (let key in headers) { if (headers.hasOwnProperty(key)) { xhr.setRequestHeader(key, headers[key]); } } } // add additional headers if (typeof this._config.headers === 'object') { let headers = this._config.headers; for (let key in headers) { if (headers.hasOwnProperty(key)) { xhr.setRequestHeader(key, headers[key]); } } } if (this._isReconnecting) { this._isReconnecting = false; } else { this._status = LoaderStatus.kConnecting; } xhr.send(); } abort() { this._internalAbort(); this._status = LoaderStatus.kComplete; } _internalAbort() { if (this._reader) { if (this._reader.readyState === 1) { // LOADING this._reader.abort(); } this._reader.onprogress = null; this._reader.onload = null; this._reader.onerror = null; this._reader = null; } if (this._xhr) { this._xhr.abort(); this._xhr.onreadystatechange = null; this._xhr = null; } } _xhrOnReadyStateChange(e) { let xhr = e.target; if (xhr.readyState === 2) { // HEADERS_RECEIVED if (xhr.status >= 200 && xhr.status <= 299) { this._status = LoaderStatus.kBuffering; if (xhr.responseURL != undefined) { let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) { this._currentRedirectedURL = redirectedURL; if (this._onURLRedirect) { this._onURLRedirect(redirectedURL); } } } let lengthHeader = xhr.getResponseHeader('Content-Length'); if (lengthHeader != null && this._contentLength == null) { let length = parseInt(lengthHeader); if (length > 0) { this._contentLength = length; if (this._onContentLengthKnown) { this._onContentLengthKnown(this._contentLength); } } } } else { this._status = LoaderStatus.kError; if (this._onError) { this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText}); } else { throw new RuntimeException('MSStreamLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText); } } } else if (xhr.readyState === 3) { // LOADING if (xhr.status >= 200 && xhr.status <= 299) { this._status = LoaderStatus.kBuffering; let msstream = xhr.response; this._reader.readAsArrayBuffer(msstream); } } } _xhrOnError(e) { this._status = LoaderStatus.kError; let type = LoaderErrors.EXCEPTION; let info = {code: -1, msg: e.constructor.name + ' ' + e.type}; if (this._onError) { this._onError(type, info); } else { throw new RuntimeException(info.msg); } } _msrOnProgress(e) { let reader = e.target; let bigbuffer = reader.result; if (bigbuffer == null) { // result may be null, workaround for buggy M$ this._doReconnectIfNeeded(); return; } let slice = bigbuffer.slice(this._lastTimeBufferSize); this._lastTimeBufferSize = bigbuffer.byteLength; let byteStart = this._totalRange.from + this._receivedLength; this._receivedLength += slice.byteLength; if (this._onDataArrival) { this._onDataArrival(slice, byteStart, this._receivedLength); } if (bigbuffer.byteLength >= this._bufferLimit) { Log.v(this.TAG, `MSStream buffer exceeded max size near ${byteStart + slice.byteLength}, reconnecting...`); this._doReconnectIfNeeded(); } } _doReconnectIfNeeded() { if (this._contentLength == null || this._receivedLength < this._contentLength) { this._isReconnecting = true; this._lastTimeBufferSize = 0; this._internalAbort(); let range = { from: this._totalRange.from + this._receivedLength, to: -1 }; this._internalOpen(this._dataSource, range, true); } } _msrOnLoad(e) { // actually it is onComplete event this._status = LoaderStatus.kComplete; if (this._onComplete) { this._onComplete(this._totalRange.from, this._totalRange.from + this._receivedLength - 1); } } _msrOnError(e) { this._status = LoaderStatus.kError; let type = 0; let info = null; if (this._contentLength && this._receivedLength < this._contentLength) { type = LoaderErrors.EARLY_EOF; info = {code: -1, msg: 'MSStream meet Early-Eof'}; } else { type = LoaderErrors.EARLY_EOF; info = {code: -1, msg: e.constructor.name + ' ' + e.type}; } if (this._onError) { this._onError(type, info); } else { throw new RuntimeException(info.msg); } } } export default MSStreamLoader;