Most web-based video downloaders share the same limitations: intrusive pop-ups, 15-minute duration caps, throttled cloud processing, and the pain of managing dozens of individual, unsorted video files.
To solve this, I built TubeMerger—a local-first, open-source desktop application that downloads YouTube playlists and stitches selected videos into a single master MP4 file, complete with seekable chapter markers.
In this article, I'll walk through the architectural hurdles of building TubeMerger: avoiding the performance overhead of Electron, overcoming common FFmpeg concatenation traps, and streaming live progress metrics from Python to React.
1. The Core Architecture
Instead of bundling an Electron runtime that bloats memory usage, TubeMerger uses a decoupled, local-first stack:
- Frontend: React 19 + TypeScript + Vite + Tailwind CSS.
- Desktop Window Container: PyWebView (leveraging native OS web engines: WebKit/Cocoa on macOS and WebView2 on Windows).
-
Local Application Server: FastAPI running over
uvicornon localhost with an SQLite WAL datastore for merge queues and job history. -
Media Processing:
yt-dlpfor stream extraction andFFmpegfor per-clip normalization and final concatenation.
+-------------------------------------------------------------+
| PyWebView (Native Desktop OS) |
| +-------------------------------------------------------+ |
| | React 19 / Tailwind SPA Client | |
| +-------------------------------------------------------+ |
| ▲ |
| SSE Events / JSON REST |
| ▼ |
| +-------------------------------------------------------+ |
| | FastAPI Local Server (Uvicorn / SQLite) | |
| +-------------------------------------------------------+ |
| ▲ |
| ▼ |
| +-------------------------------------------------------+ |
| | yt-dlp ───► FFmpeg Pipeline | |
| +-------------------------------------------------------+ |
+-------------------------------------------------------------+
2. The Hard Part: Why You Can't Just "Concat" Playlist Clips
If you run ffmpeg
If you run ffmpeg -f concat directly on raw downloaded videos from an arbitrary YouTube playlist, the stitch often fails or produces corrupt, desynced media. Here's why:
Inconsistent Video Dimensions: Creators upload videos across different years with varying resolutions (e.g., 720p mixed with 1080p or old 4:3 clips).
Mismatched Audio Frequencies: One clip might use 44.1 kHz AAC while another uses 48 kHz Opus. Concat demuxers drop audio completely when sample rates drift across segments.
Variable Frame Rates (VFR): Web-streamed clips frequently switch timing profiles, causing progressive audio-video desync over long renders.
The Normalization Strategy
To stitch clips reliably without pillarboxing or audio drift, each selected clip goes through a normalization step before passing to the concat demuxer:
def build_normalization_command(input_file: str, output_file: str, target_w: int, target_h: int):
return [
"ffmpeg", "-y", "-i", input_file,
"-vf", (
f"scale={target_w}:{target_h}:force_original_aspect_ratio=decrease,"
f"pad={target_w}:{target_h}:(ow-iw)/2:(oh-ih)/2:black,"
"setsar=1,fps=30"
),
"-c:v", "libx264", "-crf", "21", "-preset", "fast",
"-c:a", "aac", "-ar", "44100", "-ac", "2", "-b:a", "192k",
output_file
]
By enforcing:
A locked frame rate (fps=30)
A consistent canvas size and aspect ratio with letterbox padding (pad)
Uniform stereo AAC audio re-encoded at 44.1 kHz
The downstream concat demuxer runs seamlessly without audio drops or stuttering.
3. Automated Chapter Marker Injection
A 4-hour combined video file is cumbersome to navigate without chapter markers. TubeMerger generates native MP4 chapter atoms using the original video titles and duration metadata.
Before merging, an FFMETADATAFILE is generated programmatically:
`
Ini, TOML
;FFMETADATA1
major_brand=isom
minor_version=512
compatible_brands=isomiso2avc1mp41
[CHAPTER]
TIMEBASE=1/1000
START=0
END=450000
title=01. Introduction & Overview
[CHAPTER]
TIMEBASE=1/1000
START=450000
END=1230000
title=02. Architecture & Setup
During the final concatenation pass, the metadata file is attached as an extra input stream:
shell
ffmpeg -f concat -safe 0 -i concat_list.txt -i metadata.txt -map_metadata 1 -c copy output.mp4
Because -c copy is used, chapter injection takes only a few seconds. The resulting MP4 opens in players like VLC, QuickTime, and Windows Media Player with clickable, labeled chapters.
4. Real-Time Telemetry via Server-Sent Events (SSE)
Long-running local jobs require responsive UI feedback. Instead of polling REST endpoints every second, the FastAPI backend pushes live progress metrics to the React frontend using Server-Sent Events:
`python
@router.get("/api/progress")
async def stream_progress(request: Request):
async def event_generator():
while True:
if await request.is_disconnected():
break
status = merge_engine.get_active_metrics()
yield {
"event": "update",
"data": json.dumps(status)
}
await asyncio.sleep(0.5)
return EventSourceResponse(event_generator())
`
On the client side, React subscribes to the stream via an EventSource, updating live download speeds, active clip encoding steps, and estimated time remaining.
5. Summary & Getting Started
Processing everything locally avoids cloud server costs, bypasses artificial duration limits, and ensures full privacy—no URLs, video titles, or media streams are transmitted off your machine.
Website: tubemerger.com
Desktop Downloads: tubemerger.com/download
Source Code: github.com/hashamtanveer-41/tubemerger
If you're building cross-platform desktop applications with Python and modern web tooling, check out the repository, file an issue, or try running the build locally!
Top comments (0)