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:
2026-08-10 08:59:47 +03:00
parent fec9255baa
commit 17437880b1
37 changed files with 1918 additions and 18 deletions

View File

@@ -8,6 +8,8 @@ import { Select } from '@/components/ui/Select'
import { useToast } from '@/components/ui/ToastProvider'
import { useAuth } from '@/auth/useAuth'
import { changePassword, deleteMyAvatar, listTeams, updateMe, uploadMyAvatar } from '@/api/users'
import { MyBackgroundsSection } from '@/components/background/MyBackgroundsSection'
import { usePublicSettings } from '@/hooks/usePublicSettings'
import { ApiError, errorDetail } from '@/api/client'
import '@/styles/lobby.css'
import '@/styles/profile.css'
@@ -38,6 +40,11 @@ export function ProfilePage() {
const [avatarError, setAvatarError] = useState<string | null>(null)
const { data: teams } = useQuery({ queryKey: ['teams'], queryFn: listTeams })
// Раздел «Свои фоны» есть, только пока модуль включён в админке — иначе
// картинкам, которые никак не применить, в профиле не место (то же правило,
// что и с кнопкой «Фон» в комнате: выключенный модуль не оставляет мёртвых
// элементов интерфейса).
const { data: publicSettings } = usePublicSettings()
const teamOptions = [{ value: '', label: 'Без команды' }, ...((teams ?? []).map((t) => ({ value: t.id, label: t.name })))]
async function afterProfileChange() {
@@ -216,6 +223,8 @@ export function ProfilePage() {
</form>
</section>
{publicSettings?.virtual_background_enabled && <MyBackgroundsSection />}
<section className="profile-card">
<h2>Смена пароля</h2>
<form onSubmit={handlePasswordSubmit}>