hjplayer
Version:
hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;
490 lines (417 loc) • 13.9 kB
text/typescript
import EventEmitter from 'eventemitter3';
import Log from '../Utils/Logger';
import SpeedSampler from './SpeedChecker';
import BaseLoader from './BaseLoader';
import { RuntimeException } from '../Utils/Exception';
import UserConfig from '../Interfaces/UserConfig';
import RangeSeekHandler from './RangeSeekHandler';
import SeekRange from '../Interfaces/SeekRange';
// import Headers from '../Interfaces/Headers';
import LoaderStatus from './LoaderStatus';
import MediaConfig from '../Interfaces/MediaConfig';
import LoaderErrors from './LoaderErrors';
class RangeLoader extends BaseLoader {
Tag: string
/**
* use range request to seek
*/
private seekHandler: RangeSeekHandler
/**
* 初始化配置
*/
private userConfig: UserConfig
_needStash: boolean
/**
* 块数据的大小
*/
private _currentChunkSizeKB: number
/**
* 当前标准速度
*/
private _currentSpeedNormalized: number
/**
* 下载速度为0的次数
*/
private _zeroSpeedChunkCount: number
/**
* xhr
*/
_xhr: XMLHttpRequest | null
private _speedSampler: SpeedSampler
/**
* 是否阻止请求
*/
private _requestAbort: boolean
private _waitForTotalLength: boolean
/**
* 是否收到整个数据
*/
private _totalLengthReceived: boolean
/**
* 当前请求url
*/
private _currentRequestURL: string | null
private _currentRedirectedURL: string | null
/**
* 当前range
*/
_currentRequestRange: SeekRange | null
/**
* 数据的长度
*/
private _contentLength: number | null
/**
* 接受数据长度
*/
private _receivedLength: number
/**
* 最新下载进度, 字节
*/
private _lastTimeLoaded: number
/**
* chunk list
*/
private _chunkSizeKBList: number[]
/**
* 文件总长度
*/
private _totalLength: number | null
private _range: SeekRange | null
private mediaConfig: MediaConfig | null
eventEmitter: EventEmitter = new EventEmitter()
/**
* 是否支持xhr
*/
static isSupported() {
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com', true);
xhr.responseType = 'arraybuffer';
return xhr.responseType === 'arraybuffer';
} catch (e) {
Log.warn('RangeLoader', e.message);
return false;
}
}
constructor(seekHandler: RangeSeekHandler, userConfig: UserConfig) {
super('xhr-range-laoder', 'flv');
this.Tag = 'RangerLoader';
this.seekHandler = seekHandler;
this.userConfig = userConfig;
this._needStash = false;
this._chunkSizeKBList = [
128,
256,
384,
512,
768,
1024,
1536,
2048,
3072,
4096,
5120,
6144,
7168,
8192
];
this._currentChunkSizeKB = 384;
this._currentSpeedNormalized = 0;
this._zeroSpeedChunkCount = 0;
this._xhr = null;
this._speedSampler = new SpeedSampler();
this._requestAbort = false;
this._waitForTotalLength = false;
this._totalLengthReceived = false;
this._currentRequestURL = null;
this._currentRedirectedURL = null;
this._currentRequestRange = null;
this._totalLength = null;
this._contentLength = null;
this._receivedLength = 0;
this._lastTimeLoaded = 0;
this._range = null;
this.mediaConfig = null;
}
destory(): void {
if(this.isWorking()) {
this.abort();
}
if(this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr.onprogress = null;
this._xhr.onload = null;
this._xhr.onerror = null;
this._xhr = null;
}
super.destroy();
}
get currentSpeed(): number {
return this._speedSampler.lastSecondKBps;
}
startLoad(mediaConfig: MediaConfig, seekRange: SeekRange): void {
this.mediaConfig = mediaConfig;
this._range = seekRange;
this._status = LoaderStatus.kConnecting;
let useRefTotalLength = false;
if(this.mediaConfig.fileSize !== undefined && this.mediaConfig.fileSize !== 0) {
useRefTotalLength = true;
this._totalLength = this.mediaConfig.fileSize;
}
if(!this._totalLengthReceived && !useRefTotalLength) {
this._waitForTotalLength = true;
this._internalOpen(this.mediaConfig, { from: 0, to: -1 });
} else {
this._openSubRange();
}
}
/**
* 请求子range
*/
_openSubRange(): void {
if(!this._range) {
return;
}
const chunkSize = this._currentChunkSizeKB * 1024;
const from = this._range.from + this._receivedLength;
let to = from + chunkSize;
if(this._contentLength != null) {
if(to - this._range.from >= this._contentLength) {
to = this._range.from + this._contentLength - 1;
}
}
this._currentRequestRange = { from, to };
this._internalOpen(this.mediaConfig, this._currentRequestRange);
}
/**
* 请求数据
*/
_internalOpen(mediaConfig: MediaConfig | null, seekRange: SeekRange): void {
if(!mediaConfig || !this.mediaConfig) {
return;
}
this._lastTimeLoaded = 0;
let sourceURL = mediaConfig.url;
if(this.userConfig.reuseRedirectedURL) {
if(this._currentRedirectedURL) {
sourceURL = this._currentRedirectedURL;
} else if(mediaConfig.redirectedURL !== undefined) {
sourceURL = mediaConfig.redirectedURL;
}
}
const seekConfig = this.seekHandler.getConfig(sourceURL, seekRange);
this._currentRequestURL = seekConfig.url;
this._xhr = new XMLHttpRequest();
const xhr = this._xhr;
xhr.open('GET', seekConfig.url, true);
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = this._onReadyStateChange.bind(this);
xhr.onprogress = this._onProgress.bind(this);
xhr.onload = this._onLoad.bind(this);
xhr.onerror = this._onXhrError.bind(this);
if(mediaConfig.withCredentials) {
xhr.withCredentials = true;
}
if(typeof seekConfig.headers === 'object') {
const { headers } = seekConfig;
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key]);
});
}
// add additional headers
if(typeof this.userConfig.headers === 'object') {
const { headers } = this.userConfig;
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key]);
});
}
xhr.send();
}
abort(): void {
this._requestAbort = true;
this._internalAbort();
this._status = LoaderStatus.kComplete;
}
_onReadyStateChange(e: Event): void {
const xhr = <XMLHttpRequest>e.target;
if(xhr.readyState === 2) {
if(xhr.responseURL !== undefined) {
const redirectedURL: string = this.seekHandler.removeURLParameters(xhr.responseURL);
if(
xhr.responseURL !== this._currentRequestURL
&& redirectedURL !== this._currentRedirectedURL
) {
this._currentRedirectedURL = redirectedURL;
if(this._onURLRedirect) {
this._onURLRedirect(redirectedURL);
}
}
}
if(xhr.status >= 200 && xhr.status <= 299) {
if(this._waitForTotalLength) {
return;
}
this._status = LoaderStatus.kBuffering;
} else {
this._status = LoaderStatus.kError;
if(this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {
code: xhr.status,
reason: xhr.statusText
});
} else {
throw new RuntimeException(
`RangeLoader: http code invalid, ${xhr.status} ${xhr.statusText}`
);
}
}
}
}
_onProgress(e: ProgressEvent): void {
if(this._status === LoaderStatus.kError) {
return;
}
// 判断 _range null。
if(!this._range) {
return;
}
if(this._contentLength === null) {
let openNextRange: boolean = false;
if(this._waitForTotalLength) {
this._waitForTotalLength = false;
this._totalLengthReceived = true;
openNextRange = true;
const { total } = e;
this._internalAbort();
if(total != null && total !== 0) {
this._totalLength = total;
}
}
if(this._range.to === -1) {
this._contentLength = <number> this._totalLength - this._range.from;
} else {
this._contentLength = this._range.to - this._range.from + 1;
}
if(openNextRange) {
this._openSubRange();
return;
}
if(this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
const delta: number = e.loaded - this._lastTimeLoaded;
this._lastTimeLoaded = e.loaded;
this._speedSampler.addBytes(delta);
}
_onLoad(e: Event): void {
if(!this._range || !e.target) {
return;
}
if(this._status === LoaderStatus.kError) {
return;
}
if(this._waitForTotalLength) {
this._waitForTotalLength = false;
return;
}
this._lastTimeLoaded = 0;
let KBps: number = this._speedSampler.lastSecondKBps;
if(KBps === 0) {
this._zeroSpeedChunkCount++;
if(this._zeroSpeedChunkCount >= 3) {
KBps = this._speedSampler.currentKBps;
}
}
if(KBps !== 0) {
const normalized: number = this._normalizeSpeed(KBps);
if(this._currentSpeedNormalized !== normalized) {
this._currentSpeedNormalized = normalized;
this._currentChunkSizeKB = normalized;
}
}
const chunk = (e.target as XMLHttpRequest).response;
const byteStart: number = this._range.from + this._receivedLength;
this._receivedLength += chunk.byteLength;
let reportComplete = false;
if(this._contentLength !== null && this._receivedLength < this._contentLength) {
this._openSubRange();
} else {
reportComplete = true;
}
if(this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
if(reportComplete) {
this._status = LoaderStatus.kComplete;
if(this._onComplete) {
this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);
}
}
}
_onXhrError(e: ProgressEvent): void {
this._status = LoaderStatus.kError;
let type: number | string = 0;
let info: null | { code: number; reason: string };
if(
this._contentLength
&& this._receivedLength > 0
&& this._receivedLength < this._contentLength
) {
type = LoaderErrors.EARLY_EOF;
info = {
code: -1,
reason: 'RangeLoder meet Early-Eof'
};
} else {
type = LoaderErrors.EXCEPTION;
info = { code: -1, reason: `${e.constructor.name} ${e.type}` };
}
if(this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.reason);
}
}
_normalizeSpeed(input: number): number {
const list = this._chunkSizeKBList;
const last: number = list.length - 1;
let mid: number = 0;
let lbound: number = 0;
let ubound = last;
if(input < list[0]) {
return list[0];
}
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;
}
}
return list[0];
}
_internalAbort(): void {
if(this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr.onprogress = null;
this._xhr.onload = null;
this._xhr.onerror = null;
this._xhr.abort();
this._xhr = null;
}
}
on(eventName: string, callback: EventEmitter.ListenerFn): void {
this.eventEmitter.on(eventName, callback);
}
once(eventName: string, callback: EventEmitter.ListenerFn): void {
this.eventEmitter.once(eventName, callback);
}
off(eventName: string, callback?: EventEmitter.ListenerFn): void {
this.eventEmitter.off(eventName, callback);
}
}
export default RangeLoader;