UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

130 lines (112 loc) 3.58 kB
import {is} from "@e280/stz" import {Comrade, tune, Thread} from "@e280/comrade" import {ALL_FORMATS, Input, type StreamTargetChunk} from "mediabunny" import {Machina} from "./parts/machina.js" import {Compositor} from "./parts/compositor.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 ?? "/node_modules/@omnimedia/omnitool/x/driver/driver.worker.bundle.min.js", setupHost: setupDriverHost(machina), }) const compositor = await Compositor.setup() return new this(machina, thread, compositor) } constructor( public machina: Machina, public thread: Thread<DriverSchematic>, public compositor: Compositor ) {} 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() if (!audioTrack) throw new Error("primary audio track not found") 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() } decodeVideo(input: DecoderInput) { let lastFrame: VideoFrame | null = null const {port1, port2} = new MessageChannel() const videoTransform = new TransformStream<VideoFrame, VideoFrame>({ async transform(chunk, controller) { const frame = await input.onFrame?.(chunk) ?? chunk lastFrame?.close() controller.enqueue(frame) lastFrame = frame } }) this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable, port2]})({ source: input.source, cancel: port2, video: videoTransform.writable, start: input.start, end: input.end }) return { readable: videoTransform.readable, /** * use this to stop decoding (premature interruption) * */ cancel() { port1.postMessage("close") port1.close() } } } decodeAudio(input: DecoderInput) { const audioTransform = new TransformStream<AudioData, AudioData>() const {port1, port2} = new MessageChannel() this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable, port2]})({ source: input.source, cancel: port2, audio: audioTransform.writable, start: input.start, end: input.end }) return { readable: audioTransform.readable, /** * use this to stop decoding (premature interruption) * */ cancel() { port1.postMessage("close") port1.close() } } } encode({audio, video, config}: EncoderInput) { const {readable, writable} = new TransformStream<StreamTargetChunk, StreamTargetChunk>() const transfer = [audio, video, writable].filter(is.happy) const done = this.thread.work.encode[tune]({transfer})({audio, video, config, writable}) return {readable, done} } async composite( composition: Composition, ) { return await this.compositor.composite(composition) } resize(width: number, height: number) { this.compositor.resize(width, height) } }