Building a YouTube-to-DVD Converter for the Samsung DVD-E370 (and Killing Every Flicker Bug Along the Way)

2026, May 26    

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:

  1. 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.

  2. Parallel conversion — all segments are converted simultaneously using ProcessPoolExecutor with one worker per CPU core. Each worker runs a separate ffmpeg process with THREADS_PER_WORKER=2 internal threads, for a total thread count of 2 × CPU_COUNT.

  3. RAM disk — if /dev/shm has 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.

  4. Lossless merge — after all segments complete, ffmpeg -f concat -c copy stitches 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 ffmpeg or brew install ffmpeg
  • yt-dlppip 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


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.