using System; using System.IO; using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; namespace ErsatzTV.Application.Streaming.Commands { public class StartFFmpegSessionHandler : MediatR.IRequestHandler> { private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IConfigElementRepository _configElementRepository; private readonly ILocalFileSystem _localFileSystem; public StartFFmpegSessionHandler( ILocalFileSystem localFileSystem, ILogger logger, IServiceScopeFactory serviceScopeFactory, IFFmpegSegmenterService ffmpegSegmenterService, IConfigElementRepository configElementRepository) { _localFileSystem = localFileSystem; _logger = logger; _serviceScopeFactory = serviceScopeFactory; _ffmpegSegmenterService = ffmpegSegmenterService; _configElementRepository = configElementRepository; } public Task> Handle(StartFFmpegSession request, CancellationToken cancellationToken) => Validate(request) .MapT(_ => StartProcess(request)) // this weirdness is needed to maintain the error type (.ToEitherAsync() just gives BaseError) #pragma warning disable VSTHRD103 .Bind(v => v.ToEither().MapLeft(seq => seq.Head()).MapAsync, Unit>(identity)); #pragma warning restore VSTHRD103 private async Task StartProcess(StartFFmpegSession request) { TimeSpan idleTimeout = await _configElementRepository .GetValue(ConfigElementKey.FFmpegSegmenterTimeout) .Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1))); using IServiceScope scope = _serviceScopeFactory.CreateScope(); HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService(); _ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker); // fire and forget worker _ = worker.Run(request.ChannelNumber, idleTimeout) .ContinueWith( _ => _ffmpegSegmenterService.SessionWorkers.TryRemove( request.ChannelNumber, out IHlsSessionWorker _), TaskScheduler.Default); string playlistFileName = Path.Combine( FileSystemLayout.TranscodeFolder, request.ChannelNumber, "live.m3u8"); while (!File.Exists(playlistFileName)) { await Task.Delay(TimeSpan.FromMilliseconds(100)); } return Unit.Default; } private Task> Validate(StartFFmpegSession request) => SessionMustBeInactive(request) .BindT(_ => FolderMustBeEmpty(request)); private Task> SessionMustBeInactive(StartFFmpegSession request) { var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null)) .Where(success => success) .Map(_ => Unit.Default) .ToValidation(new ChannelSessionAlreadyActive()); if (result.IsFail && _ffmpegSegmenterService.SessionWorkers.TryGetValue( request.ChannelNumber, out IHlsSessionWorker worker)) { worker?.Touch(); } return result.AsTask(); } private Task> FolderMustBeEmpty(StartFFmpegSession request) { string folder = Path.Combine(FileSystemLayout.TranscodeFolder, request.ChannelNumber); _logger.LogDebug("Preparing transcode folder {Folder}", folder); _localFileSystem.EnsureFolderExists(folder); _localFileSystem.EmptyFolder(folder); return Task.FromResult>(Unit.Default); } } }