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);
}
}
}