Przyznanie i odebranie glosu oraz oba tryby (otwarty mikrofon, wideo dla wszystkich) zmieniaja uprawnienia uczestnika przez updateParticipant, bez ponownego wejscia na sale. Odebranie najpierw wycisza sciezke na serwerze, wiec dziala takze u klienta, ktory nie odebral zdarzenia. Sala LiveKit ma staly key; mikrofon wlacza sie sam po przyznaniu glosu. Rola uczestnika liczona w jednym miejscu (VideoService.participantRole) dla biletu i zmiany na zywo. RoomPermissionTest pilnuje jawnej listy zrodel publikacji. Blokada pisania na czacie ma ikone przekreslonego dymku i znacznik "bez czatu" zamiast przekreslonego mikrofonu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+8
-2
@@ -47,6 +47,7 @@ public class PublicTaskOfferController {
|
||||
private final InteractionService interactions;
|
||||
private final SalesNotifier salesNotifier;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final com.softspm.webinar.video.RoomPermissionSync permissionSync;
|
||||
|
||||
public PublicTaskOfferController(com.softspm.webinar.banner.BannerService banners,
|
||||
SpeakRequestService speakRequests,
|
||||
@@ -55,7 +56,9 @@ public class PublicTaskOfferController {
|
||||
InteractionBroadcaster broadcaster,
|
||||
InteractionService interactions,
|
||||
SalesNotifier salesNotifier,
|
||||
RegistrationRepository registrationRepository) {
|
||||
RegistrationRepository registrationRepository,
|
||||
com.softspm.webinar.video.RoomPermissionSync permissionSync) {
|
||||
this.permissionSync = permissionSync;
|
||||
this.banners = banners;
|
||||
this.speakRequests = speakRequests;
|
||||
this.tasks = tasks;
|
||||
@@ -217,7 +220,10 @@ public class PublicTaskOfferController {
|
||||
@Operation(summary = "Wycofanie zgloszenia albo rezygnacja z przyznanego glosu")
|
||||
public SpeakStateView withdrawSpeak(@PathVariable String token) {
|
||||
TaskService.ParticipantDetails participant = participant(token);
|
||||
return new SpeakStateView(speakRequests.withdraw(participant.registrationId()));
|
||||
SpeakRequestView view = speakRequests.withdraw(participant.registrationId());
|
||||
// rezygnacja z przyznanego glosu zamyka mikrofon po stronie serwera
|
||||
permissionSync.syncParticipant(participant.webinarId(), participant.registrationId());
|
||||
return new SpeakStateView(view);
|
||||
}
|
||||
|
||||
private TaskService.ParticipantDetails participant(String token) {
|
||||
|
||||
+20
-5
@@ -1,5 +1,6 @@
|
||||
package com.softspm.webinar.interaction;
|
||||
|
||||
import com.softspm.webinar.video.RoomPermissionSync;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -30,9 +31,12 @@ import java.util.List;
|
||||
public class SpeakRequestAdminController {
|
||||
|
||||
private final SpeakRequestService speakRequests;
|
||||
private final RoomPermissionSync permissionSync;
|
||||
|
||||
public SpeakRequestAdminController(SpeakRequestService speakRequests) {
|
||||
public SpeakRequestAdminController(SpeakRequestService speakRequests,
|
||||
RoomPermissionSync permissionSync) {
|
||||
this.speakRequests = speakRequests;
|
||||
this.permissionSync = permissionSync;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -44,7 +48,11 @@ public class SpeakRequestAdminController {
|
||||
@PostMapping("/{requestId}/grant")
|
||||
@Operation(summary = "Przyznanie glosu. Podnosi role uczestnika do SPEAKER.")
|
||||
public SpeakRequestView grant(@PathVariable Long id, @PathVariable Long requestId) {
|
||||
return speakRequests.grant(id, requestId);
|
||||
SpeakRequestView view = speakRequests.grant(id, requestId);
|
||||
// po zatwierdzeniu transakcji: serwer pokoju otwiera mikrofon od razu,
|
||||
// bez ponownego wejscia uczestnika na sale
|
||||
permissionSync.syncParticipant(id, view.registrationId());
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,20 +67,27 @@ public class SpeakRequestAdminController {
|
||||
@Operation(summary = "Wlaczenie albo wylaczenie trybu otwartego mikrofonu")
|
||||
public SpeakRequestService.OpenMicView setOpenMic(@PathVariable Long id,
|
||||
@RequestBody OpenMicRequest request) {
|
||||
return speakRequests.setOpenMic(id, request.openMic());
|
||||
var result = speakRequests.setOpenMic(id, request.openMic());
|
||||
permissionSync.syncWebinar(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@PutMapping("/open-video")
|
||||
@Operation(summary = "Wlaczenie albo wylaczenie trybu wideo dla wszystkich")
|
||||
public SpeakRequestService.OpenVideoView setOpenVideo(@PathVariable Long id,
|
||||
@RequestBody OpenVideoRequest request) {
|
||||
return speakRequests.setOpenVideo(id, request.openVideo());
|
||||
var result = speakRequests.setOpenVideo(id, request.openVideo());
|
||||
permissionSync.syncWebinar(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@DeleteMapping("/{requestId}")
|
||||
@Operation(summary = "Odebranie glosu albo odmowa. Przywraca role VIEWER.")
|
||||
public SpeakRequestView revoke(@PathVariable Long id, @PathVariable Long requestId) {
|
||||
return speakRequests.revoke(id, requestId);
|
||||
SpeakRequestView view = speakRequests.revoke(id, requestId);
|
||||
// serwer pokoju zamyka mikrofon takze klientowi, ktory nie posluchal zdarzenia
|
||||
permissionSync.syncParticipant(id, view.registrationId());
|
||||
return view;
|
||||
}
|
||||
|
||||
public record OpenMicRequest(boolean openMic) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.livekit.server.RoomAdmin;
|
||||
import io.livekit.server.RoomJoin;
|
||||
import io.livekit.server.RoomName;
|
||||
import io.livekit.server.RoomRecord;
|
||||
import livekit.LivekitModels;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -175,6 +176,49 @@ public class LiveKitVideoProvider implements VideoProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Te same uprawnienia co w tokenie, w postaci do zmiany na zywo.
|
||||
*
|
||||
* Sluzy `RoomPermissionSync`: przyznanie i odebranie glosu zmienia
|
||||
* uprawnienia uczestnika juz polaczonego, bez ponownego wejscia na sale.
|
||||
* Zrodla publikacji pochodza z tych samych stalych co `grantsFor`, wiec
|
||||
* token i zmiana na zywo nie moga sie rozjechac.
|
||||
*
|
||||
* UWAGA: `canPublish=true` z PUSTA lista zrodel oznacza u LiveKit "wszystkie
|
||||
* zrodla", czyli takze kamere i ekran. Dlatego lista jest zawsze jawna,
|
||||
* a VIEWER ma `canPublish=false`.
|
||||
*/
|
||||
LivekitModels.ParticipantPermission permissionFor(RoomRole role) {
|
||||
var builder = LivekitModels.ParticipantPermission.newBuilder()
|
||||
.setCanSubscribe(true)
|
||||
.setCanPublishData(true)
|
||||
.setCanPublish(role != RoomRole.VIEWER);
|
||||
for (String source : sourcesFor(role)) {
|
||||
builder.addCanPublishSources(trackSource(source));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Zrodla, ktore rola moze publikowac. Pusta lista przy VIEWER. */
|
||||
static List<String> sourcesFor(RoomRole role) {
|
||||
return switch (role) {
|
||||
case VIEWER -> List.of();
|
||||
case SPEAKER -> AUDIO_ONLY;
|
||||
case ACTIVE_PARTICIPANT -> CAMERA_AND_MIC;
|
||||
case PRESENTER -> PRESENTER_SOURCES;
|
||||
};
|
||||
}
|
||||
|
||||
static LivekitModels.TrackSource trackSource(String name) {
|
||||
return switch (name) {
|
||||
case "camera" -> LivekitModels.TrackSource.CAMERA;
|
||||
case "microphone" -> LivekitModels.TrackSource.MICROPHONE;
|
||||
case "screen_share" -> LivekitModels.TrackSource.SCREEN_SHARE;
|
||||
case "screen_share_audio" -> LivekitModels.TrackSource.SCREEN_SHARE_AUDIO;
|
||||
default -> throw new IllegalArgumentException("Nieznane zrodlo: " + name);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Nagrania.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.softspm.webinar.video;
|
||||
|
||||
import com.softspm.webinar.domain.Webinar;
|
||||
import com.softspm.webinar.repository.WebinarRepository;
|
||||
import io.livekit.server.RoomServiceClient;
|
||||
import livekit.LivekitModels;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Zmiana uprawnien uczestnika juz polaczonego z sala.
|
||||
*
|
||||
* DLACZEGO NIE WYSTARCZY NOWY TOKEN
|
||||
* ---------------------------------------------------------------------------
|
||||
* Wczesniej przyznanie i odebranie glosu zmienialo wylacznie role w bazie,
|
||||
* a przegladarka uczestnika po zdarzeniu pobierala nowy token i wchodzila na
|
||||
* sale od nowa. Skutki widac bylo na zywo:
|
||||
* - odebranie glosu dzialalo tylko wtedy, gdy przegladarka posluchala; klient,
|
||||
* do ktorego zdarzenie nie doszlo, mowil dalej ze starym tokenem,
|
||||
* - kazda zmiana oznaczala ponowne wejscie: chwila ciemnego ekranu, zerwany
|
||||
* dzwiek, migajacy licznik osob na sali.
|
||||
*
|
||||
* Teraz uprawnienia zmienia SERWER POKOJU (`updateParticipant`), a biblioteka
|
||||
* klienta dostaje je na zywo i sama pokazuje albo chowa przycisk mikrofonu.
|
||||
* Nowy token nadal jest pobierany, ale sluzy wylacznie ponownemu wejsciu po
|
||||
* zerwaniu polaczenia.
|
||||
*
|
||||
* ODEBRANIE NAJPIERW WYCISZA
|
||||
* ---------------------------------------------------------------------------
|
||||
* Sciezka, ktorej rola juz nie obejmuje, jest wyciszana po stronie serwera
|
||||
* (`mutePublishedTrack`) PRZED zmiana uprawnien. Serwer zdejmuje takie sciezki
|
||||
* takze sam przy zmianie uprawnien, ale wyciszenie jest natychmiastowe
|
||||
* i nie zalezy od tego, jak wersja serwera obsluguje odebrane zrodla.
|
||||
*
|
||||
* WYLACZNIE LIVEKIT
|
||||
* ---------------------------------------------------------------------------
|
||||
* Przy pozostalych dostawcach nie ma takiej operacji i zostaje dotychczasowa
|
||||
* droga przez nowy token. Kazdy blad jest tu logowany i POMIJANY: zmiana roli
|
||||
* jest juz zapisana w bazie, a nieudana synchronizacja nie moze cofnac
|
||||
* decyzji prowadzacego ani wywrocic jego zadania.
|
||||
*/
|
||||
@Service
|
||||
public class RoomPermissionSync {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RoomPermissionSync.class);
|
||||
|
||||
private final WebinarRepository webinarRepository;
|
||||
private final VideoService videoService;
|
||||
private final RoomPresenceService presence;
|
||||
private final LiveKitVideoProvider liveKit;
|
||||
|
||||
public RoomPermissionSync(WebinarRepository webinarRepository,
|
||||
VideoService videoService,
|
||||
RoomPresenceService presence,
|
||||
LiveKitVideoProvider liveKit) {
|
||||
this.webinarRepository = webinarRepository;
|
||||
this.videoService = videoService;
|
||||
this.presence = presence;
|
||||
this.liveKit = liveKit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jeden uczestnik: po przyznaniu, odebraniu albo wycofaniu glosu.
|
||||
*
|
||||
* Synchronicznie, bo to jedno zadanie do serwera pokoju, a prowadzacy ma
|
||||
* po kliknieciu widziec skutek, nie czekac na watek w tle.
|
||||
*/
|
||||
public void syncParticipant(Long webinarId, Long registrationId) {
|
||||
try {
|
||||
Webinar webinar = liveKitWebinar(webinarId);
|
||||
if (webinar == null) return;
|
||||
|
||||
String room = liveKit.roomIdOf(webinar);
|
||||
String identity = "reg-" + registrationId;
|
||||
RoomServiceClient client = presence.roomClient();
|
||||
|
||||
var response = client.getParticipant(room, identity).execute();
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
// nie ma go na sali: uprawnienia wezmie z tokenu przy wejsciu
|
||||
return;
|
||||
}
|
||||
RoomRole role = videoService.participantRoles(webinarId, List.of(registrationId))
|
||||
.getOrDefault(registrationId, RoomRole.VIEWER);
|
||||
apply(client, room, response.body(), role);
|
||||
} catch (Exception e) {
|
||||
log.warn("Uprawnienia: synchronizacja reg-{} w webinarze {} nieudana: {}",
|
||||
registrationId, webinarId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cala sala: po przelaczeniu otwartego mikrofonu albo wideo dla wszystkich.
|
||||
*
|
||||
* W tle, bo przy dwustu osobach to dwiescie zadan do serwera pokoju,
|
||||
* a prowadzacy nie moze czekac na nie z zablokowanym przelacznikiem.
|
||||
* Osoby, ktorych uprawnienia sie nie zmieniaja, sa pomijane bez zadania.
|
||||
*/
|
||||
@Async
|
||||
public void syncWebinar(Long webinarId) {
|
||||
try {
|
||||
Webinar webinar = liveKitWebinar(webinarId);
|
||||
if (webinar == null) return;
|
||||
|
||||
String room = liveKit.roomIdOf(webinar);
|
||||
RoomServiceClient client = presence.roomClient();
|
||||
var response = client.listParticipants(room).execute();
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<LivekitModels.ParticipantInfo> participants = response.body();
|
||||
List<Long> ids = participants.stream()
|
||||
.map(p -> RoomPresenceService.idOf(p.getIdentity(), "reg-"))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Map<Long, RoomRole> roles = videoService.participantRoles(webinarId, ids);
|
||||
|
||||
int changed = 0;
|
||||
for (LivekitModels.ParticipantInfo info : participants) {
|
||||
Long id = RoomPresenceService.idOf(info.getIdentity(), "reg-");
|
||||
// prelegent i polaczenia spoza zapisu zachowuja swoje uprawnienia
|
||||
if (id == null) continue;
|
||||
try {
|
||||
if (apply(client, room, info, roles.getOrDefault(id, RoomRole.VIEWER))) changed++;
|
||||
} catch (Exception e) {
|
||||
log.warn("Uprawnienia: {} w webinarze {} nieudane: {}",
|
||||
info.getIdentity(), webinarId, e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("Uprawnienia: webinar {}, zmieniono {} z {} uczestnikow", webinarId, changed, ids.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("Uprawnienia: synchronizacja webinaru {} nieudana: {}", webinarId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nadaje uczestnikowi uprawnienia roli.
|
||||
*
|
||||
* @return czy cokolwiek zostalo zmienione
|
||||
*/
|
||||
private boolean apply(RoomServiceClient client, String room,
|
||||
LivekitModels.ParticipantInfo info, RoomRole role) throws java.io.IOException {
|
||||
LivekitModels.ParticipantPermission target = liveKit.permissionFor(role);
|
||||
if (samePermission(info.getPermission(), target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<LivekitModels.TrackSource> allowed = new HashSet<>(target.getCanPublishSourcesList());
|
||||
for (LivekitModels.TrackInfo track : info.getTracksList()) {
|
||||
boolean permitted = target.getCanPublish() && allowed.contains(track.getSource());
|
||||
if (!permitted && !track.getMuted()) {
|
||||
client.mutePublishedTrack(room, info.getIdentity(), track.getSid(), true).execute();
|
||||
}
|
||||
}
|
||||
|
||||
var response = client.updateParticipant(room, info.getIdentity(), null, null, target, null).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
log.warn("Uprawnienia: serwer odrzucil zmiane {} (kod {})", info.getIdentity(), response.code());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean samePermission(LivekitModels.ParticipantPermission current,
|
||||
LivekitModels.ParticipantPermission target) {
|
||||
return current.getCanPublish() == target.getCanPublish()
|
||||
&& current.getCanSubscribe() == target.getCanSubscribe()
|
||||
&& current.getCanPublishData() == target.getCanPublishData()
|
||||
&& new HashSet<>(current.getCanPublishSourcesList())
|
||||
.equals(new HashSet<>(target.getCanPublishSourcesList()));
|
||||
}
|
||||
|
||||
/** Webinar, o ile jego pokoj stoi u LiveKit; inaczej null. */
|
||||
private Webinar liveKitWebinar(Long webinarId) {
|
||||
Webinar webinar = webinarRepository.findById(webinarId).orElse(null);
|
||||
return webinar != null && presence.usesLiveKit(webinar) ? webinar : null;
|
||||
}
|
||||
}
|
||||
@@ -205,13 +205,13 @@ public class RoomPresenceService {
|
||||
* ludzie siedza jeszcze u poprzedniego. Bez zalozonego pokoju decyduje
|
||||
* wybor dla webinaru, bo LiveKit zaklada pokoj przy pierwszym wejsciu.
|
||||
*/
|
||||
private boolean usesLiveKit(Webinar webinar) {
|
||||
boolean usesLiveKit(Webinar webinar) {
|
||||
VideoProvider existing = providerFactory.ofExistingRoom(webinar);
|
||||
VideoProvider provider = existing != null ? existing : providerFactory.forWebinar(webinar);
|
||||
return LiveKitVideoProvider.NAME.equals(provider.name());
|
||||
}
|
||||
|
||||
private RoomServiceClient roomClient() {
|
||||
RoomServiceClient roomClient() {
|
||||
return RoomServiceClient.createClient(
|
||||
egressService.apiHttpUrl(),
|
||||
settings.get(SettingKey.LIVEKIT_API_KEY),
|
||||
|
||||
@@ -259,32 +259,12 @@ public class VideoService {
|
||||
String displayName = (registration.getFirstName() + " " + registration.getLastName()).trim();
|
||||
|
||||
/*
|
||||
* Uprawnienia do mowienia.
|
||||
*
|
||||
* Przy otwartym mikrofonie kazdy uczestnik dostaje SPEAKER, bez
|
||||
* proszenia o glos. Decyzja musi zapasc **tutaj**, przy wydawaniu
|
||||
* Uprawnienia do mowienia i obrazu. Decyzja zapada przy wydawaniu
|
||||
* tokenu: samo odblokowanie przycisku w przegladarce niczego nie daje,
|
||||
* bo dostawca i tak odrzuci publikacje bez uprawnienia w tokenie.
|
||||
*
|
||||
* Wideo zostaje wylaczone takze w tym trybie: SPEAKER u dostawcy
|
||||
* oznacza u nas prawo do dzwieku, nie do obrazu.
|
||||
* bo dostawca odrzuci publikacje bez uprawnienia. Zmiane w trakcie
|
||||
* spotkania przenosi na polaczonych `RoomPermissionSync`.
|
||||
*/
|
||||
/*
|
||||
* Widocznosc wideo ma pierwszenstwo nad otwartym mikrofonem: tryb
|
||||
* "wideo dla wszystkich" daje uczestnikowi obraz I dzwiek. Nastepny
|
||||
* jest otwarty mikrofon (sam dzwiek). Na koncu rola z zapisu, ktora
|
||||
* uwzglednia indywidualnie przyznany glos. Uprawnienie zawsze wynika
|
||||
* z tokenu, wiec przelacznik po stronie prowadzacego nie da uczestnikowi
|
||||
* niczego, dopoki nie pobierze tokenu na nowo.
|
||||
*/
|
||||
RoomRole role;
|
||||
if (webinar.isOpenVideo()) {
|
||||
role = RoomRole.ACTIVE_PARTICIPANT;
|
||||
} else if (webinar.isOpenMic()) {
|
||||
role = RoomRole.SPEAKER;
|
||||
} else {
|
||||
role = registration.getRoomRole().toRoomRole();
|
||||
}
|
||||
RoomRole role = participantRole(webinar, registration);
|
||||
|
||||
VideoProvider.JoinTicket ticket = provider.createJoinTicket(webinar,
|
||||
new VideoProvider.JoinRequest(displayName, "reg-" + registration.getId(), role));
|
||||
@@ -294,6 +274,51 @@ public class VideoService {
|
||||
provider.name(), webinar.getRoomId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rola uczestnika z zapisu na sali.
|
||||
*
|
||||
* Widocznosc wideo ma pierwszenstwo nad otwartym mikrofonem: tryb "wideo
|
||||
* dla wszystkich" daje obraz I dzwiek. Nastepny jest otwarty mikrofon (sam
|
||||
* dzwiek). Na koncu rola z zapisu, ktora uwzglednia indywidualnie przyznany
|
||||
* glos.
|
||||
*
|
||||
* JEDNO MIEJSCE DLA BILETU I DLA ZMIANY NA ZYWO. Rozstrzyga tu zarowno
|
||||
* token wydawany przy wejsciu, jak i uprawnienia zmieniane w trakcie przez
|
||||
* `RoomPermissionSync`. Dwie kopie tej reguly rozjechalyby sie przy
|
||||
* pierwszej poprawce, a uczestnik mialby inne prawa po ponownym wejsciu
|
||||
* niz przed nim.
|
||||
*/
|
||||
public static RoomRole participantRole(Webinar webinar, Registration registration) {
|
||||
if (webinar.isOpenVideo()) {
|
||||
return RoomRole.ACTIVE_PARTICIPANT;
|
||||
}
|
||||
if (webinar.isOpenMic()) {
|
||||
return RoomRole.SPEAKER;
|
||||
}
|
||||
return registration.getRoomRole().toRoomRole();
|
||||
}
|
||||
|
||||
/**
|
||||
* Role wskazanych uczestnikow, wylacznie tych nalezacych do webinaru.
|
||||
*
|
||||
* Identyfikatory pochodza z pokoju u dostawcy, wiec zapis z innego
|
||||
* webinaru jest pomijany, a nie podnoszony.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<Long, RoomRole> participantRoles(Long webinarId, java.util.Collection<Long> registrationIds) {
|
||||
Webinar webinar = webinarRepository.findById(webinarId)
|
||||
.orElseThrow(() -> new NotFoundException("Nie znaleziono webinaru"));
|
||||
Map<Long, RoomRole> roles = new java.util.HashMap<>();
|
||||
for (Registration registration : registrationRepository.findAllById(registrationIds)) {
|
||||
if (registration.getWebinar().getId().equals(webinarId)
|
||||
&& registration.getStatus() == RegistrationStatus.CONFIRMED
|
||||
&& !registration.isRoomBanned()) {
|
||||
roles.put(registration.getId(), participantRole(webinar, registration));
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wejscie prelegenta.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.softspm.webinar.video;
|
||||
|
||||
import livekit.LivekitModels.TrackSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Uprawnienia zmieniane na zywo (`RoomPermissionSync`) musza odpowiadac tym
|
||||
* z tokenu.
|
||||
*
|
||||
* Najgrozniejsza pulapka: u LiveKit `canPublish=true` z PUSTA lista zrodel
|
||||
* znaczy "wszystkie zrodla". Uczestnik z glosem dostalby wtedy kamere
|
||||
* i udostepnianie ekranu, a blad nie bylby widoczny w interfejsie, dopoki
|
||||
* ktos nie sprobuje ich wlaczyc.
|
||||
*/
|
||||
class RoomPermissionTest {
|
||||
|
||||
private final LiveKitVideoProvider provider = new LiveKitVideoProvider(null);
|
||||
|
||||
@Test
|
||||
void viewerCannotPublishAnything() {
|
||||
var permission = provider.permissionFor(RoomRole.VIEWER);
|
||||
assertThat(permission.getCanPublish()).isFalse();
|
||||
assertThat(permission.getCanSubscribe()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void speakerGetsMicrophoneOnly() {
|
||||
var permission = provider.permissionFor(RoomRole.SPEAKER);
|
||||
assertThat(permission.getCanPublish()).isTrue();
|
||||
assertThat(Set.copyOf(permission.getCanPublishSourcesList()))
|
||||
.containsExactly(TrackSource.MICROPHONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeParticipantGetsCameraAndMicrophoneButNoScreen() {
|
||||
var permission = provider.permissionFor(RoomRole.ACTIVE_PARTICIPANT);
|
||||
assertThat(Set.copyOf(permission.getCanPublishSourcesList()))
|
||||
.containsExactlyInAnyOrder(TrackSource.CAMERA, TrackSource.MICROPHONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyPublishingRoleHasExplicitSources() {
|
||||
for (RoomRole role : RoomRole.values()) {
|
||||
var permission = provider.permissionFor(role);
|
||||
if (permission.getCanPublish()) {
|
||||
assertThat(permission.getCanPublishSourcesList())
|
||||
.as("rola %s: pusta lista zrodel oznacza u LiveKit wszystkie zrodla", role)
|
||||
.isNotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user