@omnimedia/omnitool
Version:
open source video processing tools
117 lines (100 loc) • 3.35 kB
text/typescript
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
}
}