Feat: refactor ffmpeg config (#296)

* bundle ffmpeg wasm in package

* remove ffmpeg discover & add ffmpeg-static

* fix ffprobe-static version

* fix metadata generate

* fix auto transcribe if pending

* remove console.log
This commit is contained in:
an-lee
2024-02-11 17:21:39 +08:00
committed by GitHub
parent bc22a5e2b4
commit 57d6efa547
21 changed files with 131 additions and 703 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1 @@
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./ffmpeg-core.js")).then(exports=>exports.default(Module))}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;

View File

@@ -14,6 +14,8 @@ const NATIVE_MODULES_TO_PACKAGE = [
"sqlite3",
"fluent-ffmpeg",
"electron-squirrel-startup",
"ffmpeg-static",
"@andrkrn/ffprobe-static",
];
const INCLUDE_NESTED_DEPS = true as const;
let nativeModuleDependenciesToPackage: Set<string>;

View File

@@ -31,7 +31,6 @@
"@electron-forge/plugin-vite": "^7.2.0",
"@electron-forge/publisher-github": "^7.2.0",
"@tailwindcss/typography": "^0.5.10",
"@types/adm-zip": "^0.5.5",
"@types/autosize": "^4.0.3",
"@types/command-exists": "^1.2.3",
"@types/fluent-ffmpeg": "^2.1.24",
@@ -64,6 +63,7 @@
"zx": "^7.2.3"
},
"dependencies": {
"@andrkrn/ffprobe-static": "^5.2.0",
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@hookform/resolvers": "^3.3.4",
@@ -93,7 +93,6 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@uidotdev/usehooks": "^2.4.1",
"@vidstack/react": "^1.10.5",
"adm-zip": "^0.5.10",
"autosize": "^6.0.1",
"axios": "^1.6.7",
"camelcase": "^8.0.0",
@@ -111,6 +110,7 @@
"electron-log": "^5.1.1",
"electron-settings": "^4.0.2",
"electron-squirrel-startup": "^1.0.0",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.2.0",
"html-to-text": "^9.0.5",

View File

@@ -180,12 +180,13 @@ export class Audio extends Model<Audio> {
@BeforeCreate
static async setupDefaultAttributes(audio: Audio) {
if (!settings.ffmpegConfig().ready) return;
try {
const ffmpeg = new Ffmpeg();
const fileMetadata = await ffmpeg.generateMetadata(audio.filePath);
audio.metadata = Object.assign(audio.metadata || {}, fileMetadata);
audio.metadata = Object.assign(audio.metadata || {}, {
...fileMetadata,
duration: fileMetadata.format.duration,
});
} catch (err) {
logger.error("failed to generate metadata", err.message);
}

View File

@@ -148,7 +148,6 @@ export class Video extends Model<Video> {
// generate cover and upload
async generateCover() {
if (this.coverUrl) return;
if (!settings.ffmpegConfig().ready) return;
const ffmpeg = new Ffmpeg();
const coverFile = await ffmpeg.generateCover(
@@ -202,12 +201,13 @@ export class Video extends Model<Video> {
@BeforeCreate
static async setupDefaultAttributes(video: Video) {
if (!settings.ffmpegConfig().ready) return;
try {
const ffmpeg = new Ffmpeg();
const fileMetadata = await ffmpeg.generateMetadata(video.filePath);
video.metadata = Object.assign(video.metadata || {}, fileMetadata);
video.metadata = Object.assign(video.metadata || {}, {
...fileMetadata,
duration: fileMetadata.format.duration,
});
} catch (err) {
logger.error("failed to generate metadata", err.message);
}

View File

@@ -1,38 +1,22 @@
import { ipcMain } from "electron";
import ffmpegPath from "ffmpeg-static";
import ffprobePath from "@andrkrn/ffprobe-static";
import Ffmpeg from "fluent-ffmpeg";
import settings from "@main/settings";
import log from "electron-log/main";
import path from "path";
import fs from "fs-extra";
import AdmZip from "adm-zip";
import downloader from "@main/downloader";
import storage from "@main/storage";
import readdirp from "readdirp";
import { t } from "i18next";
import uniq from "lodash/uniq";
const logger = log.scope("ffmpeg");
export default class FfmpegWrapper {
public ffmpeg: Ffmpeg.FfmpegCommand;
public config: any;
constructor(config?: {
ffmpegPath: string;
ffprobePath: string;
commandExists?: boolean;
}) {
this.config = config || settings.ffmpegConfig();
if (this.config.commandExists) {
logger.info("Using system ffmpeg");
this.ffmpeg = Ffmpeg();
} else {
logger.info("Using downloaded ffmpeg");
const ff = Ffmpeg();
ff.setFfmpegPath(this.config.ffmpegPath);
ff.setFfprobePath(this.config.ffprobePath);
this.ffmpeg = ff;
}
constructor() {
const ff = Ffmpeg();
logger.debug("Using ffmpeg path:", ffmpegPath);
logger.debug("Using ffprobe path:", ffprobePath);
ff.setFfmpegPath(ffmpegPath);
ff.setFfprobePath(ffprobePath);
this.ffmpeg = ff;
}
checkCommand(): Promise<boolean> {
@@ -191,274 +175,10 @@ export default class FfmpegWrapper {
return this.convertToWav(input, output);
}
}
export class FfmpegDownloader {
public async download(webContents?: Electron.WebContents) {
if (process.platform === "win32") {
return await this.downloadForWin32(webContents);
} else if (process.platform === "darwin") {
return await this.downloadForDarwin(webContents);
} else {
throw new Error(
`You are using ${process.platform}, please install ffmpeg manually`
);
}
}
async downloadForDarwinArm64(webContents?: Electron.WebContents) {
const DARWIN_FFMPEG_ARM64_URL = storage.getUrl(
"ffmpeg-apple-arm64-build-6.0.zip"
);
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
const ffmpegZipPath = await downloader.download(DARWIN_FFMPEG_ARM64_URL, {
webContents,
});
const ffmepgZip = new AdmZip(ffmpegZipPath);
ffmepgZip.extractEntryTo(
"ffmpeg/ffmpeg",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
ffmepgZip.extractEntryTo(
"ffmpeg/ffprobe",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
}
async downloadForDarwin(webContents?: Electron.WebContents) {
if (process.arch === "arm64") {
return this.downloadForDarwinArm64(webContents);
}
const DARWIN_FFMPEG_URL = "https://evermeet.cx/ffmpeg/getrelease/zip";
const DARWIN_FFPROBE_URL =
"https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip";
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
const ffmpegZipPath = await downloader.download(DARWIN_FFMPEG_URL, {
webContents,
});
const ffmepgZip = new AdmZip(ffmpegZipPath);
ffmepgZip.extractEntryTo(
"ffmpeg",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
const ffprobeZipPath = await downloader.download(DARWIN_FFPROBE_URL, {
webContents,
});
const ffprobeZip = new AdmZip(ffprobeZipPath);
ffprobeZip.extractEntryTo(
"ffprobe",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
return settings.ffmpegConfig();
}
async downloadForWin32(webContents?: Electron.WebContents) {
const WINDOWS_DOWNLOAD_URL =
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip";
const zipPath = await downloader.download(WINDOWS_DOWNLOAD_URL, {
webContents,
});
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
const zip = new AdmZip(zipPath);
zip.extractEntryTo(
`${path.basename(zipPath, ".zip")}/bin/ffmpeg.exe`,
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
zip.extractEntryTo(
`${path.basename(zipPath, ".zip")}/bin/ffprobe.exe`,
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
return settings.ffmpegConfig();
}
unzip(zipPath: string) {
if (!fs.existsSync(zipPath)) {
throw new Error(`File ${zipPath} does not exist`);
}
const dir = path.dirname(zipPath);
const zip = new AdmZip(zipPath);
zip.extractAllTo(dir, true);
const unzipPath = zipPath.replace(".zip", "");
return unzipPath;
}
registerIpcHandlers() {
ipcMain.handle("ffmpeg-config", async (_event) => {
return settings.ffmpegConfig();
});
ipcMain.handle("ffmpeg-set-config", async (_event, config) => {
settings.setSync("ffmpeg.ffmpegPath", config.ffmpegPath);
settings.setSync("ffmpeg.ffprobePath", config.ffrobePath);
});
ipcMain.handle("ffmpeg-download", async (event) => {
try {
return await this.download(event.sender);
} catch (err) {
logger.error(err);
event.sender.send("on-notification", {
type: "error",
message: `FFmpeg download failed: ${err.message}`,
});
}
});
ipcMain.handle("ffmpeg-check-command", async (event) => {
const ffmpeg = new FfmpegWrapper();
const valid = await ffmpeg.checkCommand();
if (valid) {
event.sender.send("on-notification", {
type: "success",
message: t("ffmpegCommandIsWorking"),
});
} else {
logger.error("FFmpeg command not valid", ffmpeg.config);
event.sender.send("on-notification", {
type: "warning",
message: t("ffmpegCommandIsNotWorking"),
});
}
return valid;
});
ipcMain.handle("ffmpeg-discover-command", async (event) => {
try {
return await discoverFfmpeg();
} catch (err) {
logger.error(err);
event.sender.send("on-notification", {
type: "error",
message: `FFmpeg discover failed: ${err.message}`,
});
}
ipcMain.handle("ffmpeg-check-command", async (_event) => {
return await this.checkCommand();
});
}
}
export const discoverFfmpeg = async () => {
const platform = process.platform;
let ffmpegPath: string;
let ffprobePath: string;
const libraryFfmpegPath = path.join(settings.libraryPath(), "ffmpeg");
let scanDirs = [...COMMAND_SCAN_DIR[platform], libraryFfmpegPath];
const currentFfmpegPath = settings.ffmpegConfig().ffmpegPath as string;
const currentFfprobePath = settings.ffmpegConfig().ffprobePath as string;
if (currentFfmpegPath) {
scanDirs.push(path.dirname(currentFfmpegPath));
}
if (currentFfprobePath) {
scanDirs.push(path.dirname(currentFfprobePath));
}
scanDirs = uniq(scanDirs);
await Promise.all(
scanDirs.map(async (dir: string) => {
if (!fs.existsSync(dir)) return;
dir = path.resolve(dir);
log.info("FFmpeg scanning: " + dir);
const fileStream = readdirp(dir, {
depth: 3,
});
for await (const entry of fileStream) {
const appName = entry.basename
.replace(".app", "")
.replace(".exe", "")
.toLowerCase();
if (appName === "ffmpeg") {
logger.info("Found ffmpeg: ", entry.fullPath);
ffmpegPath = entry.fullPath;
}
if (appName === "ffprobe") {
logger.info("Found ffprobe: ", entry.fullPath);
ffprobePath = entry.fullPath;
}
if (ffmpegPath && ffprobePath) break;
}
})
);
let valid = false;
if (ffmpegPath && ffprobePath) {
const ffmepg = new FfmpegWrapper({ ffmpegPath, ffprobePath });
valid = await ffmepg.checkCommand();
}
if (valid) {
settings.setSync("ffmpeg", {
ffmpegPath,
ffprobePath,
});
} else {
ffmpegPath = undefined;
ffprobePath = undefined;
settings.setSync("ffmpeg", null);
}
return {
ffmpegPath,
ffprobePath,
scanDirs,
};
};
export const COMMAND_SCAN_DIR: { [key: string]: string[] } = {
darwin: [
"/usr/bin",
"/usr/local/bin",
"/Applications",
process.env.HOME + "/Applications",
"/opt/homebrew/bin",
],
linux: ["/usr/bin", "/usr/local/bin", "/snap/bin"],
win32: [
process.env.SystemDrive + "\\Program Files\\",
process.env.SystemDrive + "\\Program Files (x86)\\",
process.env.LOCALAPPDATA + "\\Apps\\2.0\\",
],
};

View File

@@ -3,14 +3,10 @@ import { LIBRARY_PATH_SUFFIX, DATABASE_NAME } from "@/constants";
import { ipcMain, app } from "electron";
import path from "path";
import fs from "fs-extra";
import os from "os";
import commandExists from "command-exists";
import log from "electron-log";
import * as i18n from "i18next";
import mainWin from "@main/window";
const logger = log.scope("settings");
const language = () => {
const _language = settings.getSync("language");
@@ -95,27 +91,6 @@ const userDataPath = () => {
return userData;
};
const ffmpegConfig = () => {
const ffmpegPath = settings.getSync("ffmpeg.ffmpegPath");
const ffprobePath = settings.getSync("ffmpeg.ffprobePath");
const _commandExists =
commandExists.sync("ffmpeg") && commandExists.sync("ffprobe");
const config = {
os: os.platform(),
arch: os.arch(),
commandExists: _commandExists,
ffmpegPath,
ffprobePath,
ready: Boolean(_commandExists || (ffmpegPath && ffprobePath)),
};
logger.info("ffmpeg config", config);
return config;
};
export default {
registerIpcHandlers: () => {
ipcMain.handle("settings-get-library", (_event) => {
@@ -182,7 +157,6 @@ export default {
userDataPath,
dbPath,
whisperConfig,
ffmpegConfig,
language,
switchLanguage,
...settings,

View File

@@ -18,7 +18,7 @@ import "@main/i18n";
import log from "electron-log/main";
import { WEB_API_URL, REPO_URL } from "@/constants";
import { AudibleProvider, TedProvider } from "@main/providers";
import { FfmpegDownloader } from "@main/ffmpeg";
import Ffmpeg from "@main/ffmpeg";
import { Waveform } from "./waveform";
log.initialize({ preload: true });
@@ -26,7 +26,7 @@ const logger = log.scope("window");
const audibleProvider = new AudibleProvider();
const tedProvider = new TedProvider();
const ffmpegDownloader = new FfmpegDownloader();
const ffmpeg = new Ffmpeg();
const waveform = new Waveform();
const main = {
@@ -55,8 +55,8 @@ main.init = () => {
// Downloader
downloader.registerIpcHandlers();
// FfmpegDownloader
ffmpegDownloader.registerIpcHandlers();
// ffmpeg
ffmpeg.registerIpcHandlers();
// AudibleProvider
audibleProvider.registerIpcHandlers();

View File

@@ -386,18 +386,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
},
},
ffmpeg: {
config: () => {
return ipcRenderer.invoke("ffmpeg-config");
},
setConfig: (config: FfmpegConfigType) => {
return ipcRenderer.invoke("ffmpeg-set-config", config);
},
download: () => {
return ipcRenderer.invoke("ffmpeg-download");
},
discover: () => {
return ipcRenderer.invoke("ffmpeg-discover-command");
},
check: () => {
return ipcRenderer.invoke("ffmpeg-check-command");
},

View File

@@ -1,177 +0,0 @@
import { t } from "i18next";
import { useContext, useEffect, useState } from "react";
import { Button, Progress, toast } from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { CheckCircle2Icon, XCircleIcon, LoaderIcon } from "lucide-react";
import Markdown from "react-markdown";
export const FfmpegCheck = () => {
const { ffmpegConfig, setFfmegConfig, EnjoyApp } = useContext(
AppSettingsProviderContext
);
const [scanResult, setScanResult] = useState<{
ffmpegPath: string;
ffprobePath: string;
scanDirs: string[];
}>();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const refreshFfmpegConfig = async () => {
EnjoyApp.settings.getFfmpegConfig().then((config) => {
setFfmegConfig(config);
});
};
const discoverFfmpeg = () => {
EnjoyApp.ffmpeg.discover().then((config) => {
setScanResult(config);
if (config.ffmpegPath && config.ffprobePath) {
toast.success(
t("ffmpegFoundAt", {
path: [config.ffmpegPath, config.ffprobePath].join(", "),
})
);
refreshFfmpegConfig();
} else {
toast.error(t("ffmpegNotFound"));
}
});
};
const downloadFfmpeg = () => {
listenToDownloadState();
setDownloading(true);
EnjoyApp.ffmpeg
.download()
.then(() => {
refreshFfmpegConfig();
})
.finally(() => {
setDownloading(false);
});
};
const listenToDownloadState = () => {
EnjoyApp.download.onState((_event, downloadState) => {
const { received, total } = downloadState;
setProgress(Math.round((received * 100) / total));
});
};
useEffect(() => {
return EnjoyApp.download.removeAllListeners();
}, [ffmpegConfig?.ready]);
useEffect(() => {
discoverFfmpeg();
}, []);
return (
<div className="w-full max-w-screen-md mx-auto px-6">
{ffmpegConfig?.ready ? (
<>
<div className="flex justify-center items-center mb-8">
<img src="./assets/ffmpeg-logo.svg" className="" />
</div>
<div className="flex justify-center mb-4">
<CheckCircle2Icon className="text-green-500 w-10 h-10 mb-4" />
</div>
<div className="text-center text-sm opacity-70">
{t("ffmpegFoundAt", { path: ffmpegConfig.ffmpegPath })}
</div>
</>
) : (
<>
<div className="flex justify-center items-center mb-8">
<img src="./assets/ffmpeg-logo.svg" className="" />
</div>
<div className="flex justify-center mb-4">
<XCircleIcon className="text-red-500 w-10 h-10" />
</div>
<div className="mb-4">
<div className="text-center text-sm mb-2">
{t("ffmpegNotFound")}
</div>
{scanResult && (
<div className="text-center text-xs text-muted-foreground mb-2">
{t("tryingToFindValidFFmepgInTheseDirectories", {
dirs: scanResult.scanDirs.join(", "),
})}
</div>
)}
</div>
<div className="flex items-center justify-center space-x-4 mb-4">
<Button onClick={discoverFfmpeg} variant="default">
{t("scan")}
</Button>
{ffmpegConfig.os === "win32" && (
<Button
variant="secondary"
disabled={downloading}
onClick={downloadFfmpeg}
>
{downloading && <LoaderIcon className="animate-spin mr-2" />}
{t("download")}
</Button>
)}
</div>
{downloading && (
<div className="w-full">
<Progress value={progress} />
</div>
)}
{ffmpegConfig.os === "darwin" && (
<div className="my-6 select-text prose mx-auto border rounded-lg p-4">
<h3 className="text-center">{t("ffmpegInstallSteps")}</h3>
<h4>
1. {t("install")}{" "}
<a
className="cursor-pointer text-blue-500 hover:underline"
onClick={() => {
EnjoyApp.shell.openExternal("https://brew.sh/");
}}
>
Homebrew
</a>
</h4>
<p>{t("runTheFollowingCommandInTerminal")} </p>
<pre>
<code>
/bin/bash -c "$(curl -fsSL
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
</code>
</pre>
<h4>2. {t("install")} FFmpeg</h4>
<p>{t("runTheFollowingCommandInTerminal")} </p>
<pre>
<code>brew install ffmpeg</code>
</pre>
<h4>3. {t("scan")} FFmpeg</h4>
<p>
{t("click")}
<Button
onClick={discoverFfmpeg}
variant="default"
size="sm"
className="mx-2"
>
{t("scan")}
</Button>
, {t("willAutomaticallyFindFFmpeg")}
</p>
</div>
)}
</>
)}
</div>
);
};

View File

@@ -28,8 +28,6 @@ export * from "./whisper-model-options";
export * from "./pitch-contour";
export * from "./reset-all-button";
export * from "./ffmpeg-check";
export * from "./loader-spin";
export * from "./no-records-found";

View File

@@ -100,7 +100,7 @@ export const MediaTranscription = (props: {
removeDbListener(fetchSegmentStats);
EnjoyApp.whisper.removeProgressListeners();
};
}, [mediaId, mediaType]);
}, [mediaId, mediaType, transcription]);
useEffect(() => {
containerRef.current

View File

@@ -1,122 +0,0 @@
import { t } from "i18next";
import { Button, toast } from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { useContext, useState } from "react";
import { EditIcon } from "lucide-react";
export const FfmpegSettings = () => {
const { EnjoyApp, setFfmegConfig, ffmpegConfig } = useContext(
AppSettingsProviderContext
);
const [editing, setEditing] = useState(false);
const refreshFfmpegConfig = async () => {
EnjoyApp.ffmpeg.config().then((config) => {
setFfmegConfig(config);
});
};
const handleChooseFfmpeg = async () => {
const filePaths = await EnjoyApp.dialog.showOpenDialog({
properties: ["openFile"],
});
const path = filePaths?.[0];
if (!path) return;
if (path.includes("ffmpeg")) {
EnjoyApp.ffmpeg.setConfig({
...ffmpegConfig,
ffmpegPath: path,
});
refreshFfmpegConfig();
} else if (path.includes("ffprobe")) {
EnjoyApp.ffmpeg.setConfig({
...ffmpegConfig,
ffprobePath: path,
});
refreshFfmpegConfig();
} else {
toast.error(t("invalidFfmpegPath"));
}
};
return (
<>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">FFmpeg</div>
{editing ? (
<>
<div className="flex items-center space-x-4">
<span className=" text-sm text-muted-foreground">
<b>ffmpeg</b>: {ffmpegConfig?.ffmpegPath || ""}
</span>
<Button
onClick={handleChooseFfmpeg}
variant="ghost"
size="icon"
>
<EditIcon className="w-4 h-4 text-muted-foreground" />
</Button>
</div>
<div className="flex items-center space-x-4">
<span className=" text-sm text-muted-foreground">
<b>ffprobe</b>: {ffmpegConfig?.ffprobePath || ""}
</span>
<Button
onClick={handleChooseFfmpeg}
variant="ghost"
size="icon"
>
<EditIcon className="w-4 h-4 text-muted-foreground" />
</Button>
</div>
</>
) : (
<div className="text-xs text-muted-foreground">
{ffmpegConfig.ready ? (
<span>{t("ffmpegCommandIsWorking")}</span>
) : (
<span>{t("ffmpegCommandIsNotWorking")}</span>
)}
</div>
)}
</div>
<div className="">
<div className="flex items-center justify-end space-x-2 mb-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
EnjoyApp.ffmpeg
.discover()
.then(({ ffmpegPath, ffprobePath }) => {
if (ffmpegPath && ffprobePath) {
toast.success(
t("ffmpegFoundAt", {
path: ffmpegPath + ", " + ffprobePath,
})
);
} else {
toast.warning(t("ffmpegNotFound"));
}
refreshFfmpegConfig();
});
}}
>
{t("scan")}
</Button>
<Button
variant={editing ? "outline" : "secondary"}
size="sm"
onClick={() => setEditing(!editing)}
>
{editing ? t("cancel") : t("edit")}
</Button>
</div>
</div>
</div>
</>
);
};

View File

@@ -6,7 +6,6 @@ export * from "./default-engine-settings";
export * from "./openai-settings";
export * from "./language-settings";
export * from "./library-settings";
export * from "./ffmpeg-settings";
export * from "./whisper-settings";
export * from "./google-generative-ai-settings";

View File

@@ -9,20 +9,16 @@ import {
LanguageSettings,
LibrarySettings,
WhisperSettings,
FfmpegSettings,
OpenaiSettings,
ProxySettings,
GoogleGenerativeAiSettings,
ResetSettings,
ResetAllSettings,
} from "@renderer/components";
import { useState, useContext } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { useState } from "react";
import { Tooltip } from "react-tooltip";
export const Preferences = () => {
const { ffmpegConfig } = useContext(AppSettingsProviderContext);
const TABS = [
{
value: "basic",
@@ -42,12 +38,6 @@ export const Preferences = () => {
<Separator />
<GoogleGenerativeAiSettings />
<Separator />
{ffmpegConfig.ready && (
<>
<FfmpegSettings />
<Separator />
</>
)}
</div>
),
},

View File

@@ -16,9 +16,7 @@ type AppSettingsProviderState = {
login?: (user: UserType) => void;
logout?: () => void;
setLibraryPath?: (path: string) => Promise<void>;
ffmpegConfig?: FfmpegConfigType;
ffmpeg?: FFmpeg;
setFfmegConfig?: (config: FfmpegConfigType) => void;
EnjoyApp?: EnjoyAppType;
language?: "en" | "zh-CN";
switchLanguage?: (language: "en" | "zh-CN") => void;
@@ -46,7 +44,6 @@ export const AppSettingsProvider = ({
const [webApi, setWebApi] = useState<Client>(null);
const [user, setUser] = useState<UserType | null>(null);
const [libraryPath, setLibraryPath] = useState("");
const [ffmpegConfig, setFfmegConfig] = useState<FfmpegConfigType>(null);
const [ffmpeg, setFfmpeg] = useState<FFmpeg>(null);
const [language, setLanguage] = useState<"en" | "zh-CN">();
const [proxy, setProxy] = useState<ProxyConfigType>();
@@ -58,7 +55,6 @@ export const AppSettingsProvider = ({
fetchVersion();
fetchUser();
fetchLibraryPath();
fetchFfmpegConfig();
fetchLanguage();
loadFfmpegWASM();
fetchProxyConfig();
@@ -81,7 +77,7 @@ export const AppSettingsProvider = ({
}, [user, apiUrl, language]);
const loadFfmpegWASM = async () => {
const baseURL = "https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm";
const baseURL = "assets/libs";
ffmpegRef.current.on("log", ({ message }) => {
console.log(message);
});
@@ -124,11 +120,6 @@ export const AppSettingsProvider = ({
});
};
const fetchFfmpegConfig = async () => {
const config = await EnjoyApp.ffmpeg.config();
setFfmegConfig(config);
};
const fetchVersion = async () => {
const version = EnjoyApp.app.version;
setVersion(version);
@@ -204,9 +195,7 @@ export const AppSettingsProvider = ({
logout,
libraryPath,
setLibraryPath: setLibraryPathHandler,
ffmpegConfig,
ffmpeg,
setFfmegConfig,
proxy,
setProxy: setProxyConfigHandler,
initialized,

View File

@@ -227,15 +227,7 @@ type EnjoyAppType = {
removeProgressListeners: () => Promise<void>;
};
ffmpeg: {
config: () => Promise<FfmpegConfigType>;
setConfig: (config: FfmpegConfigType) => Promise<FfmpegConfigType>;
download: () => Promise<FfmpegConfigType>;
check: () => Promise<boolean>;
discover: () => Promise<{
ffmpegPath: string;
ffprobePath: string;
scanDirs: string[];
}>;
};
download: {
onState: (callback: (event, state) => void) => void;

View File

@@ -24,7 +24,8 @@ export default defineConfig({
"fluent-ffmpeg",
"bufferutil",
"utf-8-validate",
// "node-llama-cpp",
"ffmpeg-static",
"@andrkrn/ffprobe-static",
],
},
},
@@ -43,12 +44,6 @@ export default defineConfig({
}/${os.platform()}/*`,
dest: "lib/youtubedr",
},
{
src: `lib/ffmpeg//${
process.env.PACKAGE_OS_ARCH || os.arch()
}/${os.platform()}/*`,
dest: "lib/ffmpeg",
},
{
src: "src/main/db/migrations/*",
dest: "migrations",

102
yarn.lock
View File

@@ -29,6 +29,18 @@ __metadata:
languageName: node
linkType: hard
"@andrkrn/ffprobe-static@npm:^5.2.0":
version: 5.2.0
resolution: "@andrkrn/ffprobe-static@npm:5.2.0"
dependencies:
"@derhuerst/http-basic": "npm:^8.2.0"
env-paths: "npm:^2.2.0"
https-proxy-agent: "npm:^5.0.0"
progress: "npm:^2.0.3"
checksum: 10c0/fa151b2ef214e43ca126a5ac34b5f7fcc90884a98bf7b0445f59db50872fcc637b199eb4a8835fbad2be53ed91be4b85958972012f2dfb53139658a4b06972cc
languageName: node
linkType: hard
"@anthropic-ai/sdk@npm:^0.9.1":
version: 0.9.1
resolution: "@anthropic-ai/sdk@npm:0.9.1"
@@ -327,6 +339,18 @@ __metadata:
languageName: node
linkType: hard
"@derhuerst/http-basic@npm:^8.2.0":
version: 8.2.4
resolution: "@derhuerst/http-basic@npm:8.2.4"
dependencies:
caseless: "npm:^0.12.0"
concat-stream: "npm:^2.0.0"
http-response-object: "npm:^3.0.1"
parse-cache-control: "npm:^1.0.1"
checksum: 10c0/f500c53d8f587ce980f55638d9912ba03652b7483181b90ed41d74fb7eae85c6e05b2f739d97860413886ada9d58d169ec49208237f62c55bdd542f8003f8389
languageName: node
linkType: hard
"@electron-forge/cli@npm:^7.2.0":
version: 7.2.0
resolution: "@electron-forge/cli@npm:7.2.0"
@@ -3410,15 +3434,6 @@ __metadata:
languageName: node
linkType: hard
"@types/adm-zip@npm:^0.5.5":
version: 0.5.5
resolution: "@types/adm-zip@npm:0.5.5"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/4976dc61e33534ecfb7eee87e8a587db06420a4eb9f34eee9425aece82e9fdd01ddd22677ff6430c582f4d9735c7d714c71adb8f149e3511bc189501b4113631
languageName: node
linkType: hard
"@types/argparse@npm:1.0.38":
version: 1.0.38
resolution: "@types/argparse@npm:1.0.38"
@@ -3731,6 +3746,13 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^10.0.3":
version: 10.17.60
resolution: "@types/node@npm:10.17.60"
checksum: 10c0/0742294912a6e79786cdee9ed77cff6ee8ff007b55d8e21170fc3e5994ad3a8101fea741898091876f8dc32b0a5ae3d64537b7176799e92da56346028d2cbcd2
languageName: node
linkType: hard
"@types/node@npm:^18.11.18, @types/node@npm:^18.16.3":
version: 18.19.8
resolution: "@types/node@npm:18.19.8"
@@ -4130,13 +4152,6 @@ __metadata:
languageName: node
linkType: hard
"adm-zip@npm:^0.5.10":
version: 0.5.10
resolution: "adm-zip@npm:0.5.10"
checksum: 10c0/1f391a4e02940688b6ca6d4b3ea96cc82a9dbe1596671d7dbc052f9a53ed2efa6ba9ba253f032ea16e70081f22d6ddd1af2d65d6be700853cdee9c2fc925c20e
languageName: node
linkType: hard
"agent-base@npm:5":
version: 5.1.1
resolution: "agent-base@npm:5.1.1"
@@ -4896,7 +4911,7 @@ __metadata:
languageName: node
linkType: hard
"caseless@npm:~0.12.0":
"caseless@npm:^0.12.0, caseless@npm:~0.12.0":
version: 0.12.0
resolution: "caseless@npm:0.12.0"
checksum: 10c0/ccf64bcb6c0232cdc5b7bd91ddd06e23a4b541f138336d4725233ac538041fb2f29c2e86c3c4a7a61ef990b665348db23a047060b9414c3a6603e9fa61ad4626
@@ -5301,6 +5316,18 @@ __metadata:
languageName: node
linkType: hard
"concat-stream@npm:^2.0.0":
version: 2.0.0
resolution: "concat-stream@npm:2.0.0"
dependencies:
buffer-from: "npm:^1.0.0"
inherits: "npm:^2.0.3"
readable-stream: "npm:^3.0.2"
typedarray: "npm:^0.0.6"
checksum: 10c0/29565dd9198fe1d8cf57f6cc71527dbc6ad67e12e4ac9401feb389c53042b2dceedf47034cbe702dfc4fd8df3ae7e6bfeeebe732cc4fa2674e484c13f04c219a
languageName: node
linkType: hard
"console-control-strings@npm:^1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
@@ -6012,6 +6039,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "enjoy@workspace:enjoy"
dependencies:
"@andrkrn/ffprobe-static": "npm:^5.2.0"
"@electron-forge/cli": "npm:^7.2.0"
"@electron-forge/maker-deb": "npm:^7.2.0"
"@electron-forge/maker-rpm": "npm:^7.2.0"
@@ -6048,7 +6076,6 @@ __metadata:
"@radix-ui/react-toggle": "npm:^1.0.3"
"@radix-ui/react-tooltip": "npm:^1.0.7"
"@tailwindcss/typography": "npm:^0.5.10"
"@types/adm-zip": "npm:^0.5.5"
"@types/autosize": "npm:^4.0.3"
"@types/command-exists": "npm:^1.2.3"
"@types/fluent-ffmpeg": "npm:^2.1.24"
@@ -6066,7 +6093,6 @@ __metadata:
"@uidotdev/usehooks": "npm:^2.4.1"
"@vidstack/react": "npm:^1.10.5"
"@vitejs/plugin-react": "npm:^4.2.1"
adm-zip: "npm:^0.5.10"
autoprefixer: "npm:^10.4.17"
autosize: "npm:^6.0.1"
axios: "npm:^1.6.7"
@@ -6089,6 +6115,7 @@ __metadata:
eslint: "npm:^8.56.0"
eslint-import-resolver-typescript: "npm:^3.6.1"
eslint-plugin-import: "npm:^2.29.1"
ffmpeg-static: "npm:^5.2.0"
flora-colossus: "npm:^2.0.0"
fluent-ffmpeg: "npm:^2.1.2"
fs-extra: "npm:^11.2.0"
@@ -6866,6 +6893,18 @@ __metadata:
languageName: node
linkType: hard
"ffmpeg-static@npm:^5.2.0":
version: 5.2.0
resolution: "ffmpeg-static@npm:5.2.0"
dependencies:
"@derhuerst/http-basic": "npm:^8.2.0"
env-paths: "npm:^2.2.0"
https-proxy-agent: "npm:^5.0.0"
progress: "npm:^2.0.3"
checksum: 10c0/ac5126fc1391533476232682df593c88f49707878a392cce118576320cc33a1fc33a2d8c918f7d6ac7fa6472fb9d8a67f6c1d3e3842a760644dfcb73f7d10b46
languageName: node
linkType: hard
"file-entry-cache@npm:^6.0.1":
version: 6.0.1
resolution: "file-entry-cache@npm:6.0.1"
@@ -7815,6 +7854,15 @@ __metadata:
languageName: node
linkType: hard
"http-response-object@npm:^3.0.1":
version: 3.0.2
resolution: "http-response-object@npm:3.0.2"
dependencies:
"@types/node": "npm:^10.0.3"
checksum: 10c0/f161db99184087798563cb14c48a67eebe9405668a5ed2341faf85d3079a2c00262431df8e0ccbe274dc6415b6729179f12b09f875d13ad33d83401e4b1ed22e
languageName: node
linkType: hard
"http2-wrapper@npm:^1.0.0-beta.5.2":
version: 1.0.3
resolution: "http2-wrapper@npm:1.0.3"
@@ -10636,6 +10684,13 @@ __metadata:
languageName: node
linkType: hard
"parse-cache-control@npm:^1.0.1":
version: 1.0.1
resolution: "parse-cache-control@npm:1.0.1"
checksum: 10c0/330a0d9e3a22a7b0f6e8a973c0b9f51275642ee28544cd0d546420273946d555d20a5c7b49fca24d68d2e698bae0186f0f41f48d62133d3153c32454db05f2df
languageName: node
linkType: hard
"parse-entities@npm:^4.0.0":
version: 4.0.1
resolution: "parse-entities@npm:4.0.1"
@@ -11421,7 +11476,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
"readable-stream@npm:^3.0.2, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
@@ -12911,6 +12966,13 @@ __metadata:
languageName: node
linkType: hard
"typedarray@npm:^0.0.6":
version: 0.0.6
resolution: "typedarray@npm:0.0.6"
checksum: 10c0/6005cb31df50eef8b1f3c780eb71a17925f3038a100d82f9406ac2ad1de5eb59f8e6decbdc145b3a1f8e5836e17b0c0002fb698b9fe2516b8f9f9ff602d36412
languageName: node
linkType: hard
"typescript@npm:^5.3.3":
version: 5.3.3
resolution: "typescript@npm:5.3.3"