using System.Collections.Immutable; using System.Reflection; using System.Runtime.InteropServices; using ErsatzTV.Application.FFmpegProfiles; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Health; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities.Qsv; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; namespace ErsatzTV.Application.Troubleshooting.Queries; public class GetTroubleshootingInfoHandler : IRequestHandler { private readonly IConfigElementRepository _configElementRepository; private readonly IDbContextFactory _dbContextFactory; private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; private readonly IHealthCheckService _healthCheckService; private readonly IMemoryCache _memoryCache; private readonly IRuntimeInfo _runtimeInfo; public GetTroubleshootingInfoHandler( IDbContextFactory dbContextFactory, IHealthCheckService healthCheckService, IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, IConfigElementRepository configElementRepository, IRuntimeInfo runtimeInfo, IMemoryCache memoryCache) { _dbContextFactory = dbContextFactory; _healthCheckService = healthCheckService; _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; _configElementRepository = configElementRepository; _runtimeInfo = runtimeInfo; _memoryCache = memoryCache; } public async Task Handle(GetTroubleshootingInfo request, CancellationToken cancellationToken) { List healthCheckResults = await _healthCheckService.PerformHealthChecks(cancellationToken); string version = Assembly.GetEntryAssembly()? .GetCustomAttribute()? .InformationalVersion ?? "unknown"; var healthCheckSummaries = healthCheckResults .Filter(r => r.Status is HealthCheckStatus.Warning or HealthCheckStatus.Fail) .Map(r => new HealthCheckResultSummary(r.Title, r.Message)) .ToList(); FFmpegSettingsViewModel ffmpegSettings = await GetFFmpegSettings(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List channels = await dbContext.Channels .AsNoTracking() .ToListAsync(cancellationToken); var channelFFmpegProfiles = channels .Map(c => c.FFmpegProfileId) .ToImmutableHashSet(); List ffmpegProfiles = await dbContext.FFmpegProfiles .AsNoTracking() .Include(p => p.Resolution) .ToListAsync(cancellationToken); var activeFFmpegProfiles = ffmpegProfiles .Filter(f => channelFFmpegProfiles.Contains(f.Id)) .ToList(); string nvidiaCapabilities = null; string qsvCapabilities = null; string vaapiCapabilities = null; Option maybeFFmpegPath = await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); if (maybeFFmpegPath.IsNone) { nvidiaCapabilities = "Unable to locate ffmpeg"; } else { foreach (ConfigElement ffmpegPath in maybeFFmpegPath) { nvidiaCapabilities = await _hardwareCapabilitiesFactory.GetNvidiaOutput(ffmpegPath.Value); if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List vaapiDevices)) { vaapiDevices = new List { "/dev/dri/renderD128" }; } foreach (string qsvDevice in vaapiDevices) { QsvOutput output = await _hardwareCapabilitiesFactory.GetQsvOutput(ffmpegPath.Value, qsvDevice); qsvCapabilities += $"Checking device {qsvDevice}{Environment.NewLine}"; qsvCapabilities += $"Exit Code: {output.ExitCode}{Environment.NewLine}{Environment.NewLine}"; qsvCapabilities += output.Output; qsvCapabilities += Environment.NewLine + Environment.NewLine; } if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux)) { var allDrivers = new List { VaapiDriver.iHD, VaapiDriver.i965, VaapiDriver.RadeonSI, VaapiDriver.Nouveau }; foreach (VaapiDriver activeDriver in allDrivers) { foreach (string vaapiDevice in vaapiDevices) { foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput( Optional(GetDriverName(activeDriver)), vaapiDevice)) { vaapiCapabilities += $"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}"; vaapiCapabilities += output; vaapiCapabilities += Environment.NewLine + Environment.NewLine; } } } } } } return new TroubleshootingInfo( version, healthCheckSummaries, ffmpegSettings, activeFFmpegProfiles, channels, nvidiaCapabilities, qsvCapabilities, vaapiCapabilities); } // lifted from GetFFmpegSettingsHandler private async Task GetFFmpegSettings() { Option ffmpegPath = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPath); Option ffprobePath = await _configElementRepository.GetValue(ConfigElementKey.FFprobePath); Option defaultFFmpegProfileId = await _configElementRepository.GetValue(ConfigElementKey.FFmpegDefaultProfileId); Option saveReports = await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports); Option preferredAudioLanguageCode = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPreferredLanguageCode); Option watermark = await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalWatermarkId); Option fallbackFiller = await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalFallbackFillerId); Option hlsSegmenterIdleTimeout = await _configElementRepository.GetValue(ConfigElementKey.FFmpegSegmenterTimeout); Option workAheadSegmenterLimit = await _configElementRepository.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters); Option initialSegmentCount = await _configElementRepository.GetValue(ConfigElementKey.FFmpegInitialSegmentCount); var result = new FFmpegSettingsViewModel { FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty), FFprobePath = await ffprobePath.IfNoneAsync(string.Empty), DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0), SaveReports = await saveReports.IfNoneAsync(false), PreferredAudioLanguageCode = await preferredAudioLanguageCode.IfNoneAsync("eng"), HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60), WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1), InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1) }; foreach (int watermarkId in watermark) { result.GlobalWatermarkId = watermarkId; } foreach (int fallbackFillerId in fallbackFiller) { result.GlobalFallbackFillerId = fallbackFillerId; } return result; } private static string GetDriverName(VaapiDriver driver) { switch (driver) { case VaapiDriver.i965: return "i965"; case VaapiDriver.iHD: return "iHD"; case VaapiDriver.RadeonSI: return "radeonsi"; case VaapiDriver.Nouveau: return "nouveau"; } return null; } }