fix(room): мини-окно открывается на плитке из основного окна
В Chrome мини-плеер показывал самого пользователя вместо того, что он видел крупно. Сцена в мини-окне — отдельный экземпляр RoomStage (портал в PiP-окно), и состояние фокуса он начинал с нуля: демонстрации нет, никто прямо сейчас не говорит — pickStageFocus доходил до последнего фолбэка localKey, то есть до «показать себя». В Safari бага не было видно: там Document PiP не используется, а video-фолбэк берёт <video> прямо из фокус-плитки основного окна. Ключ фокуса теперь передаётся наружу (onFocusKeyChange), живёт в состоянии RoomPage и достаётся следующему экземпляру сцены (initialFocusKey). Работает в обе стороны — возврат из мини-плеера тоже не сбрасывает фокус. Правила выбора фокуса (pickStageFocus) не тронуты.
This commit is contained in:
@@ -124,6 +124,17 @@ function hasLiveVideo(track: TrackReferenceOrPlaceholder): boolean {
|
|||||||
* `tiles` скрывать нечего (карусели нет), переключатель там заблокирован —
|
* `tiles` скрывать нечего (карусели нет), переключатель там заблокирован —
|
||||||
* см. `StageViewOptions`.
|
* см. `StageViewOptions`.
|
||||||
*
|
*
|
||||||
|
* ФОКУС ПЕРЕЖИВАЕТ ПЕРЕЕЗД В МИНИ-ПЛЕЕР. Сцена в мини-плеере — ОТДЕЛЬНЫЙ
|
||||||
|
* экземпляр этого компонента (портал в PiP-окно), и своё состояние фокуса он
|
||||||
|
* начинал с нуля: демонстрации нет, никто прямо сейчас не говорит — и
|
||||||
|
* `pickStageFocus` доходил до последнего фолбэка `localKey`, то есть мини-окно
|
||||||
|
* открывалось на самом пользователе вместо того, что он видел крупно. В Safari
|
||||||
|
* бага не было видно: там Document PiP не используется, а video-фолбэк
|
||||||
|
* (`useRoomPiP`) берёт `<video>` прямо из фокус-плитки основного окна. Лечится
|
||||||
|
* передачей ключа наружу и обратно: `onFocusKeyChange` → состояние в
|
||||||
|
* `RoomPage` → `initialFocusKey` следующего экземпляра. Работает в обе стороны
|
||||||
|
* — возврат из мини-плеера тоже не сбрасывает фокус.
|
||||||
|
*
|
||||||
* Раскладка — вертикальная колонка миниатюр слева от основной сцены (не
|
* Раскладка — вертикальная колонка миниатюр слева от основной сцены (не
|
||||||
* горизонтальная лента, см. design/mockups/room.html после правки: узкая
|
* горизонтальная лента, см. design/mockups/room.html после правки: узкая
|
||||||
* колонка сбоку, скролл по вертикали). Это штатное поведение самого
|
* колонка сбоку, скролл по вертикали). Это штатное поведение самого
|
||||||
@@ -166,6 +177,8 @@ export function RoomStage({
|
|||||||
hideOthers = false,
|
hideOthers = false,
|
||||||
onShowOthers,
|
onShowOthers,
|
||||||
onHideOthers,
|
onHideOthers,
|
||||||
|
initialFocusKey = null,
|
||||||
|
onFocusKeyChange,
|
||||||
}: {
|
}: {
|
||||||
variant?: 'full' | 'pip'
|
variant?: 'full' | 'pip'
|
||||||
/** Выбранный пользователем режим показа; игнорируется при `variant="pip"`. */
|
/** Выбранный пользователем режим показа; игнорируется при `variant="pip"`. */
|
||||||
@@ -176,6 +189,10 @@ export function RoomStage({
|
|||||||
onShowOthers?: () => void
|
onShowOthers?: () => void
|
||||||
/** Скрыть остальных — кнопка над каруселью (только широкий экран). */
|
/** Скрыть остальных — кнопка над каруселью (только широкий экран). */
|
||||||
onHideOthers?: () => void
|
onHideOthers?: () => void
|
||||||
|
/** Чем инициализировать фокус при монтировании — см. докстринг про мини-плеер. */
|
||||||
|
initialFocusKey?: string | null
|
||||||
|
/** Сообщать наружу текущий фокус, чтобы его пережил переезд сцены в мини-плеер и обратно. */
|
||||||
|
onFocusKeyChange?: (key: string | null) => void
|
||||||
}) {
|
}) {
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const isCompact = useIsCompactViewport()
|
const isCompact = useIsCompactViewport()
|
||||||
@@ -224,7 +241,10 @@ export function RoomStage({
|
|||||||
// `pickStageFocus` меняют результат без изменения самих треков.
|
// `pickStageFocus` меняют результат без изменения самих треков.
|
||||||
const [prevTracks, setPrevTracks] = useState(tracks)
|
const [prevTracks, setPrevTracks] = useState(tracks)
|
||||||
const [prevSpeakingParticipants, setPrevSpeakingParticipants] = useState(speakingParticipants)
|
const [prevSpeakingParticipants, setPrevSpeakingParticipants] = useState(speakingParticipants)
|
||||||
const [focusKey, setFocusKey] = useState<string | null>(null)
|
// Стартовое значение — фокус, доставшийся от предыдущего экземпляра сцены
|
||||||
|
// (см. `initialFocusKey` и докстринг про мини-плеер). Дальше живёт своей
|
||||||
|
// жизнью: `pickStageFocus` пересчитывает его на каждое значимое изменение.
|
||||||
|
const [focusKey, setFocusKey] = useState<string | null>(initialFocusKey)
|
||||||
// Закрепление живёт в состоянии сцены (задача 3.1): ключ `identity:source`
|
// Закрепление живёт в состоянии сцены (задача 3.1): ключ `identity:source`
|
||||||
// плитки, которую пользователь закрепил булавкой; `null` — закрепления нет.
|
// плитки, которую пользователь закрепил булавкой; `null` — закрепления нет.
|
||||||
// Только для основного окна — в PiP плитка одна и закреплять нечего.
|
// Только для основного окна — в PiP плитка одна и закреплять нечего.
|
||||||
@@ -282,6 +302,14 @@ export function RoomStage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отдаём фокус наружу (в `RoomPage`), чтобы он пережил размонтирование этой
|
||||||
|
// сцены и достался следующей — см. `initialFocusKey`. Именно эффект, а не
|
||||||
|
// вызов в теле рендера: setState ЧУЖОГО компонента во время рендера React
|
||||||
|
// запрещает.
|
||||||
|
useEffect(() => {
|
||||||
|
onFocusKeyChange?.(focusKey)
|
||||||
|
}, [focusKey, onFocusKeyChange])
|
||||||
|
|
||||||
const focusTrack = tracks.find((t) => stageTrackKey(t) === focusKey) ?? screenShareTracks[0] ?? cameraTracks[0]
|
const focusTrack = tracks.find((t) => stageTrackKey(t) === focusKey) ?? screenShareTracks[0] ?? cameraTracks[0]
|
||||||
const focusTrackKey = focusTrack ? stageTrackKey(focusTrack) : null
|
const focusTrackKey = focusTrack ? stageTrackKey(focusTrack) : null
|
||||||
// При активной демонстрации карусель — ВСЕ камеры (включая демонстратора) И
|
// При активной демонстрации карусель — ВСЕ камеры (включая демонстратора) И
|
||||||
|
|||||||
@@ -168,6 +168,13 @@ export function RoomPage() {
|
|||||||
saveStageLayoutMode(mode)
|
saveStageLayoutMode(mode)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Ключ трека, который сцена показывает крупно. Живёт ЗДЕСЬ, а не только
|
||||||
|
// внутри `RoomStage`, потому что при открытии мини-плеера сцена
|
||||||
|
// размонтируется в основном окне и монтируется заново в PiP-окне (портал
|
||||||
|
// ниже) — без этого мостика новый экземпляр начинал бы выбор фокуса с нуля и
|
||||||
|
// открывал мини-окно на самом пользователе. Подробнее — докстринг `RoomStage`.
|
||||||
|
const [stageFocusKey, setStageFocusKey] = useState<string | null>(null)
|
||||||
|
|
||||||
// Сохранённый выбор устройств — читаем через собственный вызов
|
// Сохранённый выбор устройств — читаем через собственный вызов
|
||||||
// usePersistentUserChoices (независимый от того, что использует
|
// usePersistentUserChoices (независимый от того, что использует
|
||||||
// DeviceSettingsDialog: там свой вызов хука со своим состоянием). ВАЖНО:
|
// DeviceSettingsDialog: там свой вызов хука со своим состоянием). ВАЖНО:
|
||||||
@@ -241,6 +248,8 @@ export function RoomPage() {
|
|||||||
hideOthers={hideOthers}
|
hideOthers={hideOthers}
|
||||||
onShowOthers={() => setHideOthers(false)}
|
onShowOthers={() => setHideOthers(false)}
|
||||||
onHideOthers={() => setHideOthers(true)}
|
onHideOthers={() => setHideOthers(true)}
|
||||||
|
initialFocusKey={stageFocusKey}
|
||||||
|
onFocusKeyChange={setStageFocusKey}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{chatVisible && chatOpen && (
|
{chatVisible && chatOpen && (
|
||||||
@@ -284,8 +293,13 @@ export function RoomPage() {
|
|||||||
рендерящийся в PiP-окне, продолжает читать RoomContext/треки.
|
рендерящийся в PiP-окне, продолжает читать RoomContext/треки.
|
||||||
`variant="pip"` — мини-плеер
|
`variant="pip"` — мини-плеер
|
||||||
показывает только активное окно (одну плитку), без карусели/грида
|
показывает только активное окно (одну плитку), без карусели/грида
|
||||||
основного окна. */}
|
основного окна. `initialFocusKey` — то, что было крупно в основном
|
||||||
{pip.pipWindow && createPortal(<RoomStage variant="pip" />, pip.pipWindow.document.body)}
|
окне: без него мини-окно открывалось на самом пользователе. */}
|
||||||
|
{pip.pipWindow &&
|
||||||
|
createPortal(
|
||||||
|
<RoomStage variant="pip" initialFocusKey={stageFocusKey} onFocusKeyChange={setStageFocusKey} />,
|
||||||
|
pip.pipWindow.document.body,
|
||||||
|
)}
|
||||||
</LiveKitRoom>
|
</LiveKitRoom>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user