PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m21s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Review follow-up. The 9-element ExpectedPositions table meant a future enum member would pass every case and silently render bottom-right again -- the exact bug #503 fixes. Assert the table covers Enum.GetValues instead, and exercise the previously untested SourceContentMargins() branch.
132 lines
4.9 KiB
C#
132 lines
4.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using ErsatzTV.FFmpeg;
|
|
using ErsatzTV.FFmpeg.Filter;
|
|
using ErsatzTV.FFmpeg.Filter.Cuda;
|
|
using ErsatzTV.FFmpeg.Filter.Qsv;
|
|
using ErsatzTV.FFmpeg.Format;
|
|
using ErsatzTV.FFmpeg.State;
|
|
using LanguageExt;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.FFmpeg.Tests.Filter;
|
|
|
|
[TestFixture]
|
|
public class OverlayWatermarkFilterTests
|
|
{
|
|
private static readonly FrameSize Resolution = new(1920, 1080);
|
|
private static readonly FrameSize SquarePixelFrameSize = new(1920, 1080);
|
|
|
|
// a pillarboxed source: 1440x1080 of real content inside a 1920x1080 frame, so
|
|
// SourceContentMargins() adds half the 480px horizontal padding to the margin
|
|
private static readonly FrameSize PillarboxedSquarePixelFrameSize = new(1440, 1080);
|
|
|
|
private static WatermarkState StateFor(WatermarkLocation location, bool placeWithinSourceContent = false) =>
|
|
new(
|
|
Option<List<WatermarkFadePoint>>.None,
|
|
location,
|
|
WatermarkSize.ActualSize,
|
|
10,
|
|
10,
|
|
10,
|
|
100,
|
|
placeWithinSourceContent);
|
|
|
|
private static readonly (WatermarkLocation Location, string Expected)[] ExpectedPositions =
|
|
[
|
|
(WatermarkLocation.BottomRight, "x=W-w-192:y=H-h-108"),
|
|
(WatermarkLocation.BottomLeft, "x=192:y=H-h-108"),
|
|
(WatermarkLocation.TopRight, "x=W-w-192:y=108"),
|
|
(WatermarkLocation.TopLeft, "x=192:y=108"),
|
|
(WatermarkLocation.TopMiddle, "x=(W-w)/2:y=108"),
|
|
(WatermarkLocation.RightMiddle, "x=W-w-192:y=(H-h)/2"),
|
|
(WatermarkLocation.BottomMiddle, "x=(W-w)/2:y=H-h-108"),
|
|
(WatermarkLocation.LeftMiddle, "x=192:y=(H-h)/2"),
|
|
(WatermarkLocation.MiddleCenter, "x=(W-w)/2:y=(H-h)/2")
|
|
];
|
|
|
|
private static IEnumerable<TestCaseData> AllLocations() =>
|
|
ExpectedPositions.Select(x => new TestCaseData(x.Location, x.Expected).SetName(
|
|
$"{{m}}({x.Location})"));
|
|
|
|
[TestCaseSource(nameof(AllLocations))]
|
|
public void Software_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
|
|
{
|
|
var filter = new OverlayWatermarkFilter(
|
|
StateFor(location),
|
|
Resolution,
|
|
SquarePixelFrameSize,
|
|
new PixelFormatUnknown(),
|
|
NullLogger.Instance);
|
|
|
|
filter.Filter.ShouldBe($"overlay={expectedPosition}:format=0");
|
|
}
|
|
|
|
[TestCaseSource(nameof(AllLocations))]
|
|
public void Cuda_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
|
|
{
|
|
var filter = new OverlayWatermarkCudaFilter(
|
|
StateFor(location),
|
|
Resolution,
|
|
SquarePixelFrameSize,
|
|
NullLogger.Instance);
|
|
|
|
filter.Filter.ShouldBe($"overlay_cuda={expectedPosition}");
|
|
}
|
|
|
|
[TestCaseSource(nameof(AllLocations))]
|
|
public void Qsv_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
|
|
{
|
|
var filter = new OverlayWatermarkQsvFilter(
|
|
StateFor(location),
|
|
Resolution,
|
|
SquarePixelFrameSize,
|
|
NullLogger.Instance);
|
|
|
|
filter.Filter.ShouldBe($"overlay_qsv={expectedPosition}");
|
|
}
|
|
|
|
// #503: the position switch has a catch-all default that silently renders bottom-right, so a
|
|
// newly added WatermarkLocation would compile, pass every case above, and reintroduce the exact
|
|
// bug this fixture exists to prevent. Fail here instead, at the point the enum grows.
|
|
[Test]
|
|
public void Every_WatermarkLocation_Has_An_Expected_Position()
|
|
{
|
|
IEnumerable<WatermarkLocation> covered = ExpectedPositions.Select(x => x.Location);
|
|
|
|
Enum.GetValues<WatermarkLocation>().Except(covered).ShouldBeEmpty(
|
|
"every WatermarkLocation needs an explicit arm in OverlayWatermarkFilter.Position and a "
|
|
+ "matching entry in ExpectedPositions; otherwise it silently renders bottom-right");
|
|
}
|
|
|
|
[Test]
|
|
public void Source_Content_Margins_Offset_By_Half_The_Pillarbox_Padding()
|
|
{
|
|
var filter = new OverlayWatermarkFilter(
|
|
StateFor(WatermarkLocation.BottomRight, placeWithinSourceContent: true),
|
|
Resolution,
|
|
PillarboxedSquarePixelFrameSize,
|
|
new PixelFormatUnknown(),
|
|
NullLogger.Instance);
|
|
|
|
// horizontal: round(0.10 * 1440 + 480 / 2) = 384; vertical: round(0.10 * 1080 + 0) = 108
|
|
filter.Filter.ShouldBe("overlay=x=W-w-384:y=H-h-108:format=0");
|
|
}
|
|
|
|
[Test]
|
|
public void Unmapped_Location_Falls_Back_To_BottomRight_Position()
|
|
{
|
|
var filter = new OverlayWatermarkFilter(
|
|
StateFor((WatermarkLocation)999),
|
|
Resolution,
|
|
SquarePixelFrameSize,
|
|
new PixelFormatUnknown(),
|
|
NullLogger.Instance);
|
|
|
|
filter.Filter.ShouldBe("overlay=x=W-w-192:y=H-h-108:format=0");
|
|
}
|
|
}
|