Browse Source

use old ffmpeg pipeline for single permanent watermark (#2510)

pull/2511/head
Jason Dove 10 months ago committed by GitHub
parent
commit
5a88bfc310
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 96
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  3. 2
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

3
CHANGELOG.md

@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Fix NVIDIA startup errors on arm64 - Fix NVIDIA startup errors on arm64
### Changed
- Do not use graphics engine for single, permanent watermark
## [25.7.1] - 2025-10-09 ## [25.7.1] - 2025-10-09
### Added ### Added
- Add search field to filter blocks table - Add search field to filter blocks table

96
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -1,5 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text;
using CliWrap; using CliWrap;
using CliWrap.Buffered;
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;
@ -12,6 +14,7 @@ using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Pipeline; using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Preset; using ErsatzTV.FFmpeg.Preset;
using ErsatzTV.FFmpeg.State; using ErsatzTV.FFmpeg.State;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaStream = ErsatzTV.Core.Domain.MediaStream; using MediaStream = ErsatzTV.Core.Domain.MediaStream;
@ -21,6 +24,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IGraphicsElementLoader _graphicsElementLoader; private readonly IGraphicsElementLoader _graphicsElementLoader;
private readonly IMemoryCache _memoryCache;
private readonly ICustomStreamSelector _customStreamSelector; private readonly ICustomStreamSelector _customStreamSelector;
private readonly FFmpegProcessService _ffmpegProcessService; private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegStreamSelector _ffmpegStreamSelector; private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
@ -36,6 +40,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
IPipelineBuilderFactory pipelineBuilderFactory, IPipelineBuilderFactory pipelineBuilderFactory,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
IGraphicsElementLoader graphicsElementLoader, IGraphicsElementLoader graphicsElementLoader,
IMemoryCache memoryCache,
ILogger<FFmpegLibraryProcessService> logger) ILogger<FFmpegLibraryProcessService> logger)
{ {
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessService = ffmpegProcessService;
@ -45,6 +50,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_pipelineBuilderFactory = pipelineBuilderFactory; _pipelineBuilderFactory = pipelineBuilderFactory;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_graphicsElementLoader = graphicsElementLoader; _graphicsElementLoader = graphicsElementLoader;
_memoryCache = memoryCache;
_logger = logger; _logger = logger;
} }
@ -334,8 +340,44 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<GraphicsEngineContext> graphicsEngineContext = Option<GraphicsEngineContext>.None; Option<GraphicsEngineContext> graphicsEngineContext = Option<GraphicsEngineContext>.None;
List<GraphicsElementContext> graphicsElementContexts = []; List<GraphicsElementContext> graphicsElementContexts = [];
// use graphics engine for all watermarks // use ffmpeg for single permanent watermark, graphics engine for all others
graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm))); if (graphicsElements.Count == 0 && watermarks.Count == 1 && watermarks.All(wm => wm.Watermark.Mode is ChannelWatermarkMode.Permanent))
{
foreach (var wm in watermarks)
{
List<VideoStream> videoStreams =
[
new(
await wm.ImageStreamIndex.IfNoneAsync(0),
"unknown",
string.Empty,
new PixelFormatUnknown(),
ColorParams.Default,
new FrameSize(1, 1),
string.Empty,
string.Empty,
Option<string>.None,
!await IsWatermarkAnimated(ffprobePath, wm.ImagePath),
ScanKind.Progressive)
];
var state = new WatermarkState(
None,
wm.Watermark.Location,
wm.Watermark.Size,
wm.Watermark.WidthPercent,
wm.Watermark.HorizontalMarginPercent,
wm.Watermark.VerticalMarginPercent,
wm.Watermark.Opacity,
wm.Watermark.PlaceWithinSourceContent);
watermarkInputFile = new WatermarkInputFile(wm.ImagePath, videoStreams, state);
}
}
else
{
graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm)));
}
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind); HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind);
@ -1155,4 +1197,54 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
HardwareAccelerationKind.Rkmpp => HardwareAccelerationMode.Rkmpp, HardwareAccelerationKind.Rkmpp => HardwareAccelerationMode.Rkmpp,
_ => HardwareAccelerationMode.None _ => HardwareAccelerationMode.None
}; };
private async Task<bool> IsWatermarkAnimated(string ffprobePath, string path)
{
try
{
var cacheKey = $"image.animated.{Path.GetFileName(path)}";
if (_memoryCache.TryGetValue(cacheKey, out bool animated))
{
return animated;
}
BufferedCommandResult result = await Cli.Wrap(ffprobePath)
.WithArguments(
[
"-loglevel", "error",
"-select_streams", "v:0",
"-count_frames",
"-show_entries", "stream=nb_read_frames",
"-print_format", "csv",
path
])
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
if (result.ExitCode == 0)
{
string output = result.StandardOutput;
output = output.Replace("stream,", string.Empty);
if (int.TryParse(output, out int frameCount))
{
bool isAnimated = frameCount > 1;
_memoryCache.Set(cacheKey, isAnimated, TimeSpan.FromDays(1));
return isAnimated;
}
}
else
{
_logger.LogWarning(
"Error checking frame count for file {File} exit code {ExitCode}",
path,
result.ExitCode);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error checking frame count for file {File}", path);
}
return false;
}
} }

2
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -281,6 +281,7 @@ public class TranscodingTests
LoggerFactory.CreateLogger<PipelineBuilderFactory>()), LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
Substitute.For<IConfigElementRepository>(), Substitute.For<IConfigElementRepository>(),
graphicsElementLoader, graphicsElementLoader,
MemoryCache,
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>()); LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service); var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service);
@ -977,6 +978,7 @@ public class TranscodingTests
LoggerFactory.CreateLogger<PipelineBuilderFactory>()), LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
Substitute.For<IConfigElementRepository>(), Substitute.For<IConfigElementRepository>(),
graphicsElementLoader, graphicsElementLoader,
MemoryCache,
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>()); LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
return service; return service;

Loading…
Cancel
Save