Zmiana trybu nie wycisza juz calej sali
Wylaczenie "wideo dla wszystkich" przy wlaczonym otwartym mikrofonie
wyciszalo WSZYSTKICH uczestnikow, takze mowiacych w tej chwili. Zdarzylo
sie to na zywo i zaklocilo spotkanie.
Zlozyly sie na to dwie rzeczy:
1. Stan `voiceState` byl trojstopniowy (`none` / `granted` / `video`)
i mieszal dwa NIEZALEZNE przelaczniki prowadzacego. Wideo przykrywalo
informacje o mikrofonie, wiec jego wylaczenie wygladalo jak zmiana
uprawnien, chociaz prawo do mikrofonu zostawalo.
2. `key={join.token}` przemontowywal sale przy KAZDYM nowym tokenie,
a przemontowanie jest ponownym wejsciem: provider startuje z
`audio={false}`, wiec kazda opublikowana sciezka przepada. Token jest
przy tym zawsze inny, bo JWT niesie czas wydania.
Sam token byl poprawny: rola SPEAKER zachowuje `canPublishAudio`.
Klucz sklada teraz `canPublishAudio` i `canPublishVideo`, czyli faktyczne
uprawnienia tego uczestnika wyliczone przez backend z roli, obu trybow
i przyznanego glosu. Zmiana trybu, ktora go nie dotyczy, nie rusza jego sali.
Do tego `initialAudio`/`initialVideo`: urzadzenia wlaczone przed
przemontowaniem wracaja wlaczone, wiec przemontowanie nieuniknione (realna
zmiana uprawnien) tez nie wycisza mowiacego. Kamera wraca wylacznie wtedy,
gdy uczestnik nadal ma do niej prawo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,11 @@ import {
|
||||
LiveKitRoom as LiveKitRoomProvider,
|
||||
RoomAudioRenderer,
|
||||
VideoConference,
|
||||
useLocalParticipant,
|
||||
} from '@livekit/components-react'
|
||||
import { RoomOptions } from 'livekit-client'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { BackgroundBlurToggle } from './BackgroundBlurToggle'
|
||||
|
||||
/**
|
||||
* Pokoj LiveKit osadzony w naszym ekranie spotkania.
|
||||
@@ -36,10 +38,25 @@ export function LiveKitRoom({
|
||||
serverUrl,
|
||||
token,
|
||||
onDisconnected,
|
||||
initialAudio = false,
|
||||
initialVideo = false,
|
||||
onDevicesChange,
|
||||
}: {
|
||||
serverUrl: string
|
||||
token: string
|
||||
onDisconnected?: () => void
|
||||
/**
|
||||
* Stan urzadzen po przemontowaniu sali.
|
||||
*
|
||||
* Przemontowanie jest ponownym wejsciem, wiec bez tego kazda zmiana
|
||||
* uprawnien wycisza mowiacego: wraca on na sale z zamknietym mikrofonem
|
||||
* i nie wie, ze przestal byc slyszany. Domyslnie oba wylaczone, bo przy
|
||||
* PIERWSZYM wejsciu przypadkowo otwarty mikrofon slychac na calej sali.
|
||||
*/
|
||||
initialAudio?: boolean
|
||||
initialVideo?: boolean
|
||||
/** Zglasza wlaczenie i wylaczenie urzadzen, zeby stan przezyl przemontowanie. */
|
||||
onDevicesChange?: (state: { audio: boolean; video: boolean }) => void
|
||||
}) {
|
||||
const options = useMemo<RoomOptions>(
|
||||
() => ({
|
||||
@@ -71,15 +88,48 @@ export function LiveKitRoom({
|
||||
// wchodzimy z wylaczonymi urzadzeniami: uczestnik sam decyduje, kiedy
|
||||
// zaczyna mowic, a przypadkowo otwarty mikrofon slychac na calej sali
|
||||
connect
|
||||
video={false}
|
||||
audio={false}
|
||||
video={initialVideo}
|
||||
audio={initialAudio}
|
||||
onDisconnected={onDisconnected}
|
||||
className="h-full w-full"
|
||||
className="relative h-full w-full"
|
||||
data-lk-theme="default"
|
||||
>
|
||||
<VideoConference />
|
||||
{/* sledzi stan mikrofonu i kamery, zeby przezyl przemontowanie sali */}
|
||||
<DeviceStateReporter onChange={onDevicesChange} />
|
||||
{/*
|
||||
Rozmycie tla. Przycisk sam sie ukrywa, gdy nie ma wlasnej kamery, wiec
|
||||
uczestnik bez prawa publikacji obrazu nigdy go nie zobaczy. Lezy nad
|
||||
kafelkami, bo `VideoConference` zajmuje caly kontener.
|
||||
*/}
|
||||
<BackgroundBlurToggle className="pointer-events-auto absolute left-3 top-3 z-10" />
|
||||
{/* dzwiek pozostalych uczestnikow; bez tego widac obraz, ale nic nie slychac */}
|
||||
<RoomAudioRenderer />
|
||||
</LiveKitRoomProvider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zglasza stan mikrofonu i kamery do komponentu nadrzednego.
|
||||
*
|
||||
* Sluzy wylacznie do tego, zeby wlaczone urzadzenia przezyly przemontowanie
|
||||
* sali. Przemontowanie jest ponownym wejsciem i zawsze startuje z urzadzeniami
|
||||
* wylaczonymi, wiec bez zapamietania stanu kazda zmiana uprawnien na sali
|
||||
* wyciszalaby osobe w trakcie wypowiedzi.
|
||||
*
|
||||
* Osobny komponent, bo `useLocalParticipant` dziala wylacznie wewnatrz
|
||||
* kontekstu pokoju.
|
||||
*/
|
||||
function DeviceStateReporter({
|
||||
onChange,
|
||||
}: {
|
||||
onChange?: (state: { audio: boolean; video: boolean }) => void
|
||||
}) {
|
||||
const { isMicrophoneEnabled, isCameraEnabled } = useLocalParticipant()
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.({ audio: isMicrophoneEnabled, video: isCameraEnabled })
|
||||
}, [isMicrophoneEnabled, isCameraEnabled, onChange])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { publicClient } from '../../api/publicClient'
|
||||
import { Icon } from '../../components/Icon'
|
||||
@@ -228,21 +228,17 @@ function MeetingRoom({
|
||||
* wiec ticket trzeba pobrac na nowo. Bez tego napis "masz glos" pojawia sie,
|
||||
* a mikrofon zostaje zamkniety, bo iframe dziala na starym tokenie.
|
||||
*/
|
||||
const [voiceState, setVoiceState] = useState<'none' | 'granted' | 'video'>('none')
|
||||
useEffect(() => {
|
||||
// otwarty mikrofon i wideo dla wszystkich zmieniaja uprawnienia w tokenie
|
||||
// tak samo jak przyznany glos, wiec ticket trzeba pobrac na nowo. Wideo
|
||||
// to inny poziom niz sam glos, wiec jest osobnym stanem, zeby przelaczenie
|
||||
// miedzy nimi tez wymuszalo odswiezenie.
|
||||
const next: 'none' | 'granted' | 'video' = openVideo
|
||||
? 'video'
|
||||
: speak.granted || openMic
|
||||
? 'granted'
|
||||
: 'none'
|
||||
if (next === voiceState) return
|
||||
setVoiceState(next)
|
||||
/*
|
||||
* Sygnal od prowadzacego mowi, ze uprawnienia MOGLY sie zmienic, ale nie
|
||||
* mowi, jakie sa: rozstrzyga to backend, ktory zna role z zapisu, oba tryby
|
||||
* i indywidualnie przyznany glos. Pobieramy wiec ticket przy kazdej zmianie,
|
||||
* a o przemontowaniu sali decyduje dopiero `key` zlozony z UPRAWNIEN
|
||||
* (patrz miejsce renderowania `LiveKitRoom`), nie z samego tokenu.
|
||||
*/
|
||||
if (join.embedded) void onRefreshTicket()
|
||||
}, [speak.granted, openMic, openVideo, voiceState, join.embedded, onRefreshTicket])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [speak.granted, openMic, openVideo])
|
||||
|
||||
const src = useMemo(() => {
|
||||
if (!join.embedded) return null
|
||||
@@ -379,6 +375,16 @@ function MeetingRoom({
|
||||
* dziala jego gotowy komponent.
|
||||
*/
|
||||
function VideoArea({ join, src }: { join: JoinView; src: string | null }) {
|
||||
/*
|
||||
* Stan urzadzen przezywajacy przemontowanie sali.
|
||||
*
|
||||
* Referencja, a nie stan Reacta: przerysowanie przy kazdym wlaczeniu
|
||||
* mikrofonu byloby zbedne, a wartosc jest potrzebna wylacznie w chwili
|
||||
* montowania nowej sali. Referencja przezywa przemontowanie DZIECKA
|
||||
* (`LiveKitRoom` ma wlasny `key`), bo siedzi w komponencie nadrzednym.
|
||||
*/
|
||||
const devicesRef = useRef({ audio: false, video: false })
|
||||
|
||||
// tryb NONE: obraz jest w zewnetrznym narzedziu, panel zostaje u nas
|
||||
if (!join.embedded) {
|
||||
return <ExternalVideo url={join.url} />
|
||||
@@ -389,15 +395,44 @@ function VideoArea({ join, src }: { join: JoinView; src: string | null }) {
|
||||
<div className="min-h-[38vh] flex-1 lg:min-h-0">
|
||||
<Suspense fallback={<VideoLoading />}>
|
||||
{/*
|
||||
key={join.token}: po przyznaniu albo odebraniu glosu backend wydaje
|
||||
NOWY token z innym uprawnieniem do mikrofonu, a `load()` go pobiera.
|
||||
LiveKitRoomProvider laczy sie z tokenem podanym przy montowaniu i nie
|
||||
renegocjuje uprawnien na zywo - wiec zmiana samego propa `token` nie
|
||||
otwiera mikrofonu, dopoki uczestnik nie przeladuje strony. Zmiana
|
||||
`key` wymusza przemontowanie providera z nowym tokenem: to samo, co
|
||||
robi przeladowanie, ale bez wychodzenia ze spotkania.
|
||||
PRZEMONTOWANIE WYLACZNIE PRZY ZMIANIE WLASNYCH UPRAWNIEN
|
||||
---------------------------------------------------------------
|
||||
`LiveKitRoomProvider` laczy sie tokenem podanym przy montowaniu
|
||||
i nie renegocjuje uprawnien na zywo, wiec po przyznaniu glosu
|
||||
trzeba go przemontowac: sama podmiana propa `token` nie otworzy
|
||||
mikrofonu, dopoki uczestnik nie przeladuje strony.
|
||||
|
||||
Ale przemontowanie jest PONOWNYM WEJSCIEM: provider startuje
|
||||
z `audio={false}`, wiec kazda opublikowana sciezka przepada
|
||||
i mowiacy zostaje wyciszony. Klucz nie moze wiec zalezec od
|
||||
samego tokenu, bo backend wydaje przy kazdym odswiezeniu inny
|
||||
ciag (JWT niesie czas wydania), takze gdy uprawnienia sa te same.
|
||||
|
||||
Zdarzylo sie to na zywo: prowadzacy wylaczyl "wideo dla wszystkich"
|
||||
przy wlaczonym otwartym mikrofonie, front pobral token i przemontowal
|
||||
sale CALEJ grupie, wyciszajac ludzi w trakcie wypowiedzi. Prawo do
|
||||
mikrofonu wcale sie wtedy nie zmienilo.
|
||||
|
||||
Klucz sklada wiec faktyczne uprawnienia tego uczestnika, ktore
|
||||
backend wylicza z roli, obu trybow i przyznanego glosu. Zmiana
|
||||
trybu, ktora go nie dotyczy, nie rusza jego sali.
|
||||
*/}
|
||||
<LiveKitRoom key={join.token} serverUrl={join.url} token={join.token} />
|
||||
<LiveKitRoom
|
||||
key={`${join.canPublishAudio ? 'a' : '-'}${join.canPublishVideo ? 'v' : '-'}`}
|
||||
serverUrl={join.url}
|
||||
token={join.token}
|
||||
/*
|
||||
* Urzadzenia wlaczone przed przemontowaniem wracaja wlaczone.
|
||||
* Kamera wraca wylacznie wtedy, gdy uczestnik nadal ma do niej
|
||||
* prawo: po odebraniu "wideo dla wszystkich" serwer i tak odrzuci
|
||||
* publikacje, a proba jej wznowienia dalaby blad w konsoli.
|
||||
*/
|
||||
initialAudio={devicesRef.current.audio && join.canPublishAudio}
|
||||
initialVideo={devicesRef.current.video && join.canPublishVideo}
|
||||
onDevicesChange={(state) => {
|
||||
devicesRef.current = state
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user