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
design/backgrounds/render.py
Normal file
73
design/backgrounds/render.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Запекает векторные сцены дефолтных фонов (`*.svg`) в WebP для фронтенда.
|
||||
|
||||
Исходники сцен — собственные рисунки VidConf (см. README рядом), поэтому
|
||||
лежат в репозитории вместе с результатом: чужих фотографий с их лицензиями в
|
||||
продукте нет.
|
||||
|
||||
Запуск (нужен установленный Google Chrome и Pillow):
|
||||
|
||||
python3 design/backgrounds/render.py
|
||||
|
||||
Результат — `frontend/public/backgrounds/*.webp`, 1280×720. Chrome нужен
|
||||
только для растеризации SVG (rsvg/ImageMagick на машине может не быть),
|
||||
Pillow — для перевода PNG в WebP: браузерный скриншот WebP не отдаёт.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
WIDTH, HEIGHT = 1280, 720
|
||||
# 82 — визуально неотличимо от исходника на плавных градиентах сцены, а вес
|
||||
# втрое меньше, чем при 95. Фон грузится по сети при каждом первом показе.
|
||||
WEBP_QUALITY = 82
|
||||
|
||||
SOURCE_DIR = Path(__file__).parent
|
||||
TARGET_DIR = SOURCE_DIR.parents[1] / "frontend" / "public" / "backgrounds"
|
||||
|
||||
|
||||
def render(svg_path: Path, out_path: Path, workdir: Path) -> None:
|
||||
"""Растеризовать одну сцену и сохранить её в WebP."""
|
||||
png_path = workdir / f"{svg_path.stem}.png"
|
||||
subprocess.run(
|
||||
[
|
||||
CHROME,
|
||||
"--headless",
|
||||
"--disable-gpu",
|
||||
"--hide-scrollbars",
|
||||
"--default-background-color=00000000",
|
||||
f"--screenshot={png_path}",
|
||||
f"--window-size={WIDTH},{HEIGHT}",
|
||||
svg_path.resolve().as_uri(),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
with Image.open(png_path) as image:
|
||||
image.convert("RGB").save(out_path, "WEBP", quality=WEBP_QUALITY, method=6)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not Path(CHROME).exists():
|
||||
print(f"не найден Chrome: {CHROME}", file=sys.stderr)
|
||||
return 1
|
||||
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
workdir = Path(tmp)
|
||||
for svg_path in sorted(SOURCE_DIR.glob("*.svg")):
|
||||
out_path = TARGET_DIR / f"{svg_path.stem}.webp"
|
||||
render(svg_path, out_path, workdir)
|
||||
print(
|
||||
f"{svg_path.name} → {out_path.relative_to(SOURCE_DIR.parents[1])}"
|
||||
f" ({out_path.stat().st_size // 1024} КБ)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user