add deco default filler trim to fit setting (#1800)

* add deco default filler trim to fit setting

* implement trim to fit

* update changelog
This commit is contained in:
Jason Dove
2024-07-22 14:14:02 -05:00
committed by GitHub
parent d842cd57f6
commit b1b2c2a1e0
16 changed files with 11760 additions and 4 deletions
+2
View File
@@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- After all blocks are scheduled/added to the playout, a second pass will be made to insert filler - After all blocks are scheduled/added to the playout, a second pass will be made to insert filler
- Default filler will be shuffled and inserted in all unscheduled time between blocks - Default filler will be shuffled and inserted in all unscheduled time between blocks
- Default filler will stop scheduling when the next item would extend into primary content - Default filler will stop scheduling when the next item would extend into primary content
- Alternatively, default filler can be configured to `Trim To Fit`
- In this case, the last item that would extend into primary content is trimmed to end exactly when the primary content starts
### Fixed ### Fixed
- Add basic cache busting to XMLTV image URLs - Add basic cache busting to XMLTV image URLs
@@ -16,6 +16,7 @@ public record UpdateDeco(
int? DefaultFillerMediaItemId, int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId, int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId, int? DefaultFillerSmartCollectionId,
bool DefaultFillerTrimToFit,
DecoMode DeadAirFallbackMode, DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType, ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId, int? DeadAirFallbackCollectionId,
@@ -42,6 +42,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
existing.DefaultFillerSmartCollectionId = request.DefaultFillerMode is DecoMode.Override existing.DefaultFillerSmartCollectionId = request.DefaultFillerMode is DecoMode.Override
? request.DefaultFillerSmartCollectionId ? request.DefaultFillerSmartCollectionId
: null; : null;
existing.DefaultFillerTrimToFit = request.DefaultFillerTrimToFit;
// dead air fallback // dead air fallback
existing.DeadAirFallbackMode = request.DeadAirFallbackMode; existing.DeadAirFallbackMode = request.DeadAirFallbackMode;
@@ -15,6 +15,7 @@ public record DecoViewModel(
int? DefaultFillerMediaItemId, int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId, int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId, int? DefaultFillerSmartCollectionId,
bool DefaultFillerTrimToFit,
DecoMode DeadAirFallbackMode, DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType, ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId, int? DeadAirFallbackCollectionId,
@@ -62,6 +62,7 @@ internal static class Mapper
deco.DefaultFillerMediaItemId, deco.DefaultFillerMediaItemId,
deco.DefaultFillerMultiCollectionId, deco.DefaultFillerMultiCollectionId,
deco.DefaultFillerSmartCollectionId, deco.DefaultFillerSmartCollectionId,
deco.DefaultFillerTrimToFit,
deco.DeadAirFallbackMode, deco.DeadAirFallbackMode,
deco.DeadAirFallbackCollectionType, deco.DeadAirFallbackCollectionType,
deco.DeadAirFallbackCollectionId, deco.DeadAirFallbackCollectionId,
@@ -0,0 +1,121 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Scripting;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.FFmpeg;
[TestFixture]
public class FFmpegStreamSelectorTests
{
[TestFixture]
public class SelectAudioStream
{
[Test]
public async Task Should_Select_Audio_Stream_With_Preferred_Language()
{
// skip movie/episode script paths by using other video
var mediaItem = new OtherVideo();
var mediaVersion = new MediaVersion
{
Streams =
[
new MediaStream
{
Index = 0,
MediaStreamKind = MediaStreamKind.Audio,
Channels = 2,
Language = "ja",
Title = "Some Title",
},
new MediaStream
{
Index = 1,
MediaStreamKind = MediaStreamKind.Audio,
Channels = 6,
Language = "eng",
Title = "Another Title",
Default = true
}
]
};
var audioVersion = new MediaItemAudioVersion(mediaItem, mediaVersion);
var channel = new Channel(Guid.NewGuid())
{
PreferredAudioLanguageCode = "eng"
};
ISearchRepository searchRepository = Substitute.For<ISearchRepository>();
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>())
.Returns(Task.FromResult(new List<string> { "jpn" }));
var selector = new FFmpegStreamSelector(
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(),
searchRepository,
Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(),
Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, "jpn", "Whatever");
selectedStream.IsSome.Should().BeTrue();
foreach (MediaStream stream in selectedStream)
{
stream.Language.Should().Be("ja");
}
}
[Test]
public async Task Should_Select_Subtitle_Stream_With_Preferred_Language()
{
// skip movie/episode script paths by using other video
var subtitles = new List<Subtitle>
{
new()
{
StreamIndex = 0,
SubtitleKind = SubtitleKind.Sidecar,
Language = "eng",
Default = true
},
new()
{
StreamIndex = 1,
SubtitleKind = SubtitleKind.Sidecar,
Language = "he",
},
};
var channel = new Channel(Guid.NewGuid());
ISearchRepository searchRepository = Substitute.For<ISearchRepository>();
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>())
.Returns(Task.FromResult(new List<string> { "heb" }));
var selector = new FFmpegStreamSelector(
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(),
searchRepository,
Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(),
Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<Subtitle> selectedStream = await selector.SelectSubtitleStream(
subtitles,
channel,
"heb",
ChannelSubtitleMode.Any);
selectedStream.IsSome.Should().BeTrue();
foreach (Subtitle stream in selectedStream)
{
stream.Language.Should().Be("he");
}
}
}
}
+1
View File
@@ -24,6 +24,7 @@ public class Deco
public MultiCollection DefaultFillerMultiCollection { get; set; } public MultiCollection DefaultFillerMultiCollection { get; set; }
public int? DefaultFillerSmartCollectionId { get; set; } public int? DefaultFillerSmartCollectionId { get; set; }
public SmartCollection DefaultFillerSmartCollection { get; set; } public SmartCollection DefaultFillerSmartCollection { get; set; }
public bool DefaultFillerTrimToFit { get; set; }
// dead air fallback // dead air fallback
public DecoMode DeadAirFallbackMode { get; set; } public DecoMode DeadAirFallbackMode { get; set; }
@@ -108,13 +108,20 @@ public class BlockPlayoutFillerBuilder(
if (filler.FinishOffset > finish) if (filler.FinishOffset > finish)
{ {
pastTime = true; if (deco.DefaultFillerTrimToFit)
break; {
filler.Finish = finish.UtcDateTime;
filler.OutPoint = filler.Finish - filler.Start;
}
else
{
pastTime = true;
break;
}
} }
playout.Items.Add(filler); playout.Items.Add(filler);
// create a playout history record // create a playout history record
var nextHistory = new PlayoutHistory var nextHistory = new PlayoutHistory
{ {
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_DefaultFillerTrimToFit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DefaultFillerTrimToFit",
table: "Deco",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DefaultFillerTrimToFit",
table: "Deco");
}
}
}
@@ -2303,6 +2303,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("DefaultFillerSmartCollectionId") b.Property<int?>("DefaultFillerSmartCollectionId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<bool>("DefaultFillerTrimToFit")
.HasColumnType("tinyint(1)");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(255)");
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_DefaultFillerTrimToFit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DefaultFillerTrimToFit",
table: "Deco",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DefaultFillerTrimToFit",
table: "Deco");
}
}
}
@@ -2188,6 +2188,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("DefaultFillerSmartCollectionId") b.Property<int?>("DefaultFillerSmartCollectionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("DefaultFillerTrimToFit")
.HasColumnType("INTEGER");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
+4 -1
View File
@@ -159,6 +159,7 @@
} }
</MudSelect> </MudSelect>
} }
<MudSwitch T="bool" Class="mt-3" Label="Trim To Fit" @bind-Value="_deco.DefaultFillerTrimToFit" Color="Color.Primary" />
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
<MudCard Class="mb-6" Style="width: 350px"> <MudCard Class="mb-6" Style="width: 350px">
@@ -173,7 +174,7 @@
</MudText> </MudText>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<MudText Typo="Typo.body2">When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and cut to exactly fit until the start of the next playout item.</MudText> <MudText Typo="Typo.body2">When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and trimmed to exactly fit until the start of the next playout item.</MudText>
<MudText Typo="Typo.body2" Class="mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText> <MudText Typo="Typo.body2" Class="mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText>
</TooltipContent> </TooltipContent>
</MudTooltip> </MudTooltip>
@@ -359,6 +360,7 @@
DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId.Value) ? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId.Value)
: null, : null,
DefaultFillerTrimToFit = deco.DefaultFillerTrimToFit,
DeadAirFallbackMode = deco.DeadAirFallbackMode, DeadAirFallbackMode = deco.DeadAirFallbackMode,
DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType, DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType,
@@ -393,6 +395,7 @@
_deco.DefaultFillerMediaItem?.MediaItemId, _deco.DefaultFillerMediaItem?.MediaItemId,
_deco.DefaultFillerMultiCollection?.Id, _deco.DefaultFillerMultiCollection?.Id,
_deco.DefaultFillerSmartCollection?.Id, _deco.DefaultFillerSmartCollection?.Id,
_deco.DefaultFillerTrimToFit,
_deco.DeadAirFallbackMode, _deco.DeadAirFallbackMode,
_deco.DeadAirFallbackCollectionType, _deco.DeadAirFallbackCollectionType,
_deco.DeadAirFallbackCollection?.Id, _deco.DeadAirFallbackCollection?.Id,
+1
View File
@@ -18,6 +18,7 @@ public class DecoEditViewModel
public MultiCollectionViewModel DefaultFillerMultiCollection { get; set; } public MultiCollectionViewModel DefaultFillerMultiCollection { get; set; }
public SmartCollectionViewModel DefaultFillerSmartCollection { get; set; } public SmartCollectionViewModel DefaultFillerSmartCollection { get; set; }
public NamedMediaItemViewModel DefaultFillerMediaItem { get; set; } public NamedMediaItemViewModel DefaultFillerMediaItem { get; set; }
public bool DefaultFillerTrimToFit { get; set; }
public DecoMode DeadAirFallbackMode { get; set; } public DecoMode DeadAirFallbackMode { get; set; }
public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; } public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; }