# Call Video Guide — H.264 over the native calls engine (BETA) Companion to [`call-audio-implementation-guide.md`](./call-audio-implementation-guide.md). The audio plane is production-validated; **video is BETA** until proven against a real WhatsApp video call (the engine's media path still carries `NOT VALIDATED` markers). The engine (`meowcaller` fork under `whatsmeow-private/calls`) carries **encoded H.264 Annex-B** over the WhatsApp relay. It does **not** encode or decode pixels — an external codec (the browser's WebCodecs, ffmpeg, a hardware encoder) produces and consumes the H.264. --- ## 1. Wire format - **Codec:** H.264, **Annex-B** byte stream (NAL units prefixed with the start code `0x00 0x00 0x00 0x01`). - **Framing:** one WebSocket **binary** message = one access unit (one frame's NALUs), reassembled on the RTP marker bit inbound. - **Keyframes:** send an IDR/keyframe immediately after opening the socket and at a sensible GOP interval (~every 1–2 s). The peer cannot decode until it sees a keyframe; a decoder cannot configure until it sees the SPS. --- ## 2. Endpoints | Method | Path | Purpose | |---|---|---| | `GET` (WS) | `/call/{call_id}/video/stream?token=…` | Bidirectional H.264 Annex-B | | `GET` (SSE) | `/call/{call_id}/video/state?token=…` | Peer camera on/off + orientation | | `GET` | `/call/{call_id}/video/stats?token=…` | Live server-side video telemetry | | `GET` (SSE) | `/call/{call_id}/events?token=…` | Reactions, hand, mute, screen share, roster, phase | | `POST` | `/call/video` | Turn local video on/off mid-call (audio→video upgrade) | | `POST` | `/call/screenshare` | Tell peers the outbound video is now a screen | Video is opened **in addition to** the audio socket (`/call/{call_id}/stream`) for a call placed or answered with video (`dial { video: true }`). The rest of the call surface — dial/answer/hangup, recording, group calls, call links, the waiting room, reactions and hand raise — is listed in §7 alongside its SDK wrapper. ### 2.3 What you may pass as a call target `POST /call/dial` takes `phone`, and the name undersells it. Three forms are accepted: | Form | Example | Notes | |---|---|---| | Plain number | `5521999999999` | E.164 digits, no `+`. **Resolved to the callee's LID first** — see below. | | Phone JID | `557192856732@s.whatsapp.net` | Used as given. | | LID | `47588875227343@lid` | Used as given; skips resolution entirely. | **Anything carrying an explicit `@` is passed through untouched.** That matters more than it sounds: stripping non-digits from a target destroys the suffix, and a LID sent as a phone number comes back as *"usync returned no LID for 47588875227343"* — about the very LID it was handed. The resolution step exists because **a typed number is not always the canonical one**. WhatsApp keeps many Brazilian mobiles WITHOUT the ninth digit, so `5571992856732` is not the JID `557192856732`. Asking usync about the typed form returns an *empty* user record — no lid, no devices, no status — which surfaces as "peer unreachable or not on WhatsApp" for a contact sitting in the chat list. Dialling resolves via the query that performs that normalization, so both spellings work. A group JID (`120363…@g.us`) is **not** a valid target here — it needs the group endpoints in §2.4. ### 2.4 Group calls and WhatsApp's participant limit | Endpoint | Use | |---|---| | `POST /call/group/dial-by-id` | `{groupJid, video}` — calls **every** remote member. Refuses if the roster exceeds the limit. | | `POST /call/group/dial` | `{targets[], video, groupJid?}` — calls an **explicit** list. Not capped. | **The limit is WhatsApp's, not ours, and it is dynamic.** The server declares it as `connected-limit`, a *required* attribute of the group-control stanza — 32 including you (so 31 remote) on a group, and 8 on a call link. It arrives only once the call is established, which is why `dial-by-id` uses a static 31 as its pre-flight check. So a 105-member group is not uncallable — it just cannot be called *wholesale*. Pick up to 31 members and use `/call/group/dial`, passing **`groupJid`** so the call stays bound to that group instead of becoming an unrelated ad-hoc call that happens to hold the same people. ### 2.5 In-call control stanzas are addressed per call type Relevant if you drive the engine directly rather than through this API. Control stanzas — `screen_share`, `user_action` (hand raise) — are addressed to `@call` on a **group** call but to the **peer's LID** on a 1:1 call; the official client sends 1:1 screen share peer-to-peer. Sending a 1:1 control stanza to the group address gets it rejected, and a rejected control stanza must **not** be treated as the call failing: doing so ends a perfectly healthy call. Lifecycle acks (offer/accept/terminate) stay fatal; control acks do not. ### 2.1 `/video/stream` (WebSocket) - **Server → client** (binary): the peer's video as Annex-B access units. - **Client → server** (binary): your camera video as Annex-B access units; the engine forwards them to the peer. Do **not** send raw frames, JPEG, or WebRTC tracks. - **Backpressure:** the server buffers outbound frames and **drops the oldest** when a slow client falls behind (newest-wins) — it never blocks the media loop. One video subscriber per call (last wins). - **Outbound camera state:** on your first sent frame the engine announces your camera active to the peer (`