Skip to main content

Tutorial - TikTok Live Guest Queue

Use this tutorial when your product is host-first and revolves around guest requests, admission, and one or more spotlight guests.

Outcome

By the end, you will have:

  • a host screen with a live spotlight area
  • a visible guest queue model
  • a safe path from MediaSFU request state into your own layout

Visual target

TikTok Live-style MediaSFU guest queue preview

Use this as the first layout target: a host-first live surface, vertical action controls, and a visible request queue for admitting guests. MediaSFU should continue to own request, permission, and room state while your app owns the queue presentation.

Best starting path

Start with:

  1. Choose your UI mode
  2. Secure backend proxy
  3. Media lifecycle
  4. Custom component replacement

This product shape usually needs more than card-level overrides because the host queue is part of the main product experience.

Two valid angles for the same product shape

This tutorial's main implementation uses returnUI={true} with customComponent, but the same guest-queue experience can also be built headless.

AngleWhat you keepUse it when
returnUI={true} + customComponentMediaSFU runtime shell with your queue UI as the visible workspaceYou want to move quickly while keeping MediaSFU's room entry flow and runtime presentation contract intact.
returnUI={false} + headless shellYour own queue screen rendered from sourceParameters outside the stock room UIYou want the host app shell, queue layout, and promotion flows to live entirely in your app from the first visible screen.

Headless angle for this same queue pattern:

<>
<MediasfuGeneric
connectMediaSFU={true}
returnUI={false}
noUIPreJoinOptions={noUIPreJoinOptions}
sourceParameters={sourceParameters}
updateSourceParameters={setSourceParameters}
createMediaSFURoom={createMediaSFURoom}
joinMediaSFURoom={joinMediaSFURoom}
/>
<TikTokLiveQueue parameters={sourceParameters} />
</>

Choose the true angle when you want a fast custom workspace around MediaSFU's existing flow. Choose the false angle when the queue, spotlight, and host shell need to remain fully app-owned at every stage. The Tutorial - Support AI Console page is the closest headless reference.

Copy/paste starter: React guest queue shell

This starter gives you the shape of a live host screen. It intentionally keeps admission UI local first; wire each action to your MediaSFU request and permission flow after the baseline room connects.

Create src/TikTokLiveGuestQueueRoom.tsx, paste the component below, and keep your backend /api/mediasfu/create-room and /api/mediasfu/join-room routes forwarding to the same upstream MediaSFU Cloud rooms URL from Secure backend proxy.

import { useMemo, useState } from 'react';
import {
MediasfuGeneric,
type CreateMediaSFURoomOptions,
type JoinMediaSFURoomOptions,
} from 'mediasfu-reactjs';

type RoomResult = { data: Record<string, unknown> | null; success: boolean };
type Guest = { id: string; name: string; note: string };

async function createMediaSFURoom({ payload }: { payload: CreateMediaSFURoomOptions }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

async function joinMediaSFURoom({ payload }: { payload: JoinMediaSFURoomOptions }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

function TikTokLiveQueue({ parameters }: { parameters: any }) {
const [activeGuest, setActiveGuest] = useState<Guest | null>(null);
const guestQueue: Guest[] = (parameters.requests ?? []).map((request: any, index: number) => ({
id: request.id ?? String(index),
name: request.name ?? request.userName ?? `Guest ${index + 1}`,
note: request.type ?? 'Requested to join',
}));

return (
<main style={{ minHeight: '100vh', display: 'grid', gridTemplateColumns: '1fr 320px', background: '#09090b', color: '#fafafa' }}>
<section style={{ padding: 20, display: 'grid', gap: 16 }}>
<div style={{ minHeight: 520, borderRadius: 8, background: '#18181b', display: 'grid', placeItems: 'center' }}>
<div style={{ textAlign: 'center' }}>
<p style={{ margin: 0, color: '#f43f5e' }}>Live spotlight</p>
<h1 style={{ margin: '8px 0' }}>{activeGuest?.name ?? 'Host only'}</h1>
<p>{parameters.roomName ?? 'Connecting room...'}</p>
</div>
</div>

<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<button onClick={() => parameters.clickVideo?.({ parameters })}>Camera</button>
<button onClick={() => parameters.clickAudio?.({ parameters })}>Mic</button>
<button onClick={() => setActiveGuest(null)}>Clear guest</button>
</div>
</section>

<aside style={{ padding: 20, borderLeft: '1px solid #3f3f46' }}>
<h2>Guest queue</h2>
<div style={{ display: 'grid', gap: 10 }}>
{guestQueue.length === 0 ? <p>No guest requests yet.</p> : null}
{guestQueue.map((guest) => (
<button
key={guest.id}
onClick={() => setActiveGuest(guest)}
style={{ textAlign: 'left', padding: 12, borderRadius: 8, border: '1px solid #52525b', background: '#18181b', color: '#fafafa' }}
>
<strong>{guest.name}</strong>
<br />
<span>{guest.note}</span>
</button>
))}
</div>
</aside>
</main>
);
}

export function TikTokLiveGuestQueueRoom() {
const noUIPreJoinOptions = useMemo(
() => ({
action: 'create',
eventType: 'webinar',
userName: 'Host',
capacity: 50,
duration: 60,
}),
[]
);

return (
<MediasfuGeneric
connectMediaSFU={true}
returnUI={true}
noUIPreJoinOptions={noUIPreJoinOptions}
createMediaSFURoom={createMediaSFURoom}
joinMediaSFURoom={joinMediaSFURoom}
customComponent={TikTokLiveQueue}
/>
);
}

Render it from your app entry:

import { TikTokLiveGuestQueueRoom } from './TikTokLiveGuestQueueRoom';

export default function App() {
return <TikTokLiveGuestQueueRoom />;
}
About the helper buttons

These controls are intentionally the lowest-level wiring. In customComponent, the parameters prop is the live MediaSFU helper bundle, so direct calls like clickVideo and clickAudio are fine for proving the workflow quickly.

When MediaSFU already ships the surface you need, prefer reusing it instead of rebuilding it. Recording, chat, and similar modal-driven flows are usually better handled with stock components or uiOverrides while you keep the runtime behavior intact. Read Custom component replacement, Media lifecycle, and UI overrides before polishing the host controls.

What to wire next

  1. Replace the local setActiveGuest action with your real admit or promote flow.
  2. Use MediaSFU request and permission state as the source of truth for the queue.
  3. Add layout rules for one guest, two guests, and host-only fallback.
  4. Test request churn, guest disconnect, and reconnect behavior.