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:
@@ -0,0 +1,107 @@
|
||||
package com.softspm.webinar.video;
|
||||
|
||||
import com.softspm.webinar.domain.Registration;
|
||||
import com.softspm.webinar.domain.RegistrationRoomRole;
|
||||
import com.softspm.webinar.domain.Webinar;
|
||||
import livekit.LivekitModels.TrackSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Indywidualna zgoda prowadzacego na kamere i ekran.
|
||||
*
|
||||
* Najwazniejsze sa dwie granice: zgoda na jedno zrodlo nie moze otwierac
|
||||
* innego (sam ekran to nie kamera, sama kamera to nie mikrofon), a uczestnik
|
||||
* z zapisu nigdy nie dostaje uprawnien prelegenta.
|
||||
*/
|
||||
class PublishRightsTest {
|
||||
|
||||
private final LiveKitVideoProvider provider = new LiveKitVideoProvider(null);
|
||||
|
||||
private static Webinar webinar(boolean openMic, boolean openVideo) {
|
||||
Webinar webinar = new Webinar();
|
||||
webinar.setOpenMic(openMic);
|
||||
webinar.setOpenVideo(openVideo);
|
||||
return webinar;
|
||||
}
|
||||
|
||||
private static Registration registration(RegistrationRoomRole role, boolean camera, boolean screen) {
|
||||
Registration registration = new Registration();
|
||||
registration.setRoomRole(role);
|
||||
registration.setCameraAllowed(camera);
|
||||
registration.setScreenAllowed(screen);
|
||||
return registration;
|
||||
}
|
||||
|
||||
private Set<TrackSource> sources(PublishRights rights) {
|
||||
return Set.copyOf(provider.permissionFor(rights).getCanPublishSourcesList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void viewerWithoutGrantsPublishesNothing() {
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, false), registration(RegistrationRoomRole.VIEWER, false, false));
|
||||
assertThat(rights).isEqualTo(PublishRights.NONE);
|
||||
assertThat(provider.permissionFor(rights).getCanPublish()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cameraGrantDoesNotOpenMicrophone() {
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, false), registration(RegistrationRoomRole.VIEWER, true, false));
|
||||
assertThat(sources(rights)).containsExactly(TrackSource.CAMERA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void screenGrantDoesNotOpenCamera() {
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, false), registration(RegistrationRoomRole.VIEWER, false, true));
|
||||
assertThat(sources(rights))
|
||||
.containsExactlyInAnyOrder(TrackSource.SCREEN_SHARE, TrackSource.SCREEN_SHARE_AUDIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantsAddUpWithVoice() {
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, false), registration(RegistrationRoomRole.SPEAKER, true, true));
|
||||
assertThat(sources(rights)).containsExactlyInAnyOrder(TrackSource.MICROPHONE, TrackSource.CAMERA,
|
||||
TrackSource.SCREEN_SHARE, TrackSource.SCREEN_SHARE_AUDIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void individualCameraSurvivesTurningOffVideoForAll() {
|
||||
// tryb "wideo dla wszystkich" wylaczony, ale prowadzacy dal kamere tej osobie
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, false), registration(RegistrationRoomRole.VIEWER, true, false));
|
||||
assertThat(rights.camera()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void videoForAllStillGivesNoScreen() {
|
||||
PublishRights rights = VideoService.participantRights(
|
||||
webinar(false, true), registration(RegistrationRoomRole.VIEWER, false, false));
|
||||
assertThat(sources(rights)).containsExactlyInAnyOrder(TrackSource.CAMERA, TrackSource.MICROPHONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void participantNeverGetsPresenterRole() {
|
||||
for (boolean audio : new boolean[] {false, true}) {
|
||||
for (boolean camera : new boolean[] {false, true}) {
|
||||
for (boolean screen : new boolean[] {false, true}) {
|
||||
assertThat(new PublishRights(audio, camera, screen).closestRole())
|
||||
.isNotEqualTo(RoomRole.PRESENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void presenterKeepsEverything() {
|
||||
assertThat(sources(PublishRights.of(RoomRole.PRESENTER))).containsExactlyInAnyOrder(
|
||||
TrackSource.MICROPHONE, TrackSource.CAMERA,
|
||||
TrackSource.SCREEN_SHARE, TrackSource.SCREEN_SHARE_AUDIO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.softspm.webinar.video;
|
||||
|
||||
import com.softspm.webinar.domain.Webinar;
|
||||
import com.softspm.webinar.settings.SettingKey;
|
||||
import com.softspm.webinar.settings.SettingsService;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Token LiveKit wydany dla indywidualnych zgod na kamere i ekran.
|
||||
*
|
||||
* Bez kontekstu Springa i bez bazy: ustawienia sa podstawione, a token
|
||||
* rozkladany tak samo jak w `ProviderTokenTest`. Sprawdza to, co faktycznie
|
||||
* trafia do serwera LiveKit, czyli liste zrodel w podpisanym tokenie, a nie
|
||||
* tylko obiekt uprawnien po naszej stronie.
|
||||
*/
|
||||
class PublishRightsTokenTest {
|
||||
|
||||
private static final String SECRET = "sekret-testowy-livekit-co-najmniej-32-znaki";
|
||||
|
||||
private final LiveKitVideoProvider provider = new LiveKitVideoProvider(settings());
|
||||
|
||||
private static SettingsService settings() {
|
||||
SettingsService settings = mock(SettingsService.class);
|
||||
when(settings.get(SettingKey.LIVEKIT_URL)).thenReturn("wss://livekit.example.test");
|
||||
when(settings.get(SettingKey.LIVEKIT_API_KEY)).thenReturn("klucz-testowy");
|
||||
when(settings.get(SettingKey.LIVEKIT_API_SECRET)).thenReturn(SECRET);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> grants(PublishRights rights) {
|
||||
Webinar webinar = new Webinar();
|
||||
webinar.setId(4242L);
|
||||
String jwt = provider.createJoinTicket(webinar, new VideoProvider.JoinRequest(
|
||||
"Jan Testowy", "reg-1", rights.closestRole(), rights)).token();
|
||||
var claims = Jwts.parser()
|
||||
.verifyWith(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
|
||||
.build()
|
||||
.parseSignedClaims(jwt)
|
||||
.getPayload();
|
||||
return (Map<String, Object>) claims.get("video");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> sources(Map<String, Object> video) {
|
||||
Object raw = video.get("canPublishSources");
|
||||
return raw == null ? List.of() : (List<String>) raw;
|
||||
}
|
||||
|
||||
@Test
|
||||
void screenOnlyTokenHasNoCameraAndNoMicrophone() {
|
||||
var video = grants(new PublishRights(false, false, true));
|
||||
assertThat(video.get("canPublish")).isEqualTo(true);
|
||||
assertThat(sources(video)).containsExactlyInAnyOrder("screen_share", "screen_share_audio");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cameraOnlyTokenHasNoMicrophone() {
|
||||
var video = grants(new PublishRights(false, true, false));
|
||||
assertThat(sources(video)).containsExactly("camera");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noRightsMeansNoPublishing() {
|
||||
var video = grants(PublishRights.NONE);
|
||||
assertThat(video.get("canPublish")).isEqualTo(false);
|
||||
assertThat(sources(video)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void participantTokenNeverGetsRoomAdmin() {
|
||||
var video = grants(new PublishRights(true, true, true));
|
||||
assertThat(video.get("roomAdmin")).isNotEqualTo(true);
|
||||
assertThat(video.get("roomRecord")).isNotEqualTo(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user