Разработка комнаты ожидания для видеоконференций
Комната ожидания позволяет ведущему контролировать, кто войдёт в конференцию. Участник видит экран ожидания, ведущий получает уведомление и принимает решение — впустить или отклонить.
Реализация через LiveKit Permissions
LiveKit поддерживает изменение прав участника на лету. Участник из лобби получает токен с canPublish: false, а ведущий повышает права через 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,
});
// Уведомить участника через Data message
await svc.sendData(
roomName,
Buffer.from(JSON.stringify({ type: 'admitted' })),
DataPacket_Kind.RELIABLE,
[lobbyIdentity]
);
}
Экран ожидания для участника
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();
}
if (msg.type === 'denied') {
// Показать сообщение об отказе
}
});
// Таймер ожидания
const timer = setInterval(() => setWaitTime(t => t + 1), 1000);
return () => { clearInterval(timer); room.disconnect(); };
}, []);
const minutes = Math.floor(waitTime / 60);
const seconds = waitTime % 60;
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">
<div className="w-20 h-20 rounded-full bg-blue-600 flex items-center justify-center mx-auto mb-6">
<span className="text-3xl">{displayName[0].toUpperCase()}</span>
</div>
<h2 className="text-2xl font-semibold mb-2">Подождите немного...</h2>
<p className="text-gray-400 mb-6">
Ведущий скоро впустит вас. Время ожидания:{' '}
<span className="text-white">{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}</span>
</p>
<div className="flex justify-center gap-1">
{[0, 1, 2].map(i => (
<div key={i}
className="w-2 h-2 rounded-full bg-blue-500 animate-bounce"
style={{ animationDelay: `${i * 150}ms` }}
/>
))}
</div>
</div>
</div>
);
}
Панель ведущего — управление лобби
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, requestedAt: new Date() }
]);
}
});
}, [room]);
const admit = async (identity: string) => {
await fetch(`/api/rooms/${room.name}/admit/${identity}`, { method: 'POST' });
setLobbyRequests(prev => prev.filter(r => r.identity !== identity));
};
const deny = async (identity: string) => {
await fetch(`/api/rooms/${room.name}/deny/${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">Ожидают входа ({lobbyRequests.length})</h3>
<div className="space-y-3">
{lobbyRequests.map(req => (
<div key={req.identity} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-700">
{req.displayName[0]}
</div>
<span className="flex-1 text-sm text-gray-800 truncate">{req.displayName}</span>
<button onClick={() => admit(req.identity)}
className="text-xs px-2 py-1 bg-green-600 text-white rounded">
Впустить
</button>
<button onClick={() => deny(req.identity)}
className="text-xs px-2 py-1 bg-gray-200 text-gray-700 rounded">
Отклонить
</button>
</div>
))}
</div>
</div>
);
}
Сроки
Комната ожидания с экраном участника и панелью ведущего — 1–2 дня.







