Skip to main content

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 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, 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

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

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:

  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.