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:
@@ -81,10 +81,16 @@ type History = {
|
||||
questions: InteractionView[]
|
||||
}
|
||||
|
||||
/** Zdarzenie moderacji uczestnika: wyciszenie/blokada. */
|
||||
/**
|
||||
* Zdarzenie dotyczace jednego uczestnika: wyciszenie, blokada albo zmiana
|
||||
* indywidualnej zgody na kamere i ekran (`PUBLISH_CHANGED`).
|
||||
*/
|
||||
export type ModerationEvent = {
|
||||
kind: 'MUTED' | 'UNMUTED' | 'BANNED' | 'UNBANNED'
|
||||
kind: 'MUTED' | 'UNMUTED' | 'BANNED' | 'UNBANNED' | 'PUBLISH_CHANGED'
|
||||
registrationId: number
|
||||
/** Tylko przy PUBLISH_CHANGED. */
|
||||
camera?: boolean
|
||||
screen?: boolean
|
||||
}
|
||||
|
||||
type Event = { kind: string; payload: InteractionView }
|
||||
@@ -245,7 +251,8 @@ export function useInteractions(
|
||||
}
|
||||
if (
|
||||
event.kind === 'MUTED' || event.kind === 'UNMUTED' ||
|
||||
event.kind === 'BANNED' || event.kind === 'UNBANNED'
|
||||
event.kind === 'BANNED' || event.kind === 'UNBANNED' ||
|
||||
event.kind === 'PUBLISH_CHANGED'
|
||||
) {
|
||||
const mod = event as unknown as ModerationEvent
|
||||
// reagujemy TYLKO na zdarzenie dotyczace tego uczestnika: temat jest
|
||||
|
||||
@@ -75,6 +75,8 @@ type JoinView = {
|
||||
*/
|
||||
canPublishVideo: boolean
|
||||
canPublishAudio: boolean
|
||||
/** Udostepnianie ekranu: u uczestnika wylacznie za zgoda prowadzacego. */
|
||||
canShareScreen: boolean
|
||||
/** Publiczny klucz klienta do osadzenia (Zoom SDK Key). Pusty dla innych. */
|
||||
clientKey: string | null
|
||||
}
|
||||
@@ -369,6 +371,24 @@ function MeetingRoom({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Indywidualna zgoda prowadzacego. Przyciski kamery i ekranu pojawiaja
|
||||
sie pod obrazem same, bo uprawnienie zmienia serwer; ta informacja
|
||||
mowi uczestnikowi, ze to nie przypadek, i ostrzega przed ekranem.
|
||||
*/}
|
||||
{!isSpeaker && join.embedded && join.provider === 'LIVEKIT' && join.canPublishVideo && !openVideo && (
|
||||
<p className="border-b border-info-500/20 bg-info-50 px-3 py-2 text-xs text-info-600">
|
||||
Prowadzący pozwolił Ci włączyć kamerę. Włączysz ją przyciskiem pod obrazem.
|
||||
</p>
|
||||
)}
|
||||
{!isSpeaker && join.embedded && join.provider === 'LIVEKIT' && join.canShareScreen && (
|
||||
<p className="border-b border-warn-500/20 bg-warn-50 px-3 py-2 text-xs text-warn-600">
|
||||
Prowadzący pozwolił Ci udostępnić ekran. Wszyscy uczestnicy zobaczą to, co pokażesz,
|
||||
także powiadomienia i pocztę. Przed udostępnieniem zamknij to, czego nie chcesz pokazywać,
|
||||
albo udostępnij tylko jedno okno.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<InteractionPanel
|
||||
token={token}
|
||||
onSpeakEvent={speak.apply}
|
||||
@@ -377,6 +397,9 @@ function MeetingRoom({
|
||||
onModeration={(event) => {
|
||||
if (event.kind === 'BANNED') setBanned(true)
|
||||
if (event.kind === 'UNBANNED') setBanned(false)
|
||||
// uprawnienie zmienil juz serwer; nowy bilet niesie je do komunikatu
|
||||
// i na wypadek ponownego wejscia po zerwaniu polaczenia
|
||||
if (event.kind === 'PUBLISH_CHANGED' && join.embedded) void onRefreshTicket()
|
||||
}}
|
||||
onHistoryLoaded={(history) => {
|
||||
setOpenMic(history.openMic)
|
||||
|
||||
@@ -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