halfsize

halfsize / HDR

HDR video that turns grey after compression

The clip looked vivid in Photos. You compressed it, and now the sky is white, the skin is grey, and everything looks like it was shot through a sock. That is not a bitrate problem, and compressing harder will not fix it.

What the iPhone actually records

From the iPhone 12 on, the default camera records HLG HDR: 10-bit pixels, BT.2020 primaries, and a transfer curve named arib-std-b67. Android flagships often record HLG or PQ (smpte2084, the HDR10 curve). The numbers in the file are not scene colors; they are HDR code values that need a known display to make sense.

Two ways a compressor gets it wrong

Either way the file is valid. It plays. It just looks wrong, and the person who compressed it usually blames the bitrate.

The tell: if raising the bitrate does not bring the color back, it was never a bitrate problem.

The fix is a conversion, not a checkbox

To put HDR into a normal 8-bit SDR file you have to go through linear light, compress the dynamic range, and re-tag the result as BT.709. With FFmpeg's zscale and tonemap filters that is one chain:

zscale=tin=arib-std-b67:min=bt2020nc:pin=bt2020:t=linear:npl=100,
format=gbrpf32le,
zscale=p=bt709,
tonemap=tonemap=hable:desat=0,
zscale=t=bt709:m=bt709:r=tv,
format=yuv420p

npl is the nominal peak luminance. HLG is relative, so 100 works; PQ is graded against a real peak, usually 1000 nits. For a PQ source the first line becomes tin=smpte2084:...:npl=1000.

The order matters. Tone mapping has to happen before scaling, on the source transfer curve. Scale first and you are resampling HDR code values, then guessing.

What halfsize does since 0.1.9

It reads color_transfer during the probe, applies the matching chain before the scale filter, and tags the output bt709. --dry-run shows it:

$ npx halfsize IMG_2041.MOV --to 20mb --dry-run

  IMG_2041.MOV
  214 MB  38s  3840x2160  30.00 fps  hevc

  target     20.0 MB
  plan       H.264  1280x720  4.18 Mbps  audio 128 kbps  2-pass
  scale      3840x2160 → 1280x720
  hdr        HLG → SDR BT.709

The quality check needs the same treatment. Comparing an SDR output against an untouched HDR source scores the transfer curve as “compression loss” and produces a useless VMAF number, so the reference frame is tone-mapped too before SSIM and VMAF are calculated.

Check your own file

$ ffprobe -v error -select_streams v:0 \
  -show_entries stream=color_transfer,color_primaries,color_space,pix_fmt \
  -of default=nw=1 video.mov

color_transfer=arib-std-b67   # HLG
color_primaries=bt2020
color_space=bt2020nc
pix_fmt=yuv420p10le

If color_transfer is smpte2084, the file is PQ. If it is bt709, it is already SDR and none of this applies. A BT.2020 matrix with an SDR transfer is wide gamut, not HDR; re-encoding it is harmless.

“Can I keep HDR?”

Not with halfsize today. The output is SDR BT.709, which is what Discord, WhatsApp, WeChat, browsers, email clients and most video players actually display. Keeping HDR means a different pipeline and a viewer that supports it; for “send this to a group chat”, SDR is the version that looks right everywhere.

FAQ

Why does my iPhone video look grey after compressing?

The source is HLG HDR. The compressor made 8-bit pixels without converting the transfer curve, so the file either keeps HDR tags on SDR pixels or loses the metadata players need. Both look flat.

Does a higher bitrate fix it?

No. Bitrate changes detail, not color. A 50 Mbps export with the wrong curve is still grey.

Can I keep HDR in the compressed file?

Not with halfsize today. It converts to SDR BT.709, which is what most apps can display. Keeping HDR needs a separate pipeline.

How do I know if a file is HDR?

ffprobe and look at color_transfer: arib-std-b67 is HLG, smpte2084 is PQ.

Does tone mapping reduce quality?

It changes the look on purpose: HDR highlights are compressed into what an SDR screen can show. The alternative is not “full quality”, it is a grey file.

Related guides