import EventEmitter from "eventemitter3"; import H264Worker from "web-worker:./h264.worker.js"; import { range, isArray } from "lodash-es"; import { v4 as uuidv4 } from "uuid"; export class VDecoder extends EventEmitter { constructor({ chunkSize = 256 * 1024, maxChip = 100 }) { super(); // this.cacheSegmentCount = cacheSegmentCount; // this.chunkSize = chunkSize; this.cacheBuffer = []; this.cacheBufferTotal = null; this.worker = new H264Worker(); this.initWorker(); this.tempVideos = []; this.ready = false; this.decoding = false; this.decodingId = null; this.start = null; this.maxChip = maxChip; } static isSupport() { return !!( // UC and Quark browser (iOS/Android) support wasm/asm limited, // its iOS version make wasm/asm performance very slow (maybe hook something) // its Android version removed support for wasm/asm, it just run pure javascript codes, // so it is very easy to cause memory leaks ( !/UCBrowser|Quark/.test(window.navigator.userAgent) && window.fetch && window.ReadableStream && window.Promise && window.URL && window.URL.createObjectURL && window.Blob && window.Worker && !!new Audio().canPlayType("audio/aac;").replace(/^no$/, "") && (window.AudioContext || window.webkitAudioContext) ) ); } initWorker() { this.worker.addEventListener("message", (e) => { const message = /** @type {{type:string, width:number, height:number, data:ArrayBuffer, renderStateId:number}} */ e.data; switch (message.type) { case "pictureReady": // onPictureReady(message); // console.log( // "[VDecoder]::decodeData", // Object.assign(message, { clipId: this.decodingId }) // ); const { renderStateId } = e.data; this.emit( "decodeData", Object.assign(message, { clipId: this.decodingId }) ); this.worker.postMessage({ type: "release", renderStateId: renderStateId, }); console.log("[VDecoder]::decodeData:release",renderStateId); if (this.decoding && this.decodingId) { this.decodeNext(this.decodingId); } break; case "decoderReady": this.ready = true; this.emit("ready"); break; } }); } /** * * @param {*} array array [2,100] */ mutiFetch(data) { if (!(isArray(data) && data.length > 0)) { throw new Error("range must is an array and has value"); return; } if (!(data[0] && "path" in data[0] && "frame" in data[0])) { throw new Error("props path,frame,vid must have"); return; } if (!this.ready) { throw new Error("decoder is not ready"); return; } const allFetch = data.map((item) => { return fetch(`${item.path}/${item.frame}`).then((response) => { return response.arrayBuffer().then(function (buffer) { return new Uint8Array(buffer); }); }); }); console.log("allFetch", allFetch); return Promise.all(allFetch) .then((data) => { const clip = { id: uuidv4(), data: data }; if (data.length > 0) { this.emit("fetchDone", clip); this.cacheBuffer = data.slice(); this.tempVideos.push(clip); // console.log("[VDecoder]:获取clip,", clip); this.start = Date.now(); this.cacheBufferTotal = clip.data.length; this.decodeNext(clip.id); return Promise.resolve(clip); } else { // console.warn("[VDecoder]:fetch取帧为空", rangeFetch); } }) .catch((error) => { console.error("error", error); }); } /** * * @param {*} rangeArray array [2,100] */ fetch({ path, range: rangeArray, decode = true }) { if (!this.ready) { throw new Error("decoder is not ready"); } const url = path; if ( !( isArray(rangeArray) && (rangeArray.length === 1 || rangeArray.length === 2) ) ) { throw new Error("range must is an array!"); } if (this.tempVideos.length > this.maxChip) { this.flush(); console.log("flush"); } let rangeFetch = []; if (rangeArray[0] < 0 || rangeArray[1] < 0) { console.error( "[VDecoder]:range: 非法", `${[rangeArray[0], rangeArray[1]]}` ); return; } if (rangeArray.length > 1) { // range入口 if (rangeArray[0] < rangeArray[1]) { rangeFetch = range(rangeArray[0], rangeArray[1] + 1); console.log("[VDecoder]:顺时 +", rangeFetch); } else { rangeFetch = range(rangeArray[1], rangeArray[0] + 1).reverse(); console.log("[VDecoder]:逆时 -", rangeFetch); } } else { // 单例 [i] rangeFetch = rangeArray; console.log("[VDecoder]:single", rangeFetch); } const allFetch = rangeFetch.map((i) => { return fetch(`${url}/${i}`).then((response) => { return response.arrayBuffer().then(function (buffer) { return new Uint8Array(buffer); }); }); }); return Promise.all(allFetch) .then((data) => { const clip = { id: uuidv4(), data: data }; if (data.length > 0) { this.emit("fetchDone", clip); this.cacheBuffer = data.slice(); this.tempVideos.push(clip); // console.log("[VDecoder]:获取clip,", clip); if (decode) { this.start = Date.now(); this.cacheBufferTotal = clip.data.length; this.decodeNext(clip.id); } return Promise.resolve(clip); } else { console.warn("[VDecoder]:fetch取帧为空", rangeFetch); } }) .catch((error) => { console.error("error", error); }); } /** * @param {Uint8Array} h264Nal */ decode(h264Nal, id) { this.worker.postMessage( { type: "decode", data: h264Nal.buffer, offset: h264Nal.byteOffset, length: h264Nal.byteLength, renderStateId: id, }, [h264Nal.buffer] ); } decodeNext(clipId) { const nextFrame = this.cacheBuffer.shift(); this.decodingId = clipId; this.decoding = true; let tempId = this.cacheBufferTotal - this.cacheBuffer.length - 1; if (nextFrame) { this.decode(nextFrame, tempId); } else { // console.log("tempVideos", this.tempVideos.length); // const clip = this.tempVideos.find(({ id }) => id === this.decodingId); // if (clip) { // const fps = (1000 / (Date.now() - this.start)) * clip.data.length; // console.log( // `Decoded ${clip.data.length} frames in ${ // Date.now() - this.start // }ms @ ${fps >> 0}FPS` // ); // } else { // console.warn("不存在clip"); // } this.decoding = false; // this.decodingId = null; tempId = 0; // clip && clip.id && this.emit("decodeDone", clip.id); } } flush() { this.tempVideos = []; this.cacheBufferTotal = null; console.warn("flush"); } preloader(preload) {} }