Available
← all posts

CMCD Version 2 Changed the Shape, Not Just the Keys

I spent an afternoon designing the wrong tool. The right one was much smaller: there is nowhere to paste a Common Media Client Data payload and read what it says. Along the way I found that version 2 quietly rewrote the format of the keys you already parse.

Two payload shapes compared: version 1 sends one encoded bitrate, version 2 sends an inner list carrying a separate value for video and audio

I spent an afternoon designing the wrong tool.

The plan was tcpdump for video telemetry. Read a day of CloudFront logs, pull out what each player said about itself, stitch the requests back into sessions, and flag the ones where the buffer hit zero. I had the package layout. I had the detector thresholds. I was about four hours in when somebody asked what I was actually trying to prove.

That question is expensive, and it should be asked earlier than it usually is.

The honest answer was: whether a stall was the network or the client. Which is a real question, and worth a real tool. It is also a tool that needs raw log access, a pipeline, and somebody with a week. Meanwhile there was a much smaller thing missing, and I had walked straight past it.

You cannot read one payload.

What a player is telling your CDN

Common Media Client Data is a small standard, CTA-5004. The idea fits in a sentence: the video player attaches what it knows about itself to every media request it makes, so the delivery side can see it.

A player asks for a segment. Normally the edge log records that a file was fetched, how big it was, and whether the cache had it. With this, the player adds a short list of key and value pairs to the same request:

bl=21300,br=3200,cid="movie-42",mtp=25400,ot=v,sf=h,sid="6e2fb550-c457",st=v

Buffer holds 21.3 seconds. Fetching a 3200 kbps video segment. Measuring 25400 kbps of throughput. HLS, video on demand, and every request in this playback carries the same session id.

One request, two stories

The left half is yours to fix. The right half tells you it needed fixing.

That last one is the whole trick. Your quality of experience vendor knows a viewer stalled at 20:14. Your delivery network dashboard knows the cache hit ratio was 94 per cent that hour. Neither can tell you that this stall followed that cache miss on that segment from that edge, because the two systems describe the same second and share no key. The session id is the key.

Your analytics knows the viewer suffered. Only the edge log knows the segment was a cache miss served from a cold edge in Frankfurt, and until now nothing could tell you they were the same moment.

The part I did not expect

Version 2 was published in February 2026 as CTA-5004-A. I assumed it added keys. It does, 32 of them, which is a lot.

It also changed the shape of the ones you already parse.

v1   br=3200
v2   br=(5000;v 320;a)

That is a structured field inner list, with a token naming the object type each value belongs to. Five thousand kilobits for video, three hundred and twenty for audio, in one key. It is a genuinely better design, because a player fetching audio and video separately was always squeezing two facts into one number.

The bit that will catch people is the next sentence in the specification. The list form is required even when there is only one value. So br=(3200), not br=3200, the moment you declare version 2.

Four more version 1 keys moved the same way: buffer length, measured throughput, next object request and top bitrate. Sixteen keys use the list form in total. The next range request key was deleted outright. The version key, which version 1 told you to omit when it was 1, is now required.

So a parser written against version 1 does not degrade gracefully against version 2. It reads br=(5000;v 320;a) and gets a string that is not a number, or worse, silently keeps the wrong thing. There are 50 reserved keys across the two versions and no way to eyeball which shape you are looking at.

That is when the small tool became obvious.

What I built instead

A decoder. Paste a payload, a whole URL out of a log line, or a block of the four headers, and it tells you what each key means, which header carries it, and what is wrong with it. It is jwt.io for video delivery telemetry, and it lives at juliantellez.com/cmcd.

The design fell out of the standard rather than out of me. The specification already sorts every key into four headers by how often its value changes, from CMCD-Request, which moves on every request, to CMCD-Session, which is fixed for the whole playback. So the decoder colours the four groups along that axis, hottest for the most volatile. That is the same device jwt.io uses to split a token into three parts, except the standard did the categorising.

It runs entirely in your browser. The payload lives in the link fragment, which a browser never sends to a server, so a decoded payload is also a link you can send to a colleague. I did that for a practical reason rather than a principled one: nobody at a broadcaster is going to paste production log lines into a stranger's website, and no amount of promising would fix that. Making the upload impossible is easier than making it trustworthy.

Underneath sits an open source parser, cmcd-parser, with no dependencies. It never throws. Against real logs that matters more than it sounds, because one malformed line should not cost you the other ten thousand, so every problem is collected as an error or a warning and every key that did parse comes back anyway.

parse('sid=abc,ot=zzz,br=3200').errors
// sid: a string, so its value must be wrapped in double quotes
// ot:  "zzz" is not a valid value. Allowed: m, a, v, av, i, c, tt, k, o
 
parse('sid=abc,ot=zzz,br=3200').data.br  // still here

Testing it against the standard rather than against myself

The trap with writing a parser for a specification is that you test it against your own reading of the specification. It passes, and it is confidently wrong in exactly the places you misread.

Section 8 of the document prints 66 example payloads. Most appear three times: once as a raw payload, once as a query argument, once as a set of headers. So the test suite extracts them from the published document with a script and asserts that all three forms decode to identical data, and that serializing that data back reproduces the text the standard printed, including which header each key belongs in.

That is 166 tests, and it caught things I had wrong. It also has one property I now insist on: it fails if the fixtures do not load. A suite that discovers no cases reports success in exactly the same green as one that passed everything.

Then I broke the parser on purpose, twice, to check the suite would notice. Ignoring quotes so a comma inside a session id splits the pair: three failures. Dropping the object type tokens from inner lists: three failures. Restored: 166 passing. Green means something now.

What I skipped

The thing I originally set out to build, the session reconstruction and the stall attribution, is not built. That is the tool that answers "is it us or them", and it is still the more valuable one. This is the front door.

I have not run the parser against a production log at scale. It is tested against the specification's own examples, which is a different claim, and a weaker one. If you have a few thousand real lines and it falls over, that is the bug report I want most.

Event mode, where version 2 sends batched reports to a collector instead of riding the media request, parses key by key but its JSON body form is best effort. Batch reports, one payload per line, are not handled at all.

And npm refused to let me have the name cmcd, on the grounds that it is too close to cmdk, cac, crc and cuid. So the package is cmcd-parser and the repository keeps the short name. I am telling you because it took me two attempts to work out that the registry was not broken.

If you actually run video

Two questions decide whether any of this is useful to you. Is Common Media Client Data turned on in your players today, and do you have raw edge logs, or only the vendor's dashboard? If the answer to the first is no, the work is enabling it, not this. If the answer to the second is only the dashboard, none of it helps.

If both are yes, start by pasting one line into the decoder and seeing whether it says what you expected. That is a five second check that has told me more than the four hours of architecture did.

The parser is on GitHub. Tell me where it is wrong, and send me the payload that broke it.

ShareXLinkedIn

Questions about the build, or building something similar? Say hello. I read every message.

Comments

Sign in with GitHub to join the conversation.

Built from Scratch

A newsletter on video streaming infrastructure. No spam, unsubscribe anytime.