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,
|
LiveKitRoom as LiveKitRoomProvider,
|
||||||
RoomAudioRenderer,
|
RoomAudioRenderer,
|
||||||
VideoConference,
|
VideoConference,
|
||||||
|
useLocalParticipant,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { RoomOptions } from 'livekit-client'
|
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.
|
* Pokoj LiveKit osadzony w naszym ekranie spotkania.
|
||||||
@@ -36,10 +38,25 @@ export function LiveKitRoom({
|
|||||||
serverUrl,
|
serverUrl,
|
||||||
token,
|
token,
|
||||||
onDisconnected,
|
onDisconnected,
|
||||||
|
initialAudio = false,
|
||||||
|
initialVideo = false,
|
||||||
|
onDevicesChange,
|
||||||
}: {
|
}: {
|
||||||
serverUrl: string
|
serverUrl: string
|
||||||
token: string
|
token: string
|
||||||
onDisconnected?: () => void
|
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>(
|
const options = useMemo<RoomOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -71,15 +88,48 @@ export function LiveKitRoom({
|
|||||||
// wchodzimy z wylaczonymi urzadzeniami: uczestnik sam decyduje, kiedy
|
// wchodzimy z wylaczonymi urzadzeniami: uczestnik sam decyduje, kiedy
|
||||||
// zaczyna mowic, a przypadkowo otwarty mikrofon slychac na calej sali
|
// zaczyna mowic, a przypadkowo otwarty mikrofon slychac na calej sali
|
||||||
connect
|
connect
|
||||||
video={false}
|
video={initialVideo}
|
||||||
audio={false}
|
audio={initialAudio}
|
||||||
onDisconnected={onDisconnected}
|
onDisconnected={onDisconnected}
|
||||||
className="h-full w-full"
|
className="relative h-full w-full"
|
||||||
data-lk-theme="default"
|
data-lk-theme="default"
|
||||||
>
|
>
|
||||||
<VideoConference />
|
<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 */}
|
{/* dzwiek pozostalych uczestnikow; bez tego widac obraz, ale nic nie slychac */}
|
||||||
<RoomAudioRenderer />
|
<RoomAudioRenderer />
|
||||||
</LiveKitRoomProvider>
|
</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 { useParams } from 'react-router-dom'
|
||||||
import { publicClient } from '../../api/publicClient'
|
import { publicClient } from '../../api/publicClient'
|
||||||
import { Icon } from '../../components/Icon'
|
import { Icon } from '../../components/Icon'
|
||||||
@@ -228,21 +228,17 @@ function MeetingRoom({
|
|||||||
* wiec ticket trzeba pobrac na nowo. Bez tego napis "masz glos" pojawia sie,
|
* wiec ticket trzeba pobrac na nowo. Bez tego napis "masz glos" pojawia sie,
|
||||||
* a mikrofon zostaje zamkniety, bo iframe dziala na starym tokenie.
|
* a mikrofon zostaje zamkniety, bo iframe dziala na starym tokenie.
|
||||||
*/
|
*/
|
||||||
const [voiceState, setVoiceState] = useState<'none' | 'granted' | 'video'>('none')
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// otwarty mikrofon i wideo dla wszystkich zmieniaja uprawnienia w tokenie
|
/*
|
||||||
// tak samo jak przyznany glos, wiec ticket trzeba pobrac na nowo. Wideo
|
* Sygnal od prowadzacego mowi, ze uprawnienia MOGLY sie zmienic, ale nie
|
||||||
// to inny poziom niz sam glos, wiec jest osobnym stanem, zeby przelaczenie
|
* mowi, jakie sa: rozstrzyga to backend, ktory zna role z zapisu, oba tryby
|
||||||
// miedzy nimi tez wymuszalo odswiezenie.
|
* i indywidualnie przyznany glos. Pobieramy wiec ticket przy kazdej zmianie,
|
||||||
const next: 'none' | 'granted' | 'video' = openVideo
|
* a o przemontowaniu sali decyduje dopiero `key` zlozony z UPRAWNIEN
|
||||||
? 'video'
|
* (patrz miejsce renderowania `LiveKitRoom`), nie z samego tokenu.
|
||||||
: speak.granted || openMic
|
*/
|
||||||
? 'granted'
|
|
||||||
: 'none'
|
|
||||||
if (next === voiceState) return
|
|
||||||
setVoiceState(next)
|
|
||||||
if (join.embedded) void onRefreshTicket()
|
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(() => {
|
const src = useMemo(() => {
|
||||||
if (!join.embedded) return null
|
if (!join.embedded) return null
|
||||||
@@ -379,6 +375,16 @@ function MeetingRoom({
|
|||||||
* dziala jego gotowy komponent.
|
* dziala jego gotowy komponent.
|
||||||
*/
|
*/
|
||||||
function VideoArea({ join, src }: { join: JoinView; src: string | null }) {
|
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
|
// tryb NONE: obraz jest w zewnetrznym narzedziu, panel zostaje u nas
|
||||||
if (!join.embedded) {
|
if (!join.embedded) {
|
||||||
return <ExternalVideo url={join.url} />
|
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">
|
<div className="min-h-[38vh] flex-1 lg:min-h-0">
|
||||||
<Suspense fallback={<VideoLoading />}>
|
<Suspense fallback={<VideoLoading />}>
|
||||||
{/*
|
{/*
|
||||||
key={join.token}: po przyznaniu albo odebraniu glosu backend wydaje
|
PRZEMONTOWANIE WYLACZNIE PRZY ZMIANIE WLASNYCH UPRAWNIEN
|
||||||
NOWY token z innym uprawnieniem do mikrofonu, a `load()` go pobiera.
|
---------------------------------------------------------------
|
||||||
LiveKitRoomProvider laczy sie z tokenem podanym przy montowaniu i nie
|
`LiveKitRoomProvider` laczy sie tokenem podanym przy montowaniu
|
||||||
renegocjuje uprawnien na zywo - wiec zmiana samego propa `token` nie
|
i nie renegocjuje uprawnien na zywo, wiec po przyznaniu glosu
|
||||||
otwiera mikrofonu, dopoki uczestnik nie przeladuje strony. Zmiana
|
trzeba go przemontowac: sama podmiana propa `token` nie otworzy
|
||||||
`key` wymusza przemontowanie providera z nowym tokenem: to samo, co
|
mikrofonu, dopoki uczestnik nie przeladuje strony.
|
||||||
robi przeladowanie, ale bez wychodzenia ze spotkania.
|
|
||||||
|
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>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user