/** * Замена фона видео: список дефолтных сцен, память о выборе и ленивое создание * процессора сегментации. * * Тяжёлая часть (библиотека + wasm-рантайм MediaPipe + модель, единицы мегабайт) * НЕ попадает в основной бандл: `@livekit/track-processors` подключается * динамическим `import()` в `createBackgroundProcessor`, то есть только когда * пользователь реально включает фон. Вход в конференцию от наличия этой фичи * не становится медленнее — это было прямым требованием. * * 🔴 Ассеты берутся СО СВОЕГО домена (`assetPaths` ниже). По умолчанию * библиотека тянет wasm с jsdelivr, а модель — с storage.googleapis.com; * VidConf ставят в закрытых контурах без внешнего интернета, и там фича молча * не заработала бы. Откуда берутся файлы — см. плагин `mediapipeWasm` * в `vite.config.ts` и `public/mediapipe/NOTICE.txt`. */ import { Track } from 'livekit-client' import type { BackgroundProcessorWrapper } from '@livekit/track-processors' /** Готовая сцена, поставляемая с продуктом (собственные рисунки, см. `design/backgrounds/`). */ export interface DefaultBackground { /** Имя файла без расширения — оно же часть ключа выбора (`default:office`). */ id: string label: string url: string } export const DEFAULT_BACKGROUNDS: DefaultBackground[] = [ { id: 'office', label: 'Офис', url: '/backgrounds/office.webp' }, { id: 'beach', label: 'Пляж', url: '/backgrounds/beach.webp' }, { id: 'space-station', label: 'Космическая станция', url: '/backgrounds/space-station.webp' }, ] /** * Ключ выбранного фона: `none`, `default:` либо `custom:`. * * Хранится именно ключ, а не URL картинки: URL своей картинки перестаёт быть * валидным, как только пользователь её удалил, и по ключу это видно сразу — * записи с таким id в списке нет, значит выбор сбрасывается на «без фона» * (см. `resolveBackgroundUrl`). */ export type BackgroundKey = string export const NO_BACKGROUND: BackgroundKey = 'none' const STORAGE_KEY = 'vidconf.virtualBackground' /** * Загрузить сохранённый выбор фона. * * Выбор персистится между заходами (как режим показа сцены, * `lib/stageLayoutMode.ts`) и, что важнее, переживает переход «превью на входе * → комната»: пользователь выбирает фон на `JoinPage`, а применяется он к * публикуемому треку уже внутри конференции — передавать его через * navigation state нельзя, тот теряется при F5. */ export function loadBackgroundKey(): BackgroundKey { try { return window.localStorage.getItem(STORAGE_KEY) || NO_BACKGROUND } catch { // Приватный режим/запрет хранилища — фича должна работать и без памяти. return NO_BACKGROUND } } export function saveBackgroundKey(key: BackgroundKey): void { try { window.localStorage.setItem(STORAGE_KEY, key) } catch { // См. `loadBackgroundKey` — молча живём без персиста. } } /** Своя картинка пользователя в том виде, в каком её отдаёт API. */ export interface CustomBackground { id: string url: string } /** * URL картинки по ключу выбора; `null` — фон не нужен («без фона» либо ключ * указывает на уже удалённую свою картинку). */ export function resolveBackgroundUrl( key: BackgroundKey, customBackgrounds: CustomBackground[], ): string | null { if (key.startsWith('default:')) { const id = key.slice('default:'.length) return DEFAULT_BACKGROUNDS.find((item) => item.id === id)?.url ?? null } if (key.startsWith('custom:')) { const id = key.slice('custom:'.length) return customBackgrounds.find((item) => item.id === id)?.url ?? null } return null } /** * Поддерживает ли браузер замену фона. * * Проверка СИНХРОННАЯ и намеренно не трогает саму библиотеку: решение нужно * ДО её загрузки, чтобы не показывать кнопку, которая не сработает, и не * тянуть мегабайты впустую. Поэтому здесь буквально повторено условие * `supportsBackgroundProcessors()` из `@livekit/track-processors` — при * обновлении пакета сверять с ним. * * ⚠️ Конвейеров у библиотеки ДВА, и требовать современный нельзя: * - современный (`MediaStreamTrackProcessor`/`Generator`, Insertable Streams) * есть только в Chrome и производных; * - запасной рисует кадры в canvas и отдаёт `canvas.captureStream()` — он * работает в Safari и Firefox. * * Первая версия (0.0.35) требовала именно современный конвейер — и фича * молча отсутствовала в Safari и Firefox, хотя запасной путь там доступен. * Условие ниже — «умеет считать сегментацию» И «есть хоть какой-то конвейер». */ export function isVirtualBackgroundSupported(): boolean { if (typeof window === 'undefined') return false return canRunSegmentation() && hasAnyFramePipeline() } /** * Условие `BackgroundTransformer.isSupported`: чем библиотека считает маску и * собирает кадр. WebGL2 проверяется созданием пробного контекста — иначе никак, * но результат кэшируется на всю жизнь страницы: браузеры держат ограниченное * число живых WebGL-контекстов, и создавать новый на каждый рендер нельзя. */ let segmentationSupport: boolean | null = null function canRunSegmentation(): boolean { if (segmentationSupport !== null) return segmentationSupport const hasApis = typeof OffscreenCanvas !== 'undefined' && typeof VideoFrame !== 'undefined' && typeof createImageBitmap !== 'undefined' if (!hasApis) { segmentationSupport = false return false } const probe = document.createElement('canvas').getContext('webgl2') // Пробный контекст сразу отпускаем — он больше не нужен, а слот в лимите // браузера занимал бы до сборки мусора. probe?.getExtension('WEBGL_lose_context')?.loseContext() segmentationSupport = Boolean(probe) return segmentationSupport } /** Условие `ProcessorWrapper.isSupported`: современный конвейер ЛИБО запасной на canvas. */ function hasAnyFramePipeline(): boolean { const modern = 'MediaStreamTrackProcessor' in window && 'MediaStreamTrackGenerator' in window const fallback = typeof HTMLCanvasElement !== 'undefined' && 'captureStream' in HTMLCanvasElement.prototype return modern || fallback } /** * Ассеты MediaPipe со своего домена — см. заголовок файла и `vite.config.ts`. * * `tasksVisionFileSet` — КАТАЛОГ с wasm-рантаймом: библиотека сама выберет * simd- или nosimd-вариант по возможностям браузера, поэтому в каталоге лежат * оба. `modelAssetPath` — конкретный файл модели сегментации. */ const LOCAL_ASSET_PATHS = { tasksVisionFileSet: '/mediapipe/wasm', modelAssetPath: '/mediapipe/selfie_segmenter.tflite', } /** * Создать процессор замены фона на картинку `imagePath`. * * Библиотека грузится динамическим `import()` — первый вызов скачивает её * вместе с wasm и моделью, последующие берут из кэша модулей/браузера. */ export async function createBackgroundProcessor( imagePath: string, ): Promise { const { BackgroundProcessor } = await import('@livekit/track-processors') return BackgroundProcessor({ mode: 'virtual-background', imagePath, assetPaths: LOCAL_ASSET_PATHS, }) } /** * Запустить процессор на «сыром» треке камеры — для превью входа, где * LiveKit-трека ещё нет (в комнате всё это делает сам * `LocalVideoTrack.setProcessor`). * * ⚠️ Процессору обязателен `element` — `