UNPKG

hjplayer

Version:

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

261 lines (227 loc) 7.8 kB
import logger from '../Utils/Logger'; import { LoaderContext, LoaderConfiguration, LoaderCallbacks, XhrLoaderStats } from '../Interfaces/Loader'; import getGlobal from '../Utils/getGlobalObject'; const GlobalEnvironment = getGlobal(); const { performance } = GlobalEnvironment; class XhrLoader { public Tag: string = 'XhrLoader' private xhrSetup: ((xhr: XMLHttpRequest, url: string) => void) | null = null private loader: XMLHttpRequest | null = null /** * 请求超时时间 */ private requestTimeout: null | number = null /** * xhr 请求时间统计 */ public stats: XhrLoaderStats | null = null /** * 重试timeout */ retryTimeout: number | null = null; /* eslint-disable */ context: LoaderContext | null = null; /* eslint-endable */ config: LoaderConfiguration | null = null; /** * 回调函数 */ callbacks: LoaderCallbacks<LoaderContext> | null = null /** * 重试的delay时间 */ retryDelay: number = 0 constructor(config?: { xhrSetup: (xhr: XMLHttpRequest, url: string) => void }) { this.context = null; if(config && config.xhrSetup) { this.xhrSetup = config.xhrSetup; } } destroy(): void { this.abort(); this.loader = null; } abort(): void { const { loader } = this; if(loader && loader.readyState !== 4) { (this.stats as XhrLoaderStats).aborted = true; loader.abort(); } (GlobalEnvironment as any).clearTimeout(this.requestTimeout); this.requestTimeout = null; (GlobalEnvironment as any).clearTimeout(this.retryTimeout); this.retryTimeout = null; } load( context: LoaderContext, config: LoaderConfiguration, callback: LoaderCallbacks<LoaderContext> ): void { this.context = context; this.config = config; this.callbacks = callback; this.stats = { trequest: performance.now(), retry: 0, tfirst: 0, loaded: 0, // total number of bytes total: 0, tload: 0, aborted: false }; this.retryDelay = config.retryDelay; this.loadInternal(); } loadInternal(): void { const { context } = this; const xhr = new XMLHttpRequest(); const stats = this.stats as XhrLoaderStats; stats.tfirst = 0; stats.loaded = 0; const { xhrSetup } = this; if(!context || !this.callbacks || !this.config) { return; } try { if(xhrSetup) { try { xhrSetup(xhr, context.url); } catch (e) { xhr.open('GET', context.url, true); xhrSetup(xhr, context.url); } } if(!xhr.readyState) { xhr.open('GET', context.url, true); } } catch (e) { this.callbacks.onError( { code: xhr.status, text: e.message }, context, xhr ); return; } if(context.rangeEnd) { xhr.setRequestHeader( 'Range', `bytes=${context.rangeStart}-${context.rangeEnd - 1}` ); } // statechange progress 事件设置回调 xhr.onreadystatechange = this.readystatechange.bind(this); xhr.onprogress = this.loadprogress.bind(this); xhr.responseType = context.responseType; this.requestTimeout = GlobalEnvironment.setTimeout(this.loadtimeout.bind(this), this.config.timeout); xhr.send(); } readystatechange(event: Event): void { const xhr = event.currentTarget as XMLHttpRequest; const { readyState } = xhr; const stats = this.stats as XhrLoaderStats; const context = this.context as LoaderContext; const config = this.config as LoaderConfiguration; if(stats.aborted) { return; } if(!this.callbacks) { return; } if(readyState >= 2) { (<any>GlobalEnvironment).clearTimeout(this.requestTimeout); if(stats.tfirst === 0) { stats.tfirst = Math.max(performance.now(), stats.trequest); // 首次请求开始时间 } if(readyState === 4) { const { status } = xhr; if(status >= 200 && status < 300) { // 正常返回数据 stats.tload = Math.max(stats.tfirst, performance.now()); let data; let len; if(context.responseType === 'arraybuffer') { data = xhr.response; len = data.byteLength; } else { data = xhr.responseText; len = data.length; } stats.total = len; stats.loaded = len; const response = { url: xhr.responseURL, data }; this.callbacks.onSuccess(response, stats, context, xhr); } else if( (stats.retry as number) >= config.maxRetry || (status >= 400 && status < 499) ) { // 4xx 客户端问题,抛出错误 logger.error(this.Tag, `${status} while loading ${context.url}`); this.callbacks.onError( { code: status, text: xhr.statusText }, context, xhr ); } else { // 5xx 服务器端错误进行重试~ logger.warn( this.Tag, `${status} while loading ${context.url}, retrying in ${this.retryDelay}...` ); // abort and reset internal state this.destroy(); // schedule retry this.retryTimeout = GlobalEnvironment.setTimeout( this.loadInternal.bind(this), this.retryDelay ); // set exponential backoff this.retryDelay = Math.min(2 * this.retryDelay, config.maxRetryDelay); stats.retry++; } } else { this.requestTimeout = GlobalEnvironment.setTimeout(this.loadtimeout.bind(this), config.timeout); // 状态改变超时处理 } } } /** * 状态改变超时处理 */ loadtimeout(): void { if(!this.context || !this.callbacks || !this.stats) { return; } logger.warn(this.Tag, `timeout while loading ${this.context.url}`); this.callbacks.onTimeout(this.stats, this.context, null); } /** * 进度处理 */ loadprogress(event: ProgressEvent): void { const xhr = <XMLHttpRequest>event.currentTarget; const { stats } = this; if(!stats || !this.callbacks || !this.context) { return; } stats.loaded = event.loaded; if(event.lengthComputable) { stats.total = event.total; } const { onProgress } = this.callbacks; if(onProgress) { onProgress(stats, this.context, '', xhr); } } } export default XhrLoader;