Browse Source

optimize generated video (#498)

* use different framerate flags

* pre-generate song image and always use software encoders

* fix tests
pull/499/head
Jason Dove 4 years ago committed by GitHub
parent
commit
a90eb2d4de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  2. 56
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  3. 18
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  4. 3
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  5. 6
      ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs
  6. 38
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  7. 47
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  8. 97
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  9. 51
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  10. 6
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  11. 4
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  12. 4
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  13. 4
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  14. 4
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  15. 4
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  16. 2
      ErsatzTV/Startup.cs

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

@ -4,7 +4,7 @@ using System.Runtime.InteropServices; @@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -15,12 +15,12 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -15,12 +15,12 @@ namespace ErsatzTV.Application.Streaming.Queries
{
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
{
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IRuntimeInfo _runtimeInfo;
public GetConcatProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory)
{

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

@ -11,8 +11,8 @@ using ErsatzTV.Core.Domain; @@ -11,8 +11,8 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
@ -35,7 +35,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -35,7 +35,7 @@ namespace ErsatzTV.Application.Streaming.Queries
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
private readonly IArtistRepository _artistRepository;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
private readonly IPlexPathReplacementService _plexPathReplacementService;
@ -44,7 +44,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -44,7 +44,7 @@ namespace ErsatzTV.Application.Streaming.Queries
public GetPlayoutItemProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
@ -122,8 +122,6 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -122,8 +122,6 @@ namespace ErsatzTV.Application.Streaming.Queries
return await maybePlayoutItem.Match(
async playoutItemWithPath =>
{
Option<string> drawtextFile = None;
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion();
string videoPath = playoutItemWithPath.Path;
@ -131,9 +129,17 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -131,9 +129,17 @@ namespace ErsatzTV.Application.Streaming.Queries
string audioPath = playoutItemWithPath.Path;
MediaVersion audioVersion = version;
Option<ChannelWatermark> maybeGlobalWatermark = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId)
.BindT(
watermarkId => dbContext.ChannelWatermarks
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
if (playoutItemWithPath.PlayoutItem.MediaItem is Song song)
{
Option<string> drawtextFile = None;
videoVersion = new FallbackMediaVersion
{
Id = -1,
@ -206,18 +212,41 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -206,18 +212,41 @@ namespace ErsatzTV.Application.Streaming.Queries
{
new() { Path = videoPath }
};
Either<BaseError, string> maybeSongImage = await _ffmpegProcessService.GenerateSongImage(
ffmpegPath,
drawtextFile,
channel,
maybeGlobalWatermark,
videoVersion,
videoPath);
foreach (string si in maybeSongImage.RightToSeq())
{
videoPath = si;
videoVersion = new BackgroundImageMediaVersion
{
Chapters = new List<MediaChapter>(),
// song image has been pre-generated with correct size
Height = channel.FFmpegProfile.Resolution.Height,
Width = channel.FFmpegProfile.Resolution.Width,
SampleAspectRatio = "1:1",
Streams = new List<MediaStream>
{
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 },
},
MediaFiles = new List<MediaFile>
{
new() { Path = si }
}
};
}
}
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Option<ChannelWatermark> maybeGlobalWatermark = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId)
.BindT(
watermarkId => dbContext.ChannelWatermarks
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
Process process = await _ffmpegProcessService.ForPlayoutItem(
ffmpegPath,
saveReports,
@ -235,8 +264,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -235,8 +264,7 @@ namespace ErsatzTV.Application.Streaming.Queries
request.HlsRealtime,
playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint,
playoutItemWithPath.PlayoutItem.OutPoint,
drawtextFile);
playoutItemWithPath.PlayoutItem.OutPoint);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);

18
ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs

@ -19,7 +19,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -19,7 +19,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
{
var builder = new FFmpegComplexFilterBuilder();
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsNone.Should().BeTrue();
}
@ -31,7 +31,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -31,7 +31,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -72,7 +72,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -72,7 +72,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
.WithAlignedAudio(duration)
.WithDeinterlace(true);
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -123,7 +123,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -123,7 +123,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -279,7 +279,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -279,7 +279,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
.WithDeinterlace(deinterlace)
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -350,7 +350,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -350,7 +350,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -421,7 +421,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -421,7 +421,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
@ -543,7 +543,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -543,7 +543,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build("", 0, 0, 0, 1, false);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(

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

@ -196,8 +196,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -196,8 +196,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false,
FillerKind.None,
TimeSpan.Zero,
TimeSpan.FromSeconds(5),
None);
TimeSpan.FromSeconds(5));
process.StartInfo.RedirectStandardError = true;

6
ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Domain
{
public class BackgroundImageMediaVersion : MediaVersion
{
}
}

38
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -133,7 +133,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -133,7 +133,7 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public Option<FFmpegComplexFilter> Build(string videoPath, int videoInput, int videoStreamIndex, int audioInput, Option<int> audioStreamIndex, bool isSong)
public Option<FFmpegComplexFilter> Build(bool videoOnly, int videoInput, int videoStreamIndex, int audioInput, Option<int> audioStreamIndex, bool isSong)
{
var complexFilter = new StringBuilder();
@ -143,9 +143,9 @@ namespace ErsatzTV.Core.FFmpeg @@ -143,9 +143,9 @@ namespace ErsatzTV.Core.FFmpeg
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
bool isHardwareDecode = acceleration switch
{
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4",
HardwareAccelerationKind.Nvenc => !isSong,
HardwareAccelerationKind.Qsv => !isSong,
HardwareAccelerationKind.Vaapi => _inputCodec != "mpeg4",
HardwareAccelerationKind.Nvenc => true,
HardwareAccelerationKind.Qsv => true,
_ => false
};
@ -169,19 +169,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -169,19 +169,6 @@ namespace ErsatzTV.Core.FFmpeg
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode &&
(_deinterlace || _scaleToSize.IsSome);
if (isSong)
{
switch (acceleration)
{
case HardwareAccelerationKind.Qsv:
videoFilterQueue.Add("format=nv12");
break;
default:
videoFilterQueue.Add("format=yuv420p");
break;
}
}
switch (usesHardwareFilters, false, acceleration)
{
case (true, false, HardwareAccelerationKind.Nvenc):
@ -233,7 +220,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -233,7 +220,6 @@ namespace ErsatzTV.Core.FFmpeg
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" =>
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc when isSong => $"scale_cuda={size.Width}:{size.Height}:format=yuv420p",
HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}",
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
@ -258,8 +244,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -258,8 +244,6 @@ namespace ErsatzTV.Core.FFmpeg
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
"format=p010le,format=nv12",
HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p",
_ when isSong => "format=yuv420p",
_ => "format=nv12"
};
videoFilterQueue.Add(format);
@ -270,9 +254,14 @@ namespace ErsatzTV.Core.FFmpeg @@ -270,9 +254,14 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add("setsar=1");
}
if (videoOnly)
{
videoFilterQueue.Add("boxblur=40");
}
if (isSong)
{
videoFilterQueue.Add("boxblur=75,fps=24");
videoFilterQueue.Add("fps=30");
}
foreach (ChannelWatermark watermark in _watermark)
@ -408,12 +397,9 @@ namespace ErsatzTV.Core.FFmpeg @@ -408,12 +397,9 @@ namespace ErsatzTV.Core.FFmpeg
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None)
{
switch (isSong, acceleration)
switch (acceleration)
{
case (true, HardwareAccelerationKind.Nvenc):
complexFilter.Append(",hwupload_cuda");
break;
case (_, HardwareAccelerationKind.Qsv):
case HardwareAccelerationKind.Qsv:
complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64");
break;
default:

47
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -43,6 +43,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -43,6 +43,7 @@ namespace ErsatzTV.Core.FFmpeg
private HardwareAccelerationKind _hwAccel;
private string _outputPixelFormat;
private bool _noAutoScale;
private Option<int> _outputFramerate;
public FFmpegProcessBuilder(string ffmpegPath, bool saveReports, ILogger logger)
{
@ -248,6 +249,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -248,6 +249,7 @@ namespace ErsatzTV.Core.FFmpeg
else
{
_noAutoScale = true;
_outputFramerate = 30;
_arguments.Add("-loop");
_arguments.Add("1");
@ -276,6 +278,24 @@ namespace ErsatzTV.Core.FFmpeg @@ -276,6 +278,24 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithSongInput(
string videoPath,
Option<string> codec,
Option<string> pixelFormat)
{
_noAutoScale = true;
_outputFramerate = 30;
_complexFilterBuilder = _complexFilterBuilder
.WithInputCodec(codec)
.WithInputPixelFormat(pixelFormat);
_arguments.Add("-i");
_arguments.Add(videoPath);
return this;
}
public FFmpegProcessBuilder WithFiltergraph(string graph)
{
@ -486,6 +506,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -486,6 +506,12 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add("-noautoscale");
}
foreach (int framerate in _outputFramerate)
{
_arguments.Add("-r");
_arguments.Add(framerate.ToString());
}
return this;
}
@ -545,7 +571,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -545,7 +571,7 @@ namespace ErsatzTV.Core.FFmpeg
MediaStream videoStream,
Option<MediaStream> maybeAudioStream,
string videoPath,
string audioPath,
Option<string> audioPath,
string videoCodec)
{
_complexFilterBuilder = _complexFilterBuilder.WithVideoEncoder(videoCodec);
@ -555,21 +581,27 @@ namespace ErsatzTV.Core.FFmpeg @@ -555,21 +581,27 @@ namespace ErsatzTV.Core.FFmpeg
var videoIndex = 0;
var audioIndex = 0;
if (audioPath != videoPath)
if (audioPath.IsNone)
{
// no audio index, so use same as video
audioIndex = 0;
}
else if (audioPath.IfNone("NotARealPath") != videoPath)
{
audioIndex = 1;
_outputPixelFormat = "yuv420p";
}
var videoLabel = $"{videoIndex}:{videoStreamIndex}";
var audioLabel = $"{audioIndex}:{maybeIndex.Match(i => i.ToString(), () => "a")}";
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(
videoPath,
audioPath.IsNone,
videoIndex,
videoStreamIndex,
audioIndex,
maybeIndex,
videoPath != audioPath);
audioPath.IsSome && videoPath != audioPath.IfNone("NotARealPath"));
maybeFilter.IfSome(
filter =>
@ -588,8 +620,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -588,8 +620,11 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add("-map");
_arguments.Add(videoLabel);
_arguments.Add("-map");
_arguments.Add(audioLabel);
foreach (string _ in audioPath)
{
_arguments.Add("-map");
_arguments.Add(audioLabel);
}
return this;
}

97
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -12,7 +12,7 @@ using static LanguageExt.Prelude; @@ -12,7 +12,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.FFmpeg
{
public class FFmpegProcessService
public class FFmpegProcessService : IFFmpegProcessService
{
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly IImageCache _imageCache;
@ -48,8 +48,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -48,8 +48,7 @@ namespace ErsatzTV.Core.FFmpeg
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
Option<string> drawtextFile)
TimeSpan outPoint)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
@ -65,6 +64,15 @@ namespace ErsatzTV.Core.FFmpeg @@ -65,6 +64,15 @@ namespace ErsatzTV.Core.FFmpeg
inPoint,
outPoint);
if (videoVersion is BackgroundImageMediaVersion)
{
FFmpegPlaybackSettings errorSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
playbackSettings.HardwareAcceleration = errorSettings.HardwareAcceleration;
playbackSettings.VideoCodec = errorSettings.VideoCodec;
}
Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion);
@ -87,7 +95,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -87,7 +95,6 @@ namespace ErsatzTV.Core.FFmpeg
videoStream.Codec,
videoStream.PixelFormat)
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
.WithDrawtextFile(videoVersion, drawtextFile)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
@ -234,6 +241,83 @@ namespace ErsatzTV.Core.FFmpeg @@ -234,6 +241,83 @@ namespace ErsatzTV.Core.FFmpeg
.Build();
}
public async Task<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath,
Option<string> drawtextFile,
Channel channel,
Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion,
string videoPath)
{
try
{
// todo: generate name by channel? clean up old images?
string outputFile = Path.GetTempFileName();
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion);
FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
FFmpegPlaybackSettings scalePlaybackSettings = _playbackSettingsCalculator.CalculateSettings(
StreamingMode.TransportStream,
channel.FFmpegProfile,
videoVersion,
videoStream,
None,
DateTimeOffset.UnixEpoch,
DateTimeOffset.UnixEpoch,
TimeSpan.Zero,
TimeSpan.Zero);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1)
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat)
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
.WithDrawtextFile(videoVersion, drawtextFile);
foreach (IDisplaySize scaledSize in scalePlaybackSettings.ScaledSize)
{
builder = builder.WithScaling(scaledSize);
if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize))
{
builder = builder.WithBlackBars(channel.FFmpegProfile.Resolution);
}
}
using Process process = builder
.WithFilterComplex(
videoStream,
None,
videoPath,
None,
playbackSettings.VideoCodec)
.WithOutputFormat("apng", outputFile)
.Build();
_logger.LogInformation(
"ffmpeg song arguments {FFmpegArguments}",
string.Join(" ", process.StartInfo.ArgumentList));
process.Start();
await process.WaitForExitAsync();
return outputFile;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error generating song image");
return Left(BaseError.New(ex.Message));
}
}
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) =>
displaySize.Width != target.Width || displaySize.Height != target.Height;
@ -242,6 +326,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -242,6 +326,11 @@ namespace ErsatzTV.Core.FFmpeg
Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion)
{
if (videoVersion is BackgroundImageMediaVersion)
{
return new WatermarkOptions(None, None, None, false);
}
Option<ChannelWatermark> watermarkOverride = videoVersion is FallbackMediaVersion or CoverArtMediaVersion
? new ChannelWatermark
{

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

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.FFmpeg;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.FFmpeg
{
public interface IFFmpegProcessService
{
Task<Process> ForPlayoutItem(
string ffmpegPath,
bool saveReports,
Channel channel,
MediaVersion videoVersion,
MediaVersion audioVersion,
string videoPath,
string audioPath,
DateTimeOffset start,
DateTimeOffset finish,
DateTimeOffset now,
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint);
Process ForError(
string ffmpegPath,
Channel channel,
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime);
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
Task<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath,
Option<string> drawtextFile,
Channel channel,
Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion,
string videoPath);
}
}

6
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -6,7 +6,7 @@ using System.Linq; @@ -6,7 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -49,7 +49,7 @@ namespace ErsatzTV.Core.Metadata @@ -49,7 +49,7 @@ namespace ErsatzTV.Core.Metadata
.ToList();
private readonly IImageCache _imageCache;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
@ -61,7 +61,7 @@ namespace ErsatzTV.Core.Metadata @@ -61,7 +61,7 @@ namespace ErsatzTV.Core.Metadata
ILocalStatisticsProvider localStatisticsProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger logger)
{
_localFileSystem = localFileSystem;

4
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -5,7 +5,7 @@ using System.Linq; @@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata
ISearchRepository searchRepository,
ILibraryRepository libraryRepository,
IMediator mediator,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger<MovieFolderScanner> logger)
: base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, ffmpegProcessService, logger)
{

4
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -5,7 +5,7 @@ using System.Linq; @@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -42,7 +42,7 @@ namespace ErsatzTV.Core.Metadata @@ -42,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
IMusicVideoRepository musicVideoRepository,
ILibraryRepository libraryRepository,
IMediator mediator,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger<MusicVideoFolderScanner> logger) : base(
localFileSystem,
localStatisticsProvider,

4
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -5,7 +5,7 @@ using System.Linq; @@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Metadata @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Metadata
ISearchRepository searchRepository,
IOtherVideoRepository otherVideoRepository,
ILibraryRepository libraryRepository,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger<OtherVideoFolderScanner> logger) : base(
localFileSystem,
localStatisticsProvider,

4
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -6,7 +6,7 @@ using System.Threading.Tasks; @@ -6,7 +6,7 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata
ISearchRepository searchRepository,
ISongRepository songRepository,
ILibraryRepository libraryRepository,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger<SongFolderScanner> logger) : base(
localFileSystem,
localStatisticsProvider,

4
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -5,7 +5,7 @@ using System.Linq; @@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata
ISearchRepository searchRepository,
ILibraryRepository libraryRepository,
IMediator mediator,
FFmpegProcessService ffmpegProcessService,
IFFmpegProcessService ffmpegProcessService,
ILogger<TelevisionFolderScanner> logger) : base(
localFileSystem,
localStatisticsProvider,

2
ErsatzTV/Startup.cs

@ -304,7 +304,7 @@ namespace ErsatzTV @@ -304,7 +304,7 @@ namespace ErsatzTV
services.AddScoped<IRuntimeInfo, RuntimeInfo>();
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
services.AddScoped<IFFmpegStreamSelector, FFmpegStreamSelector>();
services.AddScoped<FFmpegProcessService>();
services.AddScoped<IFFmpegProcessService, FFmpegProcessService>();
services.AddScoped<HlsSessionWorker>();
services.AddScoped<IGitHubApiClient, GitHubApiClient>();
services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(

Loading…
Cancel
Save