mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
8.7 KiB
216 lines
8.7 KiB
using System.IO.Pipelines; |
|
using System.Runtime.InteropServices; |
|
using System.Text.Json; |
|
using System.Text.Json.Serialization; |
|
using System.Text.RegularExpressions; |
|
using CliWrap; |
|
using ErsatzTV.Core; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.Locking; |
|
using ErsatzTV.Core.Interfaces.Streaming; |
|
using ErsatzTV.Core.Interfaces.Troubleshooting; |
|
using ErsatzTV.Core.Notifications; |
|
using ErsatzTV.FFmpeg.Runtime; |
|
using Microsoft.Extensions.Logging; |
|
using Serilog.Context; |
|
using Serilog.Events; |
|
|
|
namespace ErsatzTV.Application.Troubleshooting; |
|
|
|
public partial class StartTroubleshootingPlaybackHandler( |
|
ITroubleshootingNotifier notifier, |
|
IMediator mediator, |
|
IEntityLocker entityLocker, |
|
IRuntimeInfo runtimeInfo, |
|
IGraphicsEngine graphicsEngine, |
|
InMemoryLogService logService, |
|
LoggingLevelSwitches loggingLevelSwitches, |
|
ILogger<StartTroubleshootingPlaybackHandler> logger) |
|
: IRequestHandler<StartTroubleshootingPlayback> |
|
{ |
|
private static readonly JsonSerializerOptions Options = new() |
|
{ |
|
Converters = { new JsonStringEnumConverter() }, |
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
|
WriteIndented = true |
|
}; |
|
|
|
public async Task Handle(StartTroubleshootingPlayback request, CancellationToken cancellationToken) |
|
{ |
|
var currentStreamingLevel = loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel; |
|
loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = LogEventLevel.Debug; |
|
|
|
try |
|
{ |
|
using var logContext = LogContext.PushProperty(InMemoryLogService.CorrelationIdKey, request.SessionId); |
|
|
|
// write media info without title |
|
foreach (var mediaInfo in request.MediaItemInfo) |
|
{ |
|
string infoJson = JsonSerializer.Serialize(mediaInfo with { Title = null }, Options); |
|
await File.WriteAllTextAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "media_info.json"), |
|
infoJson, |
|
cancellationToken); |
|
} |
|
|
|
// write troubleshooting info |
|
string troubleshootingInfoJson = JsonSerializer.Serialize( |
|
new |
|
{ |
|
request.TroubleshootingInfo.Version, |
|
Environment = request.TroubleshootingInfo.Environment.OrderBy(x => x.Key) |
|
.ToDictionary(x => x.Key, x => x.Value), |
|
request.TroubleshootingInfo.Health, |
|
request.TroubleshootingInfo.Cpus, |
|
request.TroubleshootingInfo.VideoControllers, |
|
request.TroubleshootingInfo.FFmpegSettings, |
|
request.TroubleshootingInfo.FFmpegProfiles, |
|
request.TroubleshootingInfo.Watermarks |
|
}, |
|
Options); |
|
await File.WriteAllTextAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "troubleshooting_info.json"), |
|
troubleshootingInfoJson, |
|
cancellationToken); |
|
|
|
// write stream selector |
|
if (!string.IsNullOrWhiteSpace(request.StreamSelector)) |
|
{ |
|
string fullPath = Path.Combine(FileSystemLayout.ChannelStreamSelectorsFolder, request.StreamSelector); |
|
if (File.Exists(fullPath)) |
|
{ |
|
File.Copy( |
|
fullPath, |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "stream-selector.yml")); |
|
} |
|
} |
|
|
|
HardwareAccelerationKind hwAccel = request.TroubleshootingInfo.FFmpegProfiles.Head().HardwareAcceleration; |
|
if (hwAccel is HardwareAccelerationKind.Qsv) |
|
{ |
|
await File.WriteAllTextAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "capabilities_qsv.txt"), |
|
request.TroubleshootingInfo.QsvCapabilities, |
|
cancellationToken); |
|
} |
|
|
|
if (hwAccel is HardwareAccelerationKind.Vaapi || hwAccel is HardwareAccelerationKind.Qsv && |
|
runtimeInfo.IsOSPlatform(OSPlatform.Linux)) |
|
{ |
|
await File.WriteAllTextAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "capabilities_vaapi.txt"), |
|
request.TroubleshootingInfo.VaapiCapabilities, |
|
cancellationToken); |
|
} |
|
|
|
if (hwAccel is HardwareAccelerationKind.Nvenc) |
|
{ |
|
await File.WriteAllTextAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "capabilities_nvidia.txt"), |
|
request.TroubleshootingInfo.NvidiaCapabilities, |
|
cancellationToken); |
|
} |
|
|
|
logger.LogDebug( |
|
"ffmpeg troubleshooting arguments {FFmpegArguments}", |
|
request.PlayoutItemResult.Process.Arguments); |
|
|
|
Option<Pipe> maybePipe = Option<Pipe>.None; |
|
|
|
try |
|
{ |
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); |
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); |
|
|
|
Command processWithPipe = request.PlayoutItemResult.Process; |
|
foreach (GraphicsEngineContext graphicsEngineContext in request.PlayoutItemResult.GraphicsEngineContext) |
|
{ |
|
var pipe = new Pipe(); |
|
maybePipe = pipe; |
|
processWithPipe = |
|
processWithPipe.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream())); |
|
|
|
// fire and forget graphics engine task |
|
_ = graphicsEngine.Run( |
|
graphicsEngineContext, |
|
pipe.Writer, |
|
linkedCts.Token); |
|
} |
|
|
|
CommandResult commandResult = await processWithPipe |
|
.WithWorkingDirectory(FileSystemLayout.TranscodeTroubleshootingFolder) |
|
.WithStandardErrorPipe(PipeTarget.Null) |
|
.WithValidation(CommandResultValidation.None) |
|
.ExecuteAsync(linkedCts.Token); |
|
|
|
logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", commandResult.ExitCode); |
|
|
|
try |
|
{ |
|
IEnumerable<string> logs = logService.Sink.GetLogs(request.SessionId); |
|
await File.WriteAllLinesAsync( |
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "logs.txt"), |
|
logs, |
|
linkedCts.Token); |
|
logService.Sink.ClearLogs(request.SessionId); |
|
} |
|
catch (Exception) |
|
{ |
|
// do nothing |
|
} |
|
|
|
Option<double> maybeSpeed = Option<double>.None; |
|
Option<string> maybeFile = Directory.GetFiles(FileSystemLayout.TranscodeTroubleshootingFolder, "ffmpeg*.log").HeadOrNone(); |
|
foreach (string file in maybeFile) |
|
{ |
|
await foreach (string line in File.ReadLinesAsync(file, linkedCts.Token)) |
|
{ |
|
Match match = FFmpegSpeed().Match(line); |
|
if (match.Success && double.TryParse(match.Groups[1].Value, out double speed)) |
|
{ |
|
maybeSpeed = speed; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
await mediator.Publish( |
|
new PlaybackTroubleshootingCompletedNotification( |
|
commandResult.ExitCode, |
|
Option<Exception>.None, |
|
maybeSpeed), |
|
linkedCts.Token); |
|
|
|
if (commandResult.ExitCode != 0) |
|
{ |
|
await linkedCts.CancelAsync(); |
|
notifier.NotifyFailed(request.SessionId); |
|
} |
|
} |
|
catch (TaskCanceledException) |
|
{ |
|
// do nothing |
|
} |
|
catch (Exception e) |
|
{ |
|
Console.WriteLine(e); |
|
} |
|
finally |
|
{ |
|
foreach (Pipe pipe in maybePipe) |
|
{ |
|
await pipe.Writer.CompleteAsync(); |
|
} |
|
} |
|
} |
|
finally |
|
{ |
|
entityLocker.UnlockTroubleshootingPlayback(); |
|
loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = currentStreamingLevel; |
|
} |
|
} |
|
|
|
[GeneratedRegex(@"speed=\s*([\d\.]+)x", RegexOptions.IgnoreCase)] |
|
private static partial Regex FFmpegSpeed(); |
|
}
|
|
|