Skip to main content

Build a familiar chat call

A call feels natural when the user starts with a person, not a meeting ID. This pattern puts a compact call button in a contact journey, lets a backend create the private room, and gives the recipient an accept or decline choice.

Build this: compare the runnable starter and its platform evidence. View source: MediaSFU Familiar Calls.

The journey

  1. The caller chooses a contact and audio or video.
  2. Your backend authenticates the caller, creates the MediaSFU room, and rings the recipient.
  3. The recipient accepts through an opaque, expiring invitation.
  4. Both users see an app-owned call shell while MediaSFU carries the media.
  5. Either user can leave; the authorized owner ends the room and the backend confirms cleanup.

The recipient should never have to copy a room name. An invite is a capability, not an API key or a reusable room secret.

Invitation and authority

The application owns contact names, call state, and the call shell. An authenticated backend creates the room, binds the invitation to the intended recipient, and returns only short-lived room-scoped join data. The media SDK owns transports, tracks, and room events. Keep the invitation opaque, expiring, and role-bound; never put a MediaSFU credential in the link.

Room lifecycle

Model idle, ringing, joining, connected, left, and ended as product states. Create the room only after the caller confirms the action. Start a call timer after connection, and let the authorized owner end the room. A participant leave should not end a call that still has an owner.

Primary and mini media

Use the remote participant as the primary surface once their live video is identified. Keep the local camera in a small, draggable self-preview. If a participant shares their screen, make that screen primary, keep it unmirrored, and use contain; the local camera can remain a small context tile.

Show microphone and camera state on every card. Mount remote audio separately from video so a camera-off participant remains audible. The same rules work in a narrow embedded panel when the mini preview is clamped to the measured media stage.

Media identity

Resolve a participant/member before looking up a producer. Keep remote camera, local camera, and screen share as distinct identities: screen share takes the primary surface, an identified remote camera is next, and local camera is the fallback. Mirror only the local self-view; show a screen with contain.

Remote audio

Mount every prepared remote-audio renderer independently from the visible call card so a camera-off participant remains audible.

Responsive layout

Measure the media stage rather than assuming viewport dimensions. Keep the remote surface large, clamp the draggable self-preview inside that stage, and move secondary controls below the media on narrow screens. Remote audio remains mounted even when cards move or video is off.

Secure room authority

The application owns names, call status, and the call shell. The backend owns identity, authorization, MediaSFU account credentials, room creation, and teardown. The MediaSFU SDK owns transports, tracks, and media events.

For a retry-safe create or join request, generate one opaque Idempotency-Key (8–128 visible ASCII characters) per logical action and reuse it only for an exact retry. Keep it in the request layer, not in a public link.

const key = crypto.randomUUID();
await fetch('/api/calls', {
method: 'POST',
headers: { 'content-type': 'application/json', 'Idempotency-Key': key },
body: JSON.stringify({ contactId, callType: 'video' }),
});

The browser receives only room-scoped join data. Your server should validate that the caller may contact the target, bind the invitation to that call, and reject a replay or role change.

Recovery and cleanup

Treat “ringing”, “joining”, “connected”, “left”, and “ended” as observed states. If a room or media request fails, show a retry action and do not start a call timer. On leave or page exit, make cleanup idempotent and report success only after the backend confirms the upstream room is gone.

Failure states

Permission denial, a rejected invitation, a failed join, a lost track, and an unconfirmed end are different messages with different next actions. Keep the call screen usable while a scoped request retries, and do not replace a useful product error with a raw socket, parser, or transport exception.

Cleanup

Stop local tracks, remove room listeners, clear timers and invitation state, and call the controller's leave/end operation once. Treat a second cleanup request as a safe no-op. Show “ended” only after the backend confirms that the room and its short-lived authority are gone.

Product evolution

Early call layouts could mistake a prepared card or display name for a live person, and URL-owned invite state could remain after the call ended. The stable lesson is to project current room parameters after rendering, read current state at action time, and keep identity, invitation, and teardown authority in the application/backend boundary.

Choose and test your platform

Start with the React or browser WHIP/WHEP implementation when you want the most complete familiar-call reference. The repository also includes Angular, Vue, React Native, Expo, Flutter, Kotlin Android, and Unity entry points. Run the chosen client on its target browser or device and test both users' audio, video, swap, drag, reconnect, leave, and cleanup before release.

The canonical MediaSFU showcase is available at /showcases/familiar-chat-call. The standalone multi-SDK familiar-call starter is available in the MediaSFU familiar calls repository. It contains React, Angular, Vue, React Native, Expo, Flutter, Kotlin Android, and WHIP/WHEP examples behind one credential-safe backend. Its React two-user room and browser WHIP/WHEP paths are the best places to begin. Test the chosen native client on every device family you plan to support.