Кнопка «Фон» в тулбаре комнаты и выбор фона в превью на входе: три готовые
сцены и свои картинки из профиля. Фон применяется процессором к самому
публикуемому треку (`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 и фон молча не включался.
84 lines
4.3 KiB
TypeScript
84 lines
4.3 KiB
TypeScript
import fs from 'node:fs'
|
||
import path from 'path'
|
||
import { defineConfig, type Plugin } from 'vite'
|
||
import react from '@vitejs/plugin-react'
|
||
import tailwindcss from '@tailwindcss/vite'
|
||
|
||
/**
|
||
* Копирует wasm-рантайм MediaPipe (`@mediapipe/tasks-vision`) в `public/mediapipe/wasm/`,
|
||
* откуда его забирает замена фона (`lib/virtualBackground.ts`).
|
||
*
|
||
* Зачем вообще: библиотека `@livekit/track-processors` по умолчанию тянет и
|
||
* wasm, и модель сегментации с ВНЕШНИХ CDN (jsdelivr + storage.googleapis.com).
|
||
* VidConf ставят в закрытых контурах, где внешнего интернета нет, — там фича
|
||
* молча не заработала бы. Поэтому все ассеты отдаются со своего домена.
|
||
*
|
||
* Зачем копировать, а не коммитить: два варианта рантайма (simd/nosimd) весят
|
||
* ~19 МБ — в репозитории им не место, а версия обязана совпадать с
|
||
* установленной в `node_modules` (иначе рантайм и `vision_bundle.mjs`
|
||
* разъедутся). Копия делается и в dev (`vite`), и в сборке (`vite build`,
|
||
* который потом переносит весь `public/` в `dist/`), каталог назначения
|
||
* gitignored. Модель сегментации (`selfie_segmenter.tflite`, ~244 КБ) npm-пакетом
|
||
* не поставляется — она лежит в репозитории рядом, см. `public/mediapipe/NOTICE.txt`.
|
||
*/
|
||
function mediapipeWasm(): Plugin {
|
||
const source = path.resolve(__dirname, 'node_modules/@mediapipe/tasks-vision/wasm')
|
||
const target = path.resolve(__dirname, 'public/mediapipe/wasm')
|
||
return {
|
||
name: 'vidconf-mediapipe-wasm',
|
||
buildStart() {
|
||
if (!fs.existsSync(source)) {
|
||
// Пакет не установлен — падать сборкой не за что: замена фона просто
|
||
// не сможет инициализироваться, остальное приложение работает.
|
||
this.warn(`не найден wasm-рантайм MediaPipe: ${source}`)
|
||
return
|
||
}
|
||
fs.mkdirSync(target, { recursive: true })
|
||
for (const name of fs.readdirSync(source)) {
|
||
const from = path.join(source, name)
|
||
const to = path.join(target, name)
|
||
// Пропускаем уже скопированное с тем же размером — иначе каждый
|
||
// перезапуск dev-сервера переписывал бы 19 МБ.
|
||
const src = fs.statSync(from)
|
||
const dst = fs.existsSync(to) ? fs.statSync(to) : null
|
||
if (dst && dst.size === src.size) continue
|
||
fs.copyFileSync(from, to)
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
// https://vite.dev/config/
|
||
export default defineConfig({
|
||
plugins: [react(), tailwindcss(), mediapipeWasm()],
|
||
resolve: {
|
||
alias: {
|
||
'@': path.resolve(__dirname, './src'),
|
||
},
|
||
},
|
||
server: {
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:8000',
|
||
changeOrigin: true,
|
||
// WS-чат конференции ходит через тот же префикс /api
|
||
ws: true,
|
||
},
|
||
// Загруженные файлы (аватары, картинки фона). На проде их раздаёт nginx
|
||
// напрямую из тома (`location /media/`), минуя backend; в dev такого
|
||
// слоя нет, и без этого прокси все `/media/...` упирались бы в SPA
|
||
// и отдавали index.html вместо картинки.
|
||
//
|
||
// ⚠️ Ключ обязан быть со СЛЭШЕМ на конце. Vite матчит префиксом, и
|
||
// ключ `/media` перехватывал заодно `/mediapipe/...` — ассеты замены
|
||
// фона уходили на backend и получали 404, из-за чего фон молча не
|
||
// включался. У nginx та же семантика: там `location /media/` тоже
|
||
// со слэшем, поэтому прод этим не задет.
|
||
'/media/': {
|
||
target: 'http://localhost:8000',
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
},
|
||
})
|