Expectation

This commit is contained in:
Manav Rathi 2024-04-21 10:26:17 +05:30
parent 36ccd3b202
commit cfced851c6
No known key found for this signature in database
4 changed files with 76 additions and 58 deletions

View file

@ -1,3 +1,3 @@
export const INPUT_PATH_PLACEHOLDER = "INPUT"; export const ffmpegPathPlaceholder = "FFMPEG";
export const FFMPEG_PLACEHOLDER = "FFMPEG"; export const inputPathPlaceholder = "INPUT";
export const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; export const outputPathPlaceholder = "OUTPUT";

View file

@ -2,9 +2,9 @@ import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time"; import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time";
import { Remote } from "comlink"; import { Remote } from "comlink";
import { import {
FFMPEG_PLACEHOLDER, ffmpegPathPlaceholder,
INPUT_PATH_PLACEHOLDER, inputPathPlaceholder,
OUTPUT_PATH_PLACEHOLDER, outputPathPlaceholder,
} from "constants/ffmpeg"; } from "constants/ffmpeg";
import { NULL_LOCATION } from "constants/upload"; import { NULL_LOCATION } from "constants/upload";
import { ElectronFile, ParsedExtractedMetadata } from "types/upload"; import { ElectronFile, ParsedExtractedMetadata } from "types/upload";
@ -19,16 +19,16 @@ export async function generateVideoThumbnail(
try { try {
return await ffmpegExec( return await ffmpegExec(
[ [
FFMPEG_PLACEHOLDER, ffmpegPathPlaceholder,
"-i", "-i",
INPUT_PATH_PLACEHOLDER, inputPathPlaceholder,
"-ss", "-ss",
`00:00:0${seekTime}`, `00:00:0${seekTime}`,
"-vframes", "-vframes",
"1", "1",
"-vf", "-vf",
"scale=-1:720", "scale=-1:720",
OUTPUT_PATH_PLACEHOLDER, outputPathPlaceholder,
], ],
file, file,
"thumb.jpeg", "thumb.jpeg",
@ -50,16 +50,16 @@ export async function extractVideoMetadata(file: File | ElectronFile) {
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file // -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
const metadata = await ffmpegExec( const metadata = await ffmpegExec(
[ [
FFMPEG_PLACEHOLDER, ffmpegPathPlaceholder,
"-i", "-i",
INPUT_PATH_PLACEHOLDER, inputPathPlaceholder,
"-c", "-c",
"copy", "copy",
"-map_metadata", "-map_metadata",
"0", "0",
"-f", "-f",
"ffmetadata", "ffmetadata",
OUTPUT_PATH_PLACEHOLDER, outputPathPlaceholder,
], ],
file, file,
`metadata.txt`, `metadata.txt`,
@ -137,16 +137,16 @@ function parseCreationTime(creationTime: string) {
export async function convertToMP4(file: File) { export async function convertToMP4(file: File) {
return await ffmpegExec( return await ffmpegExec(
[ [
FFMPEG_PLACEHOLDER, ffmpegPathPlaceholder,
"-i", "-i",
INPUT_PATH_PLACEHOLDER, inputPathPlaceholder,
"-preset", "-preset",
"ultrafast", "ultrafast",
OUTPUT_PATH_PLACEHOLDER, outputPathPlaceholder,
], ],
file, file,
"output.mp4", "output.mp4",
true, 30 * 1000,
); );
} }
@ -164,21 +164,16 @@ const ffmpegExec = async (
cmd: string[], cmd: string[],
inputFile: File | ElectronFile, inputFile: File | ElectronFile,
outputFilename: string, outputFilename: string,
dontTimeout?: boolean, timeoutMS: number = 0,
): Promise<File | ElectronFile> => { ): Promise<File | ElectronFile> => {
const electron = globalThis.electron; const electron = globalThis.electron;
if (electron) { if (electron) {
return electron.runFFmpegCmd( return electron.runFFmpegCmd(cmd, inputFile, outputFilename, timeoutMS);
cmd,
inputFile,
outputFilename,
dontTimeout,
);
} else { } else {
return workerFactory return workerFactory
.instance() .instance()
.then((worker) => .then((worker) =>
worker.run(cmd, inputFile, outputFilename, dontTimeout), worker.run(cmd, inputFile, outputFilename, timeoutMS),
); );
} }
}; };

View file

@ -3,29 +3,34 @@ import log from "@/next/log";
import { withTimeout } from "@ente/shared/utils"; import { withTimeout } from "@ente/shared/utils";
import QueueProcessor from "@ente/shared/utils/queueProcessor"; import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { generateTempName } from "@ente/shared/utils/temp"; import { generateTempName } from "@ente/shared/utils/temp";
import * as Comlink from "comlink"; import { expose } from "comlink";
import { import {
FFMPEG_PLACEHOLDER, ffmpegPathPlaceholder,
INPUT_PATH_PLACEHOLDER, inputPathPlaceholder,
OUTPUT_PATH_PLACEHOLDER, outputPathPlaceholder,
} from "constants/ffmpeg"; } from "constants/ffmpeg";
import { FFmpeg, createFFmpeg } from "ffmpeg-wasm"; import { FFmpeg, createFFmpeg } from "ffmpeg-wasm";
import { getUint8ArrayView } from "services/readerService"; import { getUint8ArrayView } from "services/readerService";
export class DedicatedFFmpegWorker { export class DedicatedFFmpegWorker {
wasmFFmpeg: WasmFFmpeg; private wasmFFmpeg: WasmFFmpeg;
constructor() { constructor() {
this.wasmFFmpeg = new WasmFFmpeg(); this.wasmFFmpeg = new WasmFFmpeg();
} }
run(cmd, inputFile, outputFileName, dontTimeout) { /**
return this.wasmFFmpeg.run(cmd, inputFile, outputFileName, dontTimeout); * Execute a FFMPEG {@link command}.
*
* This is a sibling of {@link ffmpegExec} in ipc.ts exposed by the desktop
* app. See [Note: FFMPEG in Electron].
*/
run(cmd, inputFile, outputFileName, timeoutMS) {
return this.wasmFFmpeg.run(cmd, inputFile, outputFileName, timeoutMS);
} }
} }
Comlink.expose(DedicatedFFmpegWorker, self); expose(DedicatedFFmpegWorker, self);
const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000;
export class WasmFFmpeg { export class WasmFFmpeg {
private ffmpeg: FFmpeg; private ffmpeg: FFmpeg;
@ -51,24 +56,13 @@ export class WasmFFmpeg {
cmd: string[], cmd: string[],
inputFile: File, inputFile: File,
outputFileName: string, outputFileName: string,
dontTimeout = false, timeoutMS,
) { ) {
const response = this.ffmpegTaskQueue.queueUpRequest(() => { const exec = () => this.execute(cmd, inputFile, outputFileName);
if (dontTimeout) { const request = this.ffmpegTaskQueue.queueUpRequest(() =>
return this.execute(cmd, inputFile, outputFileName); timeoutMS ? withTimeout<File>(exec(), timeoutMS) : exec(),
} else { );
return withTimeout<File>( return await request.promise;
this.execute(cmd, inputFile, outputFileName),
FFMPEG_EXECUTION_WAIT_TIME,
);
}
});
try {
return await response.promise;
} catch (e) {
log.error("ffmpeg run failed", e);
throw e;
}
} }
private async execute( private async execute(
@ -91,11 +85,11 @@ export class WasmFFmpeg {
tempOutputFilePath = `${generateTempName(10, outputFileName)}`; tempOutputFilePath = `${generateTempName(10, outputFileName)}`;
cmd = cmd.map((cmdPart) => { cmd = cmd.map((cmdPart) => {
if (cmdPart === FFMPEG_PLACEHOLDER) { if (cmdPart === ffmpegPathPlaceholder) {
return ""; return "";
} else if (cmdPart === INPUT_PATH_PLACEHOLDER) { } else if (cmdPart === inputPathPlaceholder) {
return tempInputFilePath; return tempInputFilePath;
} else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { } else if (cmdPart === outputPathPlaceholder) {
return tempOutputFilePath; return tempOutputFilePath;
} else { } else {
return cmdPart; return cmdPart;

View file

@ -236,11 +236,40 @@ export interface Electron {
maxSize: number, maxSize: number,
) => Promise<Uint8Array>; ) => Promise<Uint8Array>;
runFFmpegCmd: ( /**
cmd: string[], * Execute a FFMPEG {@link command}.
inputFile: File | ElectronFile, *
* This executes the command using the FFMPEG executable we bundle with our
* desktop app. There is also a FFMPEG WASM implementation that we use when
* running on the web, it also has a sibling function with the same
* parameters. See [Note: FFMPEG in Electron].
*
* @param command An array of strings, each representing one positional
* parameter in the command to execute. Placeholders for the input, output
* and ffmpeg's own path are replaced before executing the command
* (respectively {@link inputPathPlaceholder},
* {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}).
*
* @param inputDataOrPath The bytes of the input file, or the path to the
* input file on the user's local disk. In both cases, the data gets
* serialized to a temporary file, and then that path gets substituted in
* the FFMPEG {@link command} by {@link inputPathPlaceholder}.
*
* @param outputFileName The name of the file we instruct FFMPEG to produce
* when giving it the given {@link command}. The contents of this file get
* returned as the result.
*
* @param timeoutMS If non-zero, then throw a timeout error if the FFMPEG
* command takes more than the given number of milliseconds.
*
* @returns The contents of the output file produced by the ffmpeg command
* at {@link outputFileName}.
*/
ffmpegExec: (
command: string[],
inputDataOrPath: Uint8Array | string,
outputFileName: string, outputFileName: string,
dontTimeout?: boolean, timeoutMS: number,
) => Promise<File>; ) => Promise<File>;
// - ML // - ML