Browse Source

fix cliwrap usage (#737)

pull/738/head
Jason Dove 4 years ago committed by GitHub
parent
commit
7e55681916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs
  3. 22
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  4. 7
      ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs
  5. 4
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  6. 4
      ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs
  7. 10
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  8. 4
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  9. 26
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  10. 54
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  11. 58
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  12. 16
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  13. 17
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  14. 5
      ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs
  15. 25
      ErsatzTV/Controllers/InternalController.cs
  16. 28
      ErsatzTV/Controllers/IptvController.cs

2
CHANGELOG.md

@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Only select picture subtitles (text subtitles are not yet supported) - Only select picture subtitles (text subtitles are not yet supported)
- Supported picture subtitles are `hdmv_pgs_subtitle` and `dvd_subtitle` - Supported picture subtitles are `hdmv_pgs_subtitle` and `dvd_subtitle`
- Fix subtitles using software encoders, videotoolbox, VAAPI - Fix subtitles using software encoders, videotoolbox, VAAPI
- Fix setting VAAPI driver name
- Fix ffmpeg troubleshooting reports
## [0.5.0-beta] - 2022-04-13 ## [0.5.0-beta] - 2022-04-13
### Fixed ### Fixed

10
ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using CliWrap;
using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -70,16 +69,13 @@ public class
string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None); string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None);
Process process = _ffmpegProcessService.ResizeImage( Command process = _ffmpegProcessService.ResizeImage(
ffmpegPath, ffmpegPath,
originalPath, originalPath,
withExtension, withExtension,
request.MaxHeight.Value); request.MaxHeight.Value);
CommandResult resize = await Cli.Wrap(process.StartInfo.FileName) CommandResult resize = await process.ExecuteAsync();
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.ExecuteAsync();
if (resize.ExitCode != 0) if (resize.ExitCode != 0)
{ {

22
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using System.Text;
using System.Timers; using System.Timers;
using Bugsnag; using Bugsnag;
using CliWrap; using CliWrap;
@ -207,18 +207,15 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
await TrimAndDelete(cancellationToken); await TrimAndDelete(cancellationToken);
using Process process = processModel.Process; Command process = processModel.Process;
_logger.LogInformation( _logger.LogInformation("ffmpeg hls arguments {FFmpegArguments}", process.Arguments);
"ffmpeg hls arguments {FFmpegArguments}",
string.Join(" ", process.StartInfo.ArgumentList));
try try
{ {
BufferedCommandResult commandResult = await Cli.Wrap(process.StartInfo.FileName) BufferedCommandResult commandResult = await process
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken); .ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
if (commandResult.ExitCode == 0) if (commandResult.ExitCode == 0)
{ {
@ -256,16 +253,15 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable()) foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable())
{ {
Process errorProcess = errorProcessModel.Process; Command errorProcess = errorProcessModel.Process;
_logger.LogInformation( _logger.LogInformation(
"ffmpeg hls error arguments {FFmpegArguments}", "ffmpeg hls error arguments {FFmpegArguments}",
string.Join(" ", errorProcess.StartInfo.ArgumentList)); errorProcess.Arguments);
commandResult = await Cli.Wrap(errorProcess.StartInfo.FileName) commandResult = await errorProcess
.WithArguments(errorProcess.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken); .ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
if (commandResult.ExitCode == 0) if (commandResult.ExitCode == 0)
{ {

7
ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs

@ -1,5 +1,8 @@
using System.Diagnostics; using CliWrap;
namespace ErsatzTV.Application.Streaming; namespace ErsatzTV.Application.Streaming;
public record PlayoutItemProcessModel(Process Process, Option<TimeSpan> MaybeDuration, DateTimeOffset Until); public record PlayoutItemProcessModel(
Command Process,
Option<TimeSpan> MaybeDuration,
DateTimeOffset Until);

4
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -32,7 +32,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.ConcatChannel( Command process = _ffmpegProcessService.ConcatChannel(
ffmpegPath, ffmpegPath,
saveReports, saveReports,
channel, channel,

4
ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -27,7 +27,7 @@ public class GetErrorProcessHandler : FFmpegProcessHandler<GetErrorProcess>
string ffprobePath, string ffprobePath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Process process = await _ffmpegProcessService.ForError( Command process = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
request.MaybeDuration, request.MaybeDuration,

10
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
@ -139,7 +139,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = await _ffmpegProcessService.ForPlayoutItem( Command process = await _ffmpegProcessService.ForPlayoutItem(
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
saveReports, saveReports,
@ -185,7 +185,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
switch (error) switch (error)
{ {
case UnableToLocatePlayoutItem: case UnableToLocatePlayoutItem:
Process offlineProcess = await _ffmpegProcessService.ForError( Command offlineProcess = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,
@ -195,7 +195,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish); return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish);
case PlayoutItemDoesNotExistOnDisk: case PlayoutItemDoesNotExistOnDisk:
Process doesNotExistProcess = await _ffmpegProcessService.ForError( Command doesNotExistProcess = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,
@ -205,7 +205,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish); return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish);
default: default:
Process errorProcess = await _ffmpegProcessService.ForError( Command errorProcess = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,

4
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -32,7 +32,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.WrapSegmenter( Command process = _ffmpegProcessService.WrapSegmenter(
ffmpegPath, ffmpegPath,
saveReports, saveReports,
channel, channel,

26
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -172,8 +172,8 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark, [ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark,
[ValueSource(typeof(TestData), nameof(TestData.Subtitles))] Subtitle subtitle, [ValueSource(typeof(TestData), nameof(TestData.Subtitles))] Subtitle subtitle,
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat, [ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat,
[ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
@ -377,7 +377,7 @@ public class TranscodingTests
_ => ChannelSubtitleMode.None _ => ChannelSubtitleMode.None
}; };
using Process process = await service.ForPlayoutItem( Command process = await service.ForPlayoutItem(
ExecutableName("ffmpeg"), ExecutableName("ffmpeg"),
ExecutableName("ffprobe"), ExecutableName("ffprobe"),
false, false,
@ -426,17 +426,14 @@ public class TranscodingTests
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try try
{ {
result = await Cli.Wrap(process.StartInfo.FileName) result = await process
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToStream(Stream.Null)) .WithStandardOutputPipe(PipeTarget.ToStream(Stream.Null))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb))
.ExecuteAsync(timeoutSignal.Token); .ExecuteAsync(timeoutSignal.Token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); Assert.Fail($"Transcode failure (timeout): ffmpeg {process.Arguments}");
Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}");
return; return;
} }
@ -445,22 +442,19 @@ public class TranscodingTests
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
{ {
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); result.ExitCode.Should().Be(1, $"Error message with successful exit code? {process.Arguments}");
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); Assert.Warn($"Unsupported on this hardware: ffmpeg {process.Arguments}");
Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}");
} }
else if (error.Contains("Impossible to convert between")) else if (error.Contains("Impossible to convert between"))
{ {
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); Assert.Fail($"Transcode failure: ffmpeg {process.Arguments}");
Assert.Fail($"Transcode failure: ffmpeg {string.Join(" ", quotedArgs)}");
} }
else else
{ {
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); result.ExitCode.Should().Be(0, error + Environment.NewLine + process.Arguments);
result.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs));
if (result.ExitCode == 0) if (result.ExitCode == 0)
{ {
Console.WriteLine(string.Join(" ", quotedArgs)); Console.WriteLine(process.Arguments);
} }
} }
} }

54
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using CliWrap;
using System.Text;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -32,7 +31,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_logger = logger; _logger = logger;
} }
public async Task<Process> ForPlayoutItem( public async Task<Command> ForPlayoutItem(
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool saveReports, bool saveReports,
@ -226,7 +225,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
return GetProcess(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline); return GetCommand(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
} }
private Option<WatermarkInputFile> GetWatermarkInputFile( private Option<WatermarkInputFile> GetWatermarkInputFile(
@ -290,7 +289,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
return None; return None;
} }
public Task<Process> ForError( public Task<Command> ForError(
string ffmpegPath, string ffmpegPath,
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
@ -299,7 +298,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
long ptsOffset) => long ptsOffset) =>
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset); _ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset);
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) public Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{ {
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
@ -319,13 +318,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
concatInputFile, concatInputFile,
FFmpegState.Concat(saveReports, channel.Name)); FFmpegState.Concat(saveReports, channel.Name));
return GetProcess(ffmpegPath, None, None, None, concatInputFile, pipeline); return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline);
} }
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) => public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host); _ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host);
public Process ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height) public Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
{ {
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
inputFile, inputFile,
@ -341,13 +340,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height)); FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height));
return GetProcess(ffmpegPath, videoInputFile, None, None, None, pipeline, false); return GetCommand(ffmpegPath, videoInputFile, None, None, None, pipeline, false);
} }
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) => public Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile); _ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile);
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) => public Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile); _ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile);
public Task<Either<BaseError, string>> GenerateSongImage( public Task<Either<BaseError, string>> GenerateSongImage(
@ -383,7 +382,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
watermarkWidthPercent, watermarkWidthPercent,
cancellationToken); cancellationToken);
private Process GetProcess( private Command GetCommand(
string ffmpegPath, string ffmpegPath,
Option<VideoInputFile> videoInputFile, Option<VideoInputFile> videoInputFile,
Option<AudioInputFile> audioInputFile, Option<AudioInputFile> audioInputFile,
@ -417,35 +416,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
concatInputFile, concatInputFile,
pipeline.PipelineSteps); pipeline.PipelineSteps);
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
if (environmentVariables.Any()) if (environmentVariables.Any())
{ {
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables); _logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
} }
foreach ((string key, string value) in environmentVariables) return Cli.Wrap(ffmpegPath)
{ .WithArguments(arguments)
startInfo.EnvironmentVariables[key] = value; .WithValidation(CommandResultValidation.None)
} .WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null))
.WithEnvironmentVariables(environmentVariables.ToDictionary(e => e.Key, e => e.Value));
foreach (string argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
return new Process
{
StartInfo = startInfo
};
} }
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver) private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)

