Building a YouTube-to-DVD Converter for the Samsung DVD-E370 (and Killing Every Flicker Bug Along the Way)
The Problem
Old DVD players with USB ports are a staple in a lot of homes. The Samsung DVD-E370 in particular is a capable little machine: it reads MPEG-4/XviD AVI files off a USB stick, has a clean remote, and pairs nicely with any modern LCD. The catch is that its MPEG-4 decoder is a conservative, compatibility-first implementation from the mid-2000s — and it will visually misbehave if you feed it anything it doesn’t expect.
My goal was simple: take a YouTube URL, press go, and get back a USB-ready AVI file that plays perfectly on the E370 — no horizontal strips, no flicker, no judder, no audio sync drift.
Getting there took ten iterations of a Jupyter notebook and a fair amount of signal theory. This post documents every bug and every fix.
Why NTSC — Even If You’re Not in North America
This tripped me up first. The conventional wisdom is “use PAL if you’re in a PAL country.” That reasoning applies to CRT TVs and older composite displays where the analog signal’s field rate has to match the display’s internal scan rate. It does not apply to a modern 60 Hz digital LCD monitor.
Here’s the actual math:
| Standard | Frame rate | Signal cadence | 60 Hz LCD result |
|---|---|---|---|
| NTSC | 29.97 fps | 60 Hz | Native match |
| PAL | 25 fps | 50 Hz | Player upconverts 50 → 60 Hz internally (quality loss + judder) |
The E370 is doing a digital 50-to-60 Hz conversion in firmware when it outputs PAL to a 60 Hz panel. That conversion is not great — you get subtle judder on pans and the occasional frame-timing artifact. Switching to NTSC eliminates the conversion entirely. The player outputs 29.97 fps at 60 Hz and the monitor renders it natively.
NTSC/PAL colour encoding (the actual chroma subcarrier difference that the acronyms refer to) is completely irrelevant once you’re on a digital interface. The monitor decodes the digital signal and draws pixels; it never sees the analog chroma carrier.
Player setting: Display → Video Output → NTSC
The Horizontal Strips: Interlace on a Progressive Display
This was the most visually obvious bug and has a single definitive fix.
Interlaced video (I-scan) encodes each frame as two fields — odd lines first, even lines second — interleaved at double the nominal frame rate. CRT displays handle this naturally because they scan physically line by line. Progressive LCD panels do not; they draw the entire frame at once. When an interlaced signal hits a progressive display, the two fields of each frame arrive slightly out of time and are composited into a single image with horizontal comb-tooth artifacts: the “strips.”
The E370 has a menu setting that controls what signal it outputs on the video interface:
- I-scan — interlaced output → horizontal strips on a progressive monitor
- P-scan — progressive output → no strips, full-resolution frames
Player setting: General → Video Output → P-scan
This is the only fix for the strips. No amount of encoding flags will compensate for an interlaced output setting on the player side.
On the encoder side, we reinforce the progressive intent:
-top 0 # progressive frame order flag in bitstream
-flags +mv4+aic # advanced MPEG-4 motion vectors + advanced intra coding
The filter chain also runs setsar=1 (square pixels, 1:1 sample aspect ratio) so the player doesn’t try to apply any display aspect ratio correction that could reintroduce scaling artifacts.
The Flicker Bugs (There Were Four)
After fixing the strips, I was left with intermittent flicker on playback. It manifested differently in different scenes and took several versions to fully diagnose. Here’s the complete taxonomy.
Flicker Bug 1: B-Frames and Buffer Sync Loss
MPEG-4 B-frames (bidirectional predictive frames) reference both a past frame and a future frame. The decoder must buffer the future frame before it can decode the B-frame — which means it holds more state in memory simultaneously than a pure I/P stream.
The E370’s MPEG-4 decoder has a fixed, relatively small decode buffer. On complex scenes with high bitrate + B-frames, the buffer fills and the decoder loses sync with the bitstream. The result is inter-frame flickering: the image stutters or alternates between two states because the decoder is oscillating between “in sync” and “buffer overrun.”
Fix: BFRAMES=0 — no B-frames. The encode uses only I-frames and P-frames, which the decoder can handle with minimal buffering.
BFRAMES = '0' # was 2
Flicker Bug 2: Trellis Quantization Bitstream Incompatibility
Trellis quantization is an encoder optimization that searches for the globally optimal quantization coefficients for each macroblock. It produces more efficient bitstreams at the cost of encode time. Unfortunately it also produces coefficient patterns that some older MPEG-4 decoders misparse.
The E370’s decoder appears to be one of them. With TRELLIS=1, there was intermittent macroblock corruption — random blocks of the image would briefly display incorrect values, appearing as brief flicker or noise.
Fix: TRELLIS=0
TRELLIS = '0' # was 1
Flicker Bug 3: Long GOP Causing Decoder Buffer Overrun
The GOP (Group of Pictures) defines the keyframe interval. A larger GOP means fewer I-frames, smaller file size, but longer seek times and more decoder state to maintain between keyframes.
The original configuration used GOP=300 — a 10-second keyframe interval at 29.97 fps. That is far too long for the E370’s decoder to track without accumulating small prediction errors. Over a 10-second span, delta errors between P-frames compound, and the decoder periodically re-syncs at the next I-frame, producing a visible flash or stutter.
Fix: GOP=60 — one keyframe every 2 seconds. This is longer than the typical GOP=30 used for streaming but much more decoder-friendly than 10 seconds, and it keeps file size reasonable.
GOP_SIZE = '60' # was 300
Combined with MBD=0 (simple macroblock decision mode) to avoid any advanced macroblock-level features the decoder might struggle with:
# -mbd 0 → simple macroblock decision, maximum E370 compat
Flicker Bug 4: Boxblur Luminance Oscillation in Background Fill
The encoder fills the letterbox/pillarbox areas (when source aspect ratio doesn’t match 720×480) with a blurred, dimmed version of the video frame rather than black bars. This looks better on a TV and avoids the hard edge that can cause ringing artifacts in the encoder.
The original blur implementation used boxblur=luma_radius=15:luma_power=3. The luma_power=3 parameter applies the box blur three times in sequence, which is an approximation of a Gaussian blur. The problem: at power=3, the repeated averaging introduces slight frame-to-frame luma variance in the background fill region. On a 60 Hz display, this sub-pixel luminance oscillation becomes perceptible as background flicker — visible as a faint pulsing in the blurred border area, especially on bright or uniform scenes.
Fix: Replace boxblur with avgblur=15 — a single-pass box average with no power iteration, no luma oscillation, and a visually smoother result.
Additionally, deflicker=size=3:mode=am (inter-frame luma smoothing) was added to the output stage. This filter computes a weighted average of the current frame’s luminance against its neighbours, suppressing any residual frame-to-frame luma variance before it reaches the encoder.
avgblur=15,
deflicker=size=3:mode=am
The Full FFmpeg Filter Chain
Putting all of that together, the complete filter_complex applied per segment looks like this:
[0:v]
bwdif=mode=0:parity=-1:deint=0, ← deinterlace if source is interlaced
minterpolate=fps=30000/1001:... ← FPS conversion to 29.97 (if needed)
hqdn3d=2:2:3:3, ← conservative denoise before scale
scale=720:-2:flags=lanczos ← scale to 720×auto, preserve AR
[scaled];
[scaled]split=2[fg][bg_src];
[bg_src]
scale=720:480:flags=bilinear,
avgblur=15, ← stable single-pass blur (no oscillation)
eq=brightness=-0.2:saturation=0.6 ← dim and desaturate the bg fill
[bg];
[bg][fg]overlay=(W-w)/2:(H-h)/2[filled]; ← center content over blurred bg
[filled]
deflicker=size=3:mode=am, ← inter-frame luma smoothing
unsharp=3:3:0.5:0:0:0, ← compensate for LCD softening
eq=contrast=1.05:saturation=1.08:gamma=0.95, ← TV display compensation
setsar=1 ← enforce 1:1 sample aspect ratio
[out]
The deinterlace and FPS steps are conditional — bwdif is only applied if the source’s field order is not already progressive, and minterpolate is only used when the source frame rate differs from 29.97 by more than 5%. For a 24 fps film source, for example, the simpler fps=30000/1001 filter is used instead.
Architecture: Parallel Segment Conversion
Encoding MPEG-4 is CPU-intensive. A single-threaded encode of a 90-minute video on a typical Colab instance takes 30+ minutes. The pipeline uses a segment-parallel approach instead:
-
Keyframe-accurate split — the source is split into 60-second segments at keyframe boundaries using stream copy (no re-encode, zero quality loss). Splitting at keyframes ensures each segment starts with a clean I-frame so they can be encoded independently.
-
Parallel conversion — all segments are converted simultaneously using
ProcessPoolExecutorwith one worker per CPU core. Each worker runs a separateffmpegprocess withTHREADS_PER_WORKER=2internal threads, for a total thread count of2 × CPU_COUNT. -
RAM disk — if
/dev/shmhas more than 1 GB free (typical on Colab), segments and converted files are stored there instead of on the SSD. This eliminates I/O as a bottleneck. -
Lossless merge — after all segments complete,
ffmpeg -f concat -c copystitches them back into a single AVI. Stream copy means no re-encode and no generation loss at the join points.
USE_RAM = shutil.disk_usage('/dev/shm').free > 1024**3
BASE = Path('/dev/shm/dvd') if USE_RAM else Path('/content/dvd')
On a 4-core Colab instance, a 30-minute video converts in roughly 4 minutes. On an 8-core machine, closer to 2.
Adaptive Bitrate
MPEG-4 (XviD/DivX-era) requires roughly 3.5× the bitrate of H.264 to achieve equivalent visual quality. Modern YouTube videos are encoded in H.264 or AV1, so a direct bitrate copy would produce a noticeably lower-quality encode.
The target bitrate is computed as:
TARGET_KBPS = min(int(src_kbps * 3.5), DEVICE_MAX_KBPS)
Where DEVICE_MAX_KBPS = 3800 — the practical ceiling for reliable playback on the E370. The buffer and max rate are derived from this:
VIDEO_BITRATE = f'{TARGET_KBPS}k'
MAX_BR = f'{min(TARGET_KBPS + 200, 4000)}k'
BUF_SIZE = f'{TARGET_KBPS * 2}k'
The source bitrate floor is clamped to 300 kbps to handle badly probed or variable-bitrate sources gracefully.
The Standalone Script
The Jupyter notebook is convenient for interactive use in Google Colab, but for local use or scripted automation the same pipeline is available as a standalone Python script with a full argparse CLI.
# Basic usage — download and convert
python dvd_converter.py "https://www.youtube.com/watch?v=..."
# Use a local file instead
python dvd_converter.py --input my_video.mp4
# Override output directory and filename suffix
python dvd_converter.py "https://..." --out-dir /media/usb --suffix _E370
# Tune the encoder
python dvd_converter.py "https://..." --gop 30 --bframes 0 --max-kbps 3500
# Skip deinterlace + deflicker for a clean progressive source (faster)
python dvd_converter.py "https://..." --no-deflicker
# Dry run — print config and exit without processing
python dvd_converter.py "https://..." --dry-run
Notable CLI options:
| Flag | Default | Purpose |
|---|---|---|
--width / --height |
720×480 | Target resolution (NTSC standard) |
--max-kbps |
3800 | Bitrate ceiling for the E370 |
--bitrate-multiplier |
3.5 | MPEG-4 / H.264 quality compensation |
--fps |
30000/1001 |
Exact NTSC rational frame rate |
--gop |
60 | Keyframe interval (2 s @ 29.97) |
--bframes |
0 | B-frame count (0 = E370 safe) |
--trellis |
0 | Trellis quantization (off = E370 safe) |
--no-denoise |
— | Skip hqdn3d (faster, noisier) |
--no-deflicker |
— | Skip deflicker (faster, riskier) |
--workers |
CPU count | Parallel encode workers |
--keep-tmp |
— | Don’t delete segment files |
--skip-verify |
— | Skip post-encode ffprobe check |
--dry-run |
— | Print config only, no encoding |
Output Verification
After encoding, the pipeline probes the final file with ffprobe and checks every parameter that matters for E370 compatibility:
════════════════════════════════════════════════════════════════
OUTPUT VERIFICATION
════════════════════════════════════════════════════════════════
File : my_video_NTSC_E370.avi
Size : 842 MB
Duration : 0:28:14 (drift 0.03 s)
Codec : mpeg4 / XVID ← expect mpeg4/XVID
Resolution : 720×480 ← expect 720×480 NTSC
Field order : progressive ← expect progressive (P-scan = zero strips)
FPS : 29.97 ← expect 29.97
Bitrate : 3241 kbps ← target 3500k
Pixel fmt : yuv420p ← expect yuv420p
SAR : 1:1 ← expect 1:1
Audio : mp3 192kbps 44100Hz
════════════════════════════════════════════════════════════════
✅ All checks passed — ready for DVD player.
════════════════════════════════════════════════════════════════
Failures on any of XVID, 29.97 fps, yuv420p, progressive field order, or <2s duration drift produce explicit warnings so you know before copying to USB.
Player Settings Checklist
These settings on the DVD player itself are required. The encoder can’t fix a misconfigured player.
| Menu path | Value | Reason |
|---|---|---|
| Display → Video Output | NTSC | 60 Hz monitor native match |
| General → Video Output | P-scan | Eliminates horizontal strips |
| General → Black Level | ON | Correct black floor on LCD |
| Audio → Dolby Digital | PCM | Avoid lossy re-encode of audio |
| Audio → MPEG2 Output | PCM | Same |
| Audio → Dynamic Range | ON | Prevents clipping on compressed audio |
| Audio → PCM Downsamp | ON | Compatibility with stereo output |
USB Notes
The E370 reads FAT32 and exFAT USB drives. FAT32 has a 4 GB per-file limit. For videos longer than roughly 75 minutes at 3800 kbps, the output AVI will exceed 4 GB and requires an exFAT-formatted drive. The verification step warns when the output exceeds this threshold.
Dependencies
- Python 3.8+
- FFmpeg (with libmp3lame) —
apt-get install ffmpegorbrew install ffmpeg - yt-dlp —
pip install yt-dlp - aria2c (optional, for parallel fragment downloads) —
apt-get install aria2
On Google Colab, Cell 1 of the notebook installs everything automatically.
Files
convert_dvd_v10_final.ipynb— Google Colab notebook (interactive, auto-installs dependencies)standalone-dvd_converter.py— standalone CLI script (local use, no Colab required)
Summary of Fixes Across Versions
| Version | Problem | Fix |
|---|---|---|
| v1–v3 | Horizontal strips | P-scan setting on player |
| v4 | Wrong frame rate / judder | NTSC mode, 30000/1001 rational |
| v5 | B-frame flicker | BFRAMES=0 |
| v6 | Trellis bitstream incompatibility | TRELLIS=0 |
| v7 | Long GOP decoder desync | GOP=60 (2 s) |
| v8 | Macroblock artifacts | MBD=0 (simple mode) |
| v9 | Boxblur luma oscillation | avgblur + deflicker |
| v10 | Final integration + standalone | Parallel pipeline, adaptive bitrate, full CLI |
If you’re working with a different USB-playing DVD player, the same set of flags is a reasonable starting point — most of these compatibility constraints are common to that generation of MPEG-4 hardware decoders.