Write Color Tags on Every Export
Players guess Rec.709 and gamma 2.2 when nclc and colr are missing, so grades shift after upload. Tag Rec.709, P3 and PQ, then inspect before you send.
Players that cannot find nclc or colr guess. The usual guess is Rec.709 primaries and a gamma-2.2 transfer. That guess is why a grade that looked right in the NLE shifts after upload. The pixels did not change in the way you think. The instruction for how to turn those pixels into light went missing, and the player invented one.
This is the video-container version of a stills problem already covered for images: why colours shift between screen and export is about P3 laptops, sRGB files and assign-versus-convert. Moving image files carry the same class of instruction in a different box. Write the box. Inspect it. Then send.
What nclc and colr actually store
They do not store a grade. They store three (sometimes four) indexes that tell a decoder which tables to use.
Apple's QuickTime File Format documents a colr atom whose colour parameter type is 'nclc' (non-constant luminance coding). Inside it sit three 16-bit indexes: primaries, transfer function, matrix. Apple's colour parameter atom is the primary source for the MOV layout. ISO Base Media File Format (the MP4 family) stores the same idea in a colr box of type 'nclx', with one extra flag: full_range_flag.
The indexes are not invented by Apple. They line up with the code points in MPEG / ITU colour-signalling tables (the same numbers you will see in H.273 and in ffprobe):
| Job | Index | Meaning |
|---|---|---|
| Primaries | 1 | ITU-R BT.709 (HD SDR) |
| Primaries | 9 | ITU-R BT.2020 |
| Primaries | 11 | DCI P3 (SMPTE RP 431-2) |
| Primaries | 12 | P3-D65 / Display P3 (SMPTE EG 432-1) |
| Transfer | 1 | BT.709 OETF |
| Transfer | 16 | PQ (SMPTE ST 2084) |
| Transfer | 18 | HLG, ARIB STD-B67 |
| Matrix | 1 | BT.709 |
| Matrix | 9 | BT.2020 non-constant luminance |
| Range (nclx) | 0 or 1 | narrow (studio) or full |
The string people quote as 1-1-1 is Rec.709 primaries, BT.709 transfer, BT.709 matrix. That is the correct tag for an HD SDR master whose pixels were actually graded in Rec.709. It is the wrong tag for a P3 grade and the wrong tag for a PQ master.
Two traps live in that table.
Transfer 1 is not "gamma 2.2." BT.709 specifies an OETF, not a display gamma of 2.2. Studio displays are specified by ITU-R BT.1886, whose reference EOTF is a 2.4-ish curve with a black offset. Web players that cannot see a tag often assume sRGB / 2.2. That mismatch is a real, visible lift or crush in the midtones. It is also why two browsers can disagree about the same untagged file.
Tagging is not converting. Writing 1-1-1 on a file whose pixels were encoded as P3 or as full-range 2.2 does not magically make them Rec.709. It tells the next player to interpret the existing numbers as Rec.709. If the numbers were not Rec.709, the picture shifts. Convert, then tag. Or tag what you actually encoded. Do not "fix" a shift by writing a more optimistic atom.
How to tag Rec.709, P3 and PQ
Decide the working space in finishing, encode into that space, then write tags that match. The video editor on Versely is an assembly surface, not a colour-management layer. Generated clips still need an explicit tag on the way out of the NLE or the encoder you actually deliver from.
HD SDR (the default brand master). Primaries 1, transfer 1, matrix 1, range narrow (legal). In FFmpeg language that is bt709 / bt709 / bt709 / tv. YouTube's recommended upload encoding still describes BT.709 as the SDR colour space. Instagram Reels encoding is a social encode on top of that world, not a different colour science you invent per platform.
P3. Primaries 12 if you mean P3-D65 (Display P3). Primaries 11 if you mean DCI P3 (theatre white). Those are not the same white point. Transfer is whatever you actually encoded (often a 2.2-ish or sRGB-style curve for Display P3 graphics, not PQ). Matrix depends on whether you stored RGB or Y′CbCr; do not copy 1-1-1 onto a P3 RGB file and call it done. If the deliverable is a Rec.709 master, convert P3 to BT.709, then tag 1-1-1. Shipping untagged P3 into a Rec.709 assumption is how brand orange becomes brick.
PQ HDR. Primaries 9 (BT.2020) in most HDR video masters, transfer 16 (ST 2084), matrix 9, range narrow unless your flavour says otherwise. The quoted tag is 9-16-9. HLG is 9-18-9. Do not tag PQ as 1-1-1. A player that obeys 1-1-1 will treat PQ as SDR and the picture will collapse.
FFmpeg can write the MOV/MP4 boxes without touching pixels, which is the right tool when the encode is already correct and only the labels are missing:
ffmpeg -i in.mov -c copy \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-color_range tv \
-movflags +write_colr \
out.mov
Swap the four colour flags for the space you actually have (bt2020 / smpte2084 / bt2020nc / tv for a typical PQ master). -c copy means you are not converting. If the picture was wrong, copying tags will not grade it. If the picture was right and the player was guessing, this is the whole fix.
Range belongs on the sheet as well as in the atom. Narrow 8-bit luma is 16–235; 10-bit is 64–940. A full-range file tagged as narrow, or the reverse, shifts the grade as surely as a wrong transfer.
Inspect the tags before you send
Do not trust the export dialog's summary line. Read the file.
ffprobe -hide_banner -show_streams -select_streams v:0 deliverable.mov
You want color_primaries, color_transfer (sometimes color_trc), color_space and color_range populated with named values, not unknown or missing. MediaInfo prints the same four in prose (colour_primaries, transfer_characteristics, matrix_coefficients, colour_range). If those fields are absent, the player will guess, and the guess will not be your NLE's viewer.
A file that says unknown after an export you thought was Rec.709 is not Rec.709. It is untagged. Retag it, or re-export with the colour-tag checkbox that your NLE hides under a format tab. Premiere, Resolve and compressor-style tools all have a place to set Rec.709 / Rec.2020 / PQ. The checkbox is easy to miss. The probe is not.
What you cannot use for this: Versely's video metadata inspector reads dimensions, duration and aspect ratio in the browser. It does not report frame rate, and it does not report nclc. Use it to catch a 720×1280 recode. Use ffprobe or MediaInfo to catch a missing colour box.
Credentials are a different atom. Sign, strip, survive is the C2PA path. Colour tags and Content Credentials both die in sloppy transcodes, which is why a metadata preservation clause belongs in the contract. The practical finishing step is the same: inspect the file you will actually upload, not the timeline that produced it.
A last check on a known display: play the tagged file in the player the client uses, next to the NLE viewer. If they match, the tags are doing their job. If they match in the NLE and diverge in QuickTime, a browser, or after upload, the tag is missing, wrong, or was stripped on ingest. Fix the file. Do not grade to the broken player.
FAQ
Does writing a tag convert the pixels?
No. A tag is an instruction for the next decoder. Convert when the working space and the delivery space differ. Tag what you encoded. Writing 1-1-1 on P3 numbers is an assign, and it will shift the picture.
What does 1-1-1 mean on a QuickTime inspector?
Primaries 1, transfer 1, matrix 1: Rec.709 / BT.709 / BT.709. Correct for an HD SDR legal-range master that really is Rec.709. Incorrect as a default you stamp on every export, including P3 graphics and PQ.
Can I retag without a re-encode?
Yes, when the pixels are already in that space. ffmpeg -c copy plus the colour flags and +write_colr rewrites the container labels. If the pixels are in the wrong space, a retag will make them look worse. Convert, then tag.
Why did the grade still shift after YouTube ingested it?
Platforms recode. Some honour colr/nclc on ingest and some ignore them, and the recode may write a different tag. You still send a correctly tagged master, because that is the file you can defend. If their recode strips the box, that is the metadata-survival problem, not a reason to ship untagged.