import type * as Catalog from "../karp/catalog" import type { Frame } from "../karp/frame" import type { Component } from "./timeline" export class Renderer { #track: Catalog.Video #canvas: HTMLCanvasElement #timeline: Component #decoder!: VideoDecoder #queue: TransformStream constructor(track: Catalog.Video, canvas: HTMLCanvasElement, timeline: Component) { this.#track = track this.#canvas = canvas this.#timeline = timeline this.#queue = new TransformStream({ start: this.#start.bind(this), transform: this.#transform.bind(this), }) this.#run().catch((err) => console.error("failed to run video renderer: ", err)) } close() { // TODO } async #run() { const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader() for (;;) { const { value: frame, done } = await reader.read() if (done) break self.requestAnimationFrame(() => { this.#canvas.width = frame.displayWidth this.#canvas.height = frame.displayHeight const ctx = this.#canvas.getContext("2d") if (!ctx) throw new Error("failed to get canvas context") ctx.drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight) // TODO respect aspect ratio frame.close() }) } } #start(controller: TransformStreamDefaultController) { this.#decoder = new VideoDecoder({ output: (frame: VideoFrame) => { controller.enqueue(frame) }, error: console.error, }) this.#decoder.configure({ codec: this.#track.codec, codedHeight: this.#track.resolution.height, codedWidth: this.#track.resolution.width, description: this.#track.description, optimizeForLatency: true, }) } #transform(frame: Frame) { const chunk = new EncodedVideoChunk({ type: frame.type, data: frame.data, timestamp: frame.timestamp, }) this.#decoder.decode(chunk) } }