58
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using Bugsnag; using Bugsnag;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@ -39,7 +40,7 @@ public class FFmpegProcessService
_logger = logger; _logger = logger;
} }
public async Task<Process> ForError( public async Task<Command> ForError(
string ffmpegPath, string ffmpegPath,
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
@ -102,29 +103,34 @@ public class FFmpegProcessService
await duration.IfSomeAsync(d => builder = builder.WithDuration(d)); await duration.IfSomeAsync(d => builder = builder.WithDuration(d));
switch (channel.StreamingMode) Process process = channel.StreamingMode switch
{ {
// HLS needs to segment and generate playlist // HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter: StreamingMode.HttpLiveStreamingSegmenter =>
return builder.WithHls( builder.WithHls(
channel.Number, channel.Number,
None, None,
ptsOffset, ptsOffset,
playbackSettings.VideoTrackTimeScale, playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate) playbackSettings.FrameRate)
.Build(); .Build(),
default: _ => builder.WithFormat("mpegts")
return builder.WithFormat("mpegts") .WithPipe()
.WithPipe() .Build()
.Build(); };
}
return Cli.Wrap(process.StartInfo.FileName)
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
} }
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{ {
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger) Process process = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
@ -136,27 +142,45 @@ public class FFmpegProcessService
.WithFormat("mpegts") .WithFormat("mpegts")
.WithPipe() .WithPipe()
.Build(); .Build();
return Cli.Wrap(process.StartInfo.FileName)
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
} }
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) public Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
{ {
return new FFmpegProcessBuilder(ffmpegPath, false, _logger) Process process = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithInput(inputFile) .WithInput(inputFile)
.WithOutputFormat("apng", outputFile) .WithOutputFormat("apng", outputFile)
.Build(); .Build();
return Cli.Wrap(process.StartInfo.FileName)
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
} }
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) public Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile)
{ {
return new FFmpegProcessBuilder(ffmpegPath, false, _logger) Process process = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithInput(inputFile) .WithInput(inputFile)
.WithMap($"0:{streamIndex}") .WithMap($"0:{streamIndex}")
.WithOutputFormat("apng", outputFile) .WithOutputFormat("apng", outputFile)
.Build(); .Build();
return Cli.Wrap(process.StartInfo.FileName)
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
} }
public async Task<Either<BaseError, string>> GenerateSongImage( public async Task<Either<BaseError, string>> GenerateSongImage(
@ -414,7 +438,7 @@ public class FFmpegProcessService
path path
}) })
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(); .ExecuteBufferedAsync(Encoding.UTF8);
if (result.ExitCode == 0) if (result.ExitCode == 0)
{ {

16
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -1,4 +1,4 @@
using System.Diagnostics; using CliWrap;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IFFmpegProcessService public interface IFFmpegProcessService
{ {
Task<Process> ForPlayoutItem( Task<Command> ForPlayoutItem(
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool saveReports, bool saveReports,
@ -31,7 +31,7 @@ public interface IFFmpegProcessService
long ptsOffset, long ptsOffset,
Option<int> targetFramerate); Option<int> targetFramerate);
Task<Process> ForError( Task<Command> ForError(
string ffmpegPath, string ffmpegPath,
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
@ -39,15 +39,15 @@ public interface IFFmpegProcessService
bool hlsRealtime, bool hlsRealtime,
long ptsOffset); long ptsOffset);
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Process ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height); Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height);
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile); Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile); Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
Task<Either<BaseError, string>> GenerateSongImage( Task<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath, string ffmpegPath,

17
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using Bugsnag;
using Bugsnag;
using CliWrap; using CliWrap;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
@ -163,16 +162,13 @@ public abstract class LocalFolderScanner
{ {
// extract attached pic (and convert to png) // extract attached pic (and convert to png)
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt); string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
using Process process = _ffmpegProcessService.ExtractAttachedPicAsPng( Command process = _ffmpegProcessService.ExtractAttachedPicAsPng(
path, path,
artworkFile, artworkFile,
picIndex, picIndex,
tempName); tempName);
await Cli.Wrap(process.StartInfo.FileName) await process.ExecuteAsync(cancellationToken);
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(cancellationToken);
return tempName; return tempName;
}, },
@ -180,12 +176,9 @@ public abstract class LocalFolderScanner
{ {
// no attached pic index means convert to png // no attached pic index means convert to png
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt); string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
using Process process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName); Command process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
await Cli.Wrap(process.StartInfo.FileName) await process.ExecuteAsync(cancellationToken);
.WithArguments(process.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(cancellationToken);
return tempName; return tempName;
}); });

