OverlayWatermarkFilter.Position had no switch arm for MiddleCenter, so it silently fell into the BottomRight default and rendered bottom-right. OverlayWatermarkCudaFilter and OverlayWatermarkQsvFilter both inherit this Position property without overriding it, so they were affected too (the whole ErsatzTV.FFmpeg project has only this one WatermarkLocation switch). - Add an explicit MiddleCenter arm: x=(W-w)/2:y=(H-h)/2 - Make BottomRight an explicit arm instead of the fallthrough default - The default case now logs a warning (not throw - this runs on the playback hot path constructing ffmpeg args, and sibling filters in this project already use safe string fallbacks rather than throwing for an unmapped enum) and falls back to the BottomRight position - Strip the pre-existing UTF-8 BOM from OverlayWatermarkFilter.cs (#311 formatting gate: touching a legacy BOM'd file makes removing it ours) - Add ErsatzTV.FFmpeg.Tests/Filter/OverlayWatermarkFilterTests.cs asserting the exact position expression for every WatermarkLocation value across the software, CUDA, and QSV overlay filters, plus the unmapped-value fallback Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
4.3 KiB
C#
102 lines
4.3 KiB
C#
using ErsatzTV.FFmpeg.Format;
|
|
using ErsatzTV.FFmpeg.State;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ErsatzTV.FFmpeg.Filter;
|
|
|
|
public class OverlayWatermarkFilter : BaseFilter
|
|
{
|
|
private readonly ILogger _logger;
|
|
private readonly IPixelFormat _outputPixelFormat;
|
|
private readonly FrameSize _resolution;
|
|
private readonly FrameSize _squarePixelFrameSize;
|
|
private readonly WatermarkState _watermarkState;
|
|
|
|
public OverlayWatermarkFilter(
|
|
WatermarkState watermarkState,
|
|
FrameSize resolution,
|
|
FrameSize squarePixelFrameSize,
|
|
IPixelFormat outputPixelFormat,
|
|
ILogger logger)
|
|
{
|
|
_watermarkState = watermarkState;
|
|
_resolution = resolution;
|
|
_squarePixelFrameSize = squarePixelFrameSize;
|
|
_outputPixelFormat = outputPixelFormat;
|
|
_logger = logger;
|
|
}
|
|
|
|
public override string Filter => $"overlay={Position}:format={(_outputPixelFormat.BitDepth == 10 ? '1' : '0')}";
|
|
|
|
protected string Position
|
|
{
|
|
get
|
|
{
|
|
(double horizontalMargin, double verticalMargin) = _watermarkState.PlaceWithinSourceContent
|
|
? SourceContentMargins()
|
|
: NormalMargins();
|
|
|
|
switch (_watermarkState.Location)
|
|
{
|
|
// TODO: can these be pre-calculated (and used with accelerated overlay filters)
|
|
case WatermarkLocation.BottomLeft:
|
|
return $"x={horizontalMargin}:y=H-h-{verticalMargin}";
|
|
case WatermarkLocation.TopLeft:
|
|
return $"x={horizontalMargin}:y={verticalMargin}";
|
|
case WatermarkLocation.TopRight:
|
|
return $"x=W-w-{horizontalMargin}:y={verticalMargin}";
|
|
case WatermarkLocation.TopMiddle:
|
|
return $"x=(W-w)/2:y={verticalMargin}";
|
|
case WatermarkLocation.RightMiddle:
|
|
return $"x=W-w-{horizontalMargin}:y=(H-h)/2";
|
|
case WatermarkLocation.BottomMiddle:
|
|
return $"x=(W-w)/2:y=H-h-{verticalMargin}";
|
|
case WatermarkLocation.LeftMiddle:
|
|
return $"x={horizontalMargin}:y=(H-h)/2";
|
|
case WatermarkLocation.MiddleCenter:
|
|
return $"x=(W-w)/2:y=(H-h)/2";
|
|
case WatermarkLocation.BottomRight:
|
|
return $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}";
|
|
default:
|
|
// unmapped enum value; log loudly and fall back to the BottomRight position
|
|
// rather than throwing, since this runs on the playback hot path
|
|
_logger.LogWarning(
|
|
"Unrecognized watermark location {Location}; falling back to bottom-right overlay position",
|
|
_watermarkState.Location);
|
|
return $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}";
|
|
}
|
|
}
|
|
}
|
|
|
|
public override FrameState NextState(FrameState currentState) =>
|
|
currentState with { FrameDataLocation = FrameDataLocation.Software };
|
|
|
|
private WatermarkMargins NormalMargins()
|
|
{
|
|
double horizontalMargin = Math.Round(_watermarkState.HorizontalMarginPercent / 100.0 * _resolution.Width);
|
|
double verticalMargin = Math.Round(_watermarkState.VerticalMarginPercent / 100.0 * _resolution.Height);
|
|
|
|
return new WatermarkMargins(horizontalMargin, verticalMargin);
|
|
}
|
|
|
|
private WatermarkMargins SourceContentMargins()
|
|
{
|
|
int horizontalPadding = _resolution.Width - _squarePixelFrameSize.Width;
|
|
int verticalPadding = _resolution.Height - _squarePixelFrameSize.Height;
|
|
|
|
_logger.LogDebug("Resolution: {Width}x{Height}", _resolution.Width, _resolution.Height);
|
|
_logger.LogDebug("Square Pix: {Width}x{Height}", _squarePixelFrameSize.Width, _squarePixelFrameSize.Height);
|
|
|
|
double horizontalMargin = Math.Round(
|
|
_watermarkState.HorizontalMarginPercent / 100.0 * _squarePixelFrameSize.Width
|
|
+ horizontalPadding / 2.0);
|
|
double verticalMargin = Math.Round(
|
|
_watermarkState.VerticalMarginPercent / 100.0 * _squarePixelFrameSize.Height
|
|
+ verticalPadding / 2.0);
|
|
|
|
return new WatermarkMargins(horizontalMargin, verticalMargin);
|
|
}
|
|
|
|
private sealed record WatermarkMargins(double HorizontalMargin, double VerticalMargin);
|
|
}
|