Реализация групповых видеоконференций на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация групповых видеоконференций на сайте
Сложная
~2-4 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка системы групповых видеоконференций на сайте

Групповые видеоконференции — это 3–50+ участников, управление микрофоном/камерой ведущим, рука вверх, брейкаут-комнаты, чат, демонстрация экрана, запись. Всё это требует продуманной архитектуры как на стороне WebRTC-инфраструктуры, так и на UI.

Выбор инфраструктуры по масштабу

Участников Рекомендованное решение
2–10 LiveKit (SFU), Daily.co
10–50 LiveKit с Simulcast, 100ms
50–1000 LiveKit Broadcast, Agora, Amazon Chime
1000+ HLS-трансляция, не WebRTC

LiveKit — рекомендованная основа

npm install livekit-server-sdk  # сервер
npm install @livekit/components-react livekit-client  # клиент

Создание конференции

import { RoomServiceClient, AccessToken, RoomOptions } from 'livekit-server-sdk';

const svc = new RoomServiceClient(
  process.env.LIVEKIT_URL!,
  process.env.LIVEKIT_API_KEY!,
  process.env.LIVEKIT_API_SECRET!
);

async function createConference(conferenceId: string, options: {
  maxParticipants?: number;
  enableRecording?: boolean;
}): Promise<void> {
  await svc.createRoom({
    name: `conf-${conferenceId}`,
    maxParticipants: options.maxParticipants ?? 50,
    emptyTimeout: 300,  // 5 мин до закрытия пустой комнаты
    metadata: JSON.stringify({ conferenceId, createdAt: new Date().toISOString() }),
  } as RoomOptions);
}

function generateParticipantToken(
  roomName: string,
  userId: string,
  displayName: string,
  role: 'host' | 'moderator' | 'participant' | 'viewer'
): string {
  const at = new AccessToken(
    process.env.LIVEKIT_API_KEY!,
    process.env.LIVEKIT_API_SECRET!,
    { identity: userId, name: displayName, ttl: 4 * 60 * 60 }
  );

  at.addGrant({
    roomJoin: true,
    room: roomName,
    canPublish: role !== 'viewer',
    canSubscribe: true,
    canPublishData: true,
    roomAdmin: role === 'host',
    // Moderator может mute других
    hidden: false,
  });

  return at.toJwt();
}

Управление участниками с сервера

// Заглушить конкретного участника (по запросу ведущего)
app.post('/api/conferences/:roomName/mute/:participantId', authenticate, async (req, res) => {
  const conference = await db.conferences.findByRoomName(req.params.roomName);
  if (conference.hostId !== req.user.id) return res.status(403).end();

  await svc.mutePublishedTrack(
    req.params.roomName,
    req.params.participantId,
    'microphone-track',
    true
  );

  // Уведомить через Data channel
  await svc.sendData(
    req.params.roomName,
    Buffer.from(JSON.stringify({ type: 'muted_by_host', targetId: req.params.participantId })),
    [req.params.participantId]
  );

  res.json({ ok: true });
});

// Удалить участника
app.delete('/api/conferences/:roomName/participants/:participantId', authenticate, async (req, res) => {
  await svc.removeParticipant(req.params.roomName, req.params.participantId);
  res.json({ ok: true });
});

React компонент конференции

import {
  LiveKitRoom,
  VideoConference,
  useLocalParticipant,
  useRoomContext,
  useParticipants,
  Chat,
  RoomAudioRenderer,
  ControlBar,
} from '@livekit/components-react';
import '@livekit/components-styles';

function GroupConference({ token, roomName }: { token: string; roomName: string }) {
  return (
    <LiveKitRoom
      token={token}
      serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
      video={true}
      audio={true}
      style={{ height: '100vh' }}
      data-lk-theme="default"
    >
      <ConferenceLayout roomName={roomName} />
      <RoomAudioRenderer />
    </LiveKitRoom>
  );
}

