Build a Complete Custom Room UI
A custom MediaSFU interface is supported. The important distinction is that removing the supplied interface removes rendering and interaction, not the underlying responsibilities. Your application must deliberately render remote media, expose errors, connect controls to the current room, and clean up the session before another call begins.
This guide uses ReactJS 4.2.9 for the complete code path. Use the framework
links near the end for Angular, Vue, React Native, Expo, Flutter, Android,
Kotlin, Swift, and Unity; their public symbols and lifecycle contracts differ.
Choose the smallest customization that works
| Product need | Recommended path | MediaSFU still renders |
|---|---|---|
| Branding, colors, or selected cards | uiOverrides | The rest of the room |
| Your own room workspace | customComponent | Runtime-owned dialogs and flows you retain |
| Your own application from first paint | returnUI={false} | Nothing unless you mount it |
Start with the complete room, prove a two-user call, and then replace one layer at a time. This gives every missing behavior an observable before-and-after comparison.
What a complete custom UI must provide
| Responsibility | What to render or handle | A common failure when omitted |
|---|---|---|
| Room entry | Backend-approved create/join result and a visible pending/error state | The call appears frozen after an expired or rejected join |
| Participants | Current list, roles, waiting users, requests, joins, and leaves | Controls target stale users |
| Local media | Permission, selected device, producer state, and preview | A button changes locally although nothing was published |
| Remote audio | Mount every current audio node or attach each audio stream to an <audio> element | The other participant is connected but cannot be heard |
| Remote video | Mount current video nodes or attach streams to <video> elements | Participants appear in state but have no picture |
| Pagination | Render the current page and wire page changes to the live room parameters | Streams are subscribed but remain off-page or paused |
| Paused/resumed media | Reflect producer/consumer pause and resume updates | A tile stays frozen after media resumes |
| Screen share | Start/stop controls, shared stream, picker cancellation, and ended-track handling | Browser sharing stops while the room still shows an active share |
| Alerts | Permission, device, transport, policy, and retry messages | Camera or microphone failure looks like a dead button |
| Collaboration | Messages, polls, breakouts, whiteboard, recording, and role checks you expose | A visible modal changes without a server-confirmed action |
| Exit | Participant leave, host end, confirmation, and app navigation | Closing the screen is mistaken for ending or leaving the room |
| Teardown | Tracks, producers, consumers, transports, sockets, listeners, timers, and app snapshots | The 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
The React runtime bundle exposes getParticipantMedia. In 4.2.9 its runtime
call accepts participant ID, participant name, and media kind as positional
arguments. Use the ID when available; names may not be unique in every product.
type GetParticipantMedia = (
participantId: string,
participantName: string,
kind: 'audio' | 'video',
) => Promise<MediaStream | null>;
const getParticipantMedia = parameters.getParticipantMedia as
| GetParticipantMedia
| undefined;
const stream = await getParticipantMedia?.(
participant.id ?? '',
participant.name,
'video',
);
Do not treat a participant-list entry as proof that a media stream exists. A participant may have no producer, may be paused, or may be on another page.
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:
- backend authentication and room policy;
- room/socket connection;
- browser or operating-system permission;
- send transport and producer;
- receive transport and consumer;
- mounted audio/video element;
- leave and teardown.
Teardown before another call
After confirmed leave or room end:
- stop app-owned local and display tracks;
- detach app-owned audio and video elements;
- close or release app-owned producer, consumer, and transport references;
- remove app listeners and timers;
- clear participant, stream, page, alert, modal, and pending-action state;
- unmount the room runtime before creating a new instance;
- 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:
| SDK | Lifecycle | Collaboration and moderation |
|---|---|---|
| Angular | Room operations | Participant collaboration |
| Vue | Room operations | Participant collaboration |
| React Native | Room operations | Participant collaboration |
| Expo | Room operations | Participant collaboration |
| Flutter | Flutter SDK | Participant collaboration |
| Android | Android SDK | Native moderation |
| Kotlin Multiplatform | Room operations | Participant collaboration |
| Swift / Apple | Swift SDK | Use the hosted room controller and its bridge controls |
| Unity | Room operations | Participant collaboration |
Two-user acceptance test
Run the same sequence twice without reloading the device:
- create or resolve a room through the backend;
- join as host and participant;
- confirm both users in participant state;
- publish and remotely receive microphone and camera;
- switch camera or input where supported;
- start and stop screen share;
- exercise one moderation or collaboration action;
- leave as participant and end as host when the SDK exposes that operation;
- confirm tracks, listeners, and room state are cleared;
- 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.