Enhanced the bumper system to support a new background type, 'Clip', allowing text to be overlaid on existing video clips. This update includes the addition of a BackgroundClipShowId property in various records and classes, ensuring that the clip's duration dictates the bumper length. Updated validation rules, mapping, and rendering logic to accommodate this new feature. Localization strings were also updated to reflect the new background option and its implications for audio handling.
210 lines
8.2 KiB
C#
210 lines
8.2 KiB
C#
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using TeleWave.Application.Broadcast.Bumpers;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Streaming;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Images;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Programming;
|
|
using TeleWave.Infrastructure.Persistence;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Broadcast;
|
|
|
|
/// <summary>
|
|
/// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине
|
|
/// сегмента HLS, а плейсхолдеры подставляются образцами выбранного канала — блок общий, но
|
|
/// смотреть на него надо глазами конкретного канала.
|
|
/// </summary>
|
|
public class BumperPreviewTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
private static RenderBumperPreviewCommandHandler Handler(
|
|
AppDbContext db,
|
|
IBumperRenderer? renderer = null,
|
|
IBumperTemplateStorage? storage = null,
|
|
IImageStore? imageStore = null,
|
|
IMediaStorage? mediaStorage = null
|
|
) =>
|
|
new(
|
|
db,
|
|
renderer ?? Substitute.For<IBumperRenderer>(),
|
|
storage ?? Substitute.For<IBumperTemplateStorage>(),
|
|
imageStore ?? Substitute.For<IImageStore>(),
|
|
mediaStorage ?? Substitute.For<IMediaStorage>(),
|
|
Options.Create(new BumperOptions()),
|
|
Options.Create(new StreamingOptions { SegmentSeconds = 5 })
|
|
);
|
|
|
|
private static BumperTemplate NewTemplate()
|
|
{
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "СЕЙЧАС"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{now.title}"),
|
|
BumperLine.Create(2, BumperLineStyle.Label, BumperLineColor.Accent, "ДАЛЕЕ"),
|
|
BumperLine.Create(3, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
return template;
|
|
}
|
|
|
|
private static IReadOnlyList<BumperRenderSpec> Specs(IBumperRenderer renderer) =>
|
|
renderer
|
|
.ReceivedCalls()
|
|
.Select(c => c.GetArguments()[1])
|
|
.OfType<BumperRenderSpec>()
|
|
.ToList();
|
|
|
|
[Fact]
|
|
public async Task Render_UnknownTemplate_ReturnsNotFound()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
|
|
var result = await Handler(db)
|
|
.Handle(new RenderBumperPreviewCommand(Guid.NewGuid(), null), CancellationToken.None);
|
|
|
|
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DrawsEveryVariant_WithChannelSamplesAndAlignedDuration()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Channel.Create("Первый", "one", T0);
|
|
var template = NewTemplate();
|
|
var second = template.AddVariant("Второй подблок");
|
|
second.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{channel}"),
|
|
]);
|
|
|
|
// Шоу канала: слот сетки ссылается на группу, в группе — шоу.
|
|
var show = Show.Create("Наше шоу", ShowKind.Series);
|
|
var group = Group.Create("Кино");
|
|
group.AddElement(GroupElementKind.Show, show.Id);
|
|
var grid = ScheduleTemplate.Create(channel.Id, "Сетка");
|
|
var slot = grid.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60);
|
|
slot.UpdateContent(
|
|
new SlotContent(
|
|
slot.Title,
|
|
SlotKind.Content,
|
|
group.Id,
|
|
null,
|
|
null,
|
|
SlotBlockMode.FillSlot,
|
|
1,
|
|
OverflowPolicy.ContinueNext
|
|
)
|
|
);
|
|
channel.SetTemplate(grid.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Shows.Add(show);
|
|
seed.Groups.Add(group);
|
|
seed.ScheduleTemplates.Add(grid);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer)
|
|
.Handle(
|
|
new RenderBumperPreviewCommand(template.Id, channel.Id),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
// Оба подблока отрисованы, каждый — в свой ассет-превью.
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(template.Variants[0].Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(second.Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
|
|
var specs = Specs(renderer);
|
|
// Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек.
|
|
Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds));
|
|
|
|
var nowNext = specs.First(s => s.Lines.Count == 4);
|
|
Assert.Equal("Наше шоу", nowNext.Lines[1].Text);
|
|
|
|
var channelLine = specs.First(s => s.Lines.Count == 1);
|
|
Assert.Equal("Первый", channelLine.Lines[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_WithBackgroundImage_ResolvesPathFromRegistry()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = NewTemplate();
|
|
var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
|
|
template.SetBackgroundImage(image.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Images.Add(image);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
var imageStore = Substitute.For<IImageStore>();
|
|
imageStore.ResolvePath(image.Id, ".png").Returns("/data/images/bg.png");
|
|
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer, imageStore: imageStore)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
var spec = Specs(renderer)[0];
|
|
Assert.Equal("/data/images/bg.png", spec.BackgroundFile);
|
|
// Канала нет — образцы заглушечные, но кадр всё равно собирается.
|
|
Assert.Equal("Первое шоу", spec.Lines[1].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DropsLinesThatResolveToNothing()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "{next.genre}"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
await Handler(db, renderer)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
// Жанра у образца нет — строка схлопнулась, а не оставила дыру в кадре.
|
|
var spec = Specs(renderer)[0];
|
|
Assert.Single(spec.Lines);
|
|
}
|
|
}
|