From 57054c24b25beb7cff4c6fa6c1dca3c420bf9abd Mon Sep 17 00:00:00 2001
From: wukko <me@wukko.me>
Date: Mon, 12 Aug 2024 22:28:38 +0600
Subject: [PATCH] web: draft libav functionality

---
 pnpm-lock.yaml       |  8 ++++
 web/src/lib/libav.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 97 insertions(+)
 create mode 100644 web/src/lib/libav.ts

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5351924f..58fdabb1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -100,6 +100,9 @@ importers:
       '@imput/ffmpeg.wasm':
         specifier: ^0.12.11
         version: 0.12.11
+      '@imput/libav.js-remux-cli':
+        specifier: ^5.4.6
+        version: 5.4.6
       '@imput/version-info':
         specifier: workspace:^
         version: link:../packages/version-info
@@ -533,6 +536,9 @@ packages:
     resolution: {integrity: sha512-/krk6BPy7TdDN73KHj/g0TVNDNLvvtlTv2T8di4GwrbuIcqfv5wCa2iMR2II/Vhf7zFYJvC2m5kDYHTtDQWS5Q==}
     engines: {node: '>=18.x'}
 
+  '@imput/libav.js-remux-cli@5.4.6':
+    resolution: {integrity: sha512-lO83tppljyCyJAks2cfA0YULnPstAYE/5LQVoFAFu+CSwUcgZqB9OmBNHufMj3GnTXzYE35OORSuYHYZEyiC5w==}
+
   '@isaacs/cliui@8.0.2':
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
@@ -2496,6 +2502,8 @@ snapshots:
     dependencies:
       '@imput/ffmpeg-types': 0.12.3
 
+  '@imput/libav.js-remux-cli@5.4.6': {}
+
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
diff --git a/web/src/lib/libav.ts b/web/src/lib/libav.ts
new file mode 100644
index 00000000..266ac75c
--- /dev/null
+++ b/web/src/lib/libav.ts
@@ -0,0 +1,89 @@
+import mime from "mime";
+import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli";
+
+type InputFileKind = "video" | "audio";
+
+type FileInfo = {
+    type?: string | null,
+    kind: InputFileKind,
+    extension: string,
+}
+
+type RenderParams = {
+    file: File,
+    output?: FileInfo,
+    args: string[],
+}
+
+export default class LibAVWrapper {
+    libav!: LibAVInstance | null;
+    concurrency: number;
+
+    constructor() {
+        this.concurrency = Math.min(4, navigator.hardwareConcurrency);
+    }
+
+    async init() {
+        if (!this.libav) {
+            this.libav = await LibAV.LibAV({
+                yesthreads: true
+            })
+        }
+    }
+
+    async renderFile({ file, output, args }: RenderParams) {
+        if (!this.libav) throw new Error("LibAV wasn't initialized");
+
+        const inputKind = file.type.split("/")[0];
+        const inputExtension = mime.getExtension(file.type);
+
+        if (inputKind !== "video" && inputKind !== "audio") return;
+        if (!inputExtension) return;
+
+        const input: FileInfo = {
+            kind: inputKind,
+            extension: inputExtension,
+        }
+
+        if (!output) output = input;
+
+        output.type = mime.getType(output.extension);
+        if (!output.type) return;
+
+        const outputName = `output.${output.extension}`;
+
+        const buffer = new Blob([await file.arrayBuffer()]);
+        await this.libav.mkreadaheadfile("input", buffer);
+
+        // https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices
+        await this.libav.mkwriterdev(outputName);
+        let writtenData = new Uint8Array(0);
+
+        this.libav.onwrite = (name, pos, data) => {
+            const newLen = Math.max(writtenData.length, pos + data.length);
+            if (newLen > writtenData.length) {
+                const newData = new Uint8Array(newLen);
+                newData.set(writtenData);
+                writtenData = newData;
+            }
+            writtenData.set(data, pos);
+        };
+
+        await this.libav.ffmpeg([
+            '-threads', this.concurrency.toString(),
+            '-i', 'input',
+            ...args,
+            outputName
+        ]);
+
+        await this.libav.unlinkmkreadaheadfile("input");
+
+        const renderBlob = new Blob(
+            [writtenData],
+            { type: output.type }
+        );
+
+        if (renderBlob.size === 0) return;
+        return renderBlob;
+    }
+}