What You’ll Learn

  • The difference between HDR (HDR10/HLG) and SDR
  • Converting HDR to SDR using the zscale filter
  • Color space, color primaries, and transfer function conversion
  • YouTube/web-ready SDR output settings
  • Common errors and fixes

Verified: FFmpeg 8.1 on Windows — all 4 commands in this article were executed by the verification script and passed (2026-09-06). 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)

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:

  1. zscale=t=linear:npl=100 — Convert PQ to linear light
  2. format=gbrpf32le — Use 32-bit float for precision
  3. zscale=p=bt709 — Map BT.2020 primaries to BT.709
  4. tonemap=hable — Apply Hable tone mapping (compress highlights)
  5. zscale=t=bt709:m=bt709:r=tv — Apply BT.709 gamma, matrix, TV range
  6. format=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.


Frequently Asked Questions

What is the simplest HDR-to-SDR command?

ffmpeg -i hdr.mp4 -vf "zscale=transfer=linear,tonemap=hable,zscale=transfer=bt709,format=yuv420p" -c:v libx264 -crf 18 sdr.mp4 is the safest starter.

Why does my SDR output look washed out?

Tonemap operator is too aggressive (clip) or peak nits is wrong. Try tonemap=mobius:peak=1000 and ensure the input transfer is correctly detected.

Hable vs mobius vs reinhard — which tonemap?

Hable preserves highlights, mobius is more neutral, reinhard is the simplest but loses contrast. Try hable first; switch to mobius if highlights look wrong.

Do I need a GPU for HDR-to-SDR conversion?

No — zscale runs on CPU. Hardware accelerated paths exist via hwupload_cuda,tonemap_npp on NVIDIA but require a custom FFmpeg build.

Will tonemapping affect file size?

Slightly larger — tonemapped video has more midtone detail. Compensate by raising CRF by 1–2 for similar size.



Verified: FFmpeg 8.1 on Windows — all 4 commands in this article were executed by the verification script and passed (2026-09-06). Primary source: ffmpeg.org/ffmpeg-filters.html#zscale