function ConferenceLayout({ roomName }: { roomName: string }) {
  const participants = useParticipants();
  const { localParticipant } = useLocalParticipant();
  const room = useRoomContext();
  const [showChat, setShowChat] = useState(false);
  const [raisedHands, setRaisedHands] = useState<Set<string>>(new Set());

  // Поднять руку через Data channel
  const toggleRaiseHand = async () => {
    const isRaised = raisedHands.has(localParticipant.identity);
    const message = { type: 'hand_raise', raised: !isRaised, identity: localParticipant.identity };
    await room.localParticipant.publishData(
      new TextEncoder().encode(JSON.stringify(message)),
      { reliable: true }
    );
  };

  // Получать Data messages
  useEffect(() => {
    const handler = (payload: Uint8Array) => {
      const msg = JSON.parse(new TextDecoder().decode(payload));
      if (msg.type === 'hand_raise') {
        setRaisedHands(prev => {
          const next = new Set(prev);
          msg.raised ? next.add(msg.identity) : next.delete(msg.identity);
          return next;
        });
      }
    };
    room.on('dataReceived', handler);
    return () => { room.off('dataReceived', handler); };
  }, [room]);

  return (
    <div className="flex h-full">
      {/* Основная сетка видео */}
      <div className="flex-1 relative">
        <div className={`grid ${getGridLayout(participants.length)} gap-2 p-4 h-full`}>
          {participants.map(p => (
            <ParticipantTile
              key={p.identity}
              participant={p}
              hasRaisedHand={raisedHands.has(p.identity)}
            />
          ))}
        </div>

        {/* Панель управления */}
        <div className="absolute bottom-0 left-0 right-0 flex justify-center pb-4 gap-3">
          <ControlBar
            controls={{
              microphone: true,
              camera: true,
              screenShare: true,
              leave: true,
            }}
          />
          <button
            onClick={toggleRaiseHand}
            className={`p-3 rounded-full ${raisedHands.has(localParticipant.identity) ? 'bg-yellow-500' : 'bg-gray-700'}`}
          >
            ✋
          </button>
          <button onClick={() => setShowChat(!showChat)} className="p-3 rounded-full bg-gray-700">
            💬 {participants.length}
          </button>
        </div>
      </div>

      {/* Чат */}
      {showChat && (
        <div className="w-80 border-l border-gray-700">
          <Chat />
        </div>
      )}
    </div>
  );
}

function getGridLayout(count: number): string {
  if (count <= 1) return 'grid-cols-1';
  if (count <= 4) return 'grid-cols-2';
  if (count <= 9) return 'grid-cols-3';
  return 'grid-cols-4';
}

Simulcast для адаптивного качества

LiveKit автоматически публикует 3 слоя качества (high/medium/low). Подписчики получают нужный слой в зависимости от пропускной способности и размера плитки:

// На стороне клиента — явно задать качество для конкретного участника
participant.setTrackSubscriptionPermissions(true, [
  { trackSid: videoTrack.sid, quality: VideoQuality.MEDIUM }
]);

Брейкаут-комнаты

async function createBreakoutRooms(mainRoomName: string, groups: string[][]) {
  const breakoutRooms = await Promise.all(
    groups.map((group, i) =>
      createConference(`${mainRoomName}-breakout-${i}`, { maxParticipants: group.length + 2 })
    )
  );

  // Уведомить участников об их комнате
  for (let i = 0; i < groups.length; i++) {
    for (const participantId of groups[i]) {
      const token = generateParticipantToken(
        `conf-${mainRoomName}-breakout-${i}`,
        participantId,
        '',
        'participant'
      );
      await svc.sendData(
        `conf-${mainRoomName}`,
        Buffer.from(JSON.stringify({ type: 'breakout_invite', token, roomIndex: i })),
        [participantId]
      );
    }
  }
}

Сроки

Базовая групповая конференция с LiveKit + React компонентами — 1 неделя. С брейкаут-комнатами, поднятием руки, записью и управлением участниками — 2–3 недели.