2024-08-12 18:28:38 +02:00
|
|
|
import mime from "mime";
|
|
|
|
import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli";
|
2024-08-13 16:39:24 +02:00
|
|
|
import type { FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, FileInfo, RenderParams } from "./types/libav";
|
2024-08-13 17:33:30 +02:00
|
|
|
import type { FfprobeData } from "fluent-ffmpeg";
|
2024-08-31 19:46:10 +02:00
|
|
|
import { browser } from "$app/environment";
|
2024-08-12 18:28:38 +02:00
|
|
|
|
|
|
|
export default class LibAVWrapper {
|
2024-08-13 16:42:32 +02:00
|
|
|
libav: Promise<LibAVInstance> | null;
|
2024-08-12 18:28:38 +02:00
|
|
|
concurrency: number;
|
2024-08-13 16:39:24 +02:00
|
|
|
onProgress?: FFmpegProgressCallback;
|
2024-08-12 18:28:38 +02:00
|
|
|
|
2024-08-13 16:39:24 +02:00
|
|
|
constructor(onProgress?: FFmpegProgressCallback) {
|
2024-08-12 22:03:30 +02:00
|
|
|
this.libav = null;
|
2024-08-31 19:46:10 +02:00
|
|
|
this.concurrency = Math.min(4, browser ? navigator.hardwareConcurrency : 0);
|
2024-08-13 16:39:24 +02:00
|
|
|
this.onProgress = onProgress;
|
2024-08-12 18:28:38 +02:00
|
|
|
}
|
|
|
|
|
2024-09-09 15:36:16 +02:00
|
|
|
init() {
|
2024-08-31 19:46:10 +02:00
|
|
|
if (this.concurrency && !this.libav) {
|
2024-08-13 16:42:32 +02:00
|
|
|
this.libav = LibAV.LibAV({
|
2024-08-12 19:06:45 +02:00
|
|
|
yesthreads: true,
|
2024-08-12 22:36:24 +02:00
|
|
|
base: '/_libav'
|
|
|
|
});
|
2024-08-12 18:28:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-08 23:08:18 +02:00
|
|
|
async terminate() {
|
|
|
|
if (this.libav) {
|
|
|
|
const libav = await this.libav;
|
|
|
|
libav.terminate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-13 17:33:30 +02:00
|
|
|
async probe(blob: Blob) {
|
|
|
|
if (!this.libav) throw new Error("LibAV wasn't initialized");
|
|
|
|
const libav = await this.libav;
|
|
|
|
|
|
|
|
await libav.mkreadaheadfile('input', blob);
|
2024-09-07 13:41:56 +02:00
|
|
|
|
2024-09-07 16:23:33 +02:00
|
|
|
try {
|
|
|
|
await libav.ffprobe([
|
|
|
|
'-v', 'quiet',
|
|
|
|
'-print_format', 'json',
|
|
|
|
'-show_format',
|
|
|
|
'-show_streams',
|
|
|
|
'input',
|
|
|
|
'-o', 'output.json'
|
|
|
|
]);
|
|
|
|
|
|
|
|
const copy = await libav.readFile('output.json');
|
|
|
|
const text = new TextDecoder().decode(copy);
|
|
|
|
await libav.unlink('output.json');
|
|
|
|
|
|
|
|
return JSON.parse(text) as FfprobeData;
|
|
|
|
} finally {
|
|
|
|
await libav.unlinkreadaheadfile('input');
|
|
|
|
}
|
2024-08-13 17:33:30 +02:00
|
|
|
}
|
|
|
|
|
2024-08-17 15:45:58 +02:00
|
|
|
static getExtensionFromType(blob: Blob) {
|
|
|
|
const extensions = mime.getAllExtensions(blob.type);
|
|
|
|
const overrides = ['mp3', 'mov'];
|
|
|
|
|
|
|
|
if (!extensions)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (const override of overrides)
|
|
|
|
if (extensions?.has(override))
|
|
|
|
return override;
|
|
|
|
|
|
|
|
return [...extensions][0];
|
|
|
|
}
|
|
|
|
|
2024-08-12 19:06:45 +02:00
|
|
|
async render({ blob, output, args }: RenderParams) {
|
2024-08-12 18:28:38 +02:00
|
|
|
if (!this.libav) throw new Error("LibAV wasn't initialized");
|
2024-08-13 16:42:32 +02:00
|
|
|
const libav = await this.libav;
|
2024-08-12 19:06:45 +02:00
|
|
|
const inputKind = blob.type.split("/")[0];
|
2024-08-17 15:45:58 +02:00
|
|
|
const inputExtension = LibAVWrapper.getExtensionFromType(blob);
|
2024-08-12 18:28:38 +02:00
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
2024-09-07 16:23:33 +02:00
|
|
|
try {
|
|
|
|
await libav.mkreadaheadfile("input", blob);
|
|
|
|
|
|
|
|
// https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices
|
|
|
|
await libav.mkwriterdev(outputName);
|
|
|
|
await libav.mkwriterdev('progress.txt');
|
|
|
|
|
2024-09-07 16:15:42 +02:00
|
|
|
const MB = 1024 * 1024;
|
|
|
|
const chunks: Uint8Array[] = [];
|
|
|
|
const chunkSize = Math.min(512 * MB, blob.size);
|
|
|
|
|
2024-09-07 16:23:33 +02:00
|
|
|
// since we expect the output file to be roughly the same size
|
|
|
|
// as the original, preallocate its size for the output
|
2024-09-07 16:15:42 +02:00
|
|
|
for (let toAllocate = blob.size; toAllocate > 0; toAllocate -= chunkSize) {
|
|
|
|
chunks.push(new Uint8Array(chunkSize));
|
|
|
|
}
|
2024-09-07 16:23:33 +02:00
|
|
|
|
2024-09-07 16:15:42 +02:00
|
|
|
let actualSize = 0;
|
2024-09-07 16:23:33 +02:00
|
|
|
libav.onwrite = (name, pos, data) => {
|
|
|
|
if (name === 'progress.txt') {
|
|
|
|
try {
|
|
|
|
return this.#emitProgress(data);
|
2024-11-15 13:19:49 +01:00
|
|
|
} catch (e) {
|
2024-09-07 16:23:33 +02:00
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
} else if (name !== outputName) return;
|
|
|
|
|
2024-09-07 16:15:42 +02:00
|
|
|
const writeEnd = pos + data.length;
|
|
|
|
if (writeEnd > chunkSize * chunks.length) {
|
|
|
|
chunks.push(new Uint8Array(chunkSize));
|
|
|
|
}
|
|
|
|
|
|
|
|
const chunkIndex = pos / chunkSize | 0;
|
|
|
|
const offset = pos - (chunkSize * chunkIndex);
|
|
|
|
|
|
|
|
if (offset + data.length > chunkSize) {
|
|
|
|
chunks[chunkIndex].set(
|
|
|
|
data.subarray(0, chunkSize - offset), offset
|
|
|
|
);
|
|
|
|
chunks[chunkIndex + 1].set(
|
|
|
|
data.subarray(chunkSize - offset), 0
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
chunks[chunkIndex].set(data, offset);
|
2024-08-13 16:42:32 +02:00
|
|
|
}
|
2024-09-07 16:15:42 +02:00
|
|
|
|
|
|
|
actualSize = Math.max(writeEnd, actualSize);
|
2024-09-07 16:23:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
await libav.ffmpeg([
|
|
|
|
'-nostdin', '-y',
|
|
|
|
'-loglevel', 'error',
|
|
|
|
'-progress', 'progress.txt',
|
|
|
|
'-threads', this.concurrency.toString(),
|
|
|
|
'-i', 'input',
|
|
|
|
...args,
|
|
|
|
outputName
|
|
|
|
]);
|
|
|
|
|
|
|
|
// if we didn't need as much space as we allocated for some reason,
|
2024-09-07 16:15:42 +02:00
|
|
|
// shrink the buffers so that we don't inflate the file with zeroes
|
|
|
|
const outputView: Uint8Array[] = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < chunks.length; ++i) {
|
|
|
|
outputView.push(
|
|
|
|
chunks[i].subarray(
|
|
|
|
0, Math.min(chunkSize, actualSize)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
actualSize -= chunkSize;
|
|
|
|
if (actualSize <= 0) {
|
|
|
|
break;
|
|
|
|
}
|
2024-08-12 18:28:38 +02:00
|
|
|
}
|
2024-08-13 02:23:13 +02:00
|
|
|
|
2024-09-07 16:23:33 +02:00
|
|
|
const renderBlob = new Blob(
|
2024-09-07 16:15:42 +02:00
|
|
|
outputView,
|
2024-09-07 16:23:33 +02:00
|
|
|
{ type: output.type }
|
|
|
|
);
|
2024-08-12 18:28:38 +02:00
|
|
|
|
2024-09-07 16:23:33 +02:00
|
|
|
if (renderBlob.size === 0) return;
|
|
|
|
return renderBlob;
|
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
await libav.unlink(outputName);
|
|
|
|
await libav.unlink('progress.txt');
|
|
|
|
await libav.unlinkreadaheadfile("input");
|
2024-09-08 23:08:18 +02:00
|
|
|
} catch { /* catch & ignore */ }
|
2024-09-07 16:23:33 +02:00
|
|
|
}
|
2024-08-12 18:28:38 +02:00
|
|
|
}
|
2024-08-13 16:39:24 +02:00
|
|
|
|
|
|
|
#emitProgress(data: Uint8Array | Int8Array) {
|
2024-08-13 16:42:32 +02:00
|
|
|
if (!this.onProgress) return;
|
|
|
|
|
2024-08-13 16:39:24 +02:00
|
|
|
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";
|
|
|
|
})();
|
|
|
|
|
2024-08-13 17:34:38 +02:00
|
|
|
const tryNumber = (str: string, transform?: (n: number) => number) => {
|
2024-08-13 16:39:24 +02:00
|
|
|
if (str) {
|
|
|
|
const num = Number(str);
|
|
|
|
if (!isNaN(num)) {
|
2024-08-13 17:34:38 +02:00
|
|
|
if (transform)
|
|
|
|
return transform(num);
|
|
|
|
else
|
|
|
|
return num;
|
2024-08-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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', '')),
|
2024-08-13 17:34:38 +02:00
|
|
|
out_time_sec: tryNumber(entries.out_time_us, n => Math.floor(n / 1e6))
|
2024-08-13 16:39:24 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
this.onProgress(progress);
|
|
|
|
}
|
2024-11-15 13:19:49 +01:00
|
|
|
}
|