Розробка комнати очікування для відеоконференцій
Комната очікування дозволяє ведучому контролювати, хто впадеть в конференцію. Учасник бачить екран очікування, ведучий отримує сповіщення та приймає рішення — впустити або відхилити.
Реалізація через 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,
});
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();
});
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">Подождите...</h2>
<p className="text-gray-400 mb-6">
Ведущий скоро впустит вас. Время ожидания: {Math.floor(waitTime / 60)}:{String(waitTime % 60).padStart(2, '0')}
</p>
</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 }
]);
}
});
}, [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">Ожидают входа ({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">
Впустити
</button>
</div>
))}
</div>
</div>
);
}
Терміни
Комната очікування з екраном учасника та панеллю ведучого — 1–2 дні.







