using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using SameBeat.Application.Interfejsy; namespace SameBeat.Infrastructure.Audio; /// /// Autorytatywny timer mikrofonu (ETAP 5) -- SEDNO calej funkcji "host przyznaje mikrofon na /// DOKLADNIE okreslony czas". Dziala jako BackgroundService, co ok. 1 sekunde sprawdza w bazie, czy /// aktywny mikrofon biezacego wydarzenia wygasl, i jesli tak -- odbiera go (zapisuje /// KoniecFaktycznyUtc w HistoriaMikrofonu, czysci zywy stan na Wydarzenie, woła LiveKit zeby fizycznie /// zamknac publikacje audio, rozglasza SignalR). Klient NIGDY nie wysyla "ile mi jeszcze zostalo" -- /// dostaje tylko AktywnyMikrofonKoniecUtc do lokalnego odliczania na ekranie, ale to WYLACZNIE ten /// timer, dzialajacy tutaj po stronie serwera, fizycznie egzekwuje koniec. /// /// BackgroundService zyje POZA zakresem (scope) requestu -- IRepozytoriumWydarzen/DbContext sa scoped, /// wiec kazdy "tik" tworzy WLASNY, krotkozyjacy IServiceScope (standardowy wzorzec ASP.NET Core dla /// BackgroundService korzystajacego z DbContext), zamiast trzymac jeden DbContext przez cale zycie /// procesu. IRozgloszenieZdarzenImprezy jest bezstanowy (singleton), wiec jest wstrzykniety wprost do /// konstruktora -- nie trzeba go pobierac ze scope'u przy kazdym tiku. /// public sealed class SerwisTimeraMikrofonu : BackgroundService { private static readonly TimeSpan OdstepSprawdzania = TimeSpan.FromSeconds(1); private readonly IServiceScopeFactory _scopeFactory; private readonly IRozgloszenieZdarzenImprezy _rozgloszenieZdarzen; private readonly ILogger _logger; public SerwisTimeraMikrofonu(IServiceScopeFactory scopeFactory, IRozgloszenieZdarzenImprezy rozgloszenieZdarzen, ILogger logger) { _scopeFactory = scopeFactory; _rozgloszenieZdarzen = rozgloszenieZdarzen; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("SerwisTimeraMikrofonu wystartowal -- sprawdzanie wygasniecia co {Odstep}.", OdstepSprawdzania); using var timer = new PeriodicTimer(OdstepSprawdzania); // WhileAsync z PeriodicTimer konczy petle sama, gdy stoppingToken zostanie anulowany (host sie // zamyka) -- WaitForNextTickAsync zwraca wtedy false zamiast rzucac, wiec petla wychodzi czysto. while (await timer.WaitForNextTickAsync(stoppingToken)) { try { await SprawdzITikAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { // Zamkniecie hosta w trakcie tiku -- oczekiwane, nie blad. break; } catch (Exception wyjatek) { // Jeden nieudany tik (np. baza chwilowo niedostepna) NIE moze zabic calego timera -- // proba ponawia sie za OdstepSprawdzania. Bez tego jeden wyjatek wylaczylby na zawsze // egzekwowanie czasu mikrofonu do restartu procesu. _logger.LogError(wyjatek, "Blad podczas sprawdzania wygasniecia mikrofonu -- kolejna proba za {Odstep}.", OdstepSprawdzania); } } _logger.LogInformation("SerwisTimeraMikrofonu zatrzymany."); } private async Task SprawdzITikAsync(CancellationToken token) { using var zakres = _scopeFactory.CreateScope(); var serwisMikrofonu = zakres.ServiceProvider.GetRequiredService(); var wynik = await serwisMikrofonu.SprawdzIOdbierzWygasleAsync(token); if (wynik is not null) { await _rozgloszenieZdarzen.MikrofonOdebranyAsync(wynik, token); } } }