From 5a88bfc3102425c53ee4ec8714bb650aa9c58a4d Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:14:29 -0500 Subject: [PATCH] use old ffmpeg pipeline for single permanent watermark (#2510) --- CHANGELOG.md | 3 + .../FFmpeg/FFmpegLibraryProcessService.cs | 96 ++++++++++++++++++- .../Core/FFmpeg/TranscodingTests.cs | 2 + 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac58132f..a33670435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix NVIDIA startup errors on arm64 +### Changed +- Do not use graphics engine for single, permanent watermark + ## [25.7.1] - 2025-10-09 ### Added - Add search field to filter blocks table diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 594dd82d6..eca917545 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; +using System.Text; using CliWrap; +using CliWrap.Buffered; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.FFmpeg; @@ -12,6 +14,7 @@ using ErsatzTV.FFmpeg.OutputFormat; using ErsatzTV.FFmpeg.Pipeline; using ErsatzTV.FFmpeg.Preset; using ErsatzTV.FFmpeg.State; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using MediaStream = ErsatzTV.Core.Domain.MediaStream; @@ -21,6 +24,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService { private readonly IConfigElementRepository _configElementRepository; private readonly IGraphicsElementLoader _graphicsElementLoader; + private readonly IMemoryCache _memoryCache; private readonly ICustomStreamSelector _customStreamSelector; private readonly FFmpegProcessService _ffmpegProcessService; private readonly IFFmpegStreamSelector _ffmpegStreamSelector; @@ -36,6 +40,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService IPipelineBuilderFactory pipelineBuilderFactory, IConfigElementRepository configElementRepository, IGraphicsElementLoader graphicsElementLoader, + IMemoryCache memoryCache, ILogger logger) { _ffmpegProcessService = ffmpegProcessService; @@ -45,6 +50,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _pipelineBuilderFactory = pipelineBuilderFactory; _configElementRepository = configElementRepository; _graphicsElementLoader = graphicsElementLoader; + _memoryCache = memoryCache; _logger = logger; } @@ -334,8 +340,44 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService Option graphicsEngineContext = Option.None; List graphicsElementContexts = []; - // use graphics engine for all watermarks - graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm))); + // use ffmpeg for single permanent watermark, graphics engine for all others + if (graphicsElements.Count == 0 && watermarks.Count == 1 && watermarks.All(wm => wm.Watermark.Mode is ChannelWatermarkMode.Permanent)) + { + foreach (var wm in watermarks) + { + List videoStreams = + [ + new( + await wm.ImageStreamIndex.IfNoneAsync(0), + "unknown", + string.Empty, + new PixelFormatUnknown(), + ColorParams.Default, + new FrameSize(1, 1), + string.Empty, + string.Empty, + Option.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); @@ -1155,4 +1197,54 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService HardwareAccelerationKind.Rkmpp => HardwareAccelerationMode.Rkmpp, _ => HardwareAccelerationMode.None }; + + private async Task 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; + } } diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 9bfbba551..320908c5d 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -281,6 +281,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), Substitute.For(), graphicsElementLoader, + MemoryCache, LoggerFactory.CreateLogger()); var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service); @@ -977,6 +978,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), Substitute.For(), graphicsElementLoader, + MemoryCache, LoggerFactory.CreateLogger()); return service;