> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# Bot Camera Streaming

> Publish live WebRTC video and audio through a Google Meet, Microsoft Teams, or Zoom bot camera and microphone.

# Bot Camera Streaming

Send live audio/video into a Google Meet, Teams or Zoom bot's camera and microphone. This is **inbound** media (your publisher → bot → meeting). `live_video_required` retains its separate outbound meeting-video contract.

## Create the bot

```json
{
  "meeting_link": "https://meet.google.com/abc-defg-hij",
  "bot_camera_stream": { "websocket_url": "wss://your-service.example/camera" }
}
```

Google Meet, Microsoft Teams and Zoom are supported. For Teams and Zoom, use your meeting URL and a public `wss://` signaling endpoint; `ws://` is rejected. On Zoom, `bot_camera_stream` cannot be combined with `agent_config_id` (the request returns 400). `botCameraStream` and `websocketUrl` aliases are accepted. Recording is optional; ingress selects video container capacity for immediate and scheduled bots. The container connects outbound to your URL, so it needs no exposed inbound signaling port.

## Offer, answer, and ICE

On connection (including reconnection), the bot sends `type: "ready"`, `bot_id`, and `capabilities.protocol: "meetstream.live_media.v1"`. This announces the signaling socket; the browser may still be joining. Send:

```json
{
  "command": "start_live_media",
  "peer_connection_id": "unique-session-1",
  "offer": { "type": "offer", "sdp": "<publisher SDP>" },
  "ice_servers": [ { "urls": "stun:your-stun.example:3478" } ],
  "audio_delay_ms": 250,
  "target_video_width": 640
}
```

`webrtc_offer` and `start_webrtc_media` are aliases. `session_description` or a top-level `sdp` are also accepted. The bot receives video and optional audio; publish them as related tracks in one WebRTC stream. The bot returns:

```json
{
  "type": "LiveMediaSignal",
  "action": "answer",
  "bot_id": "<bot ID>",
  "peerConnectionId": "unique-session-1",
  "answer": { "type": "answer", "sdp": "<bot SDP>" }
}
```

Apply the answer to your publishing peer connection. Relay bot `LiveMediaSignal` messages with `action: "ice_candidate"` to your peer connection. Send publisher candidates back with:

```json
{
  "command": "webrtc_candidate",
  "peer_connection_id": "unique-session-1",
  "candidate": { "candidate": "<ICE candidate>", "sdpMid": "0", "sdpMLineIndex": 0 }
}
```

Use the same ID on the offer and every candidate. Use a **new ID** for each replacement session. The default ID is `default`. Early candidates queue until the remote description is ready (128 candidates per ID, at most four pre-offer IDs). Candidates for a different active session produce `stale_session`; stop clears pending candidates.

`ice_servers` defaults to an empty array; no managed STUN/TURN service is supplied. Supply STUN/TURN appropriate for your publisher's network. Chrome receives any TURN credentials. Use only servers you control or trust. Teams and Zoom apply the endpoint restrictions described below; GMeet does not enforce equivalent restrictions. Camera connection logs redact socket URLs. WebRTC commands are accepted only on the camera signaling socket; this does not imply that GMeet's camera socket is restricted to camera commands.

## Playback and operating limits

Chrome negotiates codecs from the publisher SDP and its supported browser codecs; no codec, bitrate, or FPS guarantee is imposed by this signaling API. Incoming video is decoded and drawn through the virtual-camera canvas. Target width defaults to 640 pixels and is clamped to 160–1280; smaller input is not upscaled and aspect ratio is preserved. Resolution is selected when the track's bridge starts. Frames normally follow the decoded-video callbacks; the emergency timer fallback draws at 5 FPS.

Audio uses the bot's synthesized microphone. `audio_delay_ms` defaults to 250 ms; GMeet clamps it to 0–1000 ms; Teams and Zoom reject values outside that range. Tune against your source; this is a fixed, configurable compensation, not automatic synchronization. Each meeting platform may further adapt transmitted quality. One GMeet timer comparison measured approximately 556 ms of video delay; it is not a latency guarantee or a Teams or Zoom measurement.

Only one ingress session runs at a time. A new valid offer replaces the old session. Ingress is refused while an avatar is active or starting; starting a valid avatar stops ingress, including negotiation in progress.

Publish at most one video track and one audio track. Zoom rejects offers with more than one audio or video section. The current Teams implementation does not safely dispose all resources for multiple simultaneous tracks of the same kind; multi-track ingress is not supported for this release.

## Status, failures, and stop

`LiveMediaStatus` and `LiveMediaError` messages include `bot_id`, `peerConnectionId`, `action`, and `timestamp`. Status actions include `answer_created`, `connection_state`, `track_received`, `track_muted`, `track_unmuted`, `track_ended`, `video_started`, `audio_started`, and `stopped`. `video_started` reports bridge dimensions; it is not proof that a meeting participant received a frame.

A missing browser connection returns `LiveMediaError` with `action: "browser_not_ready"`; retry the offer after the browser is ready. All signaling clients use bounded retries (up to five attempts, starting at 2 seconds with a 1.5× backoff). Reconnection does not restart WebRTC automatically: if media needs replacement, send a fresh offer and session ID. Google Meet has no negotiation timeout and retains media when signaling disconnects. Teams and Zoom have a 15-second negotiation timeout and stop publishing if the signaling socket disconnects. No platform automatically restarts ICE. A temporarily disconnected WebRTC connection is retained; failed/closed connections or an ended input track stop the session.

In GMeet, `audio_started` currently reports local audio routing before microphone/sender readiness is verified. Check audibility from another participant. Some GMeet peer-construction failures can throw before an upstream error is sent; absence of `LiveMediaError` is not proof of successful negotiation.

Send `{ "command": "stop_live_media" }` to stop. Stop or failure removes the bridge, disconnects incoming audio, and restores `bot_image_url` if configured, otherwise disables the camera. The microphone is disabled when no queued audio playback remains. Recovery still requires the Meet UI to be available.

## Teams security and compatibility

Teams uses a dedicated signaling client and targets only the browser document that reported admission. The publisher peer uses Chrome's native constructor, captured before Teams' recording and metadata hooks; it cannot inject participant data through that pipeline. Data-channel offers and non-camera commands are rejected. Legacy frames/PCM are not accepted on the Teams camera socket.

Teams requires public WSS signaling, validates all DNS answers, pins the connection to a verified IP, and verifies TLS against the original hostname. Remote private, loopback, link-local and mDNS ICE candidates are ignored. Offer SDP is limited to 256 KiB; signaling messages to 1 MiB and 50 per second. ICE lists allow at most eight servers with eight URLs each.

Public `stun:` and `turn:` hostnames are resolved and rewritten to verified IP addresses before Chrome uses them, preventing a second DNS resolution from reaching an internal endpoint. `stuns:` and `turns:` require a public literal IP and a TLS certificate valid for that IP; TLS ICE hostnames are currently rejected. TURN credentials reach Chrome but are not written to camera logs. Customers must authenticate their own publisher/signaling service: the bot ID is not an authentication token. Anyone authorized by that service can replace or stop the published media.

Teams status `browser_ready` announces admission; retry any earlier offer after that status. `audio_started` requires both an available/unmuted mic and a Teams audio sender; `video_started` requires a Teams video sender. These statuses still need verification from another participant. Audio delay is configurable with the same 250 ms baseline; Teams rejects values outside 0–1000 ms rather than clamping. Source width must be 160–1280 pixels. Guest and signed-in calls use the same admitted-document path, retaining native prejoin camera setup until admission.

## Zoom security and compatibility

Zoom uses the same signaling hardening as Teams: public WSS only, DNS validation with the connection pinned to a verified IP, TLS verified against the original hostname, private/loopback/link-local/mDNS ICE candidates ignored, and the same message, SDP, rate and ICE-list limits. Public `stun:`/`turn:` hostnames are rewritten to verified IPs; `stuns:`/`turns:` require a public literal IP. Data-channel offers and non-camera commands are rejected. Legacy frames/PCM are not accepted on the Zoom camera socket.

Zoom sends `browser_ready` (and `capabilities.browser_ready: true` in `ready`) once the bot's camera and microphone are ready to send in the meeting. An earlier offer gets `browser_not_ready`; retry after `browser_ready`. With `silent_recording`, the bot never sends media, so offers always get `browser_not_ready`. `video_started` and `audio_started` are sent only after the first frame or audio chunk reaches the meeting; still verify them from another participant.

Every replacement offer needs a new `peer_connection_id`; reusing a stopped ID returns `stale_session`. `target_video_width` must be 160–1280 pixels and `audio_delay_ms` 0–1000 ms; other values are rejected. Video is sent at up to 25 FPS, keeping the source aspect ratio. If no video frame arrives for 10 seconds, or the publisher mutes the track, the bot shows its `bot_image_url` (or turns the camera off) and sends `track_muted`; the last frame is never left frozen. Video resumes with `track_unmuted`.

While the stream is live it owns the bot's camera and microphone: requests to change the bot image are refused with `camera_in_use`, and other audio or video sent to the bot is dropped. Both work again after stop. Streamed audio is played into the meeting but is not added to the bot's recording or transcript.

## GMeet security boundary

Use WSS and authenticate access to your signaling service. GMeet currently permits WS as well as WSS and passes customer ICE configuration to Chrome without Teams' destination filtering. The camera socket also reaches the shared agentic dispatcher, and publisher data channels can reach Meet metadata handlers. These are outstanding hardening findings, not supported publisher capabilities. Send only documented camera commands and audio/video offers with no application/data-channel media sections. Broader release requires closing these boundaries; customer-side filtering does not replace bot-side enforcement.

## Legacy frame transport (Google Meet only)

The same camera socket accepts `sendvideo` (base64 JPEG/PNG), `sendaudio` (existing PCM chunk contract), and `stopvideo`. Binary ingress is `[0x03:uint8][width:uint16LE][height:uint16LE][format:uint8][frame bytes]` for video (`format` 0=JPEG, 1=PNG), and `[0x02:uint8][sampleRate:uint32LE][channels:uint8][PCM16LE bytes]` for audio. `stopvideo` stops legacy frame playback; use `stop_live_media` for WebRTC.

Stop WebRTC before switching to legacy playback. Legacy frames and PCM have no shared timestamp/jitter-buffer contract, so A/V sync is best effort.