Compare commits
2 Commits
d11b808e97
...
65fbcf952c
| Author | SHA1 | Date | |
|---|---|---|---|
| 65fbcf952c | |||
| b44652d6a6 |
@@ -122,7 +122,7 @@ SMTP_TIMEOUT_S=30
|
||||
# --- Версия инстанса (релиз v0.0.1) ---
|
||||
# install.sh копирует значение из корневого файла VERSION при каждой
|
||||
# установке/обновлении — руками менять не нужно.
|
||||
VIDCONF_VERSION=0.0.26
|
||||
VIDCONF_VERSION=0.0.27
|
||||
|
||||
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
||||
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -3,6 +3,36 @@
|
||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
||||
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).
|
||||
|
||||
## [0.0.27] — 2026-08-04
|
||||
|
||||
Разрыв связи больше не выбрасывает участника из конференции.
|
||||
|
||||
### Исправлено
|
||||
- Телефон с погасшим экраном выпадал из конференции, а при возвращении
|
||||
мог оказаться в лобби вместо комнаты. Причина установлена по логам
|
||||
прода: Chrome срезает ресурсы фоновой вкладке, телефон перестаёт
|
||||
отвечать по ICE, и LiveKit закрывает участника через 5 с после потери
|
||||
соединения (замерено: 37 с после блокировки экрана, 23 с после
|
||||
сворачивания браузера — то есть достаточно просто убрать вкладку в
|
||||
фон, гасить экран не обязательно). Восстановить сессию после этого
|
||||
нельзя — участника на сервере уже нет; нужен полный повторный вход,
|
||||
и `livekit-client` пытается сделать его сам, но его бюджет повторов
|
||||
(10 попыток за ~44 с) в фоновой вкладке успевает сгореть. Тогда
|
||||
приходило событие `Disconnected`, и страница комнаты уводила
|
||||
пользователя в лобби.
|
||||
|
||||
Теперь непреднамеренный разрыв не уводит со страницы: сбрасывается
|
||||
состояние входа, и работает уже имевшийся путь авто-перезахода —
|
||||
резолв конференции, свежий токен, вход заново. Намеренный выход
|
||||
отличается по нажатию кнопки «Выйти», а не по коду причины: причину
|
||||
`CLIENT_INITIATED` присылает и кнопка, и сам `livekit-client`, который
|
||||
при заморозке вкладки (событие `freeze`) вызывает `disconnect()`
|
||||
самостоятельно. Разрывы, после которых возвращаться нельзя (участника
|
||||
выгнал организатор, конференция закрыта, вход той же личностью с
|
||||
другого устройства), уводят в лобби как раньше. От цикла «вошёл —
|
||||
сразу выбросило» защищает лимит в 5 перезаходов подряд; соединение,
|
||||
прожившее дольше 30 с, счётчик обнуляет.
|
||||
|
||||
## [0.0.26] — 2026-08-04
|
||||
|
||||
Мобильная комната: свайп шторки настроек и полноэкранный режим.
|
||||
|
||||
@@ -89,7 +89,7 @@ services:
|
||||
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
||||
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
||||
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
||||
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.26}
|
||||
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.27}
|
||||
# Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан
|
||||
# на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение,
|
||||
# проверьте бюджет соединений с БД: каждый воркер держит свой пул
|
||||
|
||||
@@ -71,6 +71,14 @@ interface RoomToolbarProps extends StageViewProps {
|
||||
onLowerHandById: (identity: string) => void
|
||||
/** Тулбар в оверлее полноэкранного режима — см. докстринг `RoomTopbar.overlayVisible`, тот же механизм. */
|
||||
overlayVisible?: boolean
|
||||
/**
|
||||
* Пользователь нажал «Выйти» — вызывается ПЕРЕД тем, как `DisconnectButton`
|
||||
* отключит комнату (обработчики в `mergeProps` вызываются цепочкой). Нужен
|
||||
* `RoomPage`, чтобы отличить намеренный выход от разрыва: причина
|
||||
* `CLIENT_INITIATED` приходит и от кнопки, и от livekit-client при заморозке
|
||||
* вкладки — см. докстринг `handleDisconnected`.
|
||||
*/
|
||||
onLeave?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +124,7 @@ export function RoomToolbar({
|
||||
hideOthers,
|
||||
onHideOthersChange,
|
||||
overlayVisible,
|
||||
onLeave,
|
||||
}: RoomToolbarProps) {
|
||||
const toast = useToast()
|
||||
const isCompact = useIsCompactViewport()
|
||||
@@ -306,7 +315,7 @@ export function RoomToolbar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<DisconnectButton className="tb-btn danger" aria-label="Выйти из конференции">
|
||||
<DisconnectButton className="tb-btn danger" aria-label="Выйти из конференции" onClick={onLeave}>
|
||||
<span className="icon-shell">
|
||||
<LogOut className="lucide" aria-hidden="true" />
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { PictureInPicture2 } from 'lucide-react'
|
||||
import { LiveKitRoom, RoomAudioRenderer, usePersistentUserChoices } from '@livekit/components-react'
|
||||
import type { RoomOptions } from 'livekit-client'
|
||||
import { DisconnectReason, type RoomOptions } from 'livekit-client'
|
||||
import '@livekit/components-styles'
|
||||
import '@/styles/room.css'
|
||||
import { joinConference, resolveConference, type PublishQualityCap } from '@/api/conferences'
|
||||
@@ -28,6 +28,23 @@ const FULLSCREEN_CONTROLS_AUTO_HIDE_MS = 4000
|
||||
/** Полоса у нижнего края экрана (px) — наведение мыши в неё в полноэкранном режиме на десктопе показывает тулбар без клика. */
|
||||
const FULLSCREEN_FOOTER_HOVER_ZONE_PX = 72
|
||||
|
||||
/**
|
||||
* Сколько раз подряд страница возвращается в конференцию сама после разрыва,
|
||||
* которого пользователь не просил. Ограничение — защита от бесконечного цикла
|
||||
* «вошёл → сразу выбросило»: например, если конференция кончилась ровно в
|
||||
* момент разрыва или сервер по какой-то причине не принимает участника.
|
||||
* Исчерпав бюджет, страница ведёт себя как раньше — уводит в лобби.
|
||||
*/
|
||||
const AUTO_REJOIN_LIMIT = 5
|
||||
|
||||
/**
|
||||
* Соединение, прожившее дольше этого времени, считается состоявшимся: бюджет
|
||||
* авто-перезаходов обнуляется. Без этого редкие разрывы за длинную встречу
|
||||
* (телефон засыпал пять раз за час) постепенно съели бы лимит и шестое
|
||||
* пробуждение выбросило бы участника в лобби.
|
||||
*/
|
||||
const AUTO_REJOIN_BUDGET_RESET_MS = 30_000
|
||||
|
||||
interface RoomJoinState {
|
||||
livekitUrl: string
|
||||
token: string
|
||||
@@ -78,13 +95,88 @@ export function RoomPage() {
|
||||
const [joinState, setJoinState] = useState<RoomJoinState | null>(initialState)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Пользователь нажал «Выйти» — ЕДИНСТВЕННЫЙ признак, по которому уход со
|
||||
// страницы считается намеренным. Отдельный флаг нужен потому, что причина
|
||||
// `CLIENT_INITIATED` приходит не только от кнопки: livekit-client вешает
|
||||
// обработчик на событие `freeze` (Page Lifecycle) и при заморозке вкладки
|
||||
// сам зовёт `room.disconnect()` — с той же самой причиной. Android Chrome
|
||||
// фоновую вкладку замораживает, так что без этого флага заснувший телефон
|
||||
// выглядел бы ровно как «пользователь вышел» и уезжал бы в лобби.
|
||||
const leaveRequestedRef = useRef(false)
|
||||
// Когда установилось текущее соединение — чтобы отличить «разрыв после
|
||||
// нормальной работы» от «выбрасывает сразу после входа» (см. AUTO_REJOIN_*).
|
||||
const connectedAtRef = useRef(0)
|
||||
const autoRejoinCountRef = useRef(0)
|
||||
|
||||
const handleLeaveClick = useCallback(() => {
|
||||
leaveRequestedRef.current = true
|
||||
}, [])
|
||||
|
||||
const handleConnected = useCallback(() => {
|
||||
connectedAtRef.current = Date.now()
|
||||
// Мы снова в комнате — намерение выйти, если оно откуда-то осталось
|
||||
// невыполненным, к этому соединению уже не относится.
|
||||
leaveRequestedRef.current = false
|
||||
}, [])
|
||||
|
||||
// useCallback: стабильная ссылка на обработчик — иначе на каждом рендере
|
||||
// RoomPage пересобирался бы объект-пропс `onDisconnected`, а с ним и
|
||||
// внутренний useEffect @livekit/components-react (он в списке зависимостей
|
||||
// хука useLiveKitRoom), лишний раз пересоздавая подписки на события Room.
|
||||
const handleDisconnected = useCallback(() => {
|
||||
navigate(status === 'authenticated' ? '/lobby' : '/join')
|
||||
}, [navigate, status])
|
||||
//
|
||||
// Разрыв ≠ выход. До 0.0.27 любое отключение уводило со страницы, и на
|
||||
// мобильном это ломало обычный сценарий: экран телефона гаснет, Chrome
|
||||
// срезает фоновой вкладке ресурсы, ICE перестаёт отвечать — и LiveKit
|
||||
// закрывает участника через 5 с после потери соединения
|
||||
// (`PEER_CONNECTION_DISCONNECTED`, замерено на проде: 37 с после блокировки
|
||||
// экрана, 23 с после сворачивания браузера). Восстановить сессию после
|
||||
// этого нельзя — участника на сервере уже нет (`could not restart
|
||||
// participant`), нужен полный повторный вход. livekit-client такой вход
|
||||
// пытается сделать сам, но его бюджет повторов (10 попыток, ~44 с) в
|
||||
// фоновой вкладке успевает сгореть — и тогда приходит `Disconnected`,
|
||||
// после которого пользователь оказывался в лобби вместо конференции.
|
||||
//
|
||||
// Теперь непреднамеренный разрыв просто сбрасывает `joinState`, а дальше
|
||||
// работает уже написанный путь авто-перезахода (эффект ниже): страница
|
||||
// молча резолвит конференцию, берёт свежий токен и входит заново. Никакого
|
||||
// второго механизма восстановления не появляется.
|
||||
const handleDisconnected = useCallback(
|
||||
(reason?: DisconnectReason) => {
|
||||
const leaveForGood = () => navigate(status === 'authenticated' ? '/lobby' : '/join')
|
||||
|
||||
if (leaveRequestedRef.current) {
|
||||
leaveForGood()
|
||||
return
|
||||
}
|
||||
|
||||
// Разрывы, после которых возвращаться некуда или нельзя: участника
|
||||
// выгнал организатор, конференцию закрыли, либо этой же личностью вошли
|
||||
// с другого устройства (перезаход устроил бы пинг-понг между ними).
|
||||
if (
|
||||
reason === DisconnectReason.PARTICIPANT_REMOVED ||
|
||||
reason === DisconnectReason.ROOM_DELETED ||
|
||||
reason === DisconnectReason.ROOM_CLOSED ||
|
||||
reason === DisconnectReason.DUPLICATE_IDENTITY
|
||||
) {
|
||||
leaveForGood()
|
||||
return
|
||||
}
|
||||
|
||||
const sessionMs = connectedAtRef.current ? Date.now() - connectedAtRef.current : 0
|
||||
if (sessionMs > AUTO_REJOIN_BUDGET_RESET_MS) {
|
||||
autoRejoinCountRef.current = 0
|
||||
}
|
||||
if (autoRejoinCountRef.current >= AUTO_REJOIN_LIMIT) {
|
||||
leaveForGood()
|
||||
return
|
||||
}
|
||||
|
||||
autoRejoinCountRef.current += 1
|
||||
connectedAtRef.current = 0
|
||||
setJoinState(null)
|
||||
},
|
||||
[navigate, status],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (joinState || !slug) return
|
||||
@@ -362,6 +454,7 @@ export function RoomPage() {
|
||||
audio={false}
|
||||
video={false}
|
||||
options={roomOptions}
|
||||
onConnected={handleConnected}
|
||||
onDisconnected={handleDisconnected}
|
||||
>
|
||||
{/* Звук комнаты рендерится ЗДЕСЬ, а не внутри `RoomStage`, и ровно
|
||||
@@ -444,6 +537,7 @@ export function RoomPage() {
|
||||
hideOthers={hideOthers}
|
||||
onHideOthersChange={setHideOthers}
|
||||
overlayVisible={fullscreenControlsVisible}
|
||||
onLeave={handleLeaveClick}
|
||||
/>
|
||||
</div>
|
||||
{settingsOpen && (
|
||||
|
||||
Reference in New Issue
Block a user