FrameFuseVid combines three technologies to deliver a seamless desktop video-merging experience: Electron for the native shell, React for the UI, and FFmpeg for video encoding. In this post we walk through the architecture, explain the IPC bridge that keeps the renderer sandboxed, and show how FFmpeg filter graphs are generated for each layout.

The Three-Layer Stack

Renderer Process React 18 + Tailwind CSS + Lucide Icons — sandboxed, no Node.js access App.jsx preload.js (contextBridge) Main Process Electron 40 + Node.js — IPC handlers, window management, file I/O main.js FFmpeg (bundled binary) ffmpeg-static + ffprobe-static — spawned as child process

Each layer has a clear responsibility and minimal coupling to the others:

The IPC Bridge

Electron's security model strongly recommends context isolation and no direct Node.js access in the renderer. FrameFuseVid follows this pattern:

javascript src/main/preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // Dialog operations
  openFiles:     () => ipcRenderer.invoke('dialog:openFiles'),
  openFolder:    () => ipcRenderer.invoke('dialog:openFolder'),
  saveFile:      () => ipcRenderer.invoke('dialog:saveFile'),

  // FFmpeg operations
  combine:       (opts) => ipcRenderer.invoke('ffmpeg:combine', opts),
  cancelProcess: () => ipcRenderer.invoke('ffmpeg:cancel'),

  // Event listeners (one-way from main to renderer)
  onFFmpegProgress: (cb) =>
    ipcRenderer.on('ffmpeg:progress', (_e, data) => cb(data)),

  // ... more channels
});

The renderer calls window.electronAPI.combine(options), which invokes ipcRenderer.invoke('ffmpeg:combine', options). The main process handles this with ipcMain.handle('ffmpeg:combine', ...) and spawns FFmpeg.

Security Note

Only whitelisted channels are exposed. The renderer cannot require Node.js modules, access the filesystem, or execute arbitrary code. This is enforced by Electron's context isolation.

How FFmpeg Filter Graphs Work

FFmpeg's -filter_complex flag accepts a text-based graph description that chains video/audio filters together. FrameFuseVid generates these dynamically based on the user's layout choice.

Picture-in-Picture

The PIP layout overlays one video on another. The user's drag position and size slider are converted to pixel coordinates:

Main Video (input 0) Overlay (input 1) iw * 0.25 (x, y) # Generated filter_complex [1:v]scale=iw*0.25:-1[pip]; [0:v][pip]overlay=x:y[out]

The scale factor comes from the overlay size slider (10%–50%). The x and y values are computed from the user's drag position as percentages of the main video dimensions, then converted to absolute pixels.

Side-by-Side

Both inputs are scaled to half the output width, aspect-ratio preserved, and padded to fill their half:

ffmpeg Filter complex for side-by-side
# Scale left input to 960px wide, pad to fill 960x1080
[0:v]scale=960:1080:force_original_aspect_ratio=decrease,
  pad=960:1080:(ow-iw)/2:(oh-ih)/2[left];

# Same for the right input
[1:v]scale=960:1080:force_original_aspect_ratio=decrease,
  pad=960:1080:(ow-iw)/2:(oh-ih)/2[right];

# Stack them horizontally
[left][right]hstack[out]

Sequential (Concatenation)

Videos are concatenated using FFmpeg's concat demuxer. If subtitles are enabled, the subtitle filter is applied before output:

ffmpeg Subtitle burning filter
# Burn VTT captions into the video
-vf subtitles='captions.vtt'

Audio Merge

The simplest layout — maps the video stream from one input and the audio stream from another:

ffmpeg Audio replacement
ffmpeg -i video.mp4 -i audio.m4a \
  -map 0:v -map 1:a \
  -c:v copy -c:a aac -b:a 192k \
  output.mp4

The video stream is copied without re-encoding (-c:v copy), so this operation is fast.

Quality Presets Under the Hood

The three presets map to FFmpeg's -preset and -crf flags:

UI PresetFFmpeg -preset-crfResult
Fastultrafast28Quick encode, larger file, lower quality
Mediummedium23Balanced default
Slowslow20Slow encode, smallest file, best quality

CRF (Constant Rate Factor) controls quality on a logarithmic scale where lower numbers mean higher quality. The difference between CRF 23 and 20 is visually noticeable on detailed content like slides and code demos.

Progress Tracking

FFmpeg reports progress to stderr. The fluent-ffmpeg library parses this and emits progress events with percentage, timemark, and FPS. The main process forwards these to the renderer via IPC:

FFmpeg stderr fluent-ffmpeg .on('progress') IPC send ffmpeg:progress React UI Progress bar

File Detection Strategy

When a user scans a folder, the main process walks the directory and matches each filename against Zoom's conventions:

javascript Pattern matching logic (simplified)
function detectFileType(filename) {
  const lower = filename.toLowerCase();

  if (/shared_screen|screenshare|screen_share/.test(lower))
    return 'screen';

  if (/speaker|active_speaker|_as_|_avo_/.test(lower))
    return 'speaker';

  if (/gallery|_gv_|_gvo_/.test(lower))
    return 'gallery';

  if (/audio_only|\.m4a$|\.mp3$/.test(lower))
    return 'audio';

  if (/\.vtt$|\.srt$/.test(lower))
    return 'transcript';

  return 'unknown';
}

Users can override the detected type via a dropdown in the file list. This keeps the auto-detection non-destructive while still handling edge cases.

Build and Distribution

The CI pipeline in .github/workflows/build.yml runs on a matrix of Ubuntu, Windows, and macOS runners. Each builds the app using electron-builder and uploads platform-specific artifacts:

On version tag pushes (v*), a release job downloads all three artifacts and creates a GitHub Release with the binaries attached.

What's Next

The current architecture is intentionally simple — a single React component for the UI and a single main process file for the backend. As the feature set grows (batch processing, custom templates, CLI mode), we plan to modularize both layers while keeping the same security model intact.

Interested in the code? Browse the repository on GitHub or check the documentation for the full IPC API reference.

Explore the Source