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
- The caller chooses a contact and audio or video.
- Your backend authenticates the caller, creates the MediaSFU room, and rings the recipient.
- The recipient accepts through an opaque, expiring invitation.
- Both users see an app-owned call shell while MediaSFU carries the media.
- 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.
Related implementation
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.