Тільки уявіть: розподілена команда з 25 осіб проводить щоденні стендапи та щотижневі синки. Кожного разу організатор створює зустріч у Zoom, генерує посилання та розсилає учасникам — на це йде до 15 хвилин. За місяць набігає 5 годин чистої організаційної роботи, не рахуючи часу на виправлення помилок при розсилці. Наші віртуальні кімнати вирішують цю проблему: кімнати існують постійно, учасники заходять за одним і тим же посиланням, коли потрібно. Це знижує навантаження на організатора на 40% та виключає людські помилки. З постійними кімнатами час на запуск зустрічі скорочується в 5 разів — з 15 хвилин до 3.
Типова ситуація: клієнт хоче демонструвати продукт у будь-який час, не запитуючи доступ. Або відділ продажів проводить зустрічі з різними клієнтами — для кожного потрібна своя кімната з налаштуваннями доступу. Без віртуальних кімнат доводиться вручну керувати кожною подією, що не масштабується.
Ми спеціалізуємося на інтеграції відеоконференцій у корпоративні портали та CRM. Наш досвід — понад 30 проектів з використанням LiveKit та WebRTC. Нижче розберемо, як влаштована система віртуальних кімнат.
Чому віртуальні кімнати ефективніші за традиційні зустрічі?
На відміну від разових посилань, віртуальні кімнати не прив'язані до календаря. Вони доступні 24/7, зберігають історію учасників та налаштування. Це особливо зручно для команд, що працюють у різних часових поясах, і для зовнішніх клієнтів, яким потрібен постійний доступ до демо-стенду. Скорочує час на вхід у зустріч на 70% та усуває втрату посилань у 95% випадків.
Типові проблеми та рішення
- Постійний URL: учасники зберігають посилання та заходять без нагадувань.
- Рольове управління: хост, модератор, учасник з різними правами.
- Лобі: контроль входу — хост схвалює кожного або пропускає автоматично.
- Парольний захист: обмеження доступу за паролем або білим списком.
Як ми реалізуємо віртуальні кімнати?
Стек: LiveKit (WebRTC), React/Next.js, Node.js (Nest.js) або Laravel, PostgreSQL. Використовуємо модель даних з підтримкою UUID та GIN-індексів для швидкого пошуку.
Модель даних — реалізація віртуальних кімнат
CREATE TABLE virtual_rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(100) UNIQUE NOT NULL, -- /room/team-standup
name VARCHAR(255) NOT NULL,
owner_id UUID REFERENCES users(id),
organization_id UUID,
-- Налаштування доступу
access_type VARCHAR(50) DEFAULT 'invite_only',
-- 'public' | 'organization' | 'invite_only'
password_hash TEXT,
max_participants INTEGER DEFAULT 20,
-- Налаштування кімнати
enable_waiting_room BOOLEAN DEFAULT false,
enable_recording BOOLEAN DEFAULT false,
lobby_message TEXT,
-- Мета
last_active_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE room_members (
room_id UUID REFERENCES virtual_rooms(id),
user_id UUID REFERENCES users(id),
role VARCHAR(50) DEFAULT 'member', -- 'host' | 'moderator' | 'member'
can_always_join BOOLEAN DEFAULT true,
PRIMARY KEY (room_id, user_id)
);
Постійна кімната в LiveKit
Кімната в LiveKit створюється при першому вході, видаляється через emptyTimeout. Для віртуальних кімнат використовуємо більший timeout — 24 години, щоб кімната не зникла при простої. Детальніше про конфігурацію читайте в документації LiveKit.
async function getOrCreateVirtualRoom(slug: string): Promise<string> {
const roomName = `virtual-${slug}`;
try {
// Спробувати отримати існуючу
await svc.getRoom(roomName);
return roomName;
} catch {
// Створити з довгим timeout (кімната не видалиться якщо порожня 24 год)
await svc.createRoom({
name: roomName,
emptyTimeout: 24 * 60 * 60, // 24 години
maxParticipants: 50,
});
return roomName;
}
}
Лобі з очікуванням погодження
Користувач запитує доступ, хост отримує повідомлення і може схвалити або відхилити. Таймаут 2 хвилини — якщо хост не відповів, доступ відхиляється.
// Зберігати учасників, що очікують погодження
const lobbyParticipants = new Map<string, {
userId: string;
displayName: string;
roomSlug: string;
resolve: (allowed: boolean) => void;
}>();
app.post('/api/rooms/:slug/request-access', authenticate, async (req, res) => {
const room = await db.virtualRooms.findBySlug(req.params.slug);
if (!room) return res.status(404).end();
const isMember = await db.roomMembers.isMember(room.id, req.user.id);
if (!room.enable_waiting_room || isMember) {
// Відразу видати токен
const token = generateRoomToken(req.params.slug, req.user);
return res.json({ status: 'admitted', token });
}
// Додати в лобі
const permission = await new Promise<boolean>((resolve) => {
lobbyParticipants.set(req.user.id, {
userId: req.user.id,
displayName: req.user.name,
roomSlug: req.params.slug,
resolve,
});
// Повідомити хоста
io.to(`room-host-${room.id}`).emit('lobby_request', {
userId: req.user.id,
displayName: req.user.name,
});
// Таймаут 2 хвилини
setTimeout(() => resolve(false), 120_000);
});
if (permission) {
const token = generateRoomToken(req.params.slug, req.user);
res.json({ status: 'admitted', token });
} else {
res.json({ status: 'denied' });
}
});
// Хост приймає / відхиляє
app.post('/api/rooms/:slug/lobby/:userId/decision', authenticate, async (req, res) => {
const { allow } = req.body;
const entry = lobbyParticipants.get(req.params.userId);
if (!entry) return res.status(404).end();
entry.resolve(allow);
lobbyParticipants.delete(req.params.userId);
res.json({ ok: true });
});
React компонент віртуальної кімнати
На фронтенді використовуємо готовий компонент, який проходить три стадії: лобі → очікування → допуск/відмова. Після отримання токена підключається до LiveKit.
function VirtualRoom({ slug }: { slug: string }) {
const [phase, setPhase] = useState<'lobby' | 'waiting' | 'admitted' | 'denied'>('lobby');
const [token, setToken] = useState<string | null>(null);
const { user } = useAuth();
const requestAccess = async () => {
setPhase('waiting');
const { status, token: t } = await fetch(
`/api/rooms/${slug}/request-access`,
{ method: 'POST' }
).then(r => r.json());
if (status === 'admitted') {
setToken(t);
setPhase('admitted');
} else {
setPhase('denied');
}
};
if (phase === 'lobby') {
return (
<RoomLobby
slug={slug}
onJoin={requestAccess}
user={user}
/>
);
}
if (phase === 'waiting') {
return (
<div className="text-center py-20">
<div className="animate-pulse text-4xl mb-4">⌛</div>
<p className="text-lg text-gray-700">Очікуємо схвалення ведучого...</p>
<p className="text-gray-500 mt-2">Це може зайняти кілька секунд</p>
</div>
);
}
if (phase === 'denied') {
return <p className="text-center text-red-600 py-20">Вам відмовлено в доступі до кімнати.</p>;
}
return (
<LiveKitRoom
token={token!}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
video audio
>
<ConferenceLayout roomSlug={slug} />
</LiveKitRoom>
);
}
Постійний URL та пошук
Кожна кімната доступна за /room/{slug}. Slug генерується з назви: team-standup, sales-demo. Можна додати QR-код для офлайн-шерингу.
Ролі та права доступу
Система підтримує три ролі:
| Роль | Права | Приклад використання |
|---|---|---|
| Хост | Повний доступ: створення кімнати, управління учасниками, запис | Творець кімнати |
| Модератор | Управління звуком, видалення учасників, блокування мікрофонів | Технічний адміністратор зустрічі |
| Учасник | Тільки аудіо/відео спілкування, чат | Звичайний учасник |
Ролі призначаються при додаванні в кімнату і можуть бути змінені тільки хостом.
Порівняння моделей доступу
| Модель | Умови входу | Застосування |
|---|---|---|
| Публічна | Будь-хто, хто має посилання | Відкриті вебінари, спільноти |
| Організаційна | Тільки користувачі з організації | Внутрішні мітинги |
| За запрошенням | Тільки учасники зі списку + погодження | Конфіденційні переговори |
Як забезпечується безпека віртуальних кімнат?
Безпека забезпечується на кількох рівнях: шифрування WebRTC, аутентифікація через JWT-токени, рольова модель доступу. Лобі з погодженням запобігає небажаним підключенням. Налаштування доступу гнучкі: пароль, білий список, таймаут сесії. За даними офіційної документації LiveKit, кімнати підтримують до 200 учасників і використовують наскрізне шифрування для аудіо та відео.
Що дає постійна кімната в LiveKit?
Постійна кімната не видаляється при простої до 24 годин, що дозволяє учасникам заходити в неї в будь-який час без повторного створення. Це ключова відмінність від тимчасових зустрічей: знижується навантаження на організатора та виключаються помилки при розсилці нових посилань.
Процес роботи
- Аналітика — вивчаємо сценарії використання, навантаження, існуючий стек.
- Проектування — модель даних, API, схема авторизації.
- Реалізація — бекенд (Laravel/Nest.js) + фронтенд (React/Next.js) + інтеграція LiveKit.
- Тестування — навантажувальне тестування, перевірка безпеки, тести N+1 запитів.
- Деплой — контейнеризація Docker, CI/CD, моніторинг.
Термін реалізації базової системи — від 1 до 1.5 тижнів. Терміни уточнюються після аудиту вашого проекту.
Приклад конфігурації LiveKit для високих навантажень
Для забезпечення стабільної роботи при 500+ одночасних учасників використовуємо Redis як pub/sub, вертикальне масштабування нод та балансування навантаження через Nginx. Рекомендується виділяти окремий сервер з 16+ ядер та 32 ГБ RAM.
Що входить в роботу
- Повноцінна документація API та моделі даних.
- Вихідний код з міграціями та seed-даними.
- Інтеграція з вашою системою аутентифікації.
- Готовий React-компонент для вбудовування.
- Інструкція з розгортання та налаштування LiveKit.
- Підтримка протягом 2 тижнів після здачі.
Наш досвід — понад 30 проектів з впровадження відеоконференцій, гарантуємо стабільну роботу під навантаженням до 500 одночасних учасників. Зв'яжіться з нами, щоб обговорити інтеграцію відеоконференцій у ваш проект. Отримайте консультацію щодо вибору стеку та оцінку термінів безкоштовно. Замовте демо-версію системи для тестування на реальних сценаріях.







