feat(room): кнопка демонстрации экрана в мини-окне

В Document PiP (Chrome/Edge) своего тулбара нет, и начать демонстрацию,
не свернув мини-окно, было нельзя. Кнопка сделана по образцу кнопки
микрофона, добавленной в 0.0.15: тот же `useTrackToggle` через
`RoomContext`, поэтому она и кнопка основного тулбара — два вида одного
состояния и рассинхрону взяться неоткуда.

Главный вопрос задачи — пустит ли платформа `getDisplayMedia()`, вызванный
из кода основного окна по клику в ДРУГОМ окне. Замер в Chrome 150: после
клика в PiP `navigator.userActivation.isActive === true` в обоих окнах,
активация доезжает до опенера, вызов проходит, системный пикер выбора
экрана открывается отдельным окном поверх всего, а не прячется за
заглушкой основного окна.

Опции захвата (`SCREEN_SHARE_CAPTURE_OPTIONS`) вынесены из `RoomToolbar` в
`lib/screenShareOptions.ts`: кнопок демонстрации теперь две, и разойдись
они хотя бы в `audio`, демонстрация получалась бы разной в зависимости от
того, откуда её запустили.

Фокус в мини-окне не менялся: своя демонстрация показывается по тем же
правилам `pickStageFocus`, что и в основном окне.

В Safari мини-окно — нативный video-PiP без собственного DOM, кнопке там
негде жить; в Firefox мини-окна нет вовсе. Это ограничение платформы.
This commit is contained in:
2026-08-04 21:13:27 +03:00
parent aee76329c4
commit f89bf1ad64
4 changed files with 139 additions and 34 deletions

View File

@@ -13,33 +13,14 @@ import {
Video,
VideoOff,
} from 'lucide-react'
import { Track, type ScreenShareCaptureOptions } from 'livekit-client'
import { Track } from 'livekit-client'
import { DisconnectButton, useLocalParticipant, useTrackToggle } from '@livekit/components-react'
import { useToast } from '@/components/ui/ToastProvider'
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
import type { HandQueueEntry } from '@/hooks/useChat'
import { StageViewMenu, type StageViewProps } from '@/components/room/StageViewOptions'
import { HandQueueMenu } from '@/components/room/HandQueueMenu'
/**
* Опции захвата демонстрации экрана: `audio: true` — звук
* вкладки/экрана там, где браузер его отдаёт (Chrome/Edge — вкладка почти
* всегда, целый экран — только Windows); `selfBrowserSurface: 'exclude'`
* — не предлагать в списке
* источников собственную вкладку (зеркальный туннель самой конференции);
* `surfaceSwitching: 'include'` — разрешить переключать источник прямо во
* время демонстрации, не останавливая её; `systemAudio: 'include'` — не
* запрещать захват системного звука при выборе «весь экран». Вынесено в
* модульную константу — `useTrackToggle` держит `JSON.stringify(captureOptions)`
* в зависимостях внутреннего `useMemo`, инлайновый литерал был бы безвреден,
* но константа явнее фиксирует неизменность опций.
*/
const SCREEN_SHARE_CAPTURE_OPTIONS: ScreenShareCaptureOptions = {
audio: true,
selfBrowserSurface: 'exclude',
surfaceSwitching: 'include',
systemAudio: 'include',
}
import { SCREEN_SHARE_CAPTURE_OPTIONS } from '@/lib/screenShareOptions'
interface RoomToolbarProps extends StageViewProps {
/** Показывать ли кнопку чата — `JoinOut.chat_enabled` И чат не помечен недоступным (close-код 4404). */