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>
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>
`VideoConference` z LiveKit daje kafelek (z zaslepka sylwetki) KAZDEMU
uczestnikowi pokoju, niezaleznie od tego, czy nadaje obraz. To uklad
wideokonferencji, nie webinaru. Test z botami na produkcji pokazal skutek:
prelegent gubil sie wsrod pustych sylwetek rozlozonych na kilka stron, a widz
musial przewijac, zeby znalezc osobe, ktora mowi. Przy czterech osobach nikt
tego nie zauwazyl.
Rozroznienie rol istnialo dotad wylacznie w uprawnieniach tokenu: uczestnik
bez "wideo dla wszystkich" nie wlaczy kamery, i to dziala. Warstwa widoku tego
nie odzwierciedlala.
`WebinarStage` odwzorowuje `VideoConference` 2.9 z tych samych komponentow
(siatka, fokus na udostepnionym ekranie, pasek sterowania) i zmienia WYLACZNIE
filtr sciezek. Kafelek dostaje:
- prelegent (`spk-...`) zawsze, takze z wylaczona kamera,
- kazdy, kto nadaje kamere albo ekran, wiec uczestnicy przy wlaczonym "wideo
dla wszystkich" pojawiaja sie sami, bez osobnej logiki trybu,
- kazdy z otwartym mikrofonem, czyli uczestnik z przyznanym glosem.
Pozostali widzowie nie maja kafelkow; zastepuje je licznik "Na sali: N osob".
Filtr reaguje na publikacje i wyciszenie sciezek, a nie tylko na zmiane
aktywnego mowcy: uczestnik, ktory wlaczyl mikrofon, ma pojawic sie od razu.
Z paska sterowania zniknal przycisk czatu LiveKit. Byl drugim czatem obok
naszego, niemoderowanym i niezapisywanym, wiec mylil uczestnikow co do tego,
gdzie pisac.
Scena nie daje zadnych uprawnien: sam wyglad nie zmienia tego, kto moze
nadawac. To nadal egzekwuje token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sama data w naglowku mowi KIEDY, ale nie mowi ZA ILE: "14 wrzesnia, godz.
10:00" wymaga od odwiedzajacego przeliczenia w glowie. Licznik odpowiada na
pytanie, ktore naprawde sobie zadaje: czy to juz, czy jeszcze daleko. Przy
wydarzeniu za dwie godziny jest przy okazji sygnalem, ze warto zapisac sie
teraz, a nie wrocic pozniej.
Miejsce: pod trescia strony, nad formularzem zapisu.
ZNIKA PO TERMINIE
Odliczanie do czegos, co juz sie zaczelo, wprowadza w blad. Po `startsAt`
komponent nie renderuje sie wcale, wiec przy webinarze zakonczonym nie pokazuje
zer obok komunikatu o zamknieciu zapisow.
CZAS Z ZEGARA, NIE Z ODEJMOWANIA
Wartosc bierze sie z roznicy dat przy kazdym tyknieciu. Karta uspiona przez
przegladarke wstrzymuje `setInterval`, wiec licznik odliczajacy "o jeden"
rozjechalby sie z zegarem tym bardziej, im dluzej strona byla w tle.
STREFA CZASOWA
Backend zwraca `OffsetDateTime`, czyli moment z przesunieciem, wiec odliczanie
jest poprawne takze dla odwiedzajacego z innej strefy niz webinar. Zgodnie
z regula, ze strefa zostaje jawna tam, gdzie ma znaczenie.
`tabular-nums` (klasa `hx-data`) nie jest tu ozdoba: bez niego cyfry maja rozne
szerokosci i licznik drga przy kazdej zmianie sekundy.
Czytnik ekranu dostaje jedno zdanie z `aria-live`, a nie cztery liczby odczytane
po kolei: same cyfry z podpisami sa dla oka, a odczytane brzmialyby jak zbior
przypadkowych wartosci.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Interfejs sali mial DWA niezalezne zrodla prawdy o tym, czy uczestnik moze
mowic: napis "Masz glos, mikrofon jest wlaczony" bral sie ze stanu ZGLOSZENIA
(`speak.granted`), a faktyczny mikrofon z uprawnienia w TOKENIE
(`canPublishAudio`). Te dwie rzeczy nie sa tym samym.
Prawo do audio daje takze rola ACTIVE_PARTICIPANT, ktora uczestnik dostaje przy
wlaczonym trybie "wideo dla wszystkich". Odebranie glosu nie zmienia wtedy
uprawnien, ale zmienia stan zgloszenia: napis znikal, a mikrofon zostawal
otwarty. Po kilku cyklach nadania i odebrania obraz w panelu rozjezdzal sie
z tym, co uczestnik faktycznie moze. Zgloszone z produkcji.
Pasek akcji dostaje teraz `canPublishAudio` wprost z tokenu i to ono
rozstrzyga o napisie. Gdy glos jest juz przyznany, a token jeszcze nie dotarl,
pasek mowi "trwa wlaczanie mikrofonu" zamiast klamac w ktorakolwiek strone.
Przycisk "Oddaj glos" zostaje przy zgloszeniu, bo dotyczy wlasnie jego.
SLAD DO DIAGNOZY
Rozjazd miedzy interfejsem a serwerem jest trudny do zbadania po fakcie:
uczestnik zglasza "mikrofon nie dziala", a log serwera pokazuje poprawnie
wydany token. Konsola sali wypisuje teraz po kazdej zmianie stan tokenu obok
stanu zgloszenia i obu trybow, wiec przy nastepnym zgloszeniu widac, ktora
strona jest nieaktualna. To nie jest debugowanie do usuniecia: nigdzie indziej
nie widac obu tych rzeczy naraz.
Hipoteza o wygasajacym tokenie sprawdzona i ODRZUCONA: TTL to szesc godzin,
czyli dluzej niz najdluzszy webinar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Domyslne `blurRadius: 10` z biblioteki jest za slabe do tego zastosowania.
Sprawdzone na zywo przy prowadzeniu z domu: przez rozmycie widac uklad pokoju,
zdjecia na polkach i przechodzace osoby, czyli dokladnie to, co ma zaslaniac.
25 daje tlo nierozpoznawalne. Obrys postaci zostaje przy tym czysty, bo maska
pochodzi z modelu segmentacji i nie zalezy od sily rozmycia: podnoszenie jej
nie psuje krawedzi. Kosztuje nieco mocy procesora, bo rozmycie liczy sie dla
kazdej klatki.
Model zostaje ten sam. `selfie_segmenter.tflite` to wariant "general" 256x256,
czyli DOKLADNIEJSZY z dwoch dostepnych; szybszego wariantu "landscape" uzywa
Google Meet. Przy nieruchomym prelegencie roznica w plynnosci jest
niezauwazalna, a jakosc obrysu ma tu wieksze znaczenie.
Wartosc w stalej z wyjasnieniem, zeby nastepna zmiana nie byla zgadywaniem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KAZDE nagranie konczylo sie bledem 412 "Start signal not received" po okolo
minucie, przy poprawnie dzialajacym pokoju, szablonie osiagalnym po sieci
Dockera i wlasciwych danych MinIO.
Szablon wolal `window.START_RECORDING()`, czyli funkcje, ktorej nikt nigdy nie
przypisywal. Byla zadeklarowana wylacznie w `declare global` jako opcjonalna,
wiec warunek `tracks.length > 0 && window.START_RECORDING` byl ZAWSZE falszywy.
Nawet gdyby nie byl, wywolanie funkcji niczego by nie dalo.
Egress czyta **wyjscie konsoli przegladarki** i czeka na napis
`START_RECORDING`. Dziala bez interfejsu, wiec konsola jest jedynym kanalem,
ktorym strona moze cokolwiek powiedziec procesowi nadrzednemu. Mowi o tym
dokumentacja LiveKit: "The recorder waits for the page to log START_RECORDING
to the console".
`console.log` nie jest tu wiec debugowaniem i nie wolno go usunac przy
porzadkowaniu logow. Sprawdzone, ze napis przezywa minifikacje produkcyjna:
minifikator Vite domyslnie nie wycina `console.log`.
Powtorne wyslanie blokuje `useRef`, a nie kasowanie pola `window`.
Diagnoza zajela dluzej, bo objaw myli: zadanie dociera do Egressu i przechodzi
walidacje, kontener laczy sie z Redisem, szablon odpowiada HTTP 200, a Egress
wchodzi do pokoju i widnieje na liscie uczestnikow. Wszystko poza jedna linia
dziala.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Przycisk w sali, ktory kazdy z wlasna kamera wlacza i wylacza sam,
w trakcie spotkania. Pokazuje sie wylacznie osobie z wlaczona kamera,
wiec uczestnik bez prawa publikacji obrazu nigdy go nie zobaczy.
`@livekit/track-processors` wchodzi jako nowa zaleznosc, poza lista
z CLAUDE.md: uzgodnione. Ladowany leniwie, bo ciagnie za soba MediaPipe:
import statyczny doliczalby kilka MB do pakietu KAZDEGO wejscia na sale,
takze uczestnikom bez kamery. Sprawdzone na zbudowanym pakiecie: MediaPipe
siedzi w osobnym chunku, glowny pakiet nie urosl.
Pliki modelu ida z WLASNEJ domeny, nie z CDN: instancja w sieci zamknietej
ma miec rozmycie tak samo, a CSP nie musi dopuszczac obcych hostow.
Model (244 kB) lezy w repozytorium, a zestaw WASM kopiuje sie przy budowaniu
obrazu z `node_modules`: wazy kilkanascie MB i musi odpowiadac wersji
pakietu, wiec w repozytorium puchlby w historii i po cichu rozjezdzal sie
przy kazdej aktualizacji.
Podmiany tla na obraz swiadomie NIE ma. Segmentacja obcina wlosy i dlonie
przy slabym swietle, a kafelek wideo jest za maly, zeby cokolwiek na tle
bylo czytelne. Reklama ma wlasne miejsce w banerach, ktore sa przy tym
mierzone.
`VideoConference` zostaje nietkniety: przycisk jest obok, wiec aktualizacja
biblioteki nie wymaga odtwarzania niczego u nas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
`PublicTaskOfferController` szukal tokenu wylacznie w `registrations`,
a prelegent tam nie istnieje: nie przechodzi przez formularz zapisu i ma
wlasny token w `webinar_speakers`. Kazdy endpoint sali odpowiadal mu 404.
Objawialo sie to tym, ze prowadzacy **nie widzial wlasnych zadan**: panel
pokazywal "Nie znaleziono zapisu" przy zadaniu ogloszonym minute wczesniej
i widocznym w panelu prowadzenia. Tak samo znikaly banery i nie liczyly sie
ich wyswietlenia. Zdarzylo sie to na zywo, w trakcie spotkania.
Nowa metoda `TaskService.viewer` obsluguje oba rodzaje tokenu, wzorowana na
`PublicInteractionController`, ktory robil to juz przy historii czatu.
Przelaczone sa WYLACZNIE odczyty: lista zadan, banery i ich zdarzenia.
Zapisy (zgloszenie do zadania, wyslanie oferty) zostaja przy `participant`,
bo prelegent nie zapisuje sie na wlasne zadanie.
`registrationId` rowne null jest tu poprawna wartoscia, nie brakiem danych:
`listViews` przewidywalo to od poczatku, a `banner_events.registration_id`
jest nullowalne wlasnie dla zdarzen od osob bez zapisu.
Front: `TasksTab` dostaje tryb `readOnly`, wiec prowadzacy widzi zadania
i liczbe zapisanych, ale bez przycisku zgloszenia.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prowadzacy widzial zakladke, ktora u niego nie mogla zadzialac: endpoint
`/offer-form` szuka tokenu w `registrations`, a prelegent tam nie istnieje
(nie przechodzi przez formularz zapisu i ma wlasny token w `webinar_speakers`).
Zadanie konczylo sie bledem 404, a panel zostawal na "Wczytywanie..." bez
konca, bo front nie obslugiwal bledu.
Zakladka i tak nie mialaby sensu: zapytanie ofertowe wysyla uczestnik, a
prowadzacy oglada zgloszenia w panelu prowadzenia.
Warunek jest w dwoch miejscach celowo: przy liscie zakladek i przy
renderowaniu tresci. Stan `tab` przezywa zmiane roli, wiec sama krotsza lista
nie wystarcza.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>