Converting HDR video to SDR is one of the most common—and most misunderstood—tasks in FFmpeg. You've probably tried something like this:
# This will look terrible — washed out and grey ffmpeg -i hdr_video.mp4 -c:v libx264 -crf 18 output_sdr.mp4
The result? Washed-out colors, crushed shadows, blown highlights—a video that looks worse than a VHS tape.[reference:0] This guide will show you exactly why that happens and how to fix it.
Key takeaways
- HDR uses BT.2020 primaries and PQ/HLG transfer—SDR uses BT.709 and gamma. You can't just re-encode; you must convert.
zscaleis the filter for color space conversion (primaries, matrix, transfer).tonemapcompresses HDR brightness into SDR range—without it, you get washed-out greys.- Choose the right tonemap algorithm:
hable(best detail),reinhard(bright),mobius(balanced),bt.2390(ITU standard). - Always set output metadata with
-color_trc,-color_primaries, and-colorspace.
The Gap: Raw Parameters vs. Cohesive Guide
The FFmpeg documentation for zscale and colorspace filters lists raw parameters—primaries, matrix, transfer, range—but fails to provide a cohesive guide on how to properly tone-map HDR10 or Dolby Vision video down to SDR without ending up with a washed-out, grey image.[reference:1]
You'll find scattered forum posts with commands that "sort of work," but they rarely explain why they work or how to adapt them to your specific source. This guide bridges that gap.
The Symptom: Washed-Out, Grey Images
The main reason HDR videos look washed out on SDR screens is improper or missing tone mapping during conversion.[reference:2] HDR footage uses wider color gamuts such as BT.2020 and higher brightness levels, while SDR is based on Rec.709 with a more limited range.[reference:3]
Without tone mapping, this difference is not correctly compressed. Bright areas become overexposed, mid-tones lose depth, and the entire image appears flat and desaturated.[reference:4]
Why You Can't Just Re-encode HDR Video
HDR video uses three things that SDR displays can't interpret correctly[reference:5]:
| Property | HDR (typical) | SDR |
|---|---|---|
| Transfer function | PQ (SMPTE ST 2084) or HLG | BT.709 gamma (~2.4) |
| Color primaries | BT.2020 (wide gamut) | BT.709 (standard gamut) |
| Bit depth | 10-bit | 8-bit |
| Peak brightness | 1,000–10,000 nits | ~100 nits |
Simply changing the container or codec doesn't convert these properties.[reference:6] You need a tonemapping step that mathematically remaps the wide brightness range into the SDR range while preserving as much visual detail as possible—plus a color space conversion from BT.2020 to BT.709.[reference:7]
Check Your Input with ffprobe
Before converting, verify that your source is actually HDR and identify its characteristics[reference:8]:
# Check HDR metadata ffprobe -v quiet -show_streams -select_streams v:0 input_hdr.mp4 2>&1 | grep -E "color_|pix_fmt"
You should see something like[reference:9]:
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020
smpte2084→ HDR10/HDR10+ (PQ transfer)[reference:10]arib-std-b67→ HLG[reference:11]bt2020nc→ BT.2020 non-constant luminance[reference:12]
The zscale + tonemap CPU Pipeline
The most reliable CPU-based pipeline uses zscale for color space conversion and tonemap for brightness compression.[reference:13] Here's the quickest reliable command[reference:14]:
# Full HDR to SDR conversion with zscale + tonemap ffmpeg -i input_hdr.mp4 \ -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \ -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4
Let's break down each step[reference:15]:
zscale=t=linear:npl=100— Convert from PQ/HLG to linear light.npl=100sets the nominal peak luminance to 100 nits (SDR reference).[reference:16]format=gbrpf32le— Convert to floating-point RGB for accurate tone mapping.[reference:17]zscale=p=bt709— Convert color primaries from BT.2020 to BT.709.[reference:18]tonemap=hable:desat=0— Apply the Hable tone-mapping algorithm with no desaturation.[reference:19]zscale=t=bt709:m=bt709:r=tv— Convert transfer to BT.709 gamma, matrix to BT.709, and range to TV (limited).[reference:20]format=yuv420p— Convert back to 8-bit YUV for encoding.[reference:21]
Note: The zscale filter requires FFmpeg to be built with --enable-libzimg.[reference:22][reference:23] Most pre-built binaries don't include it, so you may need to compile from source or use a build that includes it.
Tonemapping Algorithms: Which One to Use
The tonemap filter supports several algorithms[reference:24]. Here's how to choose[reference:25][reference:26]:
| Algorithm | Best for | Trade-offs |
|---|---|---|
hable | Preserving both dark and bright details | Slightly darkens everything; best detail preservation[reference:27] |
reinhard | Bright, punchy output | Less detail in highlights; "safe" default[reference:28] |
mobius | Balance between accuracy and detail | User-adjustable threshold; default 0.3 is more accurate than reinhard[reference:29] |
bt.2390 | ITU standard (BT.2390) | Follows the official ITU recommendation for HDR to SDR |
gamma | Simple gamma adjustment | Can cause color shifts; not recommended[reference:30] |
For most use cases, hable is the best choice—it preserves detail in both shadows and highlights better than reinhard.[reference:31]
GPU Acceleration with libplacebo
If you have a GPU and want faster conversion, the libplacebo filter provides hardware-accelerated tone mapping.[reference:32] It's significantly faster than the CPU-based zscale+tonemap pipeline, especially for 4K60 HDR video.[reference:33]
# GPU-accelerated HDR to SDR with libplacebo ffmpeg -i input_hdr.mp4 \ -vf "libplacebo=tonemapping=hable:colorspace=bt709:color_primaries=bt709:color_trc=bt709" \ -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4
Note: libplacebo requires FFmpeg to be built with Vulkan support. It's available in recent FFmpeg builds but may not be included in all distributions.
Setting HDR Metadata Correctly
When creating HDR output (rather than converting to SDR), you must set the correct metadata flags[reference:34]:
# Create HDR10 output with proper metadata ffmpeg -i input.mp4 \ -vf "zscale=primaries=bt2020:matrix=bt2020_ncl:transfer=smpte2084" \ -color_trc smpte2084 -color_primaries bt2020 -colorspace bt2020nc \ -pix_fmt yuv420p10le -c:v libx265 -crf 18 output_hdr10.mp4
Key metadata flags[reference:35]:
-color_trc smpte2084— Tags the transfer function as PQ (HDR10). Usearib-std-b67for HLG.[reference:36]-color_primaries bt2020— Tags the color primaries as Rec.2020.[reference:37]-colorspace bt2020nc— Tags the color space as Rec.2020 non-constant luminance.[reference:38]-color_range 2— Tags the channel range as full.[reference:39]
Practical Example: HDR10 to SDR
Let's put it all together with a real-world example. Suppose you have an HDR10 video (PQ, BT.2020) and you want to convert it to SDR (BT.709, gamma) for playback on standard displays.
# Convert HDR10 (PQ/BT.2020) to SDR (BT.709) with hable tone mapping ffmpeg -i hdr10_video.mp4 \ -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \ -c:v libx264 -crf 18 -preset slow -c:a copy -movflags +faststart sdr_output.mp4
If your source is HLG instead of PQ, use transfer=arib-std-b67 instead of transfer=smpte2084 in the zscale chain.[reference:40]
Test in the FFmpegLab IDE
The FFmpegLab editor lets you preview color space conversions before you commit to a full encode. Upload an HDR video, adjust tone mapping parameters, and see the result in real time—then export the exact command you need.
Try it below. Load an HDR video, select your target color space (BT.709 for SDR), choose a tone mapping algorithm, and watch the preview update instantly. When you're happy, copy the generated FFmpeg command and run it locally—fully offline, nothing uploaded.
# Interactive HDR to SDR conversion with tone mapping preview ffmpeg -i hdr_input.mp4 \ -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \ -c:v libx264 -crf 18 -preset slow -c:a copy tonemapped_output.mp4
Frequently Asked Questions (FAQ)
Why does my HDR video look washed out after conversion?
The main reason is missing or improper tone mapping. HDR uses BT.2020 primaries and PQ/HLG transfer, while SDR uses BT.709 and gamma. Simply re-encoding without converting these properties results in washed-out, grey images.[reference:41] You need to use zscale for color space conversion and tonemap for brightness compression.
What's the difference between zscale and colorspace filters?
zscale is more powerful and supports HDR conversion with proper handling of primaries, matrix, and transfer characteristics.[reference:42] colorspace is simpler but may not handle all HDR scenarios correctly.[reference:43] For HDR to SDR conversion, zscale is the recommended choice.
Which tone mapping algorithm should I use?
hable is generally the best choice—it preserves detail in both shadows and highlights better than reinhard.[reference:44] Use mobius if you want a balance between accuracy and detail preservation.[reference:45] reinhard is a safe default that produces bright, punchy output but may lose some highlight detail.[reference:46]
How do I convert HLG (Hybrid Log-Gamma) HDR to SDR?
Use transfer=arib-std-b67 instead of transfer=smpte2084 in the zscale chain.[reference:47] The rest of the pipeline (primaries conversion, tone mapping) remains the same. Check your input with ffprobe—if you see color_transfer=arib-std-b67, your source is HLG.[reference:48]
Does FFmpeg support Dolby Vision conversion?
FFmpeg can read Dolby Vision metadata but full conversion support is limited. The zscale+tonemap pipeline works for the base layer, but Dolby Vision's dynamic metadata (which adjusts tone mapping per scene) is not fully supported in open-source FFmpeg.[reference:49] For best results with Dolby Vision, use professional tools or the libplacebo filter which has better HDR handling.
Final Word
Converting HDR to SDR doesn't have to be a guessing game. With zscale for color space conversion and tonemap for brightness compression, you can produce SDR video that looks good—not washed out, not grey, not overly dark.
The key is understanding what you're converting (PQ or HLG? BT.2020 or BT.709?) and how to map it. Use ffprobe to check your input, choose the right tone mapping algorithm for your content, and always set the correct output metadata.
Next time you're faced with an HDR video that needs to play on an SDR screen, you'll know exactly what to do.