Merge pull request #102 from ente-io/ffmpeg-static

add support to run FFmpeg locally
This commit is contained in:
Abhinav Kumar 2022-11-17 14:12:03 +05:30 committed by GitHub
commit d6d5d961be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 51 deletions

View file

@ -39,6 +39,11 @@
]
}
],
"asarUnpack": [
"node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg",
"node_modules/ffmpeg-static/index.js",
"node_modules/ffmpeg-static/package.json"
],
"files": [
"app/**/*",
{
@ -69,6 +74,7 @@
"devDependencies": {
"@sentry/cli": "^1.68.0",
"@types/auto-launch": "^5.0.2",
"@types/ffmpeg-static": "^3.0.1",
"@types/get-folder-size": "^2.0.0",
"@types/node-fetch": "^2.6.2",
"@types/semver-compare": "^1.0.1",
@ -91,12 +97,14 @@
"@sentry/electron": "^2.5.1",
"@types/node": "^14.14.37",
"@types/promise-fs": "^2.1.1",
"any-shell-escape": "^0.1.1",
"auto-launch": "^5.0.5",
"chokidar": "^3.5.3",
"electron-log": "^4.3.5",
"electron-reload": "^2.0.0-alpha.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.8",
"ffmpeg-static": "^5.1.0",
"get-folder-size": "^2.0.1",
"next-electron-server": "file:./thirdparty/next-electron-server",
"node-fetch": "^2.6.7",

40
src/api/ffmpeg.ts Normal file
View file

@ -0,0 +1,40 @@
import { ipcRenderer } from 'electron';
import { logError } from '../services/logging';
import { ElectronFile } from '../types';
export async function runFFmpegCmd(
cmd: string[],
inputFile: File | ElectronFile,
outputFileName: string
) {
let inputFilePath = null;
let createdTempInputFile = null;
try {
if (!inputFile.path) {
const inputFileData = new Uint8Array(await inputFile.arrayBuffer());
inputFilePath = await ipcRenderer.invoke(
'write-temp-file',
inputFileData,
inputFile.name
);
createdTempInputFile = true;
} else {
inputFilePath = inputFile.path;
}
const outputFileData = await ipcRenderer.invoke(
'run-ffmpeg-cmd',
cmd,
inputFilePath,
outputFileName
);
return new File([outputFileData], outputFileName);
} finally {
if (createdTempInputFile) {
try {
await ipcRenderer.invoke('remove-temp-file', inputFilePath);
} catch (e) {
logError(e, 'failed to deleteTempFile');
}
}
}
}

View file

@ -50,6 +50,7 @@ import { fixHotReloadNext12 } from './utils/preload';
import { isFolder, getDirFiles } from './api/fs';
import { convertHEIC } from './api/heicConvert';
import { setupLogging } from './utils/logging';
import { runFFmpegCmd } from './api/ffmpeg';
fixHotReloadNext12();
setupLogging();
@ -100,4 +101,5 @@ windowObject['ElectronAPIs'] = {
skipAppVersion,
getSentryUserID,
getAppVersion,
runFFmpegCmd,
};

70
src/services/ffmpeg.ts Normal file
View file

@ -0,0 +1,70 @@
import pathToFfmpeg from 'ffmpeg-static';
const shellescape = require('any-shell-escape');
import util from 'util';
import log from 'electron-log';
import { readFile, rmSync, writeFile } from 'promise-fs';
import { logErrorSentry } from './sentry';
import { generateTempFilePath, getTempDirPath } from '../utils/temp';
const execAsync = util.promisify(require('child_process').exec);
export const INPUT_PATH_PLACEHOLDER = 'INPUT';
export const FFMPEG_PLACEHOLDER = 'FFMPEG';
export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT';
function getFFmpegStaticPath() {
return pathToFfmpeg.replace('app.asar', 'app.asar.unpacked');
}
export async function runFFmpegCmd(
cmd: string[],
inputFilePath: string,
outputFileName: string
) {
let tempOutputFilePath: string;
try {
tempOutputFilePath = await generateTempFilePath(outputFileName);
cmd = cmd.map((cmdPart) => {
if (cmdPart === FFMPEG_PLACEHOLDER) {
return getFFmpegStaticPath();
} else if (cmdPart === INPUT_PATH_PLACEHOLDER) {
return inputFilePath;
} else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) {
return tempOutputFilePath;
} else {
return cmdPart;
}
});
cmd = shellescape(cmd);
log.info('cmd', cmd);
await execAsync(cmd);
return new Uint8Array(await readFile(tempOutputFilePath));
} catch (e) {
logErrorSentry(e, 'ffmpeg run command error');
throw e;
} finally {
try {
rmSync(tempOutputFilePath);
} catch (e) {
logErrorSentry(e, 'failed to remove tempOutputFile');
}
}
}
export async function writeTempFile(fileStream: Uint8Array, fileName: string) {
const tempFilePath = await generateTempFilePath(fileName);
await writeFile(tempFilePath, fileStream);
return tempFilePath;
}
export async function deleteTempFile(tempFilePath: string) {
const tempDirPath = await getTempDirPath();
if (!tempFilePath.startsWith(tempDirPath)) {
logErrorSentry(
Error('not a temp file'),
'tried to delete a non temp file'
);
}
rmSync(tempFilePath);
}

View file

@ -1,46 +1,27 @@
import { exec, ExecException } from 'child_process';
import { app } from 'electron';
import { existsSync, rmSync } from 'fs';
import path from 'path';
import { mkdir, readFile, writeFile } from 'promise-fs';
import { generateRandomName } from '../utils/common';
import util from 'util';
import { exec } from 'child_process';
import { rmSync } from 'fs';
import { readFile, writeFile } from 'promise-fs';
import { generateTempFilePath } from '../utils/temp';
import { logErrorSentry } from './sentry';
const asyncExec = util.promisify(exec);
export async function convertHEIC(
heicFileData: Uint8Array
): Promise<Uint8Array> {
let tempInputFilePath: string;
let tempOutputFilePath: string;
try {
const tempDir = path.join(app.getPath('temp'), 'ente');
if (!existsSync(tempDir)) {
await mkdir(tempDir);
}
const tempName = generateRandomName(10);
tempInputFilePath = path.join(tempDir, tempName + '.heic');
tempOutputFilePath = path.join(tempDir, tempName + '.jpeg');
tempInputFilePath = await generateTempFilePath('.heic');
tempOutputFilePath = await generateTempFilePath('.jpeg');
await writeFile(tempInputFilePath, heicFileData);
await new Promise((resolve, reject) => {
exec(
`sips -s format jpeg ${tempInputFilePath} --out ${tempOutputFilePath}`,
(
error: ExecException | null,
stdout: string,
stderr: string
) => {
if (error) {
reject(error);
} else if (stderr) {
reject(stderr);
} else {
resolve(stdout);
}
}
);
});
await asyncExec(
`sips -s format jpeg ${tempInputFilePath} --out ${tempOutputFilePath}`
);
const convertedFileData = new Uint8Array(
await readFile(tempOutputFilePath)
);

View file

@ -1,17 +1,2 @@
import { app } from 'electron';
export const isDev = !app.isPackaged;
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export function generateRandomName(length: number) {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * charactersLength)
);
}
return result;
}

View file

@ -20,6 +20,11 @@ import {
skipAppVersion,
updateAndRestart,
} from '../services/appUpdater';
import {
deleteTempFile,
runFFmpegCmd,
writeTempFile,
} from '../services/ffmpeg';
export default function setupIpcComs(
tray: Tray,
@ -125,4 +130,20 @@ export default function setupIpcComs(
ipcMain.handle('get-app-version', () => {
return getAppVersion();
});
ipcMain.handle(
'run-ffmpeg-cmd',
(_, cmd, inputFilePath, outputFileName) => {
return runFFmpegCmd(cmd, inputFilePath, outputFileName);
}
);
ipcMain.handle(
'write-temp-file',
(_, fileStream: Uint8Array, fileName: string) => {
return writeTempFile(fileStream, fileName);
}
);
ipcMain.handle('remove-temp-file', (_, tempFilePath: string) => {
return deleteTempFile(tempFilePath);
});
}

38
src/utils/temp.ts Normal file
View file

@ -0,0 +1,38 @@
import { app } from 'electron';
import path from 'path';
import { existsSync, mkdir } from 'promise-fs';
const ENTE_TEMP_DIRECTORY = 'ente';
const CHARACTERS =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export async function getTempDirPath() {
const tempDirPath = path.join(app.getPath('temp'), ENTE_TEMP_DIRECTORY);
if (!existsSync(tempDirPath)) {
await mkdir(tempDirPath);
}
return tempDirPath;
}
function generateTempName(length: number) {
let result = '';
const charactersLength = CHARACTERS.length;
for (let i = 0; i < length; i++) {
result += CHARACTERS.charAt(
Math.floor(Math.random() * charactersLength)
);
}
return result;
}
export async function generateTempFilePath(formatSuffix: string) {
const tempDirPath = await getTempDirPath();
const namePrefix = generateTempName(10);
const tempFilePath = path.join(
tempDirPath,
namePrefix + '-' + formatSuffix
);
return tempFilePath;
}

2
ui

@ -1 +1 @@
Subproject commit c746bf89353c8dd303778214d16434faeffce556
Subproject commit 6320f0295d811570adad53a32ce94beb912fa9f9

View file

@ -35,6 +35,16 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@derhuerst/http-basic@^8.2.0":
version "8.2.4"
resolved "https://registry.yarnpkg.com/@derhuerst/http-basic/-/http-basic-8.2.4.tgz#d021ebb8f65d54bea681ae6f4a8733ce89e7f59b"
integrity sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==
dependencies:
caseless "^0.12.0"
concat-stream "^2.0.0"
http-response-object "^3.0.1"
parse-cache-control "^1.0.1"
"@develar/schema-utils@~2.6.5":
version "2.6.5"
resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6"
@ -277,6 +287,11 @@
dependencies:
"@types/ms" "*"
"@types/ffmpeg-static@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.1.tgz#1003f003624bcd2f569b56185a62dcbacd935c39"
integrity sha512-hEJdQMv/g1olk9qTiWqh23BfbKsDKE6Tc7DilNJWF1MgZsU9fYOPKrgQ448vfT7aP2Yt5re9vgJDVv9TXEoTyQ==
"@types/fs-extra@^9.0.11":
version "9.0.13"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
@ -325,6 +340,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==
"@types/node@^10.0.3":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/node@^14.14.37", "@types/node@^14.6.2":
version "14.18.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.21.tgz#0155ee46f6be28b2ff0342ca1a9b9fd4468bef41"
@ -567,6 +587,11 @@ ansi-styles@^6.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3"
integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==
any-shell-escape@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959"
integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -900,7 +925,7 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caseless@~0.12.0:
caseless@^0.12.0, caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
@ -1089,6 +1114,16 @@ concat-stream@^1.6.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
concat-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.0.2"
typedarray "^0.0.6"
concurrently@^7.0.0:
version "7.2.2"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.2.2.tgz#4ad4a4dfd3945f668d727379de2a29502e6a531c"
@ -1778,6 +1813,16 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
ffmpeg-static@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.1.0.tgz#133500f4566570c5a0e96795152b0526d8c936ad"
integrity sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg==
dependencies:
"@derhuerst/http-basic" "^8.2.0"
env-paths "^2.2.0"
https-proxy-agent "^5.0.0"
progress "^2.0.3"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -2169,6 +2214,13 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
http-response-object@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-3.0.2.tgz#7f435bb210454e4360d074ef1f989d5ea8aa9810"
integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==
dependencies:
"@types/node" "^10.0.3"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@ -2995,6 +3047,11 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-cache-control@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e"
integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==
parse-json@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@ -3228,6 +3285,15 @@ readable-stream@^2.0.6, readable-stream@^2.2.2:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@~1.1.9:
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@ -3372,7 +3438,7 @@ rxjs@^7.0.0, rxjs@^7.5.5:
dependencies:
tslib "^2.1.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -3616,6 +3682,13 @@ string-width@^5.0.0:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@ -3963,7 +4036,7 @@ utf8-byte-length@^1.0.1:
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==
util-deprecate@~1.0.1:
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==