This commit is contained in:
Manav Rathi 2024-11-21 14:10:22 +05:30
parent e0d41f6024
commit 4509d8c23f
No known key found for this signature in database
4 changed files with 123 additions and 37 deletions

View File

@ -9,7 +9,7 @@ import {
makeTempFilePath,
} from "../utils/temp";
/* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */
/* Ditto in the web app's code (used by the WASM FFmpeg invocation). */
const ffmpegPathPlaceholder = "FFMPEG";
const inputPathPlaceholder = "INPUT";
const outputPathPlaceholder = "OUTPUT";

View File

@ -1,3 +1,5 @@
/* Ditto in the desktop app's code (used by the native FFmpeg invocation). */
export const ffmpegPathPlaceholder = "FFMPEG";
export const inputPathPlaceholder = "INPUT";
export const outputPathPlaceholder = "OUTPUT";

View File

@ -1,7 +1,6 @@
import { ensureElectron } from "@/base/electron";
import log from "@/base/log";
import type { Electron } from "@/base/types/ipc";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
import {
readConvertToMP4Done,
readConvertToMP4Stream,
@ -13,13 +12,12 @@ import {
type DesktopUploadItem,
type UploadItem,
} from "@/new/photos/services/upload/types";
import type { Remote } from "comlink";
import {
ffmpegPathPlaceholder,
inputPathPlaceholder,
outputPathPlaceholder,
} from "./constants";
import type { DedicatedFFmpegWorker } from "./worker";
import { ffmpegExecWeb } from "./web";
/**
* Generate a thumbnail for the given video using a wasm FFmpeg running in a web
@ -236,21 +234,6 @@ const parseFFMetadataDate = (s: string | undefined) => {
return d;
};
/**
* Run the given FFmpeg command using a wasm FFmpeg running in a web worker.
*
* As a rough ballpark, currently the native FFmpeg integration in the desktop
* app is 10-20x faster than the wasm one. See: [Note: FFmpeg in Electron].
*/
const ffmpegExecWeb = async (
command: string[],
blob: Blob,
outputFileExtension: string,
) => {
const worker = await workerFactory.lazy();
return await worker.exec(command, blob, outputFileExtension);
};
/**
* Convert a video from a format that is not supported in the browser to MP4.
*
@ -285,21 +268,3 @@ const convertToMP4Native = async (electron: Electron, blob: Blob) => {
await readConvertToMP4Done(electron, token);
return mp4Blob;
};
/** Lazily create a singleton instance of our worker */
class WorkerFactory {
private instance: Promise<Remote<DedicatedFFmpegWorker>> | undefined;
private createComlinkWorker = () =>
new ComlinkWorker<typeof DedicatedFFmpegWorker>(
"ffmpeg-worker",
new Worker(new URL("worker.ts", import.meta.url)),
);
async lazy() {
if (!this.instance) this.instance = this.createComlinkWorker().remote;
return this.instance;
}
}
const workerFactory = new WorkerFactory();

View File

@ -0,0 +1,119 @@
import log from "@/base/log";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import {
ffmpegPathPlaceholder,
inputPathPlaceholder,
outputPathPlaceholder,
} from "./constants";
/** Lazily initialized and loaded FFmpeg instance. */
let _ffmpeg: Promise<FFmpeg> | undefined;
/**
* Return the shared {@link FFmpeg} instance, lazily creating and loading it if
* needed.
*/
const ffmpegLazy = (): Promise<FFmpeg> => (_ffmpeg ??= createFFmpeg());
const createFFmpeg = async () => {
const ffmpeg = new FFmpeg();
// This loads @ffmpeg/core from its CDN:
// https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js
// https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.wasm
await ffmpeg.load();
return ffmpeg;
};
/**
* Run the given FFmpeg command using a wasm FFmpeg running in a web worker.
*
* This is a sibling of {@link ffmpegExec} exposed by the desktop app in `ipc.ts`.
* As a rough ballpark, currently the native FFmpeg integration in the desktop
* app is 10-20x faster than the wasm one. See: [Note: FFmpeg in Electron].
*
* @param command The FFmpeg command to execute.
*
* @param blob The input data on which to run the command, provided as a blob.
*
* @param outputFileExtension The extension of the (temporary) output file which
* will be generated by the command.
*
* @returns The contents of the output file generated as a result of executing
* {@link command} on {@link blob}.
*/
export const ffmpegExecWeb = async (
command: string[],
blob: Blob,
outputFileExtension: string,
): Promise<Uint8Array> =>
ffmpegExec(await ffmpegLazy(), command, outputFileExtension, blob);
const ffmpegExec = async (
ffmpeg: FFmpeg,
command: string[],
outputFileExtension: string,
blob: Blob,
) => {
const inputPath = randomPrefix();
const outputSuffix = outputFileExtension ? "." + outputFileExtension : "";
const outputPath = randomPrefix() + outputSuffix;
const cmd = substitutePlaceholders(command, inputPath, outputPath);
const inputData = new Uint8Array(await blob.arrayBuffer());
try {
const startTime = Date.now();
await ffmpeg.writeFile(inputPath, inputData);
await ffmpeg.exec(cmd);
const result = await ffmpeg.readFile(outputPath);
if (typeof result == "string") throw new Error("Expected binary data");
const ms = Date.now() - startTime;
log.debug(() => `[wasm] ffmpeg ${cmd.join(" ")} (${ms} ms)`);
return result;
} finally {
try {
await ffmpeg.deleteFile(inputPath);
} catch (e) {
log.error(`Failed to remove input ${inputPath}`, e);
}
try {
await ffmpeg.deleteFile(outputPath);
} catch (e) {
log.error(`Failed to remove output ${outputPath}`, e);
}
}
};
/** Generate a random string suitable for being used as a file name prefix */
const randomPrefix = () => {
const alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < 10; i++)
result += alphabet[Math.floor(Math.random() * alphabet.length)]!;
return result;
};
const substitutePlaceholders = (
command: string[],
inputFilePath: string,
outputFilePath: string,
) =>
command
.map((segment) => {
if (segment == ffmpegPathPlaceholder) {
return undefined;
} else if (segment == inputPathPlaceholder) {
return inputFilePath;
} else if (segment == outputPathPlaceholder) {
return outputFilePath;
} else {
return segment;
}
})
.filter((s) => s !== undefined);