web/libav: accept several inputs, refactor

This commit is contained in:
wukko 2025-01-30 18:48:45 +06:00
parent fd1a7530ed
commit 00106e9379
No known key found for this signature in database
GPG key ID: 3E30B3F26C7B4AA2
3 changed files with 34 additions and 57 deletions

View file

@ -1,7 +1,7 @@
import mime from "mime";
import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli"; import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli";
import type { FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, FileInfo, RenderParams } from "./types/libav";
import type { FfprobeData } from "fluent-ffmpeg"; import type { FfprobeData } from "fluent-ffmpeg";
import type { FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, RenderParams } from "$lib/types/libav";
export default class LibAVWrapper { export default class LibAVWrapper {
libav: Promise<LibAVInstance> | null; libav: Promise<LibAVInstance> | null;
@ -56,60 +56,35 @@ export default class LibAVWrapper {
} }
} }
static getExtensionFromType(blob: Blob | File) { async render({ files, output, args }: RenderParams) {
const canonicalExtension = blob instanceof File && blob.name.split('.').pop()?.toLowerCase();
const extensions = mime.getAllExtensions(blob.type);
if (canonicalExtension && extensions?.has(canonicalExtension))
return canonicalExtension;
const overrides = ['mp3', 'mov', 'opus'];
if (!extensions)
return;
for (const override of overrides)
if (extensions?.has(override))
return override;
return [...extensions][0];
}
async render({ blob, output, args }: RenderParams) {
if (!this.libav) throw new Error("LibAV wasn't initialized"); if (!this.libav) throw new Error("LibAV wasn't initialized");
const libav = await this.libav; const libav = await this.libav;
const inputKind = blob.type.split("/")[0];
const inputExtension = LibAVWrapper.getExtensionFromType(blob);
if (inputKind !== "video" && inputKind !== "audio") return; if (!(output.extension && output.type)) {
if (!inputExtension) return; throw new Error("output's extension or type is missing");
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 outputName = `output.${output.extension}`;
const ffInputs = [];
try { try {
await libav.mkreadaheadfile("input", blob); for (let i = 0; i < files.length; i++) {
await libav.mkreadaheadfile(`input${i}`, files[i]);
ffInputs.push('-i', `input${i}`);
}
// https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices
await libav.mkwriterdev(outputName); await libav.mkwriterdev(outputName);
await libav.mkwriterdev('progress.txt'); await libav.mkwriterdev('progress.txt');
const totalInputSize = files.reduce((a, b) => a + b.size, 0);
const MB = 1024 * 1024; const MB = 1024 * 1024;
const chunks: Uint8Array[] = []; const chunks: Uint8Array[] = [];
const chunkSize = Math.min(512 * MB, blob.size); const chunkSize = Math.min(512 * MB, totalInputSize);
// since we expect the output file to be roughly the same size // since we expect the output file to be roughly the same size
// as the original, preallocate its size for the output // as inputs, preallocate its size for the output
for (let toAllocate = blob.size; toAllocate > 0; toAllocate -= chunkSize) { for (let toAllocate = totalInputSize; toAllocate > 0; toAllocate -= chunkSize) {
chunks.push(new Uint8Array(chunkSize)); chunks.push(new Uint8Array(chunkSize));
} }
@ -150,7 +125,7 @@ export default class LibAVWrapper {
'-loglevel', 'error', '-loglevel', 'error',
'-progress', 'progress.txt', '-progress', 'progress.txt',
'-threads', this.concurrency.toString(), '-threads', this.concurrency.toString(),
'-i', 'input', ...ffInputs,
...args, ...args,
outputName outputName
]); ]);
@ -183,7 +158,11 @@ export default class LibAVWrapper {
try { try {
await libav.unlink(outputName); await libav.unlink(outputName);
await libav.unlink('progress.txt'); await libav.unlink('progress.txt');
await libav.unlinkreadaheadfile("input");
await Promise.allSettled(
files.map((_, i) =>
libav.unlinkreadaheadfile(`input${i}`)
));
} catch { /* catch & ignore */ } } catch { /* catch & ignore */ }
} }
} }
@ -196,7 +175,7 @@ export default class LibAVWrapper {
const entries = Object.fromEntries( const entries = Object.fromEntries(
text.split('\n') text.split('\n')
.filter(a => a) .filter(a => a)
.map(a => a.split('=', )) .map(a => a.split('='))
); );
const status: FFmpegProgressStatus = (() => { const status: FFmpegProgressStatus = (() => {

View file

@ -1,18 +1,14 @@
export type InputFileKind = "video" | "audio";
export type FileInfo = { export type FileInfo = {
type?: string | null, type?: string,
kind: InputFileKind, extension?: string,
extension: string,
} }
export type RenderParams = { export type RenderParams = {
blob: Blob | File, files: File[],
output?: FileInfo, output: FileInfo,
args: string[], args: string[],
} }
export type FFmpegProgressStatus = "continue" | "end" | "unknown"; export type FFmpegProgressStatus = "continue" | "end" | "unknown";
export type FFmpegProgressEvent = { export type FFmpegProgressEvent = {
status: FFmpegProgressStatus, status: FFmpegProgressStatus,

View file

@ -1,4 +1,3 @@
import mime from "mime";
import LibAVWrapper from "$lib/libav"; import LibAVWrapper from "$lib/libav";
const error = (code: string) => { const error = (code: string) => {
@ -54,15 +53,18 @@ const remux = async (file: File) => {
} }
}); });
if (file instanceof File && !file.type) { if (!file.type) {
file = new File([file], file.name, { // TODO: better & more appropriate error code
type: mime.getType(file.name) ?? undefined, error("remux.corrupted");
});
} }
const render = await ff const render = await ff
.render({ .render({
blob: file, files: [file],
output: {
type: file.type,
extension: file.name.split(".").pop(),
},
args: ["-c", "copy", "-map", "0"], args: ["-c", "copy", "-map", "0"],
}) })
.catch((e) => { .catch((e) => {