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