using SameBeat.Domain.Enumy; using SameBeat.Domain.Wyjatki; namespace SameBeat.Domain.Encje; /// /// Wydarzenie -- pojedyncza "impreza" (np. cykliczna, powtarzalna nazwa), w ramach ktorej moga /// odbywac sie kolejne sesje na zywo (). /// public class Wydarzenie { public Guid Id { get; private set; } public string Nazwa { get; private set; } = string.Empty; public StatusWydarzenia Status { get; private set; } public DateTime UtworzoneUtc { get; private set; } /// Uklad sceny glownej (ETAP 3) -- okresla dostepne sloty, patrz . public UkladSceny UkladSceny { get; private set; } // ---- Muzyka (ETAP 4) -------------------------------------------------------------------- // Muzyka NIE idzie przez LiveKit/SignalR jako dzwiek -- kazda przegladarka (host i kazdy widz) // samodzielnie odtwarza TEN SAM plik audio spod Utwor.Url, a ponizsze pola to tylko "zegar" // (kiedy sie zaczelo, od ktorego miejsca), do ktorego klienci synchronizuja swoja pozycje // odtwarzania. To jak radio internetowe -- backend nigdy nie przesyla samego dzwieku. /// Aktualnie wybrany utwor (biblioteka ). Null = nic nie jest ustawione (StanOdtwarzania=Zatrzymana). public Guid? UtworId { get; private set; } /// Chwila UTC, w ktorej "zegar" ruszyl od PozycjaStartowaMs. Null, gdy nie gra teraz (Zatrzymana/Wstrzymana). public DateTime? CzasStartuUtc { get; private set; } /// Pozycja w utworze (ms), od ktorej liczy sie zegar od CzasStartuUtc -- albo pozycja, na ktorej zatrzymano sie przy pauzie. public int PozycjaStartowaMs { get; private set; } public StanOdtwarzania StanOdtwarzania { get; private set; } /// Glosnosc master (0.0-1.0) -- mnoznik, ktory kazdy klient stosuje do wlasnego odtwarzacza. public float GlosnoscMaster { get; private set; } // ---- Mikrofon (ETAP 5) -------------------------------------------------------------------- // ZYWY stan (do wyswietlenia "komu teraz wolno mowic" i do autorytatywnego liczenia czasu przez // serwer) -- w odroznieniu od HistoriaMikrofonu, ktora jest logiem/audytem, nie zywym stanem // (zgodnie z zasada "baza przechowuje stan biznesowy, nie kazdy pakiet"). Wzorzec identyczny // jak przy muzyce: pola tutaj + metody domenowe, serwis aplikacji odpowiada za koordynacje // z innymi encjami (uczestnik musi byc NaScenie) i za twarde wymuszenie przez LiveKit. /// Uczestnik, ktory ma teraz przyznany mikrofon. Null = nikt. public Guid? AktywnyMikrofonUczestnikId { get; private set; } /// /// Chwila UTC, w ktorej AKTUALNIE przyznany mikrofon wygasa -- to jest AUTORYTATYWNE zrodlo /// prawdy o czasie (serwer, nie przegladarka). Klient dostaje ta wartosc wylacznie do lokalnego /// odliczania na ekranie; fizyczne odebranie prawa do publikacji audio robi backend, patrz /// SerwisAdministracjiLiveKit. /// public DateTime? AktywnyMikrofonKoniecUtc { get; private set; } private Wydarzenie() { } private Wydarzenie(Guid id, string nazwa, DateTime utworzoneUtc) { Id = id; Nazwa = nazwa; Status = StatusWydarzenia.Offline; UtworzoneUtc = utworzoneUtc; UkladSceny = UkladSceny.Grid4; UtworId = null; CzasStartuUtc = null; PozycjaStartowaMs = 0; StanOdtwarzania = StanOdtwarzania.Zatrzymana; GlosnoscMaster = 0.9f; AktywnyMikrofonUczestnikId = null; AktywnyMikrofonKoniecUtc = null; } public static Wydarzenie Utworz(string nazwa) { if (string.IsNullOrWhiteSpace(nazwa)) { throw new NieprawidlowyStanEncjiWyjatek("Nazwa wydarzenia nie moze byc pusta."); } return new Wydarzenie(Guid.NewGuid(), nazwa.Trim(), DateTime.UtcNow); } public void PrzejdzNaLive() => Status = StatusWydarzenia.Live; public void PrzejdzNaOffline() => Status = StatusWydarzenia.Offline; /// /// Zmienia uklad sceny. Korekta "sierocych" przypisan (sloty o numerze wiekszym niz dopuszcza /// nowy uklad) NIE jest robiona tutaj -- to logika miedzy-encyjna (Wydarzenie + SlotSceny), /// wykonuje ja serwis aplikacji, ktory ma dostep do obu. /// public void ZmienUklad(UkladSceny nowyUklad) => UkladSceny = nowyUklad; /// Przelacza "zegar" na dany utwor od podanej pozycji -- ruszamy teraz (CzasStartuUtc=DateTime.UtcNow), stan Odtwarzana. public void OdtworzOd(Guid utworId, int pozycjaMs) { if (utworId == Guid.Empty) { throw new NieprawidlowyStanEncjiWyjatek("Nie mozna odtworzyc pustego identyfikatora utworu."); } if (pozycjaMs < 0) { throw new NieprawidlowyStanEncjiWyjatek("Pozycja startowa utworu nie moze byc ujemna."); } UtworId = utworId; PozycjaStartowaMs = pozycjaMs; CzasStartuUtc = DateTime.UtcNow; StanOdtwarzania = StanOdtwarzania.Odtwarzana; } /// /// Wstrzymuje zegar -- przelicza aktualna pozycje (PozycjaStartowaMs + uplyw czasu od /// CzasStartuUtc) i zapisuje ja jako nowa PozycjaStartowaMs, zeby Wznow() mogl kontynuowac /// dokladnie stamtad. Idempotentne: wywolanie, gdy juz nie gra, nic nie robi (host mogl /// kliknac pauze dwa razy albo klient mial nieaktualny stan). /// public void Wstrzymaj() { if (StanOdtwarzania != StanOdtwarzania.Odtwarzana) { return; } if (CzasStartuUtc is DateTime czasStartu) { var uplynelMs = (int)Math.Max(0, (DateTime.UtcNow - czasStartu).TotalMilliseconds); PozycjaStartowaMs += uplynelMs; } CzasStartuUtc = null; StanOdtwarzania = StanOdtwarzania.Wstrzymana; } /// Wznawia od zapisanej PozycjaStartowaMs -- zegar rusza teraz (CzasStartuUtc=DateTime.UtcNow). public void Wznow() { CzasStartuUtc = DateTime.UtcNow; StanOdtwarzania = StanOdtwarzania.Odtwarzana; } /// Zatrzymuje odtwarzanie calkowicie -- brak wybranego utworu, zegar wyzerowany (koniec playlisty). public void Zatrzymaj() { UtworId = null; CzasStartuUtc = null; PozycjaStartowaMs = 0; StanOdtwarzania = StanOdtwarzania.Zatrzymana; } /// Ustawia glosnosc master. Zakres 0.0-1.0 -- poza nim rzuca (druga linia obrony, SerwisMuzyki waliduje wczesniej przez WynikOperacji). public void UstawGlosnosc(float wartosc) { if (wartosc < 0f || wartosc > 1f) { throw new NieprawidlowyStanEncjiWyjatek("Glosnosc musi byc w zakresie 0.0-1.0."); } GlosnoscMaster = wartosc; } /// /// Przyznaje mikrofon uczestnikowi na dokladnie okreslony czas -- koniec liczony TERAZ /// (DateTime.UtcNow + czasSekundy), zeby zaden posredni krok (kolejka requestu, zapis do bazy) /// nie "kradl" uczestnikowi czasu. Czy uczestnik faktycznie jest NaScenie sprawdza serwis /// aplikacji (Wydarzenie nie zna statusu UczestnikOczekujacy) -- tutaj pilnujemy tylko wlasnego, /// prostszego niezmiennika: co najwyzej jeden aktywny mikrofon na raz (host musi najpierw /// przerwac poprzedni, patrz SerwisMikrofonu -- swiadome uproszczenie zamiast automatycznego /// przerywania). /// public void PrzyznajMikrofon(Guid uczestnikId, int czasSekundy) { if (uczestnikId == Guid.Empty) { throw new NieprawidlowyStanEncjiWyjatek("Nie mozna przyznac mikrofonu pustemu identyfikatorowi uczestnika."); } if (czasSekundy <= 0) { throw new NieprawidlowyStanEncjiWyjatek("Czas przyznania mikrofonu musi byc dodatni."); } if (AktywnyMikrofonUczestnikId is not null) { throw new NieprawidlowyStanEncjiWyjatek("Mikrofon jest juz przyznany innemu uczestnikowi -- najpierw go przerwij."); } AktywnyMikrofonUczestnikId = uczestnikId; AktywnyMikrofonKoniecUtc = DateTime.UtcNow.AddSeconds(czasSekundy); } /// /// Odbiera mikrofon -- badz to dlatego, ze host przerwal recznie, badz ze serwerowy timer /// wykryl uplyw AktywnyMikrofonKoniecUtc. Idempotentne: wywolanie, gdy nikt nie ma teraz /// mikrofonu, nic nie robi (host mogl kliknac "przerwij" tuz po tym, jak timer juz go odebral). /// public void OdbierzMikrofon() { AktywnyMikrofonUczestnikId = null; AktywnyMikrofonKoniecUtc = null; } }