Created
March 24, 2023 20:00
-
-
Save nathanbabcock/59c0298bed74ac48f7f209e30cf53b00 to your computer and use it in GitHub Desktop.
Revisions
-
nathanbabcock created this gist
Mar 24, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,317 @@ import { type HlsConfig, type Loader, type LoaderCallbacks, type LoaderConfiguration, type LoaderContext, type LoaderOnProgress, type LoaderResponse, type LoaderStats, } from 'hls.js' import { ResponseType, fetch, type FetchOptions, type HttpVerb, type Response, } from '@tauri-apps/api/http' export function fetchSupported(): boolean { return !!window.__TAURI__ } const BYTERANGE = /(\d+)-(\d+)\/(\d+)/ export class TauriLoader implements Loader<LoaderContext> { private fetchSetup: (context: LoaderContext, initParams: any) => Request private requestTimeout?: number private request!: Request private response!: Response<any> private controller: AbortController public context!: LoaderContext private config: LoaderConfiguration | null = null private callbacks: LoaderCallbacks<LoaderContext> | null = null public stats: LoaderStats private loader: Response<any> | null = null constructor(config: HlsConfig) { this.fetchSetup = config.fetchSetup || getRequest this.controller = new self.AbortController() // https://github.com/video-dev/hls.js/blob/master/src/loader/load-stats.ts this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0, loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, first: 0, end: 0 }, } } destroy(): void { this.loader = this.callbacks = null this.abortInternal() } abortInternal(): void { const response = this.response if (!response?.ok) { this.stats.aborted = true this.controller.abort() } } abort(): void { this.abortInternal() if (this.callbacks?.onAbort) { this.callbacks.onAbort(this.stats, this.context, this.response) } } load( context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks<LoaderContext> ): void { const stats = this.stats if (stats.loading.start) { throw new Error('Loader can only be used once.') } stats.loading.start = self.performance.now() const initParams = getRequestParameters(context, this.controller.signal) const onProgress: LoaderOnProgress<LoaderContext> | undefined = callbacks.onProgress const isArrayBuffer = context.responseType === 'arraybuffer' this.context = context this.config = config this.callbacks = callbacks this.request = this.fetchSetup(context, initParams) self.clearTimeout(this.requestTimeout) config.timeout = 5000 // config.loadPolicy.maxTimeToFirstByteMs this.requestTimeout = self.setTimeout(() => { this.abortInternal() callbacks.onTimeout(stats, context, this.response) }, config.timeout) const options = getFetchOptions(this.request) if (isArrayBuffer) options.responseType = ResponseType.Binary else if (context.responseType === 'json') options.responseType = ResponseType.JSON else options.responseType = ResponseType.Text fetch<any>(this.request.url, options) .then((response): Promise<string | ArrayBuffer> => { this.response = this.loader = response const first = Math.max(self.performance.now(), stats.loading.start) self.clearTimeout(this.requestTimeout) config.timeout = 10000 // config.loadPolicy.maxLoadTimeMs this.requestTimeout = self.setTimeout(() => { this.abortInternal() callbacks.onTimeout(stats, context, this.response) }, 10000 /*config.loadPolicy.maxLoadTimeMs*/ - (first - stats.loading.start)) if (!response.ok) { const { status } = response throw new FetchError('fetch, bad network response', status, response) } stats.loading.first = first stats.total = getContentLength(response.headers) || stats.total // if (onProgress && Number.isFinite(config.highWaterMark)) { // return this.loadProgressively( // response, // stats, // context, // config.highWaterMark, // onProgress // ) // } if (isArrayBuffer) { return response.data } if (context.responseType === 'json') { return response.data } return response.data }) .then((responseData: string | ArrayBuffer) => { const { response } = this self.clearTimeout(this.requestTimeout) stats.loading.end = Math.max( self.performance.now(), stats.loading.first ) const total = typeof responseData === 'string' ? responseData.length : responseData.byteLength if (total) { stats.loaded = stats.total = total } const loaderResponse: LoaderResponse = { url: response.url, data: responseData, // code: response.status, } if (onProgress && !Number.isFinite(config.highWaterMark)) { onProgress(stats, context, responseData, response) } callbacks.onSuccess(loaderResponse, stats, context, response) }) .catch(error => { self.clearTimeout(this.requestTimeout) if (stats.aborted) { return } // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior // when destroying, 'error' itself can be undefined const code: number = !error ? 0 : error.code || 0 const text: string = !error ? null : error.message callbacks.onError( { code, text }, context, error ? error.details : null // stats ) }) } getCacheAge(): number | null { let result: number | null = null if (this.response) { const headers = new Headers(this.response.headers) const ageHeader = headers.get('age') result = ageHeader ? parseFloat(ageHeader) : null } return result } getResponseHeader(name: string): string | null { const headers = new Headers(this.response.headers) return this.response ? headers.get(name) : null } // private loadProgressively( // response: Response<any>, // stats: LoaderStats, // context: LoaderContext, // highWaterMark = 0, // onProgress: LoaderOnProgress<LoaderContext> // ): Promise<ArrayBuffer> { // const chunkCache = new ChunkCache() // const reader = (response.body as ReadableStream).getReader() // const pump = (): Promise<ArrayBuffer> => { // return reader // .read() // .then(data => { // if (data.done) { // if (chunkCache.dataLength) { // onProgress(stats, context, chunkCache.flush(), response) // } // return Promise.resolve(new ArrayBuffer(0)) // } // const chunk: Uint8Array = data.value // const len = chunk.length // stats.loaded += len // if (len < highWaterMark || chunkCache.dataLength) { // // The current chunk is too small to to be emitted or the cache already has data // // Push it to the cache // chunkCache.push(chunk) // if (chunkCache.dataLength >= highWaterMark) { // // flush in order to join the typed arrays // onProgress(stats, context, chunkCache.flush(), response) // } // } else { // // If there's nothing cached already, and the chache is large enough // // just emit the progress event // onProgress(stats, context, chunk, response) // } // return pump() // }) // .catch(() => { // /* aborted */ // return Promise.reject() // }) // } // return pump() // } } function getByteRangeLength(byteRangeHeader: string): number | undefined { const result = BYTERANGE.exec(byteRangeHeader) if (result) { return parseInt(result[2]) - parseInt(result[1]) + 1 } } function getContentLength(init: HeadersInit): number | undefined { const headers = new Headers(init) const contentRange = headers.get('Content-Range') if (contentRange) { const byteRangeLength = getByteRangeLength(contentRange) if (Number.isFinite(byteRangeLength)) { return byteRangeLength } } const contentLength = headers.get('Content-Length') if (contentLength) { return parseInt(contentLength) } } function getRequest(context: LoaderContext, initParams: any): Request { return new self.Request(context.url, initParams) } class FetchError extends Error { public code: number public details: any constructor(message: string, code: number, details: any) { super(message) this.code = code this.details = details } } function getRequestParameters(context: LoaderContext, signal: any): any { const initParams: any = { method: 'GET', mode: 'cors', credentials: 'same-origin', signal, headers: new self.Headers(Object.assign({}, context.headers)), } if (context.rangeEnd) { initParams.headers.set( 'Range', 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1) ) } return initParams } /** Convert a browser `fetch` request to Tauri */ function getFetchOptions(request: Request): FetchOptions { return { method: request.method as HttpVerb, headers: request.headers, } }