Skip to main content

Build a Complete Custom Room UI

Replace the supplied MediaSFU room with your own interface. Removing the supplied interface removes rendering and interaction, not the work behind them: your application must render remote media, show errors, connect controls to the current room, and clean up the session before another call begins.

The code on this page uses mediasfu-reactjs@4.4.2. For Angular, Vue, React Native, Expo, Flutter, Android, Kotlin, Swift, and Unity, use the guides linked in Platform-specific implementation guides; their public APIs and lifecycles differ.

Choose the smallest customization that works​

Product needRecommended pathMediaSFU still renders
Branding, colors, or selected cardsuiOverridesThe rest of the room
Your own room workspacecustomComponentRuntime-owned dialogs and flows you retain
Your own application from first paintreturnUI={false}Nothing unless you mount it

Start with the complete room, test a two-user call, and then replace one layer at a time. If something stops working, you know which layer caused it.

What a complete custom UI must provide​

ResponsibilityWhat to render or handleA common failure when omitted
Room entryBackend-approved create/join result and a visible pending/error stateThe call appears frozen after an expired or rejected join
ParticipantsCurrent list, roles, waiting users, requests, joins, and leavesControls target stale users
Local mediaPermission, selected device, producer state, and previewA button changes locally although nothing was published
Remote audioMount every current audio node or attach each audio stream to an <audio> elementThe other participant is connected but cannot be heard
Remote videoMount current video nodes or attach streams to <video> elementsParticipants appear in state but have no picture
PaginationRender the current page and wire page changes to the live room parametersStreams are subscribed but remain off-page or paused
Paused/resumed mediaReflect producer/consumer pause and resume updatesA tile stays frozen after media resumes
Screen shareStart/stop controls, shared stream, picker cancellation, and ended-track handlingBrowser sharing stops while the room still shows an active share
AlertsPermission, device, transport, policy, and retry messagesCamera or microphone failure looks like a dead button
CollaborationMessages, polls, breakouts, whiteboard, recording, and role checks you exposeA visible modal changes without a server-confirmed action
ExitParticipant leave, host end, confirmation, and app navigationClosing the screen is mistaken for ending or leaving the room
TeardownTracks, producers, consumers, transports, sockets, listeners, timers, and app snapshotsThe first call works and the next call fails intermittently

Render the audio and video prepared by the runtime​

consumerResume prepares React nodes and places audio-only nodes in audioOnlyStreams. The supplied room mounts them through AudioGrid. If your workspace never renders that grid, remote audio can exist without becoming audible.

import {
AudioGrid,
FlexibleGrid,
Pagination,
type PaginationParameters,
} from 'mediasfu-reactjs';
import type { ComponentProps } from 'react';

type RoomParameters = PaginationParameters & {
audioOnlyStreams?: ComponentProps<typeof AudioGrid>['componentsToRender'];
otherGridStreams?: ComponentProps<typeof FlexibleGrid>['componentsToRender'][];
numberPages?: number;
currentUserPage?: number;
};

export function CallWorkspace({ parameters }: { parameters: RoomParameters }) {
const audio = parameters.audioOnlyStreams ?? [];
const videos = parameters.otherGridStreams?.[0] ?? [];
const columns = Math.max(1, Math.min(3, videos.length));
const rows = Math.max(1, Math.ceil(videos.length / columns));
const totalPages = Math.max(1, parameters.numberPages ?? 1);
const page = Math.min(totalPages - 1, Math.max(0, parameters.currentUserPage ?? 0));

return (
<main>
<AudioGrid componentsToRender={audio} />
<FlexibleGrid
customWidth={960}
customHeight={540}
rows={rows}
columns={columns}
componentsToRender={videos}
emptyCellFallback={<div aria-hidden="true" />}
/>
{totalPages > 1 && (
<Pagination
totalPages={totalPages}
currentUserPage={page}
parameters={parameters}
position="middle"
location="bottom"
direction="horizontal"
showAspect
/>
)}
</main>
);
}

The full type-checked example is described in media rendering step by step.

Get one participant's media​

Import getParticipantMedia from the package root and pass the latest room parameters. It matches the participant by id, then by name, and returns the current audio or video stream, or null. Pass the ID when you have it; names may not be unique in every product.

import { getParticipantMedia } from 'mediasfu-reactjs';

const stream = await getParticipantMedia({
id: participant.id,
name: participant.name,
kind: 'video',
parameters, // the latest sourceParameters from the room
});
if (stream) videoElement.srcObject = stream;

A participant-list entry does not mean a media stream exists. A participant may have no producer (nothing published), may be paused, or may be on another page. getParticipantMediaState tells you whether a participant currently has audio or video before you ask for the stream.

Know where actions come from​

sourceParameters is a live room-state and media-helper bundle. It contains helpers such as clickAudio, clickVideo, clickScreenShare, device switching, transport functions, poll handlers, and getParticipantMedia.

It is not the package's complete export surface. Actions including messaging, some recording flows, and confirmation/exit helpers may need to be imported from mediasfu-reactjs and called with the current room parameters. Never use unsupported deep imports.

import {
launchConfirmExit,
launchMessages,
launchRecording,
} from 'mediasfu-reactjs';

// Build the exact options from the active room parameters. Keep these imports
// beside the UI that owns the corresponding dialog or action.

Use the generated API reference for the exact option type, then provide every required socket, state, and update callback from the active room. Do not invent a partial parameter object merely to satisfy a call.

Show errors instead of swallowing them​

The runtime exposes alert state such as alertVisible, alertMessage, alertType, and alertDuration. A custom shell can render that state in its own toast or alert region.

{parameters.alertVisible && (
<div role="alert" data-kind={parameters.alertType}>
{parameters.alertMessage}
</div>
)}

Give distinct messages for permission denied, no device, device already in use, transport failure, unauthorized action, and recoverable network interruption. Do not reduce all failures to “audio/video not working.”

Server-side create or join does not replace WebRTC setup​

Calling your application backend for create or join is the correct way to keep reusable MediaSFU credentials out of a browser or mobile bundle. The adapter must return the room response shape expected by the SDK. After that handoff, device capture, RTP device creation, send transports, producers, receive transports, consumers, and rendering remain the SDK/client lifecycle.

Therefore, debug these phases separately:

  1. backend authentication and room policy;
  2. room/socket connection;
  3. browser or operating-system permission;
  4. send transport and producer;
  5. receive transport and consumer;
  6. mounted audio/video element;
  7. leave and teardown.

Teardown before another call​

After confirmed leave or room end:

  1. stop app-owned local and display tracks;
  2. detach app-owned audio and video elements;
  3. close or release app-owned producer, consumer, and transport references;
  4. remove app listeners and timers;
  5. clear participant, stream, page, alert, modal, and pending-action state;
  6. unmount the room runtime before creating a new instance;
  7. verify a second call on the same device.

Use the SDK's exit flow for room semantics; the steps above cover the additional resources owned by your application shell.

Platform-specific implementation guides​

Each guide below uses the public symbols and lifecycle of the named SDK:

SDKLifecycleCollaboration and moderation
AngularRoom operationsParticipant collaboration
VueRoom operationsParticipant collaboration
React NativeRoom operationsParticipant collaboration
ExpoRoom operationsParticipant collaboration
FlutterFlutter SDKParticipant collaboration
AndroidAndroid SDKNative moderation
Kotlin MultiplatformRoom operationsParticipant collaboration
Swift / AppleSwift SDKUse the hosted room controller and its bridge controls
UnityRoom operationsParticipant collaboration

Two-user acceptance test​

Run the same sequence twice without reloading the device:

  1. create or resolve a room through the backend;
  2. join as host and participant;
  3. confirm both users in participant state;
  4. publish and remotely receive microphone and camera;
  5. switch camera or input where supported;
  6. start and stop screen share;
  7. exercise one moderation or collaboration action;
  8. leave as participant and end as host when the SDK exposes that operation;
  9. confirm tracks, listeners, and room state are cleared;
  10. repeat the call and compare behavior.

Test physical mobile devices for native capture and audio routing. A type check or browser-only test cannot establish those outcomes.