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,3 +73,23 @@
|
||||
text-align: center;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
/* Выбор фона под кнопками превью (сессия 35).
|
||||
Здесь плитки идут ОДНОЙ прокручиваемой строкой, а не сеткой, как в комнате
|
||||
и в профиле: карточка входа и без того высокая, а число своих картинок
|
||||
доходит до десяти — сетка выросла бы на шесть рядов и увела бы кнопку
|
||||
«Продолжить» за нижний край экрана. Строка держит высоту постоянной при
|
||||
любом числе картинок. Сами плитки — общие, см. virtual-background.css. */
|
||||
.device-check-backgrounds {
|
||||
margin: var(--space-4) auto 0;
|
||||
}
|
||||
.device-check-backgrounds .bg-grid {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
overflow-x: auto;
|
||||
padding-bottom: var(--space-2);
|
||||
}
|
||||
.device-check-backgrounds .bg-tile {
|
||||
flex: 0 0 auto;
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
124
frontend/src/styles/virtual-background.css
Normal file
124
frontend/src/styles/virtual-background.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* VidConf — выбор фона видео (сессия 35).
|
||||
*
|
||||
* Одна сетка плиток на три места: превью входа (`JoinPage`), комната
|
||||
* (`BackgroundDialog`) и профиль. Базовые правила — на токенах оболочки;
|
||||
* внутри комнаты те же классы перекрашиваются блоком `[data-theme="room"]`
|
||||
* в конце файла, как это уже сделано для модалок (см. `room.css`,
|
||||
* `.room-modal-*`): токены `--color-ink-*`/`--color-border` внутри комнаты
|
||||
* не переопределяются, и без этого блока сетка была бы светлой на тёмном.
|
||||
*/
|
||||
|
||||
.bg-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.bg-tile {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 0;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-alt);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.bg-tile img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.bg-tile:hover { border-color: var(--color-border-strong); }
|
||||
.bg-tile.is-selected { border-color: var(--color-border-pill); }
|
||||
.bg-tile:focus-visible { outline: 2px solid var(--color-border-pill); outline-offset: 2px; }
|
||||
|
||||
/* Плитка «без фона» — перечёркнутый круг вместо картинки. */
|
||||
.bg-tile-none {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-ink-500);
|
||||
}
|
||||
.bg-tile-none .lucide { width: 20px; height: 20px; }
|
||||
|
||||
/* Метка выбранной плитки: галочка либо спиннер, пока фон применяется. */
|
||||
.bg-tile-mark {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-border-pill);
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bg-tile-mark .lucide { width: 14px; height: 14px; }
|
||||
.bg-tile-spinner { animation: bg-tile-spin 1s linear infinite; }
|
||||
@keyframes bg-tile-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bg-tile-spinner { animation: none; }
|
||||
}
|
||||
|
||||
/* Подсказка под сеткой — тот же тон, что у `.field-hint` оболочки. */
|
||||
.bg-hint { font: var(--text-body); color: var(--color-ink-500); margin: var(--space-2) 0 0; }
|
||||
|
||||
/* Управление своими картинками (профиль): плитка + кнопка удаления поверх. */
|
||||
.bg-manage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.bg-manage-tile {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-alt);
|
||||
}
|
||||
.bg-manage-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.bg-tile-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bg-tile-delete .lucide { width: 13px; height: 13px; }
|
||||
.bg-tile-delete:hover { background: var(--color-danger); }
|
||||
.bg-tile-delete:disabled { opacity: 0.6; cursor: default; }
|
||||
|
||||
/* ---------- Та же сетка внутри комнаты (тёмная тема) ---------- */
|
||||
[data-theme='room'] .bg-tile {
|
||||
border-color: var(--color-room-tile-border);
|
||||
background: var(--color-room-tile);
|
||||
}
|
||||
[data-theme='room'] .bg-tile:hover { border-color: var(--color-room-text-secondary); }
|
||||
[data-theme='room'] .bg-tile.is-selected { border-color: var(--color-room-speaker-ring); }
|
||||
[data-theme='room'] .bg-tile:focus-visible { outline-color: var(--color-room-focus-ring); }
|
||||
[data-theme='room'] .bg-tile-none { color: var(--color-room-text-secondary); }
|
||||
[data-theme='room'] .bg-tile-mark {
|
||||
background: var(--color-room-speaker-ring);
|
||||
color: var(--color-room-bg);
|
||||
}
|
||||
[data-theme='room'] .bg-hint { color: var(--color-room-text-secondary); }
|
||||
Reference in New Issue
Block a user