flv-h265
Version:
HTML5 FLV Player
267 lines (231 loc) • 9.82 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 Browser from '../utils/browser.js';
import {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
/* fetch + stream IO loader. Currently working on chrome 43+.
* fetch provides a better alternative http API to XMLHttpRequest
*
* fetch spec https://fetch.spec.whatwg.org/
* stream spec https://streams.spec.whatwg.org/
*/
class FetchStreamLoader extends BaseLoader {
static isSupported() {
try {
// fetch + stream is broken on Microsoft Edge. Disable before build 15048.
// see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/
// Fixed in Jan 10, 2017. Build 15048+ removed from blacklist.
let isWorkWellEdge = Browser.msedge && Browser.version.minor >= 15048;
let browserNotBlacklisted = Browser.msedge ? isWorkWellEdge : true;
return (self.fetch && self.ReadableStream && browserNotBlacklisted);
} catch (e) {
return false;
}
}
constructor(seekHandler, config) {
super('fetch-stream-loader');
this.TAG = 'FetchStreamLoader';
this._seekHandler = seekHandler;
this._config = config;
this._needStash = true;
this._requestAbort = false;
this._contentLength = null;
this._receivedLength = 0;
}
destroy() {
if (this.isWorking()) {
this.abort();
}
super.destroy();
}
open(dataSource, range) {
this._dataSource = dataSource;
this._range = range;
let sourceURL = dataSource.url;
if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) {
sourceURL = dataSource.redirectedURL;
}
let seekConfig = this._seekHandler.getConfig(sourceURL, range);
let headers = new self.Headers();
if (typeof seekConfig.headers === 'object') {
let configHeaders = seekConfig.headers;
for (let key in configHeaders) {
if (configHeaders.hasOwnProperty(key)) {
headers.append(key, configHeaders[key]);
}
}
}
let params = {
method: 'GET',
headers: headers,
mode: 'cors',
cache: 'default',
// The default policy of Fetch API in the whatwg standard
// Safari incorrectly indicates 'no-referrer' as default policy, fuck it
referrerPolicy: 'no-referrer-when-downgrade'
};
// add additional headers
if (typeof this._config.headers === 'object') {
for (let key in this._config.headers) {
headers.append(key, this._config.headers[key]);
}
}
// cors is enabled by default
if (dataSource.cors === false) {
// no-cors means 'disregard cors policy', which can only be used in ServiceWorker
params.mode = 'same-origin';
}
// withCredentials is disabled by default
if (dataSource.withCredentials) {
params.credentials = 'include';
}
// referrerPolicy from config
if (dataSource.referrerPolicy) {
params.referrerPolicy = dataSource.referrerPolicy;
}
// add abort controller, by wmlgl 2019-5-10 12:21:27
if (self.AbortController) {
this._abortController = new self.AbortController();
params.signal = this._abortController.signal;
}
this._status = LoaderStatus.kConnecting;
self.fetch(seekConfig.url, params).then((res) => {
if (this._requestAbort) {
this._status = LoaderStatus.kIdle;
res.body.cancel();
return;
}
if (res.ok && (res.status >= 200 && res.status <= 299)) {
if (res.url !== seekConfig.url) {
if (this._onURLRedirect) {
let redirectedURL = this._seekHandler.removeURLParameters(res.url);
this._onURLRedirect(redirectedURL);
}
}
let lengthHeader = res.headers.get('Content-Length');
if (lengthHeader != null) {
this._contentLength = parseInt(lengthHeader);
if (this._contentLength !== 0) {
if (this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
}
return this._pump.call(this, res.body.getReader());
} else {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: res.status, msg: res.statusText});
} else {
throw new RuntimeException('FetchStreamLoader: Http code invalid, ' + res.status + ' ' + res.statusText);
}
}
}).catch((e) => {
if (this._abortController && this._abortController.signal.aborted) {
return;
}
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.EXCEPTION, {code: -1, msg: e.message});
} else {
throw e;
}
});
}
abort() {
this._requestAbort = true;
if (this._status !== LoaderStatus.kBuffering || !Browser.chrome) {
// Chrome may throw Exception-like things here, avoid using if is buffering
if (this._abortController) {
try {
this._abortController.abort();
} catch (e) {}
}
}
}
_pump(reader) { // ReadableStreamReader
return reader.read().then((result) => {
if (result.done) {
// First check received length
if (this._contentLength !== null && this._receivedLength < this._contentLength) {
// Report Early-EOF
this._status = LoaderStatus.kError;
let type = LoaderErrors.EARLY_EOF;
let info = {code: -1, msg: 'Fetch stream meet Early-EOF'};
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
} else {
// OK. Download complete
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);
}
}
} else {
if (this._abortController && this._abortController.signal.aborted) {
this._status = LoaderStatus.kComplete;
return;
} else if (this._requestAbort === true) {
this._status = LoaderStatus.kComplete;
return reader.cancel();
}
this._status = LoaderStatus.kBuffering;
let chunk = result.value.buffer;
let byteStart = this._range.from + this._receivedLength;
this._receivedLength += chunk.byteLength;
if (this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
this._pump(reader);
}
}).catch((e) => {
if (this._abortController && this._abortController.signal.aborted) {
this._status = LoaderStatus.kComplete;
return;
}
if (e.code === 11 && Browser.msedge) { // InvalidStateError on Microsoft Edge
// Workaround: Edge may throw InvalidStateError after ReadableStreamReader.cancel() call
// Ignore the unknown exception.
// Related issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11265202/
return;
}
this._status = LoaderStatus.kError;
let type = 0;
let info = null;
if ((e.code === 19 || e.message === 'network error') && // NETWORK_ERR
(this._contentLength === null ||
(this._contentLength !== null && this._receivedLength < this._contentLength))) {
type = LoaderErrors.EARLY_EOF;
info = {code: e.code, msg: 'Fetch stream meet Early-EOF'};
} else {
type = LoaderErrors.EXCEPTION;
info = {code: e.code, msg: e.message};
}
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
});
}
}
export default FetchStreamLoader;