I'm excited to share my journey of integrating Electron and React to develop a user-friendly interface for FFMPEG, a powerful multimedia framework. Typically, FFMPEG operates through a command-line interface, which can be daunting for many users. My goal was to create an application that makes FFMPEG accessible to those who prefer a graphical interface.
Setting Up the Environment
For a swift and efficient setup, I began by cloning the Electron-React Boilerplate. This boilerplate provided a pre-configured combination of Electron and React, along with Redux and Webpack, setting a solid foundation for the project. This choice allowed me to focus on building the unique features of my application, leveraging the boilerplate's stable and community-tested framework.
Main Process in Electron
Electron applications operate on two types of processes: the main process and renderer processes. In my app, the main process is responsible for managing the application's lifecycle, creating browser windows, and handling system events. I utilized Electron's BrowserWindow
to create the application window and ipcMain
for communication between the main and renderer processes.
Designing the Slider Interface
The slider was implemented as part of the React front-end. It's a graphical slider component that lets users choose a quality level ranging from 1 (highest quality) to 100 (lowest quality). This design choice was driven by the goal to make the application accessible to users who may not be familiar with the technical details of video encoding.
Translating Universal Quality to Codec-Specific Settings
The core functionality of this feature lies in its ability to translate a universal quality rating into codec-specific requirements. This is where the translateUniversalQualityToFormat
function in the Electron backend comes into play. Depending on the selected output format (e.g., MP4, MKV, WebM), the function maps the quality level chosen on the slider to the appropriate settings for that particular format.
Here's how I did it:
function translateUniversalQualityToFormat(
quality: number,
format: T_VideoFormat,
): number {
let min_value: number;
let max_value: number;
switch (format) {
case 'mp4':
case 'mkv':
min_value = 0; // Best for H.264
max_value = 51; // Worst for H.264
break;
case 'webm':
min_value = 4; // Best for VP8
max_value = 63; // Worst for VP8
break;
case 'mpg':
case 'wmv':
case 'flv':
min_value = 500; // Lowest bitrate for lowest quality
max_value = 100000; // Highest bitrate for highest quality
break;
case 'avi':
min_value = 1; // Best for MPEG-2 and MPEG-4
max_value = 31; // Worst for MPEG-2 and MPEG-4
break;
case 'mov':
min_value = 0; // Best for H.264 in MOV
max_value = 51; // Worst for H.264 in MOV
break;
default:
throw new Error('Unknown format');
}
if (format === 'wmv') {
const translated_value =
min_value + ((max_value - min_value) * (100 - quality)) / 99;
return Math.round(translated_value);
}
// Linear interpolation
const translated_value =
min_value + ((max_value - min_value) * (quality - 1)) / 99;
// Round the value to get an integer
return Math.round(translated_value);
}
Codec-Specific Quality Mapping
- H.264 (MP4, MKV, MOV): For formats using the H.264 codec, the quality level is mapped to a CRF (Constant Rate Factor) value, where a lower CRF means higher quality. The scale is adjusted so that the slider's value translates effectively into the CRF range used by H.264.
- VP8 (WebM): For WebM videos, the quality level adjusts the CRF and bitrate settings suitable for the VP8 codec. Special handling is implemented for two-pass encoding, which is a common practice for this format to achieve better quality and file size.
- MPEG-4 (AVI): The MPEG-4 codec, often used in AVI containers, has its own quality metrics. The slider's value is mapped to this codec's quantizer scale.
- Others (MPG, WMV, FLV): For other formats, the translation focuses on adjusting the bitrate, which directly influences the video quality and file size.
Building the FFMPEG Command
The function convertVideo
in the Electron backend is where the FFmpeg command is constructed. The command is dynamically built based on several parameters passed from the front end, such as the file path, desired output format, output directory, and quality setting:
export const convertVideo = async (props: I_ConvertVideoProps) => {
const { mainWindow = null, filePath, format, outputDir, quality = 1 } = props;
const useFFmpeg = getFFmpegPath();
const progressTempFilePath = getProgressTempFilePath();
try {
// Make sure format is in the list
if (!formats.includes(format)) {
throw new Error('Invalid format');
}
// Delete existing progress file
try {
await fs.promises.unlink(progressTempFilePath);
} catch (err) {
// Do nothing
}
const { newFilePath } = makeFilePaths(filePath, outputDir, format);
const translatedQuality = translateUniversalQualityToFormat(
quality,
format,
);
let command = '';
switch (format) {
case 'mp4':
case 'mkv':
command = `${useFFmpeg} -i "${filePath}" -c:v libx264 -crf ${translatedQuality} -progress "${progressTempFilePath}" -c:a aac -y "${newFilePath}"`;
break;
case 'webm':
{
// First pass
const passLogfile = path.join(os.tmpdir(), 'ffmpeg2pass.log');
const firstPassCmd = `${useFFmpeg} -i "${filePath}" -c:v libvpx -b:v 1M -crf ${translatedQuality} -pass 1 -an -f webm -y -progress "${progressTempFilePath}" -passlogfile "${passLogfile}" ${getDiscardPath()}`;
exec(firstPassCmd, (error) => {
if (error) {
console.error(`exec error on first pass: ${error}`);
convertError(mainWindow, error.message);
return;
}
// Second pass
const secondPassCmd = `${useFFmpeg} -i "${filePath}" -c:v libvpx -b:v 1M -crf ${translatedQuality} -pass 2 -c:a libvorbis -y -progress "${progressTempFilePath}" -passlogfile "${passLogfile}" "${newFilePath}"`;
exec(secondPassCmd, (error) => {
if (error) {
console.error(`exec error on second pass: ${error}`);
convertError(mainWindow, error.message);
return;
}
convertSuccess(mainWindow, newFilePath);
});
});
}
break;
case 'mpg':
case 'avi':
command = `${useFFmpeg} -i "${filePath}" -c:v mpeg4 -q:v ${translatedQuality} -progress "${progressTempFilePath}" -c:a mp3 -y "${newFilePath}"`;
break;
case 'wmv':
command = `${useFFmpeg} -i "${filePath}" -c:v wmv2 -b:v ${translatedQuality}k -progress "${progressTempFilePath}" -c:a wmav2 -y "${newFilePath}"`;
break;
case 'flv':
command = `${useFFmpeg} -i "${filePath}" -c:v flv -b:v ${translatedQuality}k -progress "${progressTempFilePath}" -c:a mp3 -y "${newFilePath}"`;
break;
case 'mov':
command = `${useFFmpeg} -i "${filePath}" -c:v libx264 -crf ${translatedQuality} -progress "${progressTempFilePath}" -c:a aac -y "${newFilePath}"`;
break;
default:
return null;
}
Special attention was given to the conversion process for WebM format videos, where I implemented a two-pass encoding strategy. This approach was chosen due to the VP8 codec's characteristics, which is commonly used in WebM files. The two-pass process is vital for achieving optimal balance between video quality and file size, a key concern in video encoding.
In the first pass, FFmpeg analyzes the video without producing an output file, gathering data on how best to allocate bits for efficient compression. This pass generates a log file that contains information about the video's complexity. In the second pass, this data is used to actually encode the video, allowing FFmpeg to optimize bit allocation across the entire video. This results in a higher quality video at a given bitrate compared to a single-pass encode.
Progress File Polling
A crucial part of tracking the conversion progress is the polling of the progress file. This is done via a setInterval
function, which regularly reads the contents of the progress file. The getPercentageFromProgressData
function parses this data to extract the current progress as a percentage. This percentage is then used to update the LinearProgress
component from Material-UI, providing a visual representation of the conversion progress:
// ########### Progress file polling ###########
useEffect(() => {
function getPercentageFromProgressData(progressData: string): number {
if (!progressData) {
return 0;
}
const totalDurationInSeconds = parseFloat(
loadedFile!.info.streams[0].duration!,
);
// Get all matches
const matches = [
...progressData.matchAll(/out_time=(\d+:\d+:\d+\.\d+)/g),
];
// Extract the last match
const lastMatch = matches[matches.length - 1];
if (lastMatch && lastMatch[1]) {
const [hours, minutes, seconds] = lastMatch[1]
.split(':')
.map(parseFloat);
const currentDurationInSeconds = hours * 3600 + minutes * 60 + seconds;
return (currentDurationInSeconds / totalDurationInSeconds!) * 100;
}
return 0;
}
const readProgressFile = async () => {
const data = await window.electron.ipcRenderer.readFile(progressFile!);
const percentage = getPercentageFromProgressData(data);
setProgress(percentage);
};
if (!window.electron) return;
let timer: NodeJS.Timeout | null = null;
let timer2: NodeJS.Timeout | null = null;
if (isConverting) {
// Polling timer
timer = setInterval(() => {
readProgressFile();
}, 1000);
// Create progress for "preparing file" state
timer2 = setInterval(() => {
setPrepProgress((prev) => {
if (prev < 100) {
return prev + 1;
}
return 0;
});
}, 500);
} else {
if (timer) {
clearInterval(timer);
}
if (timer2) {
clearInterval(timer2);
}
setPrepProgress(0);
}
// Cleanup function
return () => {
if (timer) {
clearInterval(timer);
}
if (timer2) {
clearInterval(timer2);
}
};
}, [isConverting]);
// ########## End of progress file polling ##########
Here's the result:
Managing conversion progress in the UI was a critical part of creating a user-friendly application. By effectively utilizing React's state management and Electron's IPC, the application provides a seamless and responsive experience, keeping users informed and engaged throughout the video conversion process.
Try it Out for Yourself
You can download Spectravert for Windows x86, MacOS x86 or ARM64 and Linux x86 (App Image). I could do more but those are the only platforms I own. It's completely free, so enjoy!
Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the Designly Blog. Also, please leave your comments! I love to hear thoughts from my readers.
If you want to support me, please follow me on Spotify!
Looking for a web developer? I'm available for hire! To inquire, please fill out a contact form.