diff --git a/web/src/lib/libav.ts b/web/src/lib/libav.ts index 64741569..fda0bff0 100644 --- a/web/src/lib/libav.ts +++ b/web/src/lib/libav.ts @@ -1,14 +1,16 @@ import mime from "mime"; import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli"; -import type { FileInfo, RenderParams } from "./types/libav"; +import type { FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, FileInfo, RenderParams } from "./types/libav"; export default class LibAVWrapper { libav: LibAVInstance | null; concurrency: number; + onProgress?: FFmpegProgressCallback; - constructor() { + constructor(onProgress?: FFmpegProgressCallback) { this.libav = null; this.concurrency = Math.min(4, navigator.hardwareConcurrency); + this.onProgress = onProgress; } async init() { @@ -45,13 +47,16 @@ export default class LibAVWrapper { // https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices await this.libav.mkwriterdev(outputName); + await this.libav.mkwriterdev('progress.txt'); // since we expect the output file to be roughly the same size // as the original, preallocate its size for the output let writtenData = new Uint8Array(blob.size), actualSize = 0; this.libav.onwrite = (name, pos, data) => { - if (name !== outputName) return; + if (name === 'progress.txt') { + return this.#emitProgress(data); + } else if (name !== outputName) return; actualSize = Math.max(pos + data.length, actualSize); const newLen = Math.max(pos + data.length, writtenData.length); @@ -65,6 +70,8 @@ export default class LibAVWrapper { await this.libav.ffmpeg([ '-nostdin', '-y', + '-loglevel', 'error', + '-progress', 'progress.txt', '-threads', this.concurrency.toString(), '-i', 'input', ...args, @@ -72,6 +79,7 @@ export default class LibAVWrapper { ]); await this.libav.unlink(outputName); + await this.libav.unlink('progress.txt'); await this.libav.unlinkreadaheadfile("input"); // if we didn't need as much space as we allocated for some reason, @@ -88,4 +96,45 @@ export default class LibAVWrapper { if (renderBlob.size === 0) return; return renderBlob; } + + #emitProgress(data: Uint8Array | Int8Array) { + const copy = new Uint8Array(data); + const text = new TextDecoder().decode(copy); + const entries = Object.fromEntries( + text.split('\n') + .filter(a => a) + .map(a => a.split('=', )) + ); + + const status: FFmpegProgressStatus = (() => { + const { progress } = entries; + + if (progress === 'continue' || progress === 'end') { + return progress; + } + + return "unknown"; + })(); + + const tryNumber = (str: string) => { + if (str) { + const num = Number(str); + if (!isNaN(num)) { + return num; + } + } + } + + const progress: FFmpegProgressEvent = { + status, + frame: tryNumber(entries.frame), + fps: tryNumber(entries.fps), + total_size: tryNumber(entries.total_size), + dup_frames: tryNumber(entries.dup_frames), + drop_frames: tryNumber(entries.drop_frames), + speed: tryNumber(entries.speed?.trim()?.replace('x', '')), + }; + + this.onProgress(progress); + } } \ No newline at end of file diff --git a/web/src/lib/types/libav.ts b/web/src/lib/types/libav.ts index daf168f4..682345c6 100644 --- a/web/src/lib/types/libav.ts +++ b/web/src/lib/types/libav.ts @@ -12,3 +12,16 @@ export type RenderParams = { args: string[], } + +export type FFmpegProgressStatus = "continue" | "end" | "unknown"; +export type FFmpegProgressEvent = { + status: FFmpegProgressStatus, + frame?: number, + fps?: number, + total_size?: number, + dup_frames?: number, + drop_frames?: number, + speed?: number, +} + +export type FFmpegProgressCallback = (info: FFmpegProgressEvent) => void;