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

@@ -0,0 +1,130 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<title>Офис — дефолтный фон замены фона видео</title>
<desc>Собственная векторная сцена VidConf. Светлый переговорный угол: окно с
тёплым светом, стеллаж с книгами и растениями. Центр кадра намеренно спокойный —
там будет человек, детали вынесены к краям.</desc>
<defs>
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f3ede4"/>
<stop offset="0.62" stop-color="#e7ded1"/>
<stop offset="1" stop-color="#d9cebe"/>
</linearGradient>
<linearGradient id="floor" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#c9b49a"/>
<stop offset="1" stop-color="#a98e70"/>
</linearGradient>
<linearGradient id="daylight" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fdfaf2"/>
<stop offset="1" stop-color="#e8eef1"/>
</linearGradient>
<linearGradient id="wood" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#b58a5f"/>
<stop offset="1" stop-color="#8d6540"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#fff6de" stop-opacity="0.85"/>
<stop offset="1" stop-color="#fff6de" stop-opacity="0"/>
</radialGradient>
<radialGradient id="vignette" cx="0.5" cy="0.45" r="0.78">
<stop offset="0.55" stop-color="#000000" stop-opacity="0"/>
<stop offset="1" stop-color="#3b2d1e" stop-opacity="0.3"/>
</radialGradient>
<!-- Мягкое расфокусирование: фон за спиной человека в кадре камеры никогда
не бывает резким, и резкая графика выдаёт подмену сильнее всего. -->
<filter id="soft" x="-6%" y="-6%" width="112%" height="112%">
<feGaussianBlur stdDeviation="5"/>
</filter>
<filter id="softer" x="-8%" y="-8%" width="116%" height="116%">
<feGaussianBlur stdDeviation="10"/>
</filter>
</defs>
<rect width="1280" height="720" fill="url(#wall)"/>
<g filter="url(#soft)">
<!-- Окно слева -->
<rect x="52" y="86" width="330" height="392" rx="10" fill="#cbb9a3"/>
<rect x="66" y="100" width="302" height="364" rx="6" fill="url(#daylight)"/>
<rect x="212" y="100" width="10" height="364" fill="#cbb9a3"/>
<rect x="66" y="276" width="302" height="10" fill="#cbb9a3"/>
<!-- Городская дымка за стеклом -->
<rect x="66" y="352" width="302" height="112" fill="#dfe6e6" opacity="0.85"/>
<rect x="96" y="300" width="46" height="164" fill="#d3dcde" opacity="0.7"/>
<rect x="158" y="330" width="34" height="134" fill="#cfd8db" opacity="0.6"/>
<rect x="248" y="316" width="52" height="148" fill="#d3dcde" opacity="0.65"/>
<rect x="314" y="344" width="30" height="120" fill="#cfd8db" opacity="0.55"/>
<!-- Стеллаж справа -->
<rect x="876" y="120" width="352" height="392" rx="8" fill="url(#wood)"/>
<rect x="892" y="136" width="320" height="112" fill="#e9dcc9" opacity="0.55"/>
<rect x="892" y="264" width="320" height="112" fill="#e9dcc9" opacity="0.5"/>
<rect x="892" y="392" width="320" height="104" fill="#e9dcc9" opacity="0.45"/>
<!-- Книги -->
<g>
<rect x="906" y="156" width="18" height="92" fill="#8a5a4a"/>
<rect x="928" y="168" width="14" height="80" fill="#6c7f6a"/>
<rect x="946" y="150" width="20" height="98" fill="#c08a4a"/>
<rect x="970" y="172" width="16" height="76" fill="#4f6478"/>
<rect x="990" y="160" width="12" height="88" fill="#9d5f5f"/>
<rect x="1010" y="176" width="22" height="72" fill="#7a6a55"/>
<rect x="906" y="292" width="16" height="84" fill="#5f7382"/>
<rect x="926" y="284" width="20" height="92" fill="#a86f4e"/>
<rect x="950" y="300" width="14" height="76" fill="#7f8f76"/>
<rect x="968" y="288" width="18" height="88" fill="#8d5a63"/>
</g>
<!-- Растение на полке -->
<g>
<path d="M1140 392c-26-14-40-42-34-72 26 6 44 30 44 60z" fill="#4f7a52"/>
<path d="M1150 392c26-16 38-46 30-76-26 8-42 34-40 64z" fill="#5f8f5f"/>
<rect x="1128" y="386" width="40" height="30" rx="5" fill="#b98a63"/>
</g>
<!-- Картина на стене между окном и стеллажом -->
<g>
<rect x="486" y="150" width="196" height="146" rx="6" fill="#c2a882"/>
<rect x="498" y="162" width="172" height="122" fill="#eae3d3"/>
<path d="M498 284l52-58 40 34 44-52 36 76z" fill="#9fb5a2"/>
<circle cx="622" cy="196" r="16" fill="#e8c579"/>
</g>
<!-- Настольная лампа и край стола справа от картины -->
<g>
<rect x="700" y="452" width="220" height="14" rx="4" fill="#c8a878"/>
<rect x="712" y="466" width="12" height="46" fill="#b2915f"/>
<rect x="896" y="466" width="12" height="46" fill="#b2915f"/>
<rect x="792" y="392" width="8" height="60" fill="#6f7b82"/>
<path d="M760 392l36-52 36 52z" fill="#7f8d95"/>
<ellipse cx="796" cy="424" rx="54" ry="18" fill="#ffe9b8" opacity="0.5"/>
</g>
</g>
<!-- Пол -->
<rect x="0" y="512" width="1280" height="208" fill="url(#floor)"/>
<rect x="0" y="506" width="1280" height="14" fill="#e9e0d3" opacity="0.7"/>
<!-- Крупное растение в левом углу — передний план, размыто сильнее -->
<g filter="url(#softer)" opacity="0.95">
<g fill="#3f6b45">
<ellipse cx="88" cy="508" rx="52" ry="34" transform="rotate(-24 88 508)"/>
<ellipse cx="206" cy="556" rx="56" ry="36" transform="rotate(18 206 556)"/>
<ellipse cx="70" cy="596" rx="48" ry="32" transform="rotate(-8 70 596)"/>
</g>
<g fill="#4c7f52">
<ellipse cx="158" cy="486" rx="50" ry="32" transform="rotate(6 158 486)"/>
<ellipse cx="248" cy="626" rx="46" ry="30" transform="rotate(32 248 626)"/>
<ellipse cx="124" cy="574" rx="54" ry="34" transform="rotate(-14 124 574)"/>
</g>
<g stroke="#35603c" stroke-width="7" fill="none" stroke-linecap="round">
<path d="M120 668c-16-56-24-108-32-160"/>
<path d="M132 668c14-52 34-96 62-136"/>
<path d="M126 668c2-44 20-76 46-96"/>
</g>
<rect x="52" y="656" width="150" height="86" rx="14" fill="#9d7550"/>
<rect x="52" y="656" width="150" height="16" rx="8" fill="#b08a63"/>
</g>
<!-- Свет из окна и общая виньетка -->
<ellipse cx="300" cy="250" rx="460" ry="330" fill="url(#glow)"/>
<rect width="1280" height="720" fill="url(#vignette)"/>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB