Skip to main content

Render Room Media Step by Step in React

This guide uses mediasfu-reactjs@4.2.9. It starts with the supplied room, then shows how to replace its layout, render MediaSFU-prepared participant nodes, or attach a media stream to your own HTML element.

Use the first approach that gives you enough control. Each lower level gives your application more responsibility for accessibility, loading, recovery, and cleanup.

What happens between Join and a visible video

These are separate steps:

  1. Create or join obtains the authorized room connection details.
  2. Produce sends this person's microphone, camera, or screen track.
  3. Consume receives another person's media through a consumer transport.
  4. Prepare turns room and stream state into React nodes for the current layout and page.
  5. Render places those nodes in AudioGrid, FlexibleGrid, or another application-owned surface.

Calling a grid does not join a room or consume a track. Passing a raw MediaStream to MediaSFU does not automatically create a participant card.

Start with the complete supplied room

Keep MediaSFU credentials on your application server. The two callbacks below send only the room payload to same-origin application endpoints.

import {
ModernMediasfuGeneric,
type CreateRoomOnMediaSFUType,
type JoinRoomOnMediaSFUType,
} from 'mediasfu-reactjs';

async function postRoomRequest<Result>(path: string, payload: unknown) {
const response = await fetch(path, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Room request failed with status ${response.status}`);
}
return response.json() as Promise<Result>;
}

const createRoomViaBackend: CreateRoomOnMediaSFUType = ({ payload }) =>
postRoomRequest('/api/mediasfu/rooms', payload);

const joinRoomViaBackend: JoinRoomOnMediaSFUType = ({ payload }) =>
postRoomRequest('/api/mediasfu/room-memberships', payload);

export function Room() {
return (
<ModernMediasfuGeneric
connectMediaSFU
createMediaSFURoom={createRoomViaBackend}
joinMediaSFURoom={joinRoomViaBackend}
/>
);
}

This path owns the room connection, media transports, participant state, permission prompts, supplied controls, stream preparation, layout, and paging. Use it to prove create, join, camera, microphone, remote media, screen share, leave, and host end before replacing UI.

Success is visible: two authorized people appear in the same room, allowed media starts after a user action, remote media renders, and leaving removes the participant from the other person's view.

Brand the supplied grids without taking over the room

UI overrides keep the room runtime and its prepared nodes. Your replacement receives the same props as the supplied component.

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

function BrandedAudioGrid(props: ComponentProps<typeof AudioGrid>) {
return (
<section aria-label="Participants with audio only">
<AudioGrid {...props} />
</section>
);
}

function BrandedVideoGrid(props: ComponentProps<typeof FlexibleGrid>) {
return (
<section aria-label="Participant videos">
<FlexibleGrid {...props} backgroundColor="#101827" />
</section>
);
}

function BrandedPagination(props: ComponentProps<typeof Pagination>) {
return (
<nav aria-label="Room pages">
<Pagination {...props} />
</nav>
);
}

const uiOverrides: MediasfuUICustomOverrides = {
audioGrid: { component: BrandedAudioGrid },
flexibleGrid: { component: BrandedVideoGrid },
flexibleGridAlt: { component: BrandedVideoGrid },
pagination: { component: BrandedPagination },
};

export function BrandedRoom() {
return <ModernMediasfuGeneric connectMediaSFU uiOverrides={uiOverrides} />;
}

Add the secure create and join callbacks from the first example when this room must create or join MediaSFU cloud rooms.

Build a room workspace from prepared React nodes

Use customComponent when the main room workspace must be yours. The room runtime supplies audioOnlyStreams and otherGridStreams as React nodes that already represent its current media state. Do not treat these arrays as raw MediaStream objects.

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

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

function Workspace({ parameters }: { parameters: WorkspaceParameters }) {
const audioNodes = parameters.audioOnlyStreams ?? [];
const videoNodes = parameters.otherGridStreams?.[0] ?? [];
const columns = Math.max(1, Math.min(3, videoNodes.length));
const rows = Math.max(1, Math.ceil(videoNodes.length / columns));
const totalPages = Math.max(1, parameters.numberPages ?? 1);
const currentPage = Math.min(
totalPages - 1,
Math.max(0, parameters.currentUserPage ?? 0),
);

return (
<main>
<AudioGrid componentsToRender={audioNodes} />
<FlexibleGrid
customWidth={960}
customHeight={540}
rows={rows}
columns={columns}
componentsToRender={videoNodes}
emptyCellFallback={<div aria-hidden="true" />}
/>
{totalPages > 1 ? (
<Pagination
totalPages={totalPages}
currentUserPage={currentPage}
parameters={parameters}
position="middle"
location="bottom"
direction="horizontal"
showAspect
/>
) : null}
</main>
);
}

export function CustomRoom() {
return <ModernMediasfuGeneric connectMediaSFU customComponent={Workspace} />;
}

Pagination needs the complete current room parameters. Those parameters also carry breakout-room and navigation behavior; do not create a partial object just to make the component render. Let the active room runtime supply it.

Render a raw stream in your own element

Choose this path when your application already owns the correct consumed or local stream. Remote and local cleanup are different:

  • detach a remote stream from your element, but do not stop tracks owned by the room runtime;
  • stop tracks that your application created and owns, such as a local preview;
  • handle browser autoplay rejection with a visible play action.
import { useEffect, useRef } from 'react';
import type { Stream } from 'mediasfu-reactjs';

export function RemoteVideo({ item }: { item: Stream }) {
const ref = useRef<HTMLVideoElement>(null);

useEffect(() => {
const video = ref.current;
if (!video || !item.stream) return;

video.srcObject = item.stream;
void video.play().catch(() => {
// Show a visible Play button when the browser blocks autoplay.
});

return () => {
video.srcObject = null;
};
}, [item.stream]);

return (
<video
ref={ref}
aria-label={item.name ?? 'Remote participant'}
playsInline
autoPlay
/>
);
}

Paused, muted, missing, and off-page media

These states need different UI:

StateWhat the person should seeWhat your app should do
Muted microphoneMuted indicator and participant identityKeep the audio element/state available; do not show it as a network failure
Camera stoppedAvatar or camera-off placeholderRemove or hide the video track without removing the participant
Consumer pausedLoading or paused stateResume through the current room runtime when allowed
Autoplay blockedPlay audio/video actionRetry playback only after that user action
Participant is on another pagePage count and navigationKeep room state; render only the active page's prepared nodes
Screen share endedReturn to camera layoutRemove the screen tile without stopping unrelated camera/audio tracks

Do not use track.enabled, track.muted, and producer/consumer paused state as interchangeable signals. Observe the room state that owns the track.

Screen sharing and annotation are separate

clickScreenShare starts or stops screen capture for the active room. A screen picker cancellation is not an error that should trigger an automatic retry.

Screenboard and ScreenboardModal provide a board-oriented experience, and Whiteboard with ConfigureWhiteboardModal provides a collaborative board. They are not proof that every captured screen can be annotated. If your product adds drawing over a shared screen, own the overlay, pointer mapping, permission rules, and cleanup, then publish that as an application feature.

Moderation and breakout rooms

Use the supplied participant, waiting, request, panelist, permission, and breakout-room surfaces from the same live room. They carry the active person's role and the room's current state.

  • Remove disconnects someone from the current room. Ban is a separate action: use the authorized exit operation with ban: true when the product intends to prevent the same room identity from joining again. Bind that identity to an authenticated account and do not let a banned person evade the rule by choosing another username.
  • Muting or stopping participant media is a current-room media action, not a permanent restriction.
  • Co-host, panelist, and permission controls are distinct; do not present them as one generic role editor.
  • A breakout assignment or update is complete only after the room reports the new assignment. Keep the host workspace mounted while people move.

See React participant workspaces and React collaboration controls for the complete supplied surfaces and confirmation flows.

Recovery and cleanup

  • Keep the room mounted while transports and tracks are active.
  • Show denied permission, cancelled capture, autoplay blocked, and connection loss as different recoverable states.
  • On participant leave, wait for the participant list to reflect the change.
  • On host end, use the semantic host-end action and show everyone the ended state; do not relabel an ordinary participant leave as host end.
  • Stop application-created local tracks, remove application listeners, detach media elements, and let the MediaSFU room dispose what it owns.

Before release

  • Create and join use your backend and contain no client-side MediaSFU key.
  • Two people can produce, consume, and render camera and microphone media.
  • Audio-only participants remain audible without a video tile.
  • More participants than one page can be reached with pagination.
  • Muted, camera-off, paused, autoplay-blocked, and disconnected states are visually different.
  • Screen-share start, cancel, stop, and permission loss are tested.
  • Remote room tracks and application-owned local tracks have the correct cleanup behavior.
  • Remove, media control, breakout moves, participant leave, and host end each produce the expected current-room result.

The complete example files used by this guide are in the examples/react-media-rendering folder of the documentation project. They type-check against mediasfu-reactjs@4.2.9; test them with two authorized people in your supported browsers before release.