feat(room): замена фона видео на картинку — только на десктопе
Кнопка «Фон» в тулбаре комнаты и выбор фона в превью на входе: три готовые
сцены и свои картинки из профиля. Фон применяется процессором к самому
публикуемому треку (`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 и фон молча не включался.
This commit is contained in:
73
frontend/src/components/background/BackgroundPicker.tsx
Normal file
73
frontend/src/components/background/BackgroundPicker.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Ban, Check, Loader2 } from 'lucide-react'
|
||||
import type { MyBackground } from '@/api/users'
|
||||
import { DEFAULT_BACKGROUNDS, NO_BACKGROUND, type BackgroundKey } from '@/lib/virtualBackground'
|
||||
import '@/styles/virtual-background.css'
|
||||
|
||||
interface BackgroundPickerProps {
|
||||
value: BackgroundKey
|
||||
onChange: (key: BackgroundKey) => void
|
||||
/** Свои картинки пользователя; у гостя список всегда пуст — профиля у него нет. */
|
||||
customBackgrounds: MyBackground[]
|
||||
/** Идёт применение фона (первый раз — ещё и загрузка модели) — показать индикатор на выбранной плитке. */
|
||||
busy?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Сетка выбора фона: «без фона», три дефолтные сцены и свои картинки.
|
||||
*
|
||||
* Один компонент на все три места, где выбирают фон (превью входа, комната,
|
||||
* профиль): плитки везде выглядят одинаково, а цвета берутся из токенов темы —
|
||||
* внутри комнаты (`[data-theme="room"]`) те же классы перекрашиваются в тёмную
|
||||
* палитру, см. `styles/virtual-background.css`.
|
||||
*
|
||||
* Кнопки — `aria-pressed`, а не радиогруппа: выбор применяется немедленно и
|
||||
* никуда не отправляется формой.
|
||||
*/
|
||||
export function BackgroundPicker({
|
||||
value,
|
||||
onChange,
|
||||
customBackgrounds,
|
||||
busy = false,
|
||||
}: BackgroundPickerProps) {
|
||||
const renderTile = (key: BackgroundKey, label: string, url: string | null) => {
|
||||
const selected = value === key
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`bg-tile${selected ? ' is-selected' : ''}`}
|
||||
aria-pressed={selected}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={() => onChange(key)}
|
||||
>
|
||||
{url ? (
|
||||
<img src={url} alt="" loading="lazy" />
|
||||
) : (
|
||||
<span className="bg-tile-none">
|
||||
<Ban className="lucide" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
{selected && (
|
||||
<span className="bg-tile-mark" aria-hidden="true">
|
||||
{busy ? (
|
||||
<Loader2 className="lucide bg-tile-spinner" />
|
||||
) : (
|
||||
<Check className="lucide" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-grid">
|
||||
{renderTile(NO_BACKGROUND, 'Без фона', null)}
|
||||
{DEFAULT_BACKGROUNDS.map((item) => renderTile(`default:${item.id}`, item.label, item.url))}
|
||||
{customBackgrounds.map((item, index) =>
|
||||
renderTile(`custom:${item.id}`, `Своя картинка ${index + 1}`, item.url),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user