Browse Source

enable graphics engine in playback troubleshooting (#2274)

* enable graphics engine in playback troubleshooting

* fix text subtitles with graphics engine (watermarks)
pull/2275/head
Jason Dove 10 months ago committed by GitHub
parent
commit
f2b6f5b919
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  2. 4
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs
  3. 12
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  4. 5
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs
  5. 61
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  6. 10
      ErsatzTV.Core/Interfaces/Troubleshooting/ITroubleshootingNotifier.cs
  7. 24
      ErsatzTV.Core/Troubleshooting/TroubleshootingNotifier.cs
  8. 4
      ErsatzTV.FFmpeg/Filter/ComplexFilter.cs
  9. 4
      ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs
  10. 89
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  11. 3
      ErsatzTV/Startup.cs

10
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -463,6 +463,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -463,6 +463,8 @@ public class HlsSessionWorker : IHlsSessionWorker
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var processWithPipe = process;
foreach (var graphicsEngineContext in processModel.GraphicsEngineContext)
{
@ -474,13 +476,13 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -474,13 +476,13 @@ public class HlsSessionWorker : IHlsSessionWorker
_ = _graphicsEngine.Run(
graphicsEngineContext,
pipe.Writer,
cancellationToken);
linkedCts.Token);
}
CommandResult commandResult = await processWithPipe
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(cancellationToken);
.ExecuteAsync(linkedCts.Token);
if (commandResult.ExitCode == 0)
{
@ -493,6 +495,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -493,6 +495,8 @@ public class HlsSessionWorker : IHlsSessionWorker
}
else
{
await linkedCts.CancelAsync();
// detect the non-zero exit code and transcode the ffmpeg error message instead
string errorMessage = stdErrBuffer.ToString();
if (string.IsNullOrWhiteSpace(errorMessage))
@ -515,6 +519,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -515,6 +519,7 @@ public class HlsSessionWorker : IHlsSessionWorker
processModel.MaybeDuration,
processModel.Until,
errorMessage),
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
cancellationToken);
foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable())
@ -527,6 +532,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -527,6 +532,7 @@ public class HlsSessionWorker : IHlsSessionWorker
commandResult = await errorProcess
.WithValidation(CommandResultValidation.None)
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
if (commandResult.ExitCode == 0)

