Browse Source

feat: more improvements to next troubleshooting (#2974)

* feat: more stream selection with next troubleshooting

* feat: improve next logging

* feat: log next troubleshooting speed
pull/2975/head
Jason Dove 1 month ago committed by GitHub
parent
commit
9088ae1a5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
  2. 33
      ErsatzTV.Application/Streaming/NextLogger.cs
  3. 4
      ErsatzTV.Application/Streaming/NextSessionWorker.cs
  4. 3
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  5. 35
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  6. 16
      ErsatzTV.Core/InMemoryLogService.cs
  7. 2
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutItemConverter.cs
  8. 23
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs
  9. 25
      ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

2
ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs

@ -253,6 +253,8 @@ public partial class SyncNextPlayoutHandler(
maybeGlobalWatermark, maybeGlobalWatermark,
playoutOffset, playoutOffset,
playoutItem, playoutItem,
Option<List<Subtitle>>.None,
shouldLogMessages: false,
cancellationToken); cancellationToken);
foreach (var nextPlayoutItem in maybeNextPlayoutItem) foreach (var nextPlayoutItem in maybeNextPlayoutItem)

33
ErsatzTV.Application/Streaming/NextLogger.cs

@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Streaming;
public partial class NextLogger
{
[GeneratedRegex(
@"^\[\S+ (?<level>TRACE|DEBUG|INFO|WARN|ERROR) (?<target>[^\]]+)\] (?<msg>.*)$",
RegexOptions.Singleline)]
private static partial Regex NextLogLine();
public static void LogNextLine(string line, ILogger logger)
{
Match match = NextLogLine().Match(line);
if (!match.Success)
{
logger.LogDebug("{Line:l}", line);
return;
}
LogLevel level = match.Groups["level"].Value switch
{
"ERROR" => LogLevel.Error,
"WARN" => LogLevel.Warning,
"INFO" => LogLevel.Information,
"TRACE" => LogLevel.Trace,
_ => LogLevel.Debug
};
logger.Log(level, "[{Target:l}] {Line:l}", match.Groups["target"].Value, match.Groups["msg"].Value);
}
}

4
ErsatzTV.Application/Streaming/NextSessionWorker.cs

@ -120,8 +120,8 @@ public class NextSessionWorker(
CommandResult commandResult = await Cli.Wrap(channelBinary) CommandResult commandResult = await Cli.Wrap(channelBinary)
.WithArguments(arguments) .WithArguments(arguments)
.WithStandardInputPipe(PipeSource.FromString(channelConfig.ToJson())) .WithStandardInputPipe(PipeSource.FromString(channelConfig.ToJson()))
.WithStandardOutputPipe(PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l))) .WithStandardOutputPipe(PipeTarget.ToDelegate(l => NextLogger.LogNextLine(l, logger)))
.WithStandardErrorPipe(PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l))) .WithStandardErrorPipe(PipeTarget.ToDelegate(l => NextLogger.LogNextLine(l, logger)))
//.WithStandardOutputPipe(PipeTarget.ToDelegate(progressParser.ParseLine)) //.WithStandardOutputPipe(PipeTarget.ToDelegate(progressParser.ParseLine))
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteAsync(_cancellationTokenSource.Token); .ExecuteAsync(_cancellationTokenSource.Token);

3
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -195,6 +195,7 @@ public class PrepareTroubleshootingPlaybackHandler(
Name = "ETV", Name = "ETV",
Number = FileSystemLayout.TranscodeTroubleshootingChannel, Number = FileSystemLayout.TranscodeTroubleshootingChannel,
FFmpegProfile = ffmpegProfile, FFmpegProfile = ffmpegProfile,
StreamingEngine = request.StreamingEngine,
StreamingMode = request.StreamingMode, StreamingMode = request.StreamingMode,
StreamSelectorMode = ChannelStreamSelectorMode.Troubleshooting, StreamSelectorMode = ChannelStreamSelectorMode.Troubleshooting,
SubtitleMode = SubtitleMode SubtitleMode = SubtitleMode
@ -354,6 +355,8 @@ public class PrepareTroubleshootingPlaybackHandler(
watermarks.HeadOrNone().Map(wm => wm.Watermark), watermarks.HeadOrNone().Map(wm => wm.Watermark),
TimeSpan.Zero, TimeSpan.Zero,
playoutItem, playoutItem,
await GetSubtitles(mediaItem, request),
shouldLogMessages: true,
cancellationToken); cancellationToken);
foreach (var nextPlayoutItem in maybeNextPlayoutItem) foreach (var nextPlayoutItem in maybeNextPlayoutItem)

35
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs

@ -1,12 +1,15 @@
using System.IO.Abstractions;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CliWrap; using CliWrap;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Streaming; using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Interfaces.Troubleshooting; using ErsatzTV.Core.Interfaces.Troubleshooting;
using ErsatzTV.Core.Notifications; using ErsatzTV.Core.Notifications;
@ -25,6 +28,8 @@ public class StartTroubleshootingPlaybackHandler(
IGraphicsEngine graphicsEngine, IGraphicsEngine graphicsEngine,
InMemoryLogService logService, InMemoryLogService logService,
LoggingLevelSwitches loggingLevelSwitches, LoggingLevelSwitches loggingLevelSwitches,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILogger<StartTroubleshootingPlaybackHandler> logger) ILogger<StartTroubleshootingPlaybackHandler> logger)
: IRequestHandler<StartTroubleshootingPlayback> : IRequestHandler<StartTroubleshootingPlayback>
{ {
@ -147,11 +152,11 @@ public class StartTroubleshootingPlaybackHandler(
var outputPipe = request.StreamingEngine is StreamingEngine.Legacy var outputPipe = request.StreamingEngine is StreamingEngine.Legacy
? PipeTarget.ToDelegate(progressParser.ParseLine) ? PipeTarget.ToDelegate(progressParser.ParseLine)
: PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l)); : PipeTarget.ToDelegate(l => NextLogger.LogNextLine(l, logger));
var errorPipe = request.StreamingEngine is StreamingEngine.Legacy var errorPipe = request.StreamingEngine is StreamingEngine.Legacy
? PipeTarget.Null ? PipeTarget.Null
: PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l)); : PipeTarget.ToDelegate(l => NextLogger.LogNextLine(l, logger));
CommandResult commandResult = await processWithPipe CommandResult commandResult = await processWithPipe
.WithWorkingDirectory(FileSystemLayout.TranscodeTroubleshootingFolder) .WithWorkingDirectory(FileSystemLayout.TranscodeTroubleshootingFolder)
@ -160,7 +165,31 @@ public class StartTroubleshootingPlaybackHandler(
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteAsync(linkedCts.Token); .ExecuteAsync(linkedCts.Token);
logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", commandResult.ExitCode); string processName = request.StreamingEngine is StreamingEngine.Legacy
? "ffmpeg"
: "ersatztv-channel";
logger.LogDebug(
"Troubleshooting playback ({ProcessName}) completed with exit code {ExitCode}",
processName,
commandResult.ExitCode);
if (request.StreamingEngine is StreamingEngine.Next)
{
foreach (string dir in localFileSystem.ListSubdirectories(
FileSystemLayout.TranscodeTroubleshootingFolder))
{
foreach (string file in localFileSystem.ListFiles(dir, "ffreport.log"))
{
foreach (string line in await fileSystem.File.ReadAllLinesAsync(file, cancellationToken))
{
progressParser.ParseLine(line);
}
break;
}
}
}
progressParser.LogSpeed( progressParser.LogSpeed(
request.MediaItemInfo.Map(i => i.Id), request.MediaItemInfo.Map(i => i.Id),

16
ErsatzTV.Core/InMemoryLogService.cs

@ -2,6 +2,7 @@ using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using Serilog.Formatting.Display;
namespace ErsatzTV.Core; namespace ErsatzTV.Core;
@ -9,6 +10,10 @@ public class InMemorySink : ILogEventSink
{ {
private readonly ConcurrentDictionary<Guid, ConcurrentQueue<string>> _logs = new(); private readonly ConcurrentDictionary<Guid, ConcurrentQueue<string>> _logs = new();
private static readonly MessageTemplateTextFormatter Formatter = new(
"[{Timestamp:HH:mm:ss} {Level}] {Message:lj}{NewLine}{Exception}",
CultureInfo.InvariantCulture);
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (logEvent.Properties.TryGetValue(InMemoryLogService.CorrelationIdKey, out var correlationIdValue) && if (logEvent.Properties.TryGetValue(InMemoryLogService.CorrelationIdKey, out var correlationIdValue) &&
@ -18,15 +23,8 @@ public class InMemorySink : ILogEventSink
using (var writer = new StringWriter()) using (var writer = new StringWriter())
{ {
writer.Write($"[{logEvent.Timestamp:HH:mm:ss} {logEvent.Level}] "); Formatter.Format(logEvent, writer);
logEvent.RenderMessage(writer, CultureInfo.CurrentCulture); logQueue.Enqueue(writer.ToString().TrimEnd());
if (logEvent.Exception != null)
{
writer.WriteLine();
writer.Write(logEvent.Exception);
}
logQueue.Enqueue(writer.ToString());
} }
while (logQueue.Count > 100) while (logQueue.Count > 100)

2
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutItemConverter.cs

@ -14,5 +14,7 @@ public interface IPlayoutItemConverter
Option<ChannelWatermark> maybeGlobalWatermark, Option<ChannelWatermark> maybeGlobalWatermark,
TimeSpan playoutOffset, TimeSpan playoutOffset,
PlayoutItem playoutItem, PlayoutItem playoutItem,
Option<List<Subtitle>> subtitles,
bool shouldLogMessages,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

23
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -67,6 +67,8 @@ public class PlayoutItemConverter(
maybeGlobalWatermark, maybeGlobalWatermark,
playoutOffset, playoutOffset,
playoutItem, playoutItem,
Option<List<Subtitle>>.None,
false,
cancellationToken); cancellationToken);
} }
@ -75,6 +77,8 @@ public class PlayoutItemConverter(
Option<ChannelWatermark> maybeGlobalWatermark, Option<ChannelWatermark> maybeGlobalWatermark,
TimeSpan playoutOffset, TimeSpan playoutOffset,
PlayoutItem playoutItem, PlayoutItem playoutItem,
Option<List<Subtitle>> subtitles,
bool shouldLogMessages,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (playoutItem is not DynamicPlayoutItem && if (playoutItem is not DynamicPlayoutItem &&
@ -200,6 +204,8 @@ public class PlayoutItemConverter(
playoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle, playoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle,
playoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode, playoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode,
playoutItem.SubtitleMode ?? channel.SubtitleMode, playoutItem.SubtitleMode ?? channel.SubtitleMode,
subtitles,
shouldLogMessages,
cancellationToken); cancellationToken);
await SelectWatermark( await SelectWatermark(
maybeGlobalWatermark, maybeGlobalWatermark,
@ -360,9 +366,15 @@ public class PlayoutItemConverter(
string preferredAudioTitle, string preferredAudioTitle,
string preferredSubtitleLanguage, string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode, ChannelSubtitleMode subtitleMode,
Option<List<Subtitle>> subtitles,
bool shouldLogMessages,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
List<Subtitle> allSubtitles = await GetSubtitles(channel, audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint); List<Subtitle> allSubtitles = await subtitles.IfNoneAsync(
await GetSubtitles(channel, audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint));
// TODO: external image subtitles
allSubtitles.RemoveAll(s => s.IsImage && s.SubtitleKind is not SubtitleKind.Embedded);
Option<MediaStream> maybeAudioStream = Option<MediaStream>.None; Option<MediaStream> maybeAudioStream = Option<MediaStream>.None;
Option<Subtitle> maybeSubtitle = Option<Subtitle>.None; Option<Subtitle> maybeSubtitle = Option<Subtitle>.None;
@ -374,7 +386,7 @@ public class PlayoutItemConverter(
nextPlayoutItem.Start, nextPlayoutItem.Start,
audioVersion, audioVersion,
allSubtitles, allSubtitles,
shouldLogMessages: false); shouldLogMessages);
maybeAudioStream = result.AudioStream; maybeAudioStream = result.AudioStream;
maybeSubtitle = result.Subtitle; maybeSubtitle = result.Subtitle;
} }
@ -388,7 +400,7 @@ public class PlayoutItemConverter(
channel, channel,
preferredAudioLanguage, preferredAudioLanguage,
preferredAudioTitle, preferredAudioTitle,
shouldLogMessages: false, shouldLogMessages,
cancellationToken); cancellationToken);
maybeSubtitle = maybeSubtitle =
@ -397,7 +409,7 @@ public class PlayoutItemConverter(
channel, channel,
preferredSubtitleLanguage, preferredSubtitleLanguage,
subtitleMode, subtitleMode,
shouldLogMessages: false, shouldLogMessages,
cancellationToken); cancellationToken);
} }
@ -585,9 +597,6 @@ public class PlayoutItemConverter(
allSubtitles.RemoveAll(s => s.Codec == "eia_608"); allSubtitles.RemoveAll(s => s.Codec == "eia_608");
} }
// TODO: external image subtitles
allSubtitles.RemoveAll(s => s.IsImage && s.SubtitleKind is not SubtitleKind.Embedded);
return allSubtitles; return allSubtitles;
} }

25
ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

@ -81,20 +81,17 @@
} }
</MudSelect> </MudSelect>
</MudStack> </MudStack>
@if (_streamingEngine is StreamingEngine.Legacy) <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
{ <div class="d-flex">
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudText>Stream Selector</MudText>
<div class="d-flex"> </div>
<MudText>Stream Selector</MudText> <MudSelect @bind-Value="_streamSelector" For="@(() => _streamSelector)" Clearable="true" Disabled="@(_streamSelectors.Count == 0)">
</div> @foreach (string selector in _streamSelectors)
<MudSelect @bind-Value="_streamSelector" For="@(() => _streamSelector)" Clearable="true" Disabled="@(_streamSelectors.Count == 0)"> {
@foreach (string selector in _streamSelectors) <MudSelectItem T="string" Value="@selector">@selector</MudSelectItem>
{ }
<MudSelectItem T="string" Value="@selector">@selector</MudSelectItem> </MudSelect>
} </MudStack>
</MudSelect>
</MudStack>
}
@if (_channelMode) @if (_channelMode)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

Loading…
Cancel
Save