Waiting Room Development for Video Conferencing
Waiting room allows host to control who enters the conference. Participant sees waiting screen, host gets notified and approves or rejects.
LiveKit Permissions Implementation
LiveKit supports real-time permission changes. Lobby participant gets token with canPublish: false, then host upgrades via API.
function generateLobbyToken(roomName: string, userId: string, displayName: string): string {
const at = new AccessToken(
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!,
{ identity: `lobby-${userId}`, name: displayName }
);
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: false,
canSubscribe: false,
canPublishData: true,
});
return at.toJwt();
}
async function admitParticipant(roomName: string, lobbyIdentity: string): Promise<void> {
await svc.updateParticipant(roomName, lobbyIdentity, undefined, {
canPublish: true,
canSubscribe: true,
});
await svc.sendData(
roomName,
Buffer.from(JSON.stringify({ type: 'admitted' })),
DataPacket_Kind.RELIABLE,
[lobbyIdentity]
);
}
Waiting Screen Component
function WaitingRoom({ roomName, userId, displayName, onAdmitted }) {
const [waitTime, setWaitTime] = useState(0);
useEffect(() => {
const room = new Room();
room.connect(process.env.NEXT_PUBLIC_LIVEKIT_URL, lobbyToken);
room.on('connected', async () => {
await room.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify({
type: 'lobby_request',
userId,
displayName,
})),
{ reliable: true }
);
});
room.on('dataReceived', (payload) => {
const msg = JSON.parse(new TextDecoder().decode(payload));
if (msg.type === 'admitted') onAdmitted();
});
const timer = setInterval(() => setWaitTime(t => t + 1), 1000);
return () => { clearInterval(timer); room.disconnect(); };
}, []);
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center text-white">
<div className="text-center max-w-md px-6">
<h2 className="text-2xl font-semibold mb-2">Wait a moment...</h2>
<p className="text-gray-400 mb-6">
Host will admit you soon. Waiting time: {Math.floor(waitTime / 60)}:{String(waitTime % 60).padStart(2, '0')}
</p>
</div>
</div>
);
}
Host Control Panel
function HostLobbyPanel({ room }: { room: Room }) {
const [lobbyRequests, setLobbyRequests] = useState<LobbyRequest[]>([]);
useEffect(() => {
room.on('dataReceived', (payload, participant) => {
const msg = JSON.parse(new TextDecoder().decode(payload));
if (msg.type === 'lobby_request') {
setLobbyRequests(prev => [
...prev,
{ identity: participant?.identity ?? '', displayName: msg.displayName }
]);
}
});
}, [room]);
const admit = async (identity: string) => {
await fetch(`/api/rooms/${room.name}/admit/${identity}`, { method: 'POST' });
setLobbyRequests(prev => prev.filter(r => r.identity !== identity));
};
if (lobbyRequests.length === 0) return null;
return (
<div className="absolute top-4 right-4 w-72 bg-white rounded-xl shadow-lg p-4 z-50">
<h3 className="font-semibold text-gray-800 mb-3">Waiting ({lobbyRequests.length})</h3>
<div className="space-y-3">
{lobbyRequests.map(req => (
<div key={req.identity} className="flex items-center gap-3">
<span className="flex-1 text-sm text-gray-800">{req.displayName}</span>
<button onClick={() => admit(req.identity)}
className="text-xs px-2 py-1 bg-green-600 text-white rounded">
Admit
</button>
</div>
))}
</div>
</div>
);
}
Timeline
Waiting room with participant screen and host panel—1–2 days.