5
ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs

@ -1,4 +1,5 @@
using CliWrap; using System.Text;
using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using ErsatzTV.Core.Health; using ErsatzTV.Core.Health;
@ -37,7 +38,7 @@ public abstract class BaseHealthCheck
BufferedCommandResult result = await Cli.Wrap(path) BufferedCommandResult result = await Cli.Wrap(path)
.WithArguments(arguments) .WithArguments(arguments)
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken); .ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
return result.StandardOutput; return result.StandardOutput;
} }

25
ErsatzTV/Controllers/InternalController.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using CliWrap;
using ErsatzTV.Application.Streaming; using ErsatzTV.Application.Streaming;
using ErsatzTV.Extensions; using ErsatzTV.Extensions;
using MediatR; using MediatR;
@ -43,11 +44,27 @@ public class InternalController : ControllerBase
result.Match<IActionResult>( result.Match<IActionResult>(
processModel => processModel =>
{ {
Process process = processModel.Process; Command command = processModel.Process;
_logger.LogInformation("ffmpeg arguments {FFmpegArguments}", command.Arguments);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command.TargetFilePath,
Arguments = command.Arguments,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
}
};
foreach ((string key, string value) in command.EnvironmentVariables)
{
process.StartInfo.Environment[key] = value;
}
_logger.LogInformation(
"ffmpeg arguments {FFmpegArguments}",
string.Join(" ", process.StartInfo.ArgumentList));
process.Start(); process.Start();
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
}, },

28
ErsatzTV/Controllers/IptvController.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using CliWrap;
using ErsatzTV.Application.Channels; using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Images; using ErsatzTV.Application.Images;
using ErsatzTV.Application.Streaming; using ErsatzTV.Application.Streaming;
@ -92,15 +93,30 @@ public class IptvController : ControllerBase
result => result.Match<IActionResult>( result => result.Match<IActionResult>(
processModel => processModel =>
{ {
Process process = processModel.Process; Command command = processModel.Process;
_logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber); _logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber);
_logger.LogDebug( _logger.LogInformation("ffmpeg arguments {FFmpegArguments}", command.Arguments);
"ffmpeg ts arguments {FFmpegArguments}", var process = new Process
string.Join(" ", process.StartInfo.ArgumentList)); {
StartInfo = new ProcessStartInfo
{
FileName = command.TargetFilePath,
Arguments = command.Arguments,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
}
};
foreach ((string key, string value) in command.EnvironmentVariables)
{
process.StartInfo.Environment[key] = value;
}
process.Start(); process.Start();
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); },
},
error => BadRequest(error.Value))); error => BadRequest(error.Value)));
} }

Loading…
Cancel
Save