4
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Application.Troubleshooting;
@ -9,4 +9,4 @@ public record PrepareTroubleshootingPlayback( @@ -9,4 +9,4 @@ public record PrepareTroubleshootingPlayback(
int WatermarkId,
int? SubtitleId,
bool StartFromBeginning)
: IRequest<Either<BaseError, Command>>;
: IRequest<Either<BaseError, PlayoutItemResult>>;

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

@ -29,9 +29,9 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -29,9 +29,9 @@ public class PrepareTroubleshootingPlaybackHandler(
ILocalFileSystem localFileSystem,
IEntityLocker entityLocker,
ILogger<PrepareTroubleshootingPlaybackHandler> logger)
: IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, Command>>
: IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, PlayoutItemResult>>
{
public async Task<Either<BaseError, Command>> Handle(PrepareTroubleshootingPlayback request, CancellationToken cancellationToken)
public async Task<Either<BaseError, PlayoutItemResult>> Handle(PrepareTroubleshootingPlayback request, CancellationToken cancellationToken)
{
try
{
@ -39,7 +39,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -39,7 +39,7 @@ public class PrepareTroubleshootingPlaybackHandler(
Validation<BaseError, Tuple<MediaItem, string, string, FFmpegProfile>> validation = await Validate(dbContext, request);
return await validation.Match(
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4),
error => Task.FromResult<Either<BaseError, Command>>(error.Join()));
error => Task.FromResult<Either<BaseError, PlayoutItemResult>>(error.Join()));
}
catch (Exception ex)
{
@ -49,7 +49,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -49,7 +49,7 @@ public class PrepareTroubleshootingPlaybackHandler(
}
}
private async Task<Either<BaseError, Command>> GetProcess(
private async Task<Either<BaseError, PlayoutItemResult>> GetProcess(
TvContext dbContext,
PrepareTroubleshootingPlayback request,
MediaItem mediaItem,
@ -150,9 +150,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -150,9 +150,7 @@ public class PrepareTroubleshootingPlaybackHandler(
FileSystemLayout.TranscodeTroubleshootingFolder,
_ => { });
// TODO: graphics engine?
return playoutItemResult.Process;
return playoutItemResult;
}
private static async Task<List<Subtitle>> GetSelectedSubtitle(MediaItem mediaItem, PrepareTroubleshootingPlayback request)

5
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs

@ -1,9 +1,10 @@ @@ -1,9 +1,10 @@
using CliWrap;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Application.Troubleshooting;
public record StartTroubleshootingPlayback(
Command Command,
Guid SessionId,
PlayoutItemResult PlayoutItemResult,
MediaItemInfo MediaItemInfo,
TroubleshootingInfo TroubleshootingInfo) : IRequest, IFFmpegWorkerRequest;

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

@ -1,11 +1,13 @@ @@ -1,11 +1,13 @@
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using CliWrap;
using CliWrap.Buffered;
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;
@ -13,9 +15,11 @@ using Microsoft.Extensions.Logging; @@ -13,9 +15,11 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Troubleshooting;
public class StartTroubleshootingPlaybackHandler(
ITroubleshootingNotifier notifier,
IMediator mediator,
IEntityLocker entityLocker,
IRuntimeInfo runtimeInfo,
IGraphicsEngine graphicsEngine,
ILogger<StartTroubleshootingPlaybackHandler> logger)
: IRequestHandler<StartTroubleshootingPlayback>
{
@ -83,17 +87,56 @@ public class StartTroubleshootingPlaybackHandler( @@ -83,17 +87,56 @@ public class StartTroubleshootingPlaybackHandler(
cancellationToken);
}
logger.LogDebug("ffmpeg troubleshooting arguments {FFmpegArguments}", request.Command.Arguments);
logger.LogDebug("ffmpeg troubleshooting arguments {FFmpegArguments}", request.PlayoutItemResult.Process.Arguments);
BufferedCommandResult result = await request.Command
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken);
var maybePipe = Option<Pipe>.None;
await mediator.Publish(
new PlaybackTroubleshootingCompletedNotification(result.ExitCode),
cancellationToken);
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var processWithPipe = request.PlayoutItemResult.Process;
foreach (var 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
.WithStandardErrorPipe(PipeTarget.Null)
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(linkedCts.Token);
await mediator.Publish(
new PlaybackTroubleshootingCompletedNotification(commandResult.ExitCode),
linkedCts.Token);
logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", commandResult.ExitCode);
logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", result.ExitCode);
if (commandResult.ExitCode != 0)
{
await linkedCts.CancelAsync();
notifier.NotifyFailed(request.SessionId);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
foreach (var pipe in maybePipe)
{
await pipe.Writer.CompleteAsync();
}
}
}
finally
{

10
ErsatzTV.Core/Interfaces/Troubleshooting/ITroubleshootingNotifier.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Interfaces.Troubleshooting;
public interface ITroubleshootingNotifier
{
bool IsFailed(Guid sessionId);
void NotifyFailed(Guid sessionId);
void RemoveSession(Guid sessionId);
}

24
ErsatzTV.Core/Troubleshooting/TroubleshootingNotifier.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using System.Collections.Concurrent;
using ErsatzTV.Core.Interfaces.Troubleshooting;
namespace ErsatzTV.Core.Troubleshooting;
public class TroubleshootingNotifier : ITroubleshootingNotifier
{
private readonly ConcurrentDictionary<Guid, bool> _failedSessions = new();
public bool IsFailed(Guid sessionId)
{
return _failedSessions.TryGetValue(sessionId, out _);
}
public void NotifyFailed(Guid sessionId)
{
_failedSessions[sessionId] = true;
}
public void RemoveSession(Guid sessionId)
{
_failedSessions.TryRemove(sessionId, out _);
}
}

4
ErsatzTV.FFmpeg/Filter/ComplexFilter.cs

@ -91,7 +91,7 @@ public class ComplexFilter : IPipelineStep @@ -91,7 +91,7 @@ public class ComplexFilter : IPipelineStep
}
}
foreach ((string path, _) in _maybeSubtitleInputFile)
foreach ((string path, _) in _maybeSubtitleInputFile.Filter(s => s.IsImageBased))
{
if (!distinctPaths.Contains(path))
{
@ -149,7 +149,7 @@ public class ComplexFilter : IPipelineStep @@ -149,7 +149,7 @@ public class ComplexFilter : IPipelineStep
}
foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s =>
s.Method == SubtitleMethod.Burn))
s is { IsImageBased: true, Method: SubtitleMethod.Burn }))
{
int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path);
foreach ((int index, _, _) in subtitleInputFile.Streams)

4
ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs

