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

Tested with: FFmpeg 7.0
Platform: Windows / macOS / Linux


HDR vs SDR

PropertySDR (Standard Dynamic Range)HDR (High Dynamic Range)
Color spaceBT.709BT.2020
Transfer functionBT.709 (gamma)PQ (HDR10) / HLG
Bit depth8-bit10-bit
Luminance range100 nit1,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
AlgorithmCharacteristicsBest for
hableNatural highlight compression, good contrastGeneral use
reinhardSimple, neutral lookNatural footage
mobiusBalanced highlights and shadowsDocumentaries
clipFast, simple cutoffTesting

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.



Tested with ffmpeg 7.0 / Ubuntu 24.04
Primary source: ffmpeg.org/ffmpeg-filters.html#zscale