UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

117 lines (100 loc) 3.35 kB
import {Comrade, tune, Thread} from "@e280/comrade" import {ALL_FORMATS, Input, type StreamTargetChunk} from "mediabunny" import {Machina} from "./parts/machina.js" import {setupDriverHost} from "./fns/host.js" import {loadDecoderSource} from "./utils/load-decoder-source.js" import {DecoderInput, DriverSchematic, Composition, EncoderInput, DecoderSource} from "./fns/schematic.js" export type DriverOptions = { workerUrl: URL | string } export class Driver { static async setup(options: DriverOptions) { const machina = new Machina() const thread = await Comrade.thread<DriverSchematic>({ label: "OmnitoolDriver", workerUrl: options.workerUrl, setupHost: setupDriverHost(machina), }) return new this(machina, thread) } constructor( public machina: Machina, public thread: Thread<DriverSchematic> ) {} async hello() { return this.thread.work.hello() } async getAudioDuration(source: DecoderSource) { const input = new Input({ source: await loadDecoderSource(source), formats: ALL_FORMATS }) const audioTrack = await input.getPrimaryAudioTrack() return await audioTrack?.computeDuration() } async getVideoDuration(source: DecoderSource) { const input = new Input({ source: await loadDecoderSource(source), formats: ALL_FORMATS }) const videoTrack = await input.getPrimaryVideoTrack() return await videoTrack?.computeDuration() } decode(input: DecoderInput) { let lastFrame: VideoFrame | null = null const videoTransform = new TransformStream<VideoFrame, VideoFrame>({ async transform(chunk, controller) { const frame = await input.onFrame?.(chunk) ?? chunk // below code is to prevent mem leaks and hardware accelerated decoder stall lastFrame?.close() controller.enqueue(frame) lastFrame = frame } }) const audioTransform = new TransformStream<AudioData, AudioData>() this.thread.work.decode[tune]({transfer: [videoTransform.writable, audioTransform.writable]})({ source: input.source, video: videoTransform.writable, audio: audioTransform.writable, }) return { audio: audioTransform.readable, video: videoTransform.readable } } async encode({readables, config}: EncoderInput) { const handle = await window.showSaveFilePicker() const writable = await handle.createWritable() // making bridge because file picker writable is not transferable const bridge = new WritableStream<StreamTargetChunk>({ async write(chunk) { await writable.write(chunk) }, async close() { await writable.close() } }) return await this.thread.work.encode[tune]({transfer: [readables.audio, readables.video, bridge]})({readables, config, bridge}) } async composite( composition: Composition, ) { const transfer = this.#collectTransferablesFromComposition(composition) return await this.thread.work.composite[tune]({transfer})(composition) } #collectTransferablesFromComposition(composition: Composition) { const transferables: Transferable[] = [] const visit = (node: Composition) => { if (Array.isArray(node)) { for (const child of node) visit(child) } else if (node && typeof node === 'object' && 'kind' in node) { if (node.kind === 'image' && node.frame instanceof VideoFrame) transferables.push(node.frame) } } visit(composition) return transferables } }