feat(admin): рычаги нагрузки медиа — потолок качества публикации и лимит плиток
instance_settings.media_limits (publish_quality_cap: off/720p/360p/180p, stage_max_tiles: 4/9/16/25) — новая вкладка «Нагрузка» в админке, дефолты (off/25) сохраняют текущее поведение существующих инсталляций. Настройка отдаётся не только GET /admin/settings, но и в join-ответе (JoinOut) — участнику нужно иметь её на руках ДО публикации трека, а /admin/settings доступен только администратору. Потолок качества применяется через RoomOptions.publishDefaults (videoEncoding + videoSimulcastLayers на пресетах VideoPresets LiveKit) — режет битрейт верхнего слоя симулкаста, реальное разрешение WebRTC подстраивает сам. Лимит плиток — фильтрация STAGE_GRID_LAYOUTS по columns*rows в StageGrid, лишние участники уходят на страницу пагинации вместо подписки. Значение приезжает в joinState вместе с токеном ДО первого рендера LiveKitRoom (RoomPage не рендерит его, пока joinState не заполнен целиком), поэтому смена настройки не переподключает уже вошедшего участника — roomOptions пересчитывается по стабильной ссылке на joinState, которая после подключения не меняется. Проверено вживую на локальном стенде (docker compose --profile media): сохранение/персист настроек, join отдаёт актуальные значения, уже подключённый участник не разрывается при смене настройки в другом окне.
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
Ключи зеркалят секции конфигурации (`transcriber`, `summarizer`, `chat`,
|
||||
`ai_level`, `summary_recipients`, `display_timezone`,
|
||||
`registration_team_choice`, `registration_email_domain`, `contact_email`) —
|
||||
`registration_team_choice`, `registration_email_domain`, `contact_email`,
|
||||
`media_limits`) —
|
||||
новая настройка не требует миграции, только новая строка. Бутстрап (`ensure_bootstrapped`)
|
||||
импортирует дефолты `config/plugins.yaml` через `INSERT ... ON CONFLICT DO
|
||||
NOTHING` в lifespan backend — однократно и идемпотентно: повторный вызов
|
||||
@@ -28,7 +29,10 @@ from core.plugins.config import (
|
||||
AiLevel,
|
||||
ChatConfig,
|
||||
InstanceConfig,
|
||||
MediaLimitsConfig,
|
||||
PluginsConfig,
|
||||
PublishQualityCap,
|
||||
StageMaxTiles,
|
||||
SummarizerConfig,
|
||||
SummaryRecipientsMode,
|
||||
TranscriberConfig,
|
||||
@@ -47,6 +51,7 @@ _KEY_DISPLAY_TIMEZONE = "display_timezone"
|
||||
_KEY_REGISTRATION_TEAM_CHOICE = "registration_team_choice"
|
||||
_KEY_REGISTRATION_EMAIL_DOMAIN = "registration_email_domain"
|
||||
_KEY_CONTACT_EMAIL = "contact_email"
|
||||
_KEY_MEDIA_LIMITS = "media_limits"
|
||||
|
||||
BOOTSTRAP_MANAGED_KEYS: tuple[str, ...] = (
|
||||
_KEY_CHAT,
|
||||
@@ -70,6 +75,7 @@ _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE: dict[str, Any] = {"enabled": False, "d
|
||||
для обратной совместимости с уже развёрнутыми инстансами; при первом же
|
||||
`update()` значение переписывается в новую форму (см. `update`)."""
|
||||
_DEFAULT_CONTACT_EMAIL_VALUE: dict[str, Any] = {"enabled": False, "email": None}
|
||||
_DEFAULT_MEDIA_LIMITS_VALUE: dict[str, Any] = {"publish_quality_cap": "off", "stage_max_tiles": 25}
|
||||
|
||||
# Простой паттерн доменного имени: минимум один символ, минимум одна точка,
|
||||
# метки из латинских букв/цифр/дефисов (без ведущего/конечного дефиса),
|
||||
@@ -103,6 +109,8 @@ class SettingsUpdateIn(BaseModel):
|
||||
registration_email_domains: list[str] | None = None
|
||||
contact_email_enabled: bool | None = None
|
||||
contact_email: str | None = None
|
||||
publish_quality_cap: PublishQualityCap | None = None
|
||||
stage_max_tiles: StageMaxTiles | None = None
|
||||
|
||||
|
||||
class BootstrapOverrides(BaseModel):
|
||||
@@ -152,6 +160,7 @@ def build_bootstrap_defaults(
|
||||
_KEY_REGISTRATION_TEAM_CHOICE: dict(_DEFAULT_REGISTRATION_TEAM_CHOICE_VALUE),
|
||||
_KEY_REGISTRATION_EMAIL_DOMAIN: dict(_DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE),
|
||||
_KEY_CONTACT_EMAIL: dict(_DEFAULT_CONTACT_EMAIL_VALUE),
|
||||
_KEY_MEDIA_LIMITS: dict(_DEFAULT_MEDIA_LIMITS_VALUE),
|
||||
}
|
||||
if overrides is None:
|
||||
return defaults
|
||||
@@ -340,6 +349,20 @@ class InstanceSettingsService:
|
||||
await self._set(_KEY_TRANSCRIBER, cfg.transcriber.model_dump(mode="json"))
|
||||
await self._set(_KEY_SUMMARIZER, cfg.summarizer.model_dump(mode="json"))
|
||||
|
||||
if patch.publish_quality_cap is not None or patch.stage_max_tiles is not None:
|
||||
cap = (
|
||||
patch.publish_quality_cap
|
||||
if patch.publish_quality_cap is not None
|
||||
else cfg.media_limits.publish_quality_cap
|
||||
)
|
||||
max_tiles = (
|
||||
patch.stage_max_tiles
|
||||
if patch.stage_max_tiles is not None
|
||||
else cfg.media_limits.stage_max_tiles
|
||||
)
|
||||
cfg.media_limits = MediaLimitsConfig(publish_quality_cap=cap, stage_max_tiles=max_tiles)
|
||||
await self._set(_KEY_MEDIA_LIMITS, cfg.media_limits.model_dump(mode="json"))
|
||||
|
||||
await self._session.commit()
|
||||
return cfg
|
||||
|
||||
@@ -489,4 +512,7 @@ def _build_config(rows: dict[str, Any]) -> InstanceConfig:
|
||||
"enabled", False
|
||||
),
|
||||
contact_email=rows.get(_KEY_CONTACT_EMAIL, _DEFAULT_CONTACT_EMAIL_VALUE).get("email"),
|
||||
media_limits=MediaLimitsConfig.model_validate(
|
||||
rows.get(_KEY_MEDIA_LIMITS, _DEFAULT_MEDIA_LIMITS_VALUE)
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user