fix(auth): превью и проверка устройств на шаге "Подключиться к конференции"

Продолжение 33: раньше превью показывалось только на карточке "Как вас
зовут?" (guest-info), а авторизованный пользователь, входящий через
/join, этот шаг вообще не проходит (сразу connecting) — значит, никогда
не видел проверку устройств и не мог задать enterWithVideo/Audio.

Теперь превью и кнопки — на обоих шагах (input и guest-info), с одним
непрерывным потоком: hook enabled/release эффект завязаны на общий флаг
"мы на одном из шагов с превью", а не на конкретный step, иначе переход
input -> guest-info выглядел бы для эффекта как уход с гашением камеры.

Заодно нашёл и починил реальную грабли: <video> на разных шагах — это
разные DOM-узлы (разные позиции в JSX), обычный ref.current не пережил
бы переезд между ними — поток остаётся жив, но картинка гаснет в чёрный
прямоугольник. videoRef хука теперь callback-ref, переподключающий уже
открытый поток к любому новому узлу автоматически.
This commit is contained in:
2026-08-09 21:22:34 +03:00
parent 9463e64e73
commit ca0b1e23fa
3 changed files with 90 additions and 37 deletions

View File

@@ -6,9 +6,16 @@ export type DeviceCheckStatus = 'idle' | 'pending' | 'granted' | 'denied'
interface UseDeviceCheckAccessResult {
videoStatus: DeviceCheckStatus
audioStatus: DeviceCheckStatus
/** Привязать к `<video>` превью — хук сам управляет `srcObject` по мере
* появления/остановки потока камеры. */
videoRef: React.RefObject<HTMLVideoElement | null>
/** Привязать к `ref` `<video>` превью — коллбэк, НЕ объект-реф: карточка
* превью рендерится в разных местах JSX-дерева на разных шагах
* (`input`/`guest-info` в `JoinPage`), и React монтирует для каждого места
* СВОЙ DOM-узел `<video>`, хотя тип компонента один и тот же — обычный
* `ref.current` продолжал бы указывать на старый (уже отмонтированный)
* узел. Коллбэк вызывается при каждом монтировании нового узла и сам
* подключает уже открытый поток (см. докстринг хука) — без этого переход
* между шагами давал бы на месте превью чёрный прямоугольник: поток жив,
* но не подключён к новому элементу. */
videoRef: (node: HTMLVideoElement | null) => void
/** Повесить на карточку (`onPointerDown`/`onKeyDown`) — первое взаимодействие
* внутри неё запускает запрос доступа. Идемпотентно, повторные вызовы —
* no-op (см. `requestedRef`). */
@@ -88,7 +95,17 @@ export function useDeviceCheckAccess(enabled: boolean): UseDeviceCheckAccessResu
const toggleAudioEnabled = useCallback(() => setAudioEnabled((v) => !v), [])
const videoStreamRef = useRef<MediaStream | null>(null)
const videoRef = useRef<HTMLVideoElement | null>(null)
const videoNodeRef = useRef<HTMLVideoElement | null>(null)
// См. докстринг `videoRef` в интерфейсе выше — коллбэк-реф, переподключает
// уже открытый поток к КАЖДОМУ новому DOM-узлу `<video>` сам, без этого
// переход между шагами с превью терял бы картинку (но не поток — камера
// продолжала бы физически работать, просто без видимого превью).
const videoRef = useCallback((node: HTMLVideoElement | null) => {
videoNodeRef.current = node
if (node) {
node.srcObject = videoStreamRef.current
}
}, [])
const requestedRef = useRef(false)
// Полный сброс — не только остановка треков, но и статусы/флаги/охрана
@@ -103,8 +120,8 @@ export function useDeviceCheckAccess(enabled: boolean): UseDeviceCheckAccessResu
stream.getTracks().forEach((track) => track.stop())
videoStreamRef.current = null
}
if (videoRef.current) {
videoRef.current.srcObject = null
if (videoNodeRef.current) {
videoNodeRef.current.srcObject = null
}
requestedRef.current = false
setVideoStatus('idle')
@@ -127,8 +144,8 @@ export function useDeviceCheckAccess(enabled: boolean): UseDeviceCheckAccessResu
try {
const stream = await openStream('video', userChoicesRef.current.videoDeviceId)
videoStreamRef.current = stream
if (videoRef.current) {
videoRef.current.srcObject = stream
if (videoNodeRef.current) {
videoNodeRef.current.srcObject = stream
}
setVideoStatus('granted')
// Первичная верификация сразу показывает превью — «включено» по факту
@@ -152,8 +169,8 @@ export function useDeviceCheckAccess(enabled: boolean): UseDeviceCheckAccessResu
stream.getTracks().forEach((track) => track.stop())
videoStreamRef.current = null
}
if (videoRef.current) {
videoRef.current.srcObject = null
if (videoNodeRef.current) {
videoNodeRef.current.srcObject = null
}
setVideoEnabled(false)
return