@ -87,6 +87,10 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine @@ -87,6 +87,10 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine
frameCount++;
}
}
catch (Exception)
{
// do nothing; don't want to throw on a background task
}
finally
{
await pipeWriter.CompleteAsync();

89
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -1,11 +1,12 @@ @@ -1,11 +1,12 @@
using System.Threading.Channels;
using CliWrap;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Troubleshooting;
using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@ -15,6 +16,7 @@ namespace ErsatzTV.Controllers.Api; @@ -15,6 +16,7 @@ namespace ErsatzTV.Controllers.Api;
public class TroubleshootController(
ChannelWriter<IFFmpegWorkerRequest> channelWriter,
ILocalFileSystem localFileSystem,
ITroubleshootingNotifier notifier,
IMediator mediator) : ControllerBase
{
[HttpHead("api/troubleshoot/playback.m3u8")]
@ -32,50 +34,75 @@ public class TroubleshootController( @@ -32,50 +34,75 @@ public class TroubleshootController(
bool startFromBeginning,
CancellationToken cancellationToken)
{
Either<BaseError, Command> result = await mediator.Send(
new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark, subtitleId, startFromBeginning),
cancellationToken);
try
{
Either<BaseError, PlayoutItemResult> result = await mediator.Send(
new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark, subtitleId, startFromBeginning),
cancellationToken);
if (result.IsLeft)
{
return NotFound();
}
return await result.MatchAsync<IActionResult>(
async command =>
foreach (var playoutItemResult in result.RightToSeq())
{
Either<BaseError, MediaItemInfo> maybeMediaInfo = await mediator.Send(new GetMediaItemInfo(mediaItem), cancellationToken);
Either<BaseError, MediaItemInfo> maybeMediaInfo =
await mediator.Send(new GetMediaItemInfo(mediaItem), cancellationToken);
foreach (MediaItemInfo mediaInfo in maybeMediaInfo.RightToSeq())
{
TroubleshootingInfo troubleshootingInfo = await mediator.Send(
new GetTroubleshootingInfo(),
cancellationToken);
var sessionId = Guid.NewGuid();
// filter ffmpeg profiles
troubleshootingInfo.FFmpegProfiles.RemoveAll(p => p.Id != ffmpegProfile);
try
{
TroubleshootingInfo troubleshootingInfo = await mediator.Send(
new GetTroubleshootingInfo(),
cancellationToken);
// filter watermarks
troubleshootingInfo.Watermarks.RemoveAll(p => p.Id != watermark);
// filter ffmpeg profiles
troubleshootingInfo.FFmpegProfiles.RemoveAll(p => p.Id != ffmpegProfile);
await channelWriter.WriteAsync(
new StartTroubleshootingPlayback(command, mediaInfo, troubleshootingInfo),
cancellationToken);
// filter watermarks
troubleshootingInfo.Watermarks.RemoveAll(p => p.Id != watermark);
string playlistFile = Path.Combine(
FileSystemLayout.TranscodeFolder,
".troubleshooting",
"live.m3u8");
await channelWriter.WriteAsync(
new StartTroubleshootingPlayback(sessionId, playoutItemResult, mediaInfo,
troubleshootingInfo),
cancellationToken);
while (!localFileSystem.FileExists(playlistFile))
{
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
if (cancellationToken.IsCancellationRequested)
string playlistFile = Path.Combine(
FileSystemLayout.TranscodeFolder,
".troubleshooting",
"live.m3u8");
while (!localFileSystem.FileExists(playlistFile))
{
break;
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
if (cancellationToken.IsCancellationRequested || notifier.IsFailed(sessionId))
{
break;
}
}
if (!notifier.IsFailed(sessionId))
{
return Redirect("~/iptv/session/.troubleshooting/live.m3u8");
}
}
return Redirect("~/iptv/session/.troubleshooting/live.m3u8");
}
finally
{
notifier.RemoveSession(sessionId);
}
}
}
}
catch (Exception)
{
// do nothing
}
return NotFound();
},
_ => NotFound());
return NotFound();
}
[HttpHead("api/troubleshoot/playback/archive")]

3
ErsatzTV/Startup.cs

@ -31,6 +31,7 @@ using ErsatzTV.Core.Interfaces.Scripting; @@ -31,6 +31,7 @@ using ErsatzTV.Core.Interfaces.Scripting;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
@ -39,6 +40,7 @@ using ErsatzTV.Core.Scheduling.BlockScheduling; @@ -39,6 +40,7 @@ using ErsatzTV.Core.Scheduling.BlockScheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling;
using ErsatzTV.Core.Search;
using ErsatzTV.Core.Trakt;
using ErsatzTV.Core.Troubleshooting;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Runtime;
@ -614,6 +616,7 @@ public class Startup @@ -614,6 +616,7 @@ public class Startup
services.AddSingleton<ISearchTargets, SearchTargets>();
services.AddSingleton<ISmartCollectionCache, SmartCollectionCache>();
services.AddSingleton<SearchQueryParser>();
services.AddSingleton<ITroubleshootingNotifier, TroubleshootingNotifier>();
if (SearchHelper.IsElasticSearchEnabled)
{

Loading…
Cancel
Save