Browse Source

add troubleshooting page (#1206)

pull/1207/head
Jason Dove 3 years ago committed by GitHub
parent
commit
17dcbfc344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Troubleshooting/HealthCheckResultSummary.cs
  3. 3
      ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfo.cs
  4. 193
      ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
  5. 13
      ErsatzTV.Application/Troubleshooting/TroubleshootingInfo.cs
  6. 2
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  7. 93
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  8. 4
      ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs
  9. 87
      ErsatzTV/Pages/Troubleshooting.razor
  10. 5
      ErsatzTV/Shared/MainLayout.razor
  11. 20
      ErsatzTV/Shared/MarkdownView.razor.cs

4
CHANGELOG.md

@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add `Troubleshooting` page with aggregated settings/hardware accel info for easy reference
### Fixed
- Fix scaling anamorphic content from non-local libraries
- Fix direct streaming content from Jellyfin that has external subtitles
@ -11,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -11,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix Jellyfin, Emby and Plex library scans that wouldn't work in certain timezones
- Fix song normalization to match FFmpeg Profile bit depth
- Fix bug playing some external subtitle files (e.g. with an apostrophe in the file name)
- Fix bug detecting VAAPI capabilities when no device is selected in active FFmpeg Profile
### Changed
- Ignore case of video and audio file extensions in local folder scanner

3
ErsatzTV.Application/Troubleshooting/HealthCheckResultSummary.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Troubleshooting;
public record HealthCheckResultSummary(string Title, string Message);

3
ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfo.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Troubleshooting.Queries;
public record GetTroubleshootingInfo : IRequest<TroubleshootingInfo>;

193
ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs

@ -0,0 +1,193 @@ @@ -0,0 +1,193 @@
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.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Runtime;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Troubleshooting.Queries;
public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingInfo, TroubleshootingInfo>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHealthCheckService _healthCheckService;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly IConfigElementRepository _configElementRepository;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IMemoryCache _memoryCache;
public GetTroubleshootingInfoHandler(
IDbContextFactory<TvContext> 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<TroubleshootingInfo> Handle(GetTroubleshootingInfo request, CancellationToken cancellationToken)
{
List<HealthCheckResult> healthCheckResults = await _healthCheckService.PerformHealthChecks(cancellationToken);
string version = Assembly.GetEntryAssembly()?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.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<Channel> channels = await dbContext.Channels
.AsNoTracking()
.ToListAsync(cancellationToken);
var channelFFmpegProfiles = channels
.Map(c => c.FFmpegProfileId)
.ToImmutableHashSet();
List<FFmpegProfile> 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 vaapiCapabilities = null;
Option<ConfigElement> maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone)
{
nvidiaCapabilities = "Unable to locate ffmpeg";
}
else
{
foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
{
nvidiaCapabilities = await _hardwareCapabilitiesFactory.GetNvidiaOutput(ffmpegPath.Value);
if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux))
{
var allDrivers = new List<VaapiDriver>
{ VaapiDriver.iHD, VaapiDriver.i965, VaapiDriver.RadeonSI, VaapiDriver.Nouveau };
foreach (VaapiDriver activeDriver in allDrivers)
{
if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List<string> vaapiDevices))
{
vaapiDevices = new List<string> { "/dev/dri/renderD128" };
}
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 += $@"```shell
{output}
```";
vaapiCapabilities += " " + Environment.NewLine + " " + Environment.NewLine;
}
}
}
}
}
}
return new TroubleshootingInfo(
version,
healthCheckSummaries,
ffmpegSettings,
activeFFmpegProfiles,
channels,
nvidiaCapabilities,
vaapiCapabilities);
}
// lifted from GetFFmpegSettingsHandler
private async Task<FFmpegSettingsViewModel> GetFFmpegSettings()
{
Option<string> ffmpegPath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath);
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
Option<int> defaultFFmpegProfileId =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
Option<bool> saveReports =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredAudioLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
Option<int> hlsSegmenterIdleTimeout =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(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 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;
}
}

13
ErsatzTV.Application/Troubleshooting/TroubleshootingInfo.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Troubleshooting;
public record TroubleshootingInfo(
string Version,
IEnumerable<HealthCheckResultSummary> Health,
FFmpegSettingsViewModel FFmpegSettings,
IEnumerable<FFmpegProfile> FFmpegProfiles,
IEnumerable<Channel> Channels,
string NvidiaCapabilities,
string VaapiCapabilities);

2
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -701,7 +701,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -701,7 +701,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
private static Option<string> VaapiDeviceName(HardwareAccelerationMode accelerationMode, string vaapiDevice) =>
accelerationMode == HardwareAccelerationMode.Vaapi ||
OperatingSystem.IsLinux() && accelerationMode == HardwareAccelerationMode.Qsv
? vaapiDevice
? string.IsNullOrWhiteSpace(vaapiDevice) ? "/dev/dri/renderD128" : vaapiDevice
: Option<string>.None;
private static string GetVideoFormat(FFmpegPlaybackSettings playbackSettings) =>

93
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -50,6 +50,56 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -50,6 +50,56 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
};
}
public async Task<string> GetNvidiaOutput(string ffmpegPath)
{
string[] arguments =
{
"-f", "lavfi",
"-i", "nullsrc",
"-c:v", "h264_nvenc",
"-gpu", "list",
"-f", "null", "-"
};
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
? result.StandardError
: result.StandardOutput;
return output;
}
public async Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice)
{
BufferedCommandResult whichResult = await Cli.Wrap("which")
.WithArguments("vainfo")
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
if (whichResult.ExitCode != 0)
{
return Option<string>.None;
}
var envVars = new Dictionary<string, string?>();
foreach (string libvaDriverName in vaapiDriver)
{
envVars.Add("LIBVA_DRIVER_NAME", libvaDriverName);
}
BufferedCommandResult result = await Cli.Wrap("vainfo")
.WithArguments($"--display drm --device {vaapiDevice}")
.WithEnvironmentVariables(envVars)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
return result.StandardOutput;
}
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(string ffmpegPath, string capabilities)
{
var cacheKey = string.Format(FFmpegCapabilitiesCacheKeyFormat, capabilities);
@ -109,32 +159,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -109,32 +159,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
}
BufferedCommandResult whichResult = await Cli.Wrap("which")
.WithArguments("vainfo")
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
if (whichResult.ExitCode != 0)
Option<string> output = await GetVaapiOutput(vaapiDriver, device);
if (output.IsNone)
{
_logger.LogWarning("Unable to determine VAAPI capabilities; please install vainfo");
return new DefaultHardwareCapabilities();
}
var envVars = new Dictionary<string, string?>();
foreach (string libvaDriverName in vaapiDriver)
{
envVars.Add("LIBVA_DRIVER_NAME", libvaDriverName);
}
BufferedCommandResult result = await Cli.Wrap("vainfo")
.WithArguments($"--display drm --device {device}")
.WithEnvironmentVariables(envVars)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
profileEntrypoints = new List<VaapiProfileEntrypoint>();
foreach (string line in result.StandardOutput.Split("\n"))
foreach (string line in string.Join("", output).Split("\n"))
{
const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)";
Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN);
@ -187,23 +221,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -187,23 +221,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
_logger);
}
string[] arguments =
{
"-f", "lavfi",
"-i", "nullsrc",
"-c:v", "h264_nvenc",
"-gpu", "list",
"-f", "null", "-"
};
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
? result.StandardError
: result.StandardOutput;
string output = await GetNvidiaOutput(ffmpegPath);
Option<string> maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU")));
foreach (string line in maybeLine)
@ -226,8 +244,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -226,8 +244,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
}
_logger.LogWarning(
"Error detecting NVIDIA GPU capabilities; some hardware accelerated features will be unavailable: {ExitCode}",
result.ExitCode);
"Error detecting NVIDIA GPU capabilities; some hardware accelerated features will be unavailable");
return new NoHardwareCapabilities();
}

4
ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs

@ -10,4 +10,8 @@ public interface IHardwareCapabilitiesFactory @@ -10,4 +10,8 @@ public interface IHardwareCapabilitiesFactory
HardwareAccelerationMode hardwareAccelerationMode,
Option<string> vaapiDriver,
Option<string> vaapiDevice);
Task<string> GetNvidiaOutput(string ffmpegPath);
Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice);
}

87
ErsatzTV/Pages/Troubleshooting.razor

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
@page "/system/troubleshooting"
@using ErsatzTV.Application.Troubleshooting.Queries
@using System.Text.Json
@using System.Text.Json.Serialization
@using ErsatzTV.Application.Troubleshooting
@implements IDisposable
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard Class="mb-6">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">General</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MarkdownView Content="@_troubleshootingInfo"/>
</MudCardContent>
</MudCard>
<MudCard Class="mb-6">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Nvidia Capabilities</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MarkdownView Content="@_nvidiaCapabilities"/>
</MudCardContent>
</MudCard>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Vaapi Capabilities</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MarkdownView Content="@_vaapiCapabilities"/>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private string _troubleshootingInfo;
private string _nvidiaCapabilities;
private string _vaapiCapabilities;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
try
{
TroubleshootingInfo info = await Mediator.Send(new GetTroubleshootingInfo(), _cts.Token);
string json = JsonSerializer.Serialize(
new { info.Version, info.Health, info.FFmpegSettings, info.Channels, info.FFmpegProfiles },
new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
});
_troubleshootingInfo = $@"```json
{json}
```";
string formattedCapabilities = info.NvidiaCapabilities.Replace("\n", $" {Environment.NewLine}");
_nvidiaCapabilities = $@"```shell
{formattedCapabilities}
```";
_vaapiCapabilities = info.VaapiCapabilities;
}
catch (Exception ex)
{
_troubleshootingInfo = $@"```
{ex}
```";
}
}
}

5
ErsatzTV/Shared/MainLayout.razor

@ -111,7 +111,10 @@ @@ -111,7 +111,10 @@
<MudNavLink Href="schedules">Schedules</MudNavLink>
<MudNavLink Href="playouts">Playouts</MudNavLink>
<MudNavLink Href="settings">Settings</MudNavLink>
<MudNavLink Href="system/logs">Logs</MudNavLink>
<MudNavGroup Title="Support" Expanded="true">
<MudNavLink Href="system/logs">Logs</MudNavLink>
<MudNavLink Href="system/troubleshooting">Troubleshooting</MudNavLink>
</MudNavGroup>
<MudDivider Class="my-6" DividerType="DividerType.Middle"/>
<MudContainer Style="text-align: right" Class="mr-6">
<MudText Typo="Typo.body2">ErsatzTV Version</MudText>

20
ErsatzTV/Shared/MarkdownView.razor.cs

@ -18,14 +18,30 @@ public partial class MarkdownView @@ -18,14 +18,30 @@ public partial class MarkdownView
[Parameter]
public string Content { get; set; }
public MarkupString HtmlContent => _markupContent ?? (_markupContent = ConvertStringToMarkupString(Content)).Value;
public MarkupString? HtmlContent
{
get
{
if (string.IsNullOrWhiteSpace(Content))
{
return null;
}
return _markupContent ?? (_markupContent = ConvertStringToMarkupString(Content)).Value;
}
}
private MarkupString ConvertStringToMarkupString(string value)
{
if (!string.IsNullOrWhiteSpace(value))
{
// Convert markdown string to HTML
string html = Markdown.ToHtml(value, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
string html = Markdown.ToHtml(
value,
new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UseSoftlineBreakAsHardlineBreak()
.Build());
// Sanitize HTML before rendering
string sanitizedHtml = HtmlSanitizer.Sanitize(html);

Loading…
Cancel
Save