Kamera i ekran dla konkretnego uczestnika z panelu prowadzenia
Deploy PROD / deploy (push) Successful in 26s
Deploy PROD / deploy (push) Successful in 26s
Karta "Na sali" dostala przelaczniki kamery i udostepniania ekranu przy kazdym uczestniku (wylacznie LiveKit). Uprawnienia licza sie z trzech niezaleznych praw (PublishRights: mikrofon, kamera, ekran) zamiast jednej roli, w jednym miejscu dla tokenu i zmiany na zywo; serwer pokoju nadaje i odbiera je bez ponownego wejscia na sale. Kolumny camera_allowed i screen_allowed (V32), domyslnie false. Zgoda indywidualna przezywa wylaczenie "wideo dla wszystkich". Uczestnik dostaje komunikat, a przy ekranie ostrzezenie, ze sala zobaczy wszystko. PublishRightsTest i PublishRightsTokenTest pilnuja, ze zgoda na jedno zrodlo nie otwiera innego; oba sprawdzone w obie strony. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,10 @@ type Participant = {
|
||||
screen: boolean
|
||||
chatMuted: boolean
|
||||
roomBanned: boolean
|
||||
/** Indywidualna zgoda prowadzacego na kamere. */
|
||||
cameraAllowed: boolean
|
||||
/** Indywidualna zgoda prowadzacego na udostepnienie ekranu. */
|
||||
screenAllowed: boolean
|
||||
}
|
||||
|
||||
type Presence = {
|
||||
@@ -143,6 +147,38 @@ export function RoomParticipants({ webinarId, timezone }: { webinarId: string; t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zgoda na kamere albo ekran dla jednej osoby.
|
||||
*
|
||||
* Bez okna potwierdzenia: to nie kasuje ani nie blokuje, a cofniecie jest
|
||||
* jednym kliknieciem i dziala od razu, bo uprawnienie zmienia serwer.
|
||||
*/
|
||||
async function togglePublish(participant: Participant, what: 'camera' | 'screen') {
|
||||
if (participant.registrationId === null) return
|
||||
const next = what === 'camera' ? !participant.cameraAllowed : !participant.screenAllowed
|
||||
setBusy(participant.identity)
|
||||
try {
|
||||
await client.post(
|
||||
`/admin/webinars/${webinarId}/room/participants/${participant.registrationId}/publish`,
|
||||
{ [what]: next },
|
||||
)
|
||||
toasts.success(
|
||||
what === 'camera'
|
||||
? next
|
||||
? `${participant.name} może włączyć kamerę`
|
||||
: `Wyłączono kamerę: ${participant.name}`
|
||||
: next
|
||||
? `${participant.name} może udostępnić ekran`
|
||||
: `Wyłączono udostępnianie ekranu: ${participant.name}`,
|
||||
)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
toasts.error(errorMessage(error, 'Nie udało się zmienić uprawnień'))
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function unban(registrationId: number, name: string) {
|
||||
setBusy(`ban-${registrationId}`)
|
||||
try {
|
||||
@@ -263,6 +299,53 @@ export function RoomParticipants({ webinarId, timezone }: { webinarId: string; t
|
||||
|
||||
{participant.kind !== 'SPEAKER' && (
|
||||
<div className="flex shrink-0 items-center gap-0.5">
|
||||
{/*
|
||||
Kamera i ekran tylko przy LiveKit: wylacznie tam serwer
|
||||
egzekwuje osobno kazde zrodlo. Przy innych dostawcach
|
||||
przycisk obiecywalby cos, czego nikt nie pilnuje.
|
||||
*/}
|
||||
{participant.kind === 'PARTICIPANT' && data?.source === 'LIVEKIT' && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy === participant.identity}
|
||||
onClick={() => void togglePublish(participant, 'camera')}
|
||||
aria-pressed={participant.cameraAllowed}
|
||||
aria-label={
|
||||
participant.cameraAllowed
|
||||
? `Odbierz kamerę: ${participant.name}`
|
||||
: `Pozwól włączyć kamerę: ${participant.name}`
|
||||
}
|
||||
title={participant.cameraAllowed ? 'Odbierz kamerę' : 'Pozwól włączyć kamerę'}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-md disabled:opacity-40 ${
|
||||
participant.cameraAllowed
|
||||
? 'bg-ok-50 text-ok-600 hover:bg-ok-50'
|
||||
: 'text-slate-400 hover:bg-slate-100 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Icon name={participant.cameraAllowed ? 'video' : 'video-off'} size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy === participant.identity}
|
||||
onClick={() => void togglePublish(participant, 'screen')}
|
||||
aria-pressed={participant.screenAllowed}
|
||||
aria-label={
|
||||
participant.screenAllowed
|
||||
? `Odbierz udostępnianie ekranu: ${participant.name}`
|
||||
: `Pozwól udostępnić ekran: ${participant.name}`
|
||||
}
|
||||
title={participant.screenAllowed ? 'Odbierz udostępnianie ekranu' : 'Pozwól udostępnić ekran'}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-md disabled:opacity-40 ${
|
||||
participant.screenAllowed
|
||||
? 'bg-ok-50 text-ok-600 hover:bg-ok-50'
|
||||
: 'text-slate-400 hover:bg-slate-100 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Icon name="presentation" size={16} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{participant.kind === 'PARTICIPANT' && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user