What You’ll Learn
- The difference between HDR (HDR10/HLG) and SDR
- Converting HDR to SDR using the
zscalefilter - Color space, color primaries, and transfer function conversion
- YouTube/web-ready SDR output settings
- Common errors and fixes
Tested with: FFmpeg 7.0
Platform: Windows / macOS / Linux
HDR vs SDR
| Property | SDR (Standard Dynamic Range) | HDR (High Dynamic Range) |
|---|---|---|
| Color space | BT.709 | BT.2020 |
| Transfer function | BT.709 (gamma) | PQ (HDR10) / HLG |
| Bit depth | 8-bit | 10-bit |
| Luminance range | 100 nit | 1,000–10,000 nit |
Playing HDR video on an SDR display without conversion results in washed-out or color-shifted video. Proper tone mapping produces a natural-looking SDR output.
Step 1 — Inspect the Input File
ffprobe -v quiet -select_streams v:0 \
-show_entries stream=color_space,color_transfer,color_primaries,pix_fmt \
-of default=noprint_wrappers=1 input.mp4
HDR10 example output:
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020
HLG example output:
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=arib-std-b67
color_primaries=bt2020
Basic Command — HDR10 → SDR (BT.709)
Using zscale (Recommended)
The zscale filter (requires libzimg) produces the highest quality conversion:
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy \
output_sdr.mp4
What each step does:
zscale=t=linear:npl=100— Convert PQ to linear lightformat=gbrpf32le— Use 32-bit float for precisionzscale=p=bt709— Map BT.2020 primaries to BT.709tonemap=hable— Apply Hable tone mapping (compress highlights)zscale=t=bt709:m=bt709:r=tv— Apply BT.709 gamma, matrix, TV rangeformat=yuv420p— Convert to 8-bit YUV 4:2:0
Tone Mapping Algorithm Comparison
# Hable (film look, recommended)
tonemap=hable
# Reinhard (simple, neutral)
tonemap=reinhard
# Mobius (balanced highlights/shadows)
tonemap=mobius
# Clip (fast, may clip highlights)
tonemap=clip
| Algorithm | Characteristics | Best for |
|---|---|---|
| hable | Natural highlight compression, good contrast | General use |
| reinhard | Simple, neutral look | Natural footage |
| mobius | Balanced highlights and shadows | Documentaries |
| clip | Fast, simple cutoff | Testing |
HLG → SDR Conversion
ffmpeg -i input_hlg.mp4 \
-vf "zscale=t=linear,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy \
output_sdr.mp4
For HLG, omit npl=100 (HLG uses relative luminance).
Without zscale — Using the colorspace Filter
If libzimg is not available:
ffmpeg -i input_hdr.mp4 \
-vf "colorspace=bt709:iall=bt2020:fast=1,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy \
output_sdr.mp4
Note: The colorspace filter has lower precision than zscale.
YouTube Upload Settings
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-pix_fmt yuv420p \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-c:a aac -b:a 256k \
output_youtube_sdr.mp4
The three metadata flags (-color_primaries, -color_trc, -colorspace) tell players and platforms to interpret the video as SDR BT.709.
Batch Conversion Script
#!/bin/bash
for f in *.mp4; do
ffmpeg -i "$f" \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-pix_fmt yuv420p \
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
-c:a copy \
"sdr_${f}"
done
Troubleshooting
No such filter: 'zscale'
FFmpeg was built without libzimg. Install a full-featured FFmpeg build:
# macOS
brew install ffmpeg # Homebrew build includes libzimg
# Ubuntu
sudo apt install ffmpeg
Video appears green or pink
The color space parameters are wrong. Use ffprobe to check color_transfer. Use t=arib-std-b67 for HLG and t=smpte2084 for HDR10.
Video is too dark / blown out
Adjust the npl (nominal peak luminance) value or switch tone mapping algorithm:
# Brighter output: increase npl
-vf "zscale=t=linear:npl=203,..."
Pixel format error with libx264
Always append format=yuv420p at the end of the filter chain when outputting to 8-bit H.264.
Related Articles
Tested with ffmpeg 7.0 / Ubuntu 24.04
Primary source: ffmpeg.org/ffmpeg-filters.html#zscale