Expectation
This commit is contained in:
parent
36ccd3b202
commit
cfced851c6
|
@ -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";
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue