import { PromiseResolver } from "@yume-chan/async"; import type { Disposable } from "@yume-chan/event"; import { AutoDisposable, EventEmitter } from "@yume-chan/event"; let worker: Worker | undefined; let workerReady = false; const pendingResolvers: PromiseResolver[] = []; let streamId = 0; export interface PictureReadyEventArgs { renderStateId: number; width: number; height: number; data: ArrayBuffer; } const PICTURE_READY_SUBSCRIPTIONS = new Map< number, (e: PictureReadyEventArgs) => void >(); function subscribePictureReady( streamId: number, handler: (e: PictureReadyEventArgs) => void, ): Disposable { PICTURE_READY_SUBSCRIPTIONS.set(streamId, handler); return { dispose() { PICTURE_READY_SUBSCRIPTIONS.delete(streamId); }, }; } export class TinyH264Wrapper extends AutoDisposable { readonly streamId: number; readonly #pictureReadyEvent = new EventEmitter(); get onPictureReady() { return this.#pictureReadyEvent.event; } constructor(streamId: number) { super(); this.streamId = streamId; this.addDisposable( subscribePictureReady(streamId, this.#handlePictureReady), ); } #handlePictureReady = (e: PictureReadyEventArgs) => { this.#pictureReadyEvent.fire(e); }; feed(data: ArrayBuffer) { worker!.postMessage( { type: "decode", data: data, offset: 0, length: data.byteLength, renderStateId: this.streamId, }, [data], ); } override dispose() { super.dispose(); worker!.postMessage({ type: "release", renderStateId: this.streamId, }); } } interface TinyH264MessageBase { type: string; } interface TinyH264DecoderReadyMessage extends TinyH264MessageBase { type: "decoderReady"; } interface TinyH264PictureReadyMessage extends TinyH264MessageBase, PictureReadyEventArgs { type: "pictureReady"; } type TinyH264Message = | TinyH264DecoderReadyMessage | TinyH264PictureReadyMessage; export function createTinyH264Wrapper(): Promise { if (!worker) { worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module", }); worker.addEventListener( "message", ({ data }: MessageEvent) => { switch (data.type) { case "decoderReady": workerReady = true; for (const resolver of pendingResolvers) { resolver.resolve(new TinyH264Wrapper(streamId)); streamId += 1; } pendingResolvers.length = 0; break; case "pictureReady": PICTURE_READY_SUBSCRIPTIONS.get(data.renderStateId)?.( data, ); break; } }, ); } if (!workerReady) { const resolver = new PromiseResolver(); pendingResolvers.push(resolver); return resolver.promise; } const decoder = new TinyH264Wrapper(streamId); streamId += 1; return Promise.resolve(decoder); }