23 KiB
Интерфейс комнаты конференции
Ссылки: frontend/src/pages/RoomPage.tsx, frontend/src/components/room/*, @livekit/components-react, LiveKit JS SDK
Обзор
Комната конференции — это отдельный замкнутый UI с собственной тёмной темой (§ frontend-themes.md):
- Диалог настроек устройств (микрофон, камера) с персист выбора
- Аватары участников (или инициалы при отсутствии)
- Fullscreen API (кнопка)
- Мини-плеер: Document Picture-in-Picture для активной плитки (Chrome/Edge 116+) с фолбэком на Video Picture-in-Picture (Safari/Firefox)
- Демонстрация экрана/окна/вкладки (любой участник, focus-раскладка, last-wins, звук где браузер отдаёт)
1. Настройки устройств (Device Settings)
Архитектура
UI (DeviceSettingsDialog.tsx):
- Кнопка-шестерёнка в тулбаре комнаты → диалог настроек
- Селекты «Микрофон» и «Камера»
Список устройств, переключение активного и персист выбора между заходами в
комнату целиком делегированы хукам @livekit/components-react:
useMediaDeviceSelect({ kind })— список устройств (подписан наRoomEvent.MediaDevicesChanged),activeDeviceId,setActiveMediaDevice()usePersistentUserChoices()— сохраняет выбранныеdeviceId(localStorage, ключи и формат — внутренняя реализация библиотеки);RoomPage.tsxчитает сохранённый выбор черезoptions-пропLiveKitRoom, чтобы применить его при следующем входе
Диалог должен рендериться внутри <LiveKitRoom>: useMediaDeviceSelect без
явно переданного room берёт активную комнату из RoomContext.
Ключевой момент: ошибка переключения устройства (занято/отключено)
показывается тостом; activeDeviceId хука остаётся источником истины —
состояние селекта само не «откатывается».
2. Аватары участников
Механизм отображения
Общий компонент frontend/src/components/ui/Avatar.tsx используется и в
топбаре/админке/пикере участников, и в комнате (RoomParticipantTile.tsx):
- Если передан
avatarUrl— рендерится<img> - Иначе — инициалы имени (первые буквы первых двух слов), на фоне базового
класса
.avatar; отдельного визуального различия между зарегистрированным пользователем без аватара и гостем нет — оба показывают инициалы одинаково
Передача аватара в LiveKit
LiveKit-токен (выдаётся POST /api/v1/conferences/{id}/join для
зарегистрированных участников) содержит метаданные:
{
"metadata": "{\"avatar_url\": \"https://vidconf.example.com/media/avatars/550e8400....jpg\"}"
}
Парсинг — инлайн-функция parseAvatarUrl() в RoomParticipantTile.tsx:
разбирает participant.metadata (реактивно, через useParticipantInfo),
возвращает null при пустых/невалидных метаданных. Для гостей avatar_url в
токен не кладётся — parseAvatarUrl вернёт null, показываются инициалы.
3. Fullscreen API
Реализация
Хук frontend/src/hooks/useFullscreen.ts — единственный источник истины о
состоянии — событие fullscreenchange документа (не промис
requestFullscreen(): выход по Esc браузер выполняет сам, без обратного
вызова). Цель — корневой контейнер комнаты (div[data-theme="room"] в
RoomPage.tsx), чтобы тулбар и чат оставались видны внутри полноэкранного
режима. supported = document.fullscreenEnabled — кнопка в
RoomToolbar.tsx скрывается, если false.
Поддержка: все современные браузеры (Chrome, Firefox, Safari, Edge).
4. Мини-плеер (Picture-in-Picture)
Матрица поддержки
| Браузер | Document PiP | Video PiP | Что использует |
|---|---|---|---|
| Chrome 116+ / Edge 116+ | ✓ Да | ✓ Да | Document PiP (активная плитка в отдельном окне) |
| Safari 17+ | ✗ Нет | ✓ Да | Video PiP (только видео активной плитки) |
| Firefox | ✗ Нет | ✓ Да | Video PiP (только видео активной плитки) |
Оба флага детектируются в рантайме ('documentPictureInPicture' in window,
document.pictureInPictureEnabled) — если ни один браузер API не
поддерживает, supported: false и кнопка скрывается (очень старые браузеры).
Единый хук useRoomPiP
frontend/src/hooks/useRoomPiP.ts инкапсулирует оба режима за одним API
(supported, active, mode, pipWindow, toggle):
- Document Picture-in-Picture (Chrome/Edge) —
toggle()синхронно (в рамках user gesture) вызываетwindow.documentPictureInPicture.requestWindow(), копирует таблицы стилей текущего документа в PiP-окно (copyStyleSheets; внешние cross-origin стили — ссылкой<link>, не инлайном) и проставляетdata-theme="room"на<html>PiP-окна. Содержимое —RoomPage.tsxрендерит<RoomStage variant="pip" />порталом (createPortal) прямо вpipWindow.document.body; React-контекстLiveKitRoomостаётся в основном дереве, поэтому хуки треков продолжают работать. Вvariant="pip"сцена показывает только одну активную плитку (без карусели/грида), фокус живо следует за активным спикером. - Video Picture-in-Picture (Safari) — фолбэк, классический
videoEl.requestPictureInPicture()на видео из фокус-плитки основного окна. - Ни то, ни другое не поддерживается (Firefox) —
supported: false, кнопка в тулбаре скрывается.
Ошибки открытия (например, requestWindow() отклонён) показываются тостом,
не проваливаются молча. Закрытие PiP-окна пользователем через системный
крестик отслеживается через событие pagehide окна; при размонтировании
хука (уход со страницы) осиротевшее PiP-окно закрывается явно.
5. Вёрстка и CSS
Токены цвета комнаты
Комната всегда использует [data-theme="room"] и токены:
[data-theme="room"] {
--color-room-bg: #1E1E1E; /* Чёрный фон */
--color-room-text-primary: #E8E8E8; /* Светлый текст */
--color-room-mic-on: #7FDDA8; /* Мята (mic включен) */
--color-room-mic-off: #EB93A1; /* Роза (mic выключен) */
--color-room-camera-on: #7FDDA8; /* Мята (camera включена) */
--color-room-camera-off: #EB93A1; /* Роза (camera выключена) */
--color-room-speaker-ring: #D6A83D; /* Янтарь (спикер) */
}
Аватар (CSS)
Базовый класс .avatar — общий для всей оболочки (frontend/src/styles/shell.css); в комнате плитка добавляет модификатор .room-tile-avatar (frontend/src/styles/room.css) для адаптивного размера внутри плитки участника:
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--color-ink-700);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
/* Модификатор для плитки участника комнаты — адаптивный размер */
.room-tile-avatar {
width: 40%;
height: 40%;
min-width: 32px;
min-height: 32px;
max-width: 96px;
max-height: 96px;
font-size: clamp(12px, 3vw, 28px);
font-weight: 700;
}
Отдельного визуального варианта для гостей нет — инициалы гостя рендерятся тем же .avatar.
Диалог настроек устройств
Диалог использует общие модальные классы комнаты (frontend/src/styles/room.css):
.room-modal-overlay { /* полноэкранная подложка с затемнением */ }
.room-modal-panel { /* сама карточка диалога, --color-room-bg фон */ }
.room-modal-head { display: flex; align-items: flex-start; justify-content: space-between; }
.room-modal-close { /* кнопка закрытия */ }
6. Демонстрация экрана
Обзор
Участники конференции (включая гостей) могут поделиться своим экраном или отдельным окном. Демонстрация — это перманентный источник видео (как и камера), публикуется через Track.Source.ScreenShare, отображается крупно в фокус-плитке при наличии, а другие участники — в карусели сбоку. Новый демонстратор автоматически перехватывает фокус (политика last-wins); предыдущий остаётся виден как обычная плитка в карусели.
Управление (UI)
Кнопка в тулбаре комнаты (RoomToolbar.tsx, строки 128–142):
- Иконка:
ScreenShare/ScreenShareOff(из lucide-react) - Текст: «Демонстрация»
- Состояние: отражает, активна ли локальная демонстрация текущего участника
- Клик: вызывает
useTrackToggle({ source: Track.Source.ScreenShare, captureOptions: SCREEN_SHARE_CAPTURE_OPTIONS })
Жизненный цикл:
- Начало демонстрации: пользователь нажимает кнопку → браузер показывает диалог выбора экрана/окна/вкладки → пользователь выбирает источник или отменяет → состояние кнопки и сцена обновляются
- Во время демонстрации:
- Трек ScreenShare остаётся активным (публикуется)
- Сцена переходит на focus-раскладку (см. ниже)
- Пользователь может закончить в любой момент: нажать кнопку ещё раз ИЛИ нажать системную кнопку браузера «Прекратить доступ» (в браузере, обычно справа в адресной строке) → состояние синхронизируется автоматически
- Конец демонстрации: трек удаляется, фокус переходит на активного спикера или первого участника
Опции захвата (ScreenShareCaptureOptions)
Константа SCREEN_SHARE_CAPTURE_OPTIONS (RoomToolbar.tsx, строки 32–37):
{
audio: true, // Захватывать звук (вкладка/экран, где доступно)
selfBrowserSurface: 'exclude', // Не предлагать саму вкладку конференции
surfaceSwitching: 'include', // Разрешить переключать источник во время демо
systemAudio: 'include' // Не запрещать системный звук (если браузер отдаёт)
}
Обработка ошибок:
NotAllowedError(пользователь нажал «Отмена» в браузерном диалоге) — игнорируется молча- Прочие ошибки (например,
NotReadableErrorпри занятом источнике) — показываются в тосте: «Не удалось начать демонстрацию экрана»
Отображение на сцене (RoomStage.tsx)
Focus-раскладка при активной демонстрации
При наличии хотя бы одного активного трека Track.Source.ScreenShare (RoomStage.tsx, переменная hasScreenShare):
FocusLayoutContainerвключается БЕЗУСЛОВНО (независимо от количества участников)- Фокус-плитка: первая активная демонстрация (по приоритету выбора)
- Карусель слева: все camera-треки + прочие screenshare-треки (проигравшие фокус)
- Инициалы/аватары заменены плитками видео, но если камера выключена — показывается аватар
Политика last-wins для нескольких демонстраторов
Логика в функции pickStageFocus (stageFocus.ts):
- Если новый участник запустил демонстрацию → её трек становится фокусом
- Предыдущая демонстрация остаётся в карусели как обычная плитка (не удаляется)
- Каждый демонстратор может остановить свою демонстрацию независимо
Остановка собственной демонстрации
В фокус-плитке, когда текущий участник демонстрирует экран:
- Отображается чип/кнопка: «Вы демонстрируете экран» ← click → вызов
room.localParticipant.setScreenShareEnabled(false) - Трек прекращается, фокус переходит на спикера
- Логика в
RoomStage.tsx(функцияhandleStopSharing, передаётся вRoomParticipantTile)
Поведение в fullscreen/PiP
Fullscreen:
- Демонстрация экрана работает в fullscreen-режиме как обычно
- Focus-раскладка сохраняется: демонстрация крупно, участники узкой колонкой
- Выход из fullscreen (кнопка или ESC) возвращает стандартный вид
Document Picture-in-Picture (Chrome/Edge 116+):
- При открытии PiP-окна показывается только активная плитка (без карусели/грида)
- Если активна демонстрация → в PiP она и отображается (фокус)
- Фокус в PiP живо следует за активным спикером (
followSpeaker), в отличие от основного окна - Закрытие PiP-окна возвращает вид на основное окно
Video Picture-in-Picture (Safari/Firefox фолбэк):
- Показывает только активное видео из фокус-плитки основного окна
- При активной демонстрации → в PiP видно именно её
Права доступа
- Любой участник может начать демонстрацию экрана, включая гостей
- Нет специальных прав или ролей для демонстрации
- Ограничение: браузер может запросить разрешение на доступ к экрану у ОС (обычно да/нет в диалоге браузера)
Звук демонстрации: матрица браузеров
Функция getDisplayMedia (WebRTC API) отдаёт аудио-дорожку демонстрации там, где браузер и ОС позволяют. Ниже матрица по браузерам и ОС.
| Браузер / ОС | Вкладка (tab) | Весь экран | Отдельное окно | Примечания |
|---|---|---|---|---|
| Chrome/Edge — Windows | ✓ Да | ✓ Да (системный звук) | ⚠ Обычно нет | Вкладка: звук вкладки; весь экран: системный звук; окно — редко отдаёт звук (зависит от окна) |
| Chrome/Edge — Linux | ✓ Да | ✓ Да (PulseAudio) | ⚠ Редко | PulseAudio выбирает источник; окно ≈ как на Windows |
| Chrome/Edge — macOS | ✓ Да | ✗ Нет (ОС не отдаёт) | ✗ Нет (ОС не отдаёт) | Вкладка работает; весь экран и окно — ОС macOS не предоставляет системный звук браузеру для безопасности |
| Safari 16+ — macOS | ✓ Да | ✗ Нет | ✗ Нет | Поддержка getDisplayMedia есть, звук не отдаётся; некоторые поля ScreenShareCaptureOptions (systemAudio) игнорируются |
| Firefox — Windows/Linux/macOS | ✓ Да | ✓ Да | ⚠ Редко | Firefox поддерживает getDisplayMedia, поля systemAudio/selfBrowserSurface могут игнорироваться — безопасная деградация |
Ключевые моменты:
- SDK не фейлит старт без аудио: если браузер не может захватить звук,
getDisplayMedia()всё равно вернёт видео-дорожку (без аудио) — демонстрация работает, просто без звука - RoomAudioRenderer: компонент (
RoomStage.tsx) проигрывает аудио-дорожки удалённых демонстраций, если они присутствуют в треках - Почему macOS без системного звука? Согласно WebRTC спецификации и политике безопасности Apple, браузеры на macOS не получают системный звук через
getDisplayMedia()— только звук текущей вкладки. Пользователь должен явно выбрать вкладку (браузер, плеер, Zoom и т. д.) в диалоге браузера, чтобы захватить её звук.
Примечания
- Аватары гостей: гости не получают
avatar_urlв метаданных LiveKit-токена, поэтому всегда видят инициалы. - Document PiP требует взаимодействия: запрос можно сделать только в ответ на
clickили похожий пользовательский жест (security policy браузера) —toggle()хукаuseRoomPiPпоэтому вызывается синхронно из обработчика клика. - Video PiP показывает только одну плитку: если нужна сетка целиком, используйте Document PiP (Chrome/Edge).
- Fullscreen работает везде: но некоторые браузеры могут показать UI-запрос перед вводом.
- Персист выбора устройств — через
usePersistentUserChoicesиз@livekit/components-react; наличие сохранённого устройства не гарантирует, что оно всё ещё подключено — библиотека сама обрабатывает этот случай при следующем входе. - Демонстрация экрана требует пользовательского жеста: браузер требует клика/касания перед открытием диалога выбора экрана (Permissions Policy, безопасность).
- Звук демонстрации теряется в macOS: если требуется захват системного звука, пользователю на Mac нужно выбрать конкретную вкладку браузера/плеера (не «весь экран»).
Ссылки
frontend/src/pages/RoomPage.tsx— главный компонент комнаты, подключение LiveKit, порталы PiPfrontend/src/components/room/RoomToolbar.tsx— тулбар (микрофон, камера, демонстрация, настройки, fullscreen, PiP, чат, выход)frontend/src/components/room/RoomStage.tsx— сцена с focus-раскладкой и логикой демонстрации экранаfrontend/src/components/room/stageFocus.ts— чистая функция выбора фокуса (pickStageFocus, last-wins)frontend/src/components/room/RoomParticipantTile.tsx— плитка участника, аватар, парсинг метаданныхfrontend/src/components/room/DeviceSettingsDialog.tsx— диалог настроек микрофона/камерыfrontend/src/hooks/useFullscreen.ts— полноэкранный режимfrontend/src/hooks/useRoomPiP.ts— мини-плеер (Document PiP + Video PiP фолбэк)- Document Picture-in-Picture Spec — W3C
- Picture-in-Picture Spec — W3C (video PiP)
- Fullscreen API — MDN
- Screen Capture API (getDisplayMedia) — W3C
docs/architecture/frontend-themes.md— тёмная тема комнаты