#How a video call works
AddedEvery video call, whatever product it runs in, is two conversations happening at once. The first is signalling: the endpoints find each other, say who is calling whom, agree which codecs, resolutions and encryption to use, and later agree to hang up. The second is media: a continuous stream of small packets carrying compressed audio, video and shared content, tens to hundreds of them every second, usually over UDP. Signalling is a handful of messages per call and it must be reliable; media is thousands of packets per minute and it must be fast, because a packet that arrives late is as useless as one that never arrives.
A good mental model is booking a table versus eating the meal. Signalling is the phone call to the restaurant: who you are, how many people, what time, any allergies, and later "we'll pay now". It can go through a receptionist, it can be slow, it can be repeated. Media is the meal itself: it happens directly at the table, continuously, and nobody routes your food back through the receptionist. Most of the confusion in video troubleshooting ("the call connected but there's no video", "it works in the office but not from home") comes from forgetting that these are two separate paths that can fail separately.
The two halves are joined by one document: the session description, written in SDP (Session Description Protocol, RFC 8866, 2021). The caller's SDP is an offer ("I can send and receive audio in these codecs, video in these, send it to this address and port"); the other side replies with an answer that picks from it. That offer/answer exchange (RFC 3264, 2002) is the same idea in SIP and in WebRTC; what differs is how the SDP is carried and how the endpoints find a network path.
The model: one SIP call, end to end
This model runs a whole SIP call between two room systems through one proxy. It builds a real-format SIP INVITE with an SDP offer, lets the callee answer with the codecs it supports, and then traces every message, so you can see which ones the proxy handles and where the media actually goes.
// A SIP call between two room systems through one proxy (no Record-Route).
// Signalling goes through the proxy; the media address comes from the SDP, so RTP goes direct.
const rooms = {
"room-a": { ip: "198.51.100.10", audio: 49170, video: 51372, codecs: ["G722/8000", "PCMU/8000", "PCMA/8000"] },
"room-b": { ip: "203.0.113.20", audio: 5004, video: 5006, codecs: ["PCMU/8000", "G722/8000"] },
};
const staticPt = { "PCMU/8000": 0, "PCMA/8000": 8, "G722/8000": 9 }; // RFC 3551 static payload types
const sdp = (name, codecs) => {
const r = rooms[name];
return [
"v=0", `o=${name} 2890844526 2890844526 IN IP4 ${r.ip}`, "s=-", `c=IN IP4 ${r.ip}`, "t=0 0",
`m=audio ${r.audio} RTP/AVP ${codecs.map((c) => staticPt[c]).join(" ")}`,
...codecs.map((c) => `a=rtpmap:${staticPt[c]} ${c}`),
`m=video ${r.video} RTP/AVP 109`, "a=rtpmap:109 H264/90000", "",
].join("\r\n");
};
const parse = (body) => {
const ip = body.match(/^c=IN IP4 (\S+)/m)[1];
const audio = body.match(/^m=audio (\d+) \S+ ([\d ]+)/m);
const names = audio[2].split(" ").map((pt) => body.match(new RegExp(`^a=rtpmap:${pt} (\\S+)`, "m"))[1]);
return { ip, port: Number(audio[1]), codecs: names };
};
const offer = sdp("room-a", rooms["room-a"].codecs);
const invite = [
"INVITE sip:room-b@example.com SIP/2.0",
"Via: SIP/2.0/TLS 198.51.100.10:5061;branch=z9hG4bK776asdhds",
"Max-Forwards: 70",
"To: <sip:room-b@example.com>",
"From: <sip:room-a@example.com>;tag=1928301774",
"Call-ID: a84b4c76e66710@198.51.100.10",
"CSeq: 314159 INVITE",
"Contact: <sip:room-a@198.51.100.10:5061;transport=tls>",
"Content-Type: application/sdp",
`Content-Length: ${Buffer.byteLength(offer)}`,
"", offer,
].join("\r\n");
console.log(invite.split("\r\n").slice(0, 10).join("\n"));
console.log(`(${Buffer.byteLength(offer)}-byte SDP offer follows)\n`);
// The answerer keeps only the codecs it supports, in the offer's order (RFC 3264 lets it pick a subset).
const seenOffer = parse(offer);
const chosen = seenOffer.codecs.filter((c) => rooms["room-b"].codecs.includes(c));
const answer = sdp("room-b", chosen);
const trace = [];
const count = { signalling: 0, viaProxy: 0 };
const send = (from, to, what) => {
trace.push(`${String(trace.length + 1).padStart(2)} ${from.padEnd(6)} → ${to.padEnd(6)} ${what}`);
if (!what.startsWith("RTP")) count.signalling++;
if (from === "proxy" || to === "proxy") count.viaProxy++;
};
const fmt = (s) => { const p = parse(s); return `${p.ip}:${p.port} ${p.codecs.join(" ")}`; };
send("room-a", "proxy", `INVITE offer ${fmt(offer)}`);
send("proxy", "room-a", "100 Trying");
send("proxy", "room-b", "INVITE (proxy adds its own Via)");
send("room-b", "proxy", "180 Ringing");
send("proxy", "room-a", "180 Ringing");
send("room-b", "proxy", `200 OK answer ${fmt(answer)}`);
send("proxy", "room-a", "200 OK");
send("room-a", "room-b", "ACK sent to the Contact in the 200 OK (no Record-Route)");
const a = parse(offer), b = parse(answer);
send("room-a", "room-b", `RTP ${a.ip}:${a.port} → ${b.ip}:${b.port} audio ${b.codecs[0]}`);
send("room-b", "room-a", `RTP ${b.ip}:${b.port} → ${a.ip}:${a.port} audio ${b.codecs[0]}`);
send("room-a", "room-b", "BYE (in-dialog request, also direct)");
send("room-b", "room-a", "200 OK");
console.log(trace.join("\n"));
console.log(`\nsignalling messages: ${count.signalling}, through the proxy: ${count.viaProxy}; RTP through the proxy: 0`);INVITE sip:room-b@example.com SIP/2.0
Via: SIP/2.0/TLS 198.51.100.10:5061;branch=z9hG4bK776asdhds
Max-Forwards: 70
To: <sip:room-b@example.com>
From: <sip:room-a@example.com>;tag=1928301774
Call-ID: a84b4c76e66710@198.51.100.10
CSeq: 314159 INVITE
Contact: <sip:room-a@198.51.100.10:5061;transport=tls>
Content-Type: application/sdp
Content-Length: 241
(241-byte SDP offer follows)
1 room-a → proxy INVITE offer 198.51.100.10:49170 G722/8000 PCMU/8000 PCMA/8000
2 proxy → room-a 100 Trying
3 proxy → room-b INVITE (proxy adds its own Via)
4 room-b → proxy 180 Ringing
5 proxy → room-a 180 Ringing
6 room-b → proxy 200 OK answer 203.0.113.20:5004 G722/8000 PCMU/8000
7 proxy → room-a 200 OK
8 room-a → room-b ACK sent to the Contact in the 200 OK (no Record-Route)
9 room-a → room-b RTP 198.51.100.10:49170 → 203.0.113.20:5004 audio G722/8000
10 room-b → room-a RTP 203.0.113.20:5004 → 198.51.100.10:49170 audio G722/8000
11 room-a → room-b BYE (in-dialog request, also direct)
12 room-b → room-a 200 OK
signalling messages: 10, through the proxy: 7; RTP through the proxy: 0- The
INVITEis a text message, HTTP-like by design (RFC 3261, 2002): a request line, then headers.Viarecords the path so responses can retrace it;FromandToname the parties by SIP URI;Call-IDplus theFromtag identify the dialogue;CSeqorders requests;Contactsays where Room A can be reached directly.Content-Length: 241is the size in bytes of the SDP body that follows the blank line. - The SDP offer (built by
sdp()) says "send audio to198.51.100.10:49170in payload types9 0 8" — G.722, PCMU (G.711 µ-law) and PCMA (G.711 A-law), whose numbers are fixed by RFC 3551 — "and video to port51372as H.264". Order is preference: G.722 first. - The proxy replies
100 Tryingat once (so Room A stops retransmitting) and forwards theINVITE, adding its ownVia. Room B rings (180) and answers200 OKwith its own SDP: it kept only the codecs it supports, in the offer's order, so the answer says G.722 then PCMU at203.0.113.20:5004. - Message 8 is the turning point: the
ACKgoes straight to Room B, to theContactfrom the200 OK, because this proxy didn't add aRecord-Routeheader asking to stay in the path. A proxy that needs to see the whole dialogue (for billing or policy) addsRecord-Route, and thenACKandBYEgo through it too. - Messages 9 and 10 are the media: RTP from one SDP address to the other, never via the proxy. The last line counts it: 10 signalling messages, 7 of them through the proxy, and no media. That's the design: a proxy can serve thousands of rooms because it only handles a few messages per call.
- If you changed Room B's codec list to
["PCMA/8000"], the answer would list only PCMA and the call would use it. If Room B supported none of the offered codecs, it would reject the audio stream (port0in the answer) or the whole call with488 Not Acceptable Here.
A codec (coder-decoder) compresses captured audio or video into a bitstream and expands it again at the far end. It's where most of the bandwidth decisions live: raw 1080p video at 30 frames per second is about 750 Mbit/s (1920 × 1080 pixels × 12 bits per pixel in 4:2:0 colour × 30 frames), and a conferencing codec squeezes that into 1–4 Mbit/s by sending mostly the differences between frames.
| Kind | Codec | Where you meet it |
|---|---|---|
| Audio | G.711 (PCMU/PCMA), 64 kbit/s, narrowband | the telephone network; every SIP device supports it as the fallback |
| Audio | G.722, 64 kbit/s, wideband | SIP desk phones and room systems; Chrome still offers it |
| Audio | Opus (RFC 6716, 2012), 6–510 kbit/s | WebRTC's mandatory codec (with G.711, RFC 7874); adapts its bitrate and has built-in loss protection |
| Audio | Siren family (Siren 7/14/22, LPR variants) | Polycom/Poly systems: the Poly REST API's sample statistics show "SirenLPR" |
| Video | H.264 / AVC | everywhere; WebRTC's other mandatory video codec (with VP8, RFC 7742) |
| Video | H.265 / HEVC | higher efficiency; Poly systems use it only in point-to-point SIP calls between Poly systems |
| Video | VP8, VP9, AV1 | browsers; AV1 gives the best compression at the highest CPU cost |
Chrome 156's own offer, captured for the next topic, lists Opus, RED, G.722, PCMU, PCMA, comfort noise and DTMF for audio, and VP8, several H.264 profiles, AV1, VP9 and H.265 for video. Poly's Video Mode Administrator Guide 4.7.0 (HP, July 2025) says its systems support H.264 in both H.323 and SIP calls and H.265 only "during point-to-point SIP calls" with other Poly systems; the guides I read don't list the audio codecs in one place.
There are three families of signalling you'll hear about on a video device team. Two are old, standards-based and built for interoperable hardware; one is built into every browser.
| SIP | H.323 | WebRTC | |
|---|---|---|---|
| From | IETF, RFC 3261 (2002) | ITU-T recommendation family (first version 1996) | W3C API + IETF protocols (overview: RFC 8825, 2021) |
| Messages | text, HTTP-like (INVITE, 200 OK, ACK, BYE) | binary ASN.1: H.225 call signalling and RAS, H.245 capability exchange | not specified: the app sends SDP any way it likes (WebSocket, HTTPS) |
| Capabilities | SDP offer/answer in the message body | H.245 capability sets | SDP offer/answer (JSEP, RFC 9429, 2024) |
| Directory / routing | registrar and proxies; addresses like sip:room-b@example.com | gatekeeper (registration, admission, E.164 aliases) | whatever the application builds |
| Media | RTP/RTCP; SRTP when encrypted | RTP/RTCP; H.235 for security | always SRTP, keyed by DTLS; ICE always on |
| NAT handling | session border controllers, ICE optional | H.460.18/.19 firewall traversal | ICE with STUN and TURN, built in |
| Typical home | desk phones, room systems, PBXs, service providers | older room systems and MCUs, still in large enterprises | browsers, web apps, many cloud meeting services' web clients |
For a Poly room system in Poly Video mode (its own conferencing app, as opposed to running Teams or Zoom), the admin guide documents both SIP and H.323 under Call Configuration: SIP with a registrar and proxy over TLS, TCP or UDP ("Auto" tries them in that order when a proxy server is used, or only TCP then UDP on an unregistered system, and Poly recommends TLS), and H.323 with a gatekeeper, an H.323 name and an E.164 extension, with optional H.235 Annex D authentication. Encryption is AES: for SIP the SRTP suites AES_CM_128_HMAC_SHA1_80/_32 and their 256-bit versions (RFCs 3711, 4568, 6188); for H.323, AES-CBC per H.235.6. When a room system has to reach a cloud service from behind a firewall, the guide describes H.460.18/H.460.19 traversal through a device such as Poly Clariti Edge, and lists the ports a strict firewall must allow out: 1720 TCP, 14085–15084 TCP, 1719 UDP and 16386–25386 UDP.
A codec's bitrate is only the payload. Every RTP packet also carries an RTP header, a UDP header and an IP header, and with SRTP an authentication tag. At 50 packets a second those bytes add up, which is why "64 kbit/s audio" never fits in a 64 kbit/s link.
// What a media stream really costs on the network: payload + RTP + UDP + IPv4 (+ SRTP's auth tag).
const HEADERS = { ipv4: 20, udp: 8, rtp: 12, srtpTag80: 10 }; // bytes; tag of AES_CM_128_HMAC_SHA1_80
const perPacket = HEADERS.ipv4 + HEADERS.udp + HEADERS.rtp + HEADERS.srtpTag80;
function audio(name, payloadKbps, ptimeMs) {
const packetsPerSec = 1000 / ptimeMs;
const payloadBytes = (payloadKbps * 1000 / 8) * (ptimeMs / 1000);
const wireKbps = ((payloadBytes + perPacket) * 8 * packetsPerSec) / 1000;
return { name, payloadKbps, packetsPerSec, payloadBytes, wireKbps };
}
function video(name, payloadKbps, fps, maxPayload = 1200) {
const frameBytes = (payloadKbps * 1000 / 8) / fps;
const packetsPerFrame = Math.ceil(frameBytes / maxPayload);
const packetsPerSec = packetsPerFrame * fps;
const wireKbps = (payloadKbps * 1000 + perPacket * 8 * packetsPerSec) / 1000;
return { name, payloadKbps, packetsPerSec, payloadBytes: Math.round(frameBytes / packetsPerFrame), wireKbps };
}
const rows = [
audio("G.711 µ-law 64k, 20 ms", 64, 20),
audio("G.722 64k, 20 ms", 64, 20),
audio("Opus 32k, 20 ms", 32, 20),
audio("Opus 32k, 10 ms", 32, 10),
video("H.264 2,000k, 30 fps", 2000, 30),
];
console.log(`headers per packet: ${perPacket} bytes (IPv4 20 + UDP 8 + RTP 12 + SRTP tag 10)\n`);
console.log("stream pkts/s payload B on the wire overhead");
for (const r of rows) {
const overhead = ((r.wireKbps - r.payloadKbps) / r.wireKbps) * 100;
console.log(
`${r.name.padEnd(23)} ${String(r.packetsPerSec).padStart(6)} ${String(r.payloadBytes).padStart(9)} ` +
`${r.wireKbps.toFixed(1).padStart(7)} kbps ${overhead.toFixed(0).padStart(6)}%`,
);
}headers per packet: 50 bytes (IPv4 20 + UDP 8 + RTP 12 + SRTP tag 10)
stream pkts/s payload B on the wire overhead
G.711 µ-law 64k, 20 ms 50 160 84.0 kbps 24%
G.722 64k, 20 ms 50 160 84.0 kbps 24%
Opus 32k, 20 ms 50 80 52.0 kbps 38%
Opus 32k, 10 ms 100 40 72.0 kbps 56%
H.264 2,000k, 30 fps 210 1190 2084.0 kbps 4%- Every packet pays a fixed
50bytes: IPv4 (20), UDP (8), RTP (12) and the 10-byte authentication tag ofAES_CM_128_HMAC_SHA1_80, the SRTP suite Chrome negotiated in the loopback run in the next topic. Ethernet or Wi-Fi framing comes on top of this; the model stops at the IP layer. - G.711 with 20 ms packets sends 50 packets a second of
160bytes (64,000 bits ÷ 8 × 0.02 s). Add 50 bytes each and it's84.0kbit/s on the wire: almost a quarter of the traffic is headers. G.722 has the same bitrate and the same overhead. - Opus at 32 kbit/s (the
targetBitrateChrome reported for its audio) sends only 80-byte payloads, so the same 50 bytes of headers are now 38% of the stream. Halving the packet time to 10 ms (Chrome's offer allowsminptime=10) doubles the packet rate and pushes the overhead to 56%: lower latency, much more bandwidth. - Video is the opposite: a 2 Mbit/s H.264 stream at 30 fps produces about 8.3 KB per frame, split into 7 packets of up to 1,200 bytes (kept under the path's MTU), 210 packets a second. Headers add only 4%, so for video you budget the codec rate plus a few percent.
- This is the arithmetic behind every vendor bandwidth table and every QoS policy: audio is small but packet-heavy, video is large but header-light, and both count packets per second, which is what routers and Wi-Fi actually struggle with.
Interop: when the room system and the meeting speak different languages
A SIP/H.323 room system can't join a Teams meeting by itself: Teams uses its own signalling. Microsoft's answer is Cloud Video Interop (CVI): a certified partner runs a gateway that joins the Teams meeting on one side and talks SIP/H.323 to the room on the other. Microsoft Learn's CVI page (updated September 2026) lists three certified partners: Pexip Infinity, Cisco Webex Video Integration, and Poly CloudConnect powered by Pexip. Microsoft describes CVI as an intermediate solution while you move to native Teams Rooms devices, which is exactly what a room system in Teams Rooms provider mode is (see Room systems and provider modes).
In the model, why does message 8 (ACK) go straight from Room A to Room B instead of through the proxy, and what single header would change that?
Show answer
Because the proxy didn't add Record-Route. Adding it keeps the proxy in every later request of the dialogue.
- A SIP dialogue is set up by the
INVITEtransaction; requests inside the dialogue (ACKfor a2xx,BYE, re-INVITE) are sent according to the dialogue's route set. - The route set is built from
Record-Routeheaders that proxies insert while forwarding theINVITE. The model's proxy inserted none, so the route set is empty. - With an empty route set, Room A sends in-dialogue requests to the remote target: the URI in Room B's
Contactheader from the200 OK. That's why theACKand later theBYEgo direct. - If the proxy adds
Record-Route: <sip:proxy.example.com;lr>, both rooms put it in their route sets and every later request goes through the proxy. Media still doesn't: RTP follows the SDP, not the route set.