Files
ersatztv/ErsatzTV.FFmpeg/Filter/OverlayWatermarkFilter.cs
T
timothyandClaude Sonnet 5 97bc346539 fix(503): map WatermarkLocation.MiddleCenter to a centered overlay position
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>
2026-07-25 15:19:23 +02:00

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);
}