2023-07-30 02:40:05 +02:00
|
|
|
import { BrowserWindow, app, dialog, ipcMain } from "electron";
|
2023-08-01 15:57:01 +02:00
|
|
|
import { statSync } from "fs";
|
2023-08-01 09:59:37 +02:00
|
|
|
import {
|
2023-08-01 15:57:01 +02:00
|
|
|
deleteFile,
|
|
|
|
deleteTwoPassFiles,
|
2023-08-01 09:59:37 +02:00
|
|
|
execute,
|
|
|
|
getNewFilename,
|
|
|
|
getVideoDuration,
|
|
|
|
printAndDevTool,
|
|
|
|
} from "./utils/misc";
|
2023-07-29 14:29:14 +02:00
|
|
|
import path = require("path");
|
2023-07-29 19:23:18 +02:00
|
|
|
import ffmpegPath = require("ffmpeg-static");
|
2023-07-30 13:44:47 +02:00
|
|
|
|
2024-04-04 12:05:33 +02:00
|
|
|
let error = false;
|
|
|
|
|
2023-07-30 13:44:47 +02:00
|
|
|
const moviesFilter = {
|
|
|
|
name: "Videos",
|
|
|
|
extensions: ["mp4", "mkv"],
|
|
|
|
};
|
|
|
|
|
2023-08-23 23:14:03 +02:00
|
|
|
const metadataAudio = `-metadata:s:a:0 title="System sounds and microphone" \
|
|
|
|
-metadata:s:a:1 title="System sounds" \
|
|
|
|
-metadata:s:a:2 title="Microphone"`;
|
|
|
|
|
2024-01-13 02:01:42 +01:00
|
|
|
const extraArgs = "-movflags +faststart";
|
|
|
|
|
2024-04-04 12:05:33 +02:00
|
|
|
/** Register a new error */
|
|
|
|
const registerError = (win: BrowserWindow, err: string) => {
|
|
|
|
error = true;
|
|
|
|
printAndDevTool(win, err);
|
|
|
|
};
|
|
|
|
|
2023-07-30 00:14:20 +02:00
|
|
|
/** Create a new window */
|
2023-07-28 02:32:18 +02:00
|
|
|
const createWindow = () => {
|
|
|
|
const win = new BrowserWindow({
|
2023-08-01 11:07:29 +02:00
|
|
|
width: 600,
|
|
|
|
height: 340,
|
2023-08-01 10:18:08 +02:00
|
|
|
icon: "./image/icon.ico",
|
2023-08-01 11:07:29 +02:00
|
|
|
autoHideMenuBar: true,
|
2023-07-29 14:29:14 +02:00
|
|
|
webPreferences: {
|
|
|
|
preload: path.join(__dirname, "preload.js"),
|
|
|
|
},
|
2023-07-28 02:32:18 +02:00
|
|
|
});
|
|
|
|
|
2023-07-29 15:30:41 +02:00
|
|
|
win.loadFile(path.join(path.resolve(__dirname, ".."), "pages", "index.html"));
|
2023-07-30 02:01:16 +02:00
|
|
|
|
|
|
|
return win;
|
2023-07-28 02:32:18 +02:00
|
|
|
};
|
2023-07-29 15:30:41 +02:00
|
|
|
|
2023-07-30 02:40:05 +02:00
|
|
|
/* Ready to create the window */
|
2023-07-28 02:32:18 +02:00
|
|
|
app.whenReady().then(() => {
|
2023-07-30 02:01:16 +02:00
|
|
|
const win = createWindow();
|
|
|
|
|
2023-08-24 15:59:46 +02:00
|
|
|
/** Ask user files */
|
|
|
|
const askFiles = () => {
|
2023-07-30 02:01:16 +02:00
|
|
|
return dialog.showOpenDialogSync(win, {
|
|
|
|
filters: [moviesFilter],
|
2023-08-24 15:59:46 +02:00
|
|
|
properties: ["openFile", "dontAddToRecent", "multiSelections"],
|
2023-07-30 02:01:16 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/** Send confirmation to user */
|
|
|
|
const confirmation = async (message: string) => {
|
2023-08-01 11:10:35 +02:00
|
|
|
await dialog.showMessageBox(win, { message });
|
2023-07-30 02:01:16 +02:00
|
|
|
};
|
|
|
|
|
2023-08-24 15:59:46 +02:00
|
|
|
/** Get filename of a path */
|
|
|
|
const getFilename = (filepath: string) => path.parse(filepath).base;
|
|
|
|
|
2023-11-19 21:43:35 +01:00
|
|
|
/** Merge all audios track of a video into one
|
|
|
|
* In case video have only one track, silently pass */
|
2023-08-01 09:59:37 +02:00
|
|
|
const mergeAudio = async (file: string) => {
|
|
|
|
const tmpFile = getNewFilename(file, "TMP_");
|
2023-11-19 21:43:35 +01:00
|
|
|
let outFile;
|
2023-08-01 09:59:37 +02:00
|
|
|
|
2023-11-19 22:01:18 +01:00
|
|
|
// One track for the video
|
|
|
|
let nbTracks = 1;
|
|
|
|
|
2023-08-01 09:59:37 +02:00
|
|
|
// Merge 2 audio
|
2023-08-04 20:53:21 +02:00
|
|
|
// See: https://trac.ffmpeg.org/wiki/AudioChannelManipulation#a2stereostereo
|
2023-08-01 09:59:37 +02:00
|
|
|
await execute(
|
2023-08-23 21:30:20 +02:00
|
|
|
`"${ffmpegPath}" -y \
|
2023-08-24 15:56:19 +02:00
|
|
|
-i "${file}" \
|
|
|
|
-filter_complex "[0:a]amerge=inputs=2[a]" -ac 2 -map 0:v -map "[a]" \
|
|
|
|
-c:v copy \
|
|
|
|
"${tmpFile}"`
|
2023-11-19 21:43:35 +01:00
|
|
|
)
|
|
|
|
.catch(async (e) => {
|
|
|
|
if (
|
|
|
|
`${e}`.includes(
|
|
|
|
"Cannot find a matching stream for unlabeled input pad 1 on filter"
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
// Only one audio in the file
|
|
|
|
outFile = getNewFilename(file, "(processed) ");
|
2023-11-19 22:01:18 +01:00
|
|
|
nbTracks += 1;
|
2023-11-19 21:43:35 +01:00
|
|
|
|
|
|
|
// Do a copy
|
2024-01-16 20:05:14 +01:00
|
|
|
await execute(`"${ffmpegPath}" -y \
|
2024-01-13 02:01:42 +01:00
|
|
|
-i "${file}" \
|
|
|
|
-codec copy \
|
2024-01-16 19:55:26 +01:00
|
|
|
${extraArgs} \
|
2024-04-04 12:05:33 +02:00
|
|
|
"${outFile}"`).catch((e) => registerError(win, e));
|
2023-11-19 21:43:35 +01:00
|
|
|
|
|
|
|
// We throw the error since we do not want to merge any audio
|
|
|
|
return Promise.resolve("skip");
|
|
|
|
} else {
|
|
|
|
// Error handling
|
2024-04-04 12:05:33 +02:00
|
|
|
registerError(win, e);
|
2023-11-19 21:43:35 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(async (val) => {
|
|
|
|
if (val == "skip") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
outFile = getNewFilename(file, "(merged audio) ");
|
2023-11-19 22:01:18 +01:00
|
|
|
nbTracks += 3;
|
2023-11-19 21:43:35 +01:00
|
|
|
// Add merged audio as first position to original video and make it default
|
|
|
|
// About disposition: https://ffmpeg.org/ffmpeg.html#Main-options
|
|
|
|
// Also rename all tracks accordingly to what they are
|
|
|
|
await execute(
|
|
|
|
`"${ffmpegPath}" -y \
|
|
|
|
-i "${tmpFile}" -i "${file}" \
|
|
|
|
-map 0 -map 1:a -c:v copy \
|
|
|
|
-disposition:a 0 -disposition:a:0 default \
|
|
|
|
${metadataAudio} \
|
2024-01-13 02:01:42 +01:00
|
|
|
${extraArgs} \
|
2023-11-19 21:43:35 +01:00
|
|
|
"${outFile}"`
|
2024-04-04 12:05:33 +02:00
|
|
|
).catch((e) => registerError(win, e));
|
2023-11-19 21:43:35 +01:00
|
|
|
|
|
|
|
// Delete the temporary video file
|
|
|
|
deleteFile(tmpFile);
|
|
|
|
});
|
2023-08-01 09:59:37 +02:00
|
|
|
|
2023-08-01 15:30:25 +02:00
|
|
|
const duration = getVideoDuration(outFile);
|
2023-08-01 09:59:37 +02:00
|
|
|
const stats = statSync(outFile);
|
|
|
|
|
2023-11-19 22:01:18 +01:00
|
|
|
return {
|
|
|
|
title: outFile,
|
|
|
|
size: stats.size / 1024 / 1024,
|
|
|
|
duration,
|
|
|
|
nbTracks,
|
|
|
|
};
|
2023-08-01 09:59:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Reduce size of a file */
|
2023-11-19 22:01:18 +01:00
|
|
|
const reduceSize = async (
|
|
|
|
file: string,
|
|
|
|
bitrate: number,
|
|
|
|
nbTracks: number
|
|
|
|
) => {
|
2023-08-01 09:59:37 +02:00
|
|
|
const audioBitrate = 400; // keep some room
|
2023-12-02 23:56:32 +01:00
|
|
|
let videoBitrate = bitrate - audioBitrate;
|
2023-08-01 09:59:37 +02:00
|
|
|
|
|
|
|
const finalFile = getNewFilename(file, "Compressed - ");
|
|
|
|
|
|
|
|
// Trash the output, depends on the platform
|
|
|
|
const nul = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
|
|
|
2023-11-19 22:01:18 +01:00
|
|
|
// Mapping of tracks for FFMPEG
|
|
|
|
const mappingTracks = Array(nbTracks)
|
|
|
|
.fill("-map 0:")
|
|
|
|
.map(function (str, index) {
|
|
|
|
return str + index;
|
|
|
|
})
|
|
|
|
.join(" ");
|
|
|
|
|
2023-12-02 23:56:32 +01:00
|
|
|
let codec = "libx264";
|
|
|
|
let hwAcc = "";
|
|
|
|
|
|
|
|
const argv = process.argv;
|
|
|
|
if (argv.includes("/nvenc")) {
|
|
|
|
// Use NVenc
|
|
|
|
codec = "h264_nvenc";
|
|
|
|
hwAcc = "-hwaccel cuda";
|
|
|
|
|
|
|
|
// Increase video bitrate
|
|
|
|
videoBitrate = Math.floor(videoBitrate * 1.85);
|
|
|
|
}
|
|
|
|
|
2023-08-24 03:29:46 +02:00
|
|
|
// Compress the video
|
|
|
|
// Add metadata to audio's track
|
2023-08-01 09:59:37 +02:00
|
|
|
await execute(
|
2023-12-02 23:56:32 +01:00
|
|
|
`"${ffmpegPath}" -y ${hwAcc} \
|
2023-08-24 15:56:19 +02:00
|
|
|
-i "${file}" \
|
2023-12-02 23:56:32 +01:00
|
|
|
-c:v ${codec} -b:v ${videoBitrate}k -pass 1 -an -f mp4 \
|
2023-08-24 15:56:19 +02:00
|
|
|
${nul} \
|
|
|
|
&& \
|
2023-12-02 23:56:32 +01:00
|
|
|
"${ffmpegPath}" -y ${hwAcc} \
|
2023-08-24 15:56:19 +02:00
|
|
|
-i "${file}" \
|
2023-12-02 23:56:32 +01:00
|
|
|
-c:v ${codec} -b:v ${videoBitrate}k -pass 2 -c:a copy \
|
2023-11-19 22:01:18 +01:00
|
|
|
${mappingTracks} -f mp4 \
|
2023-08-24 15:56:19 +02:00
|
|
|
${metadataAudio} \
|
2024-01-13 02:01:42 +01:00
|
|
|
${extraArgs} \
|
2023-08-24 15:56:19 +02:00
|
|
|
"${finalFile}"`
|
2024-04-04 12:05:33 +02:00
|
|
|
).catch((e) => registerError(win, e));
|
2023-08-01 09:59:37 +02:00
|
|
|
|
2023-08-01 15:57:01 +02:00
|
|
|
// Delete the old video file
|
|
|
|
deleteFile(file);
|
|
|
|
|
|
|
|
// Delete the 2 pass temporary files
|
2024-04-04 12:33:48 +02:00
|
|
|
deleteTwoPassFiles(process.cwd());
|
2023-08-01 09:59:37 +02:00
|
|
|
|
|
|
|
return finalFile;
|
|
|
|
};
|
|
|
|
|
2023-07-30 00:14:20 +02:00
|
|
|
/* Context bridge */
|
2023-07-29 20:20:55 +02:00
|
|
|
ipcMain.handle("argv", () => process.argv);
|
2023-07-30 00:14:20 +02:00
|
|
|
ipcMain.handle("allowedExtensions", () => moviesFilter);
|
2023-08-24 15:59:46 +02:00
|
|
|
ipcMain.handle("getFilename", (_, filepath: string) => getFilename(filepath));
|
|
|
|
ipcMain.handle("askFiles", () => askFiles());
|
2023-07-30 01:40:06 +02:00
|
|
|
ipcMain.handle("mergeAudio", (_, file: string) => mergeAudio(file));
|
2023-11-19 22:01:18 +01:00
|
|
|
ipcMain.handle(
|
|
|
|
"reduceSize",
|
|
|
|
(_, file: string, bitrate: number, nbTracks: number) =>
|
|
|
|
reduceSize(file, bitrate, nbTracks)
|
2023-07-30 13:44:47 +02:00
|
|
|
);
|
2024-04-04 12:05:33 +02:00
|
|
|
ipcMain.handle("exit", () => (error ? {} : app.quit()));
|
2023-08-01 11:10:35 +02:00
|
|
|
ipcMain.handle("confirmation", (_, text: string) => confirmation(text));
|
2023-07-28 02:32:18 +02:00
|
|
|
});
|