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:
@@ -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
|
||||
- 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
|
||||
- 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
|
||||
- Add basic cache busting to XMLTV image URLs
|
||||
|
||||
@@ -16,6 +16,7 @@ public record UpdateDeco(
|
||||
int? DefaultFillerMediaItemId,
|
||||
int? DefaultFillerMultiCollectionId,
|
||||
int? DefaultFillerSmartCollectionId,
|
||||
bool DefaultFillerTrimToFit,
|
||||
DecoMode DeadAirFallbackMode,
|
||||
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
|
||||
int? DeadAirFallbackCollectionId,
|
||||
|
||||
@@ -42,6 +42,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
existing.DefaultFillerSmartCollectionId = request.DefaultFillerMode is DecoMode.Override
|
||||
? request.DefaultFillerSmartCollectionId
|
||||
: null;
|
||||
existing.DefaultFillerTrimToFit = request.DefaultFillerTrimToFit;
|
||||
|
||||
// dead air fallback
|
||||
existing.DeadAirFallbackMode = request.DeadAirFallbackMode;
|
||||
|
||||
@@ -15,6 +15,7 @@ public record DecoViewModel(
|
||||
int? DefaultFillerMediaItemId,
|
||||
int? DefaultFillerMultiCollectionId,
|
||||
int? DefaultFillerSmartCollectionId,
|
||||
bool DefaultFillerTrimToFit,
|
||||
DecoMode DeadAirFallbackMode,
|
||||
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
|
||||
int? DeadAirFallbackCollectionId,
|
||||
|
||||
@@ -62,6 +62,7 @@ internal static class Mapper
|
||||
deco.DefaultFillerMediaItemId,
|
||||
deco.DefaultFillerMultiCollectionId,
|
||||
deco.DefaultFillerSmartCollectionId,
|
||||
deco.DefaultFillerTrimToFit,
|
||||
deco.DeadAirFallbackMode,
|
||||
deco.DeadAirFallbackCollectionType,
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class Deco
|
||||
public MultiCollection DefaultFillerMultiCollection { get; set; }
|
||||
public int? DefaultFillerSmartCollectionId { get; set; }
|
||||
public SmartCollection DefaultFillerSmartCollection { get; set; }
|
||||
public bool DefaultFillerTrimToFit { get; set; }
|
||||
|
||||
// dead air fallback
|
||||
public DecoMode DeadAirFallbackMode { get; set; }
|
||||
|
||||
@@ -107,14 +107,21 @@ public class BlockPlayoutFillerBuilder(
|
||||
};
|
||||
|
||||
if (filler.FinishOffset > finish)
|
||||
{
|
||||
if (deco.DefaultFillerTrimToFit)
|
||||
{
|
||||
filler.Finish = finish.UtcDateTime;
|
||||
filler.OutPoint = filler.Finish - filler.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
pastTime = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
playout.Items.Add(filler);
|
||||
|
||||
|
||||
// create a playout history record
|
||||
var nextHistory = new PlayoutHistory
|
||||
{
|
||||
|
||||
Generated
+5857
File diff suppressed because it is too large
Load Diff
+29
@@ -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")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("DefaultFillerTrimToFit")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
|
||||
ErsatzTV.Infrastructure.Sqlite/Migrations/20240722185307_Add_Deco_DefaultFillerTrimToFit.Designer.cs
Generated
+5696
File diff suppressed because it is too large
Load Diff
+29
@@ -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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DefaultFillerTrimToFit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
<MudSwitch T="bool" Class="mt-3" Label="Trim To Fit" @bind-Value="_deco.DefaultFillerTrimToFit" Color="Color.Primary" />
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
<MudCard Class="mb-6" Style="width: 350px">
|
||||
@@ -173,7 +174,7 @@
|
||||
</MudText>
|
||||
</ChildContent>
|
||||
<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>
|
||||
</TooltipContent>
|
||||
</MudTooltip>
|
||||
@@ -359,6 +360,7 @@
|
||||
DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue
|
||||
? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId.Value)
|
||||
: null,
|
||||
DefaultFillerTrimToFit = deco.DefaultFillerTrimToFit,
|
||||
|
||||
DeadAirFallbackMode = deco.DeadAirFallbackMode,
|
||||
DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType,
|
||||
@@ -393,6 +395,7 @@
|
||||
_deco.DefaultFillerMediaItem?.MediaItemId,
|
||||
_deco.DefaultFillerMultiCollection?.Id,
|
||||
_deco.DefaultFillerSmartCollection?.Id,
|
||||
_deco.DefaultFillerTrimToFit,
|
||||
_deco.DeadAirFallbackMode,
|
||||
_deco.DeadAirFallbackCollectionType,
|
||||
_deco.DeadAirFallbackCollection?.Id,
|
||||
|
||||
@@ -18,6 +18,7 @@ public class DecoEditViewModel
|
||||
public MultiCollectionViewModel DefaultFillerMultiCollection { get; set; }
|
||||
public SmartCollectionViewModel DefaultFillerSmartCollection { get; set; }
|
||||
public NamedMediaItemViewModel DefaultFillerMediaItem { get; set; }
|
||||
public bool DefaultFillerTrimToFit { get; set; }
|
||||
|
||||
public DecoMode DeadAirFallbackMode { get; set; }
|
||||
public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user