On this pageOPENCLOSE
Capstone: ship a 1:1 video app
Combine rooms, signaling, reconnection, matchmaking, observability, and cleanup into a production-ready call flow.
Goal
- 01Assemble signaling, media, data, recovery, observability, and cleanup into one 1:1 call flow.
- 02Write a production checklist that you can verify with real networks and failure injection.
- 03Ship a call experience that is clear at every state: waiting, connecting, connected, recovering, and ended.
The concept, plainly
Build the 1:1 flow as a sequence: authenticate or create an anonymous room identity, join signaling, exchange presence, negotiate media, establish ICE and DTLS, open data channels, monitor, recover, and clean up. Each step needs a visible state and a timeout. 'Connecting…' is not a state machine; it is an apology. Show whether signaling is open, whether a peer is present, whether ICE has a pair, and whether DTLS has completed.
Make rooms boring and deterministic. Generate a non-identifying room code, support an invite link, limit the room to two active peers, and define the newcomer-offers rule. Enforce limits on the server. Handle the case where both people arrive at once, one refreshes, a socket reconnects, or a tab stays open after the other leaves. Room presence should be server-authoritative and expire stale connections with heartbeats.
Keep signaling, media, and data separate in the code. A signaling socket reconnect should not silently restart a healthy media path. A failed ICE check should not clear the chat history. A closed data channel should not stop the camera. Separate state containers and separate recovery policies make the system understandable. Avoid a single `connected` boolean that hides three different subsystems.
Plan for media failure before codecs. Capture with clear permission errors and a synthetic fallback. Attach tracks before the first offer. Check transceiver direction on both peers. Render a local preview immediately and a remote preview with an explicit 'waiting for tracks' state. Use the selected pair and quality stats to tell whether a bad experience is network, CPU, or the UI. Offer a relay-only mode only if privacy requirements demand it, and make the cost visible.
Treat cleanup as a feature. Stop local tracks, close senders, close the data channel, close the peer connection, clear timers, revoke object URLs, and leave the room. Cancel fetches and abort an in-flight TURN request. On page hide, end the room or mark the peer as away; on return, rejoin cleanly. Test by watching the camera indicator, network panel, memory timeline, and server presence list after closing the tab.
Production also means operational truth. Record aggregate quality samples and connection outcomes, not full SDP. Track TURN relay share, time to connect, failed ICE rate by network, permission-denied rate, browser versions, and data-channel errors. Define what a healthy call looks like and alert when the distribution changes. Ship a diagnostics copy button with redaction. Then complete the labs, review your own failure tests, and claim the completion record—none of this requires a name or personal profile.
See it move
Interactive diagram
The complete 1:1 call flow
Arrive without an account
A room code or invite link is enough. Generate a non-identifying device handle and keep it out of the UI copy. No name field is needed.
Every subsystem has a state, a failure mode, a recovery path, and a cleanup owner.
Behind the scenes
A real product has more states than the API
The browser exposes new, connecting, connected, disconnected, failed, and closed. Your product also needs waiting-for-peer, media-denied, waiting-for-answer, data-channel-closed, and ended-by-peer. Map browser states into product states explicitly and test every transition. Loading spinners that never resolve are usually missing product states.
The server is not optional just because media is peer-to-peer
Even a P2P call needs a signaling service, TURN fallback, room authorization, and observability. The media path can be direct while everything around it is server-assisted. Budget for the control plane and relay even if your marketing calls it peer-to-peer.
Completion is not a substitute for testing
Marking a lab complete records that you worked through it. The certificate is a completion record, not a guarantee of production readiness. The capstone checklist should be verified with a second device, a restricted network, permission denial, a dropped socket, and a page refresh.
Code walkthrough
Every exit goes through the same function. Idempotence matters because unmount, hangup, page hide, and errors can all race.
export function createTeardown(options: {pc: RTCPeerConnection | null;channel: RTCDataChannel | null;stream: MediaStream | null;timers: Array<number | undefined>;objectUrls: string[];leaveRoom: () => void;}) {let done = false;return function teardown() {if (done) return;done = true;for (const timer of options.timers) window.clearTimeout(timer);for (const track of options.stream?.getTracks() ?? []) {track.stop();track.srcObject = null;}options.channel?.close();if (options.pc) {options.pc.onicecandidate = null;options.pc.ontrack = null;options.pc.onconnectionstatechange = null;options.pc.close();}for (const url of options.objectUrls) URL.revokeObjectURL(url);options.leaveRoom();};}
Do and don't
Do
Show every state honestly
Waiting for a peer, connecting, connected, recovering, and ended are different experiences. Design for each one.
Test with a second device and a restricted network
Localhost and two tabs cannot reveal TURN, mobile NAT, or permission behavior.
Keep one idempotent teardown
Stop tracks, close channels and peers, clear timers, revoke URLs, and leave the room from every exit path.
Record outcomes, not just errors
Time to connect, relay share, permission-denied rate, and quality distribution reveal more than a crash counter.
Don't
Don't ask for a name to issue completion
A completion record can be anonymous. A name is personal data with no functional need here.
Don't ship a single 'connected' boolean
Signaling, media, data, and recovery are separate. One boolean hides the reason a call failed.
Don't leave camera or timers running
A closed peer connection does not stop local tracks. An uncancelled reconnect timer can resurrect a dead session.
Don't call the demo production-ready
A working loopback is a learning milestone. Production requires load, network, browser, privacy, and failure testing.
Practice
Hands-on lab
Complete the field guide
Finish all fifteen lessons and all four labs, then open the certificate page. The certificate is an anonymous completion record: no name, email, or account is requested.
- 1Complete the WebSocket, 1:1, mesh, and SFU labs.
- 2Run the capstone verification list against your own 1:1 flow or the site lab.
- 3Open the certificate page and confirm every requirement is met.
- 4Generate the credential, print or save it, and review the diagnostic checklist once more.
Solo? Every lab can be completed without a second person: loopback, simulated peers, and the SFU simulation cover the core behaviors.
Quick quiz
Knowledge check
0 of 5 answered
- 01
Which is a product state that the raw RTCPeerConnection API does not provide?
- 02
When should signaling be allowed to restart media?
- 03
What belongs in the idempotent teardown path?
- 04
What is the certificate based on?
- 05
Which production metric is most useful for diagnosing relay dependence?
Key takeaways
- A complete call has explicit states for waiting, discovery, negotiation, connected, degraded, recovery, and teardown.01
- Rooms and presence are server concerns; media and data are separate transports with separate recovery.02
- Cleanup is a first-class feature: tracks, timers, URLs, sockets, and room membership all need owners.03
- Completion is earned by working through every lesson and lab; the certificate records it anonymously.04
Common errors and fixes
Terms in this lesson
Done with this lesson?
Mark it complete, then keep the thread.