Кнопка «Фон» в тулбаре комнаты и выбор фона в превью на входе: три готовые
сцены и свои картинки из профиля. Фон применяется процессором к самому
публикуемому треку (`LocalVideoTrack.setProcessor`), а НЕ пересозданием
`RoomOptions` — ссылка на них обязана оставаться стабильной, иначе
`LiveKitRoom` переподключается к комнате.
Фича только для ДЕСКТОПА, и «десктоп» определяется по возможностям устройства
(`pointer: fine` + `hover: hover` + `maxTouchPoints`), а НЕ по ширине окна:
узкое окно на десктопе — всё ещё десктоп, а широкий планшет — всё ещё планшет,
который сегментация греет. На мобильном кнопки нет вовсе, а не задизейбленной.
Ассеты сегментации отдаются СО СВОЕГО домена: библиотека по умолчанию тянет
wasm с jsdelivr, а модель с storage.googleapis.com, и в закрытом контуре фича
молча не работала бы. Модель (Apache 2.0, см. NOTICE.txt) лежит в репозитории,
wasm-рантайм (~19 МБ) копируется из node_modules плагином сборки. Сама
библиотека и модель грузятся ЛЕНИВО — только когда фон реально включают, вход
в конференцию не стал медленнее.
Три дефолтные сцены — собственные векторные рисунки (`design/backgrounds/`),
а не фотографии из интернета: у нарисованной сцены нет чужой лицензии, а
продукт расходится по инсталляциям, и проверять права на каждую копию некому.
Свои картинки — в профиле, до 10 штук, с уменьшением до 1280px и переводом в
WebP прямо в браузере перед отправкой. Удаление применённого сейчас фона
сбрасывает выбор на «без фона»: хранится ключ записи, а не URL картинки.
Смена камеры фон не теряет (`restartTrack` перезапускает процессор сам),
выключение и включение камеры — навешивает его на новый трек заново.
⚠️ Прокси dev-сервера для `/media/` — обязательно со слэшем: ключ `/media`
Vite матчит префиксом и перехватывает заодно `/mediapipe/...`, из-за чего
модель получала 404 и фон молча не включался.
104 lines
5.5 KiB
TypeScript
104 lines
5.5 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import { Mic, MicOff, Video, VideoOff } from 'lucide-react'
|
||
import type { DeviceCheckStatus } from '@/hooks/useDeviceCheckAccess'
|
||
import '@/styles/device-check.css'
|
||
|
||
interface DeviceCheckCardProps {
|
||
/** Коллбэк-реф из `useDeviceCheckAccess` — см. докстринг там: карточка
|
||
* может рендериться в разных местах JSX-дерева (разные шаги `JoinPage`),
|
||
* обычный объект-реф не пережил бы такой переезд. */
|
||
videoRef: (node: HTMLVideoElement | null) => void
|
||
videoStatus: DeviceCheckStatus
|
||
audioStatus: DeviceCheckStatus
|
||
/** `useDeviceCheckAccess().hint` — `null`, пока отказа нет (см. докстринг хука). */
|
||
hint: string | null
|
||
/** Одновременно и «войти с камерой/микрофоном включёнными», и (для видео)
|
||
* реальное состояние превью — см. докстринг `useDeviceCheckAccess`. */
|
||
videoEnabled: boolean
|
||
audioEnabled: boolean
|
||
onToggleVideo: () => void
|
||
onToggleAudio: () => void
|
||
/**
|
||
* Выбор фона под кнопками (сессия 35) — приходит готовым узлом, а не набором
|
||
* пропсов: карточка про доступ к устройствам и знать про фоны, их лимиты и
|
||
* загрузку не обязана. `undefined` — модуль выключен, устройство не
|
||
* десктопное либо браузер не умеет сегментацию; тогда блока нет вовсе.
|
||
*/
|
||
backgroundPicker?: ReactNode
|
||
}
|
||
|
||
/**
|
||
* Превью камеры + переключатели «микрофон/камера» — окошко превью и кнопки
|
||
* составляют одну композицию (кнопки не шире окошка, см. `device-check.css`).
|
||
* Кнопки без подписи — только пиктограмма (иначе не умещаются под окошком
|
||
* шириной в половину карточки); машиночитаемое состояние — `aria-label`.
|
||
* Кликабельны только после реального разрешения (`granted`) — до этого
|
||
* непонятно, что вообще включать.
|
||
*
|
||
* `<video>` рендерится ВСЕГДА (не только при `videoEnabled`) — коллбэк-реф
|
||
* хука подключает `srcObject` при КАЖДОМ монтировании узла (см. докстринг
|
||
* `useDeviceCheckAccess`); если бы элемент монтировался условно, в момент
|
||
* присвоения его ещё не было бы в DOM и поток повис бы никуда не
|
||
* подключённым. Плейсхолдер лежит поверх, пока превью не готово ИЛИ камера
|
||
* выключена кнопкой (`!videoEnabled`) — кнопка камеры реально останавливает
|
||
* поток (см. докстринг хука), а не просто прячет картинку.
|
||
*
|
||
* `muted`+`playsInline`+`autoPlay` — обязательны: без них iOS Safari не
|
||
* запустит воспроизведение живого потока, и вместо своего лица пользователь
|
||
* увидит чёрный прямоугольник.
|
||
*/
|
||
export function DeviceCheckCard({
|
||
videoRef,
|
||
videoStatus,
|
||
audioStatus,
|
||
hint,
|
||
videoEnabled,
|
||
audioEnabled,
|
||
onToggleVideo,
|
||
onToggleAudio,
|
||
backgroundPicker,
|
||
}: DeviceCheckCardProps) {
|
||
const showPlaceholder = videoStatus !== 'granted' || !videoEnabled
|
||
return (
|
||
<div className="device-check">
|
||
<div className="device-check-inner">
|
||
<div className="device-check-frame">
|
||
<video ref={videoRef} muted playsInline autoPlay className="device-check-video" />
|
||
{showPlaceholder && (
|
||
<div className="device-check-placeholder" aria-hidden="true">
|
||
<VideoOff className="lucide" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="device-check-toggles">
|
||
<button
|
||
type="button"
|
||
className={`device-check-toggle${audioEnabled ? ' is-on' : ''}`}
|
||
disabled={audioStatus !== 'granted'}
|
||
onClick={onToggleAudio}
|
||
aria-pressed={audioEnabled}
|
||
aria-label={audioEnabled ? 'Войти с выключенным микрофоном' : 'Войти с включённым микрофоном'}
|
||
>
|
||
{audioEnabled ? <Mic className="lucide" aria-hidden="true" /> : <MicOff className="lucide" aria-hidden="true" />}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`device-check-toggle${videoEnabled ? ' is-on' : ''}`}
|
||
disabled={videoStatus !== 'granted'}
|
||
onClick={onToggleVideo}
|
||
aria-pressed={videoEnabled}
|
||
aria-label={videoEnabled ? 'Выключить камеру' : 'Включить камеру'}
|
||
>
|
||
{videoEnabled ? <Video className="lucide" aria-hidden="true" /> : <VideoOff className="lucide" aria-hidden="true" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{backgroundPicker && <div className="device-check-backgrounds">{backgroundPicker}</div>}
|
||
|
||
{hint && <p className="field-hint device-check-hint">{hint}</p>}
|
||
</div>
|
||
)
|
||
}
|