---
name: webrtc-mock
description: Use when touching any WebRTC code during the mockup phase — local getUserMedia, peer connection, remote stream simulation, signalling stubs. The contract is: real local stream + faithfully-faked remote stream, so the code path is identical when real signalling lands in Phase 5.
---

# webrtc-mock

WebRTC is the part most likely to surprise us in Phase 5 because the API is fiddly and the failure modes are platform-specific. The mock strategy is therefore: **use the real API for everything that touches the user** (camera permission, local preview, track add/remove, screen capture) and **fake only the signalling and the remote stream**. Anything we test in the mock should behave the same against a real backend.

## When to use

- Touching any WebRTC-related file (`*RtcConnection.ts`, video tile components, screen-share buttons, permission prompts)
- Wiring local camera/mic preview in the widget or agent app
- Implementing the "incoming call" UX (ringing, accept, decline)
- Simulating an agent or visitor on the other end during demos

Skip for non-media code (chat messages, soft locks) — those go via the mock event bus, not this skill.

## Do

- Use **real `navigator.mediaDevices.getUserMedia`** for the local stream. We want the real camera permission UX — there is no faking it.
- Use **real `getDisplayMedia`** for screen sharing for the same reason.
- Stub `RTCPeerConnection` behind a `PeerConnectionLike` class that has the same shape (`addTrack`, `createOffer`, `createAnswer`, `setRemoteDescription`, `addIceCandidate`, `ontrack`, `onicecandidate`). The mock pipes local tracks straight to the remote `MediaStream` it exposes via `ontrack`.
- The "remote" video stream on the **visitor side** is rendered from a `<canvas>` showing an animated "Agent placeholder" tile (initials in a Fraunces letter, ticking 1Hz waveform). Capture it with `canvas.captureStream(15)`.
- The "remote" stream on the **agent side** is a small looped `<video>` element of stock footage piped through `video.captureStream(30)` — so the agent sees motion, not a still.
- Emit/consume signalling events through the mock bus using the existing `webrtc:offer`/`webrtc:answer`/`webrtc:ice` schemas. **Do not invent a parallel transport.**
- Track every active `MediaStream` and every `RTCPeerConnection` in a registry so a single `cleanup()` call can stop all tracks and close all connections — the demo's "reset mock state" button calls it.
- Always call `stream.getTracks().forEach(t => t.stop())` when ending a session. Browsers leak the camera light otherwise.

## Don't

- Don't fake `getUserMedia`. The camera permission flow is the most fragile UX in the product; faking it means we discover real bugs at the worst possible moment (Phase 5 demo).
- Don't store `MediaStream` objects in a Zustand/store value as a deep observable. They're not serializable. Hold a stream id and look it up in the registry.
- Don't bypass the mock bus for signalling. The whole point is that swapping to Socket.IO doesn't touch this layer.
- Don't use `<video autoplay muted>` without `playsinline` on iOS Safari — full-screen takeover surprise.
- Don't call `setRemoteDescription` with a `null` SDP. Always check both `offer.sdp` and `answer.sdp` exist before forwarding.
- Don't ship a `MediaStream` to the host page (widget context). All WebRTC objects live inside the widget's Shadow DOM tree.

## One short example

```ts
// packages/widget/src/webrtc/MockPeerConnection.ts
export class MockPeerConnection {
  remoteStream = new MediaStream();
  onicecandidate: ((c: RTCIceCandidateInit | null) => void) | null = null;
  ontrack: ((stream: MediaStream) => void) | null = null;

  addTrack(track: MediaStreamTrack): void {
    // Mock loopback: visitor's own track also appears on the "remote" stream
    // so the agent-side canvas placeholder is what the visitor sees, and
    // the visitor's camera is what the agent (in the mock) sees.
    this.remoteStream.addTrack(track);
  }

  async createOffer(): Promise<RTCSessionDescriptionInit> {
    return { type: 'offer', sdp: `mock-offer\n` };
  }
  async createAnswer(): Promise<RTCSessionDescriptionInit> {
    return { type: 'answer', sdp: `mock-answer\n` };
  }
  async setRemoteDescription(_d: RTCSessionDescriptionInit): Promise<void> {
    queueMicrotask(() => this.ontrack?.(this.remoteStream));
  }
  async addIceCandidate(_c: RTCIceCandidateInit): Promise<void> {}
  close(): void {
    this.remoteStream.getTracks().forEach((t) => t.stop());
  }
}
```

## Reference

- Event schema (signalling events): `packages/shared/src/events/schema.ts`
- Bus interface: `packages/shared/src/events/mock-bus.ts`
- Phase docs: `dev-docs/phase-1-widget-mockup.md` (Sessions 1.4, 1.5)
