Browse Source

fix bugs with playout mode multiple (#2160)

pull/2161/head
Jason Dove 4 weeks ago committed by GitHub
parent
commit
464c1e2ea8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 107
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  3. 71
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  4. 2
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  5. 25
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  6. 102
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  7. 88
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

4
CHANGELOG.md

@ -106,6 +106,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -106,6 +106,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Split main `Settings` page into multiple pages
- Update UI layout on all pages to be less cramped and to work better on mobile
- Add CPU and Video Controller info to `Troubleshooting` > `General` output
- Expand special zero-count case for `Multiple` playout mode with playlists
- This configuration will automatically maintain the multiple count so that it is equal to the number of items in each playlist item
- This configuration should be used if you want to play every media item in a playlist item exactly once before advancing
### Fixed
- Fix QSV acceleration in docker with older Intel devices
@ -127,6 +130,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -127,6 +130,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix some NVIDIA edge cases when media servers don't provide video bit depth information
- Fix VAAPI tonemap failure
- Fix green bars after VAAPI tonemap
- Fix bug where playout mode `Multiple` would ignore fixed start time
## [25.2.0] - 2025-06-24
### Added

107
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -840,6 +840,113 @@ public class PlayoutBuilderTests @@ -840,6 +840,113 @@ public class PlayoutBuilderTests
result.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(7));
}
[Test]
public async Task FloodContent_Should_FloodAroundFixedContent_Multiple_With_Gap()
{
var floodCollection = new Collection
{
Id = 1,
Name = "Flood Items",
MediaItems = new List<MediaItem>
{
TestMovie(1, TimeSpan.FromMinutes(50), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
}
};
var fixedCollection = new Collection
{
Id = 2,
Name = "Fixed Items",
MediaItems = new List<MediaItem>
{
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2))
}
};
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(floodCollection.Id, floodCollection.MediaItems.ToList()),
(fixedCollection.Id, fixedCollection.MediaItems.ToList())));
var items = new List<ProgramScheduleItem>
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological
},
new ProgramScheduleItemMultiple
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
StartTime = TimeSpan.FromHours(3),
Count = 2,
PlaybackOrder = PlaybackOrder.Chronological
}
};
var playout = new Playout
{
ProgramSchedule = new ProgramSchedule
{
Items = items
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>(),
FillGroupIndices = []
};
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
artistRepo,
factory,
localFileSystem,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.ShouldBe(6);
result.Items[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.Zero);
result.Items[0].MediaItemId.ShouldBe(1);
result.Items[1].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromMinutes(50));
result.Items[1].MediaItemId.ShouldBe(2);
result.Items[2].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromMinutes(50 + 60));
result.Items[2].MediaItemId.ShouldBe(1);
result.Items[3].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(3));
result.Items[3].MediaItemId.ShouldBe(3);
result.Items[4].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(5));
result.Items[4].MediaItemId.ShouldBe(4);
result.Items[5].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6));
result.Items[5].MediaItemId.ShouldBe(2);
result.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(7));
}
[Test]
public async Task FloodContent_Should_FloodWithFixedStartTime()
{

71
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

@ -16,6 +16,77 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase @@ -16,6 +16,77 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
private CancellationToken _cancellationToken;
[Test]
public void Should_Respect_Fixed_Start_Time()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = TimeSpan.FromHours(1),
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 0,
CustomTitle = "CustomTitle"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var collectionItemCount = new Dictionary<CollectionKey, int>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems.Count }
}.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionItemCount, Substitute.For<ILogger>());
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
startState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.ShouldBe(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.ShouldBe(2); // one guide group here because of custom title
playoutBuilderState.DurationFinish.IsNone.ShouldBeTrue();
playoutBuilderState.InFlood.ShouldBeFalse();
playoutBuilderState.MultipleRemaining.IsNone.ShouldBeTrue();
playoutBuilderState.InDurationFiller.ShouldBeFalse();
playoutBuilderState.ScheduleItemsEnumerator.State.Index.ShouldBe(0);
enumerator.State.Index.ShouldBe(0);
playoutItems.Count.ShouldBe(2);
playoutItems[0].MediaItemId.ShouldBe(1);
playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime.AddHours(1));
playoutItems[0].GuideGroup.ShouldBe(1);
playoutItems[0].FillerKind.ShouldBe(FillerKind.None);
playoutItems[0].CustomTitle.ShouldBe("CustomTitle");
playoutItems[1].MediaItemId.ShouldBe(2);
playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime.AddHours(2));
playoutItems[1].GuideGroup.ShouldBe(1);
playoutItems[1].FillerKind.ShouldBe(FillerKind.None);
playoutItems[1].CustomTitle.ShouldBe("CustomTitle");
}
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{

2
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -24,6 +24,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -24,6 +24,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public ImmutableList<PlaylistEnumeratorCollectionKey> ChildEnumerators { get; private set; }
public bool CurrentEnumeratorPlayAll => _playAll[EnumeratorIndex];
public int EnumeratorIndex { get; private set; }
public void ResetState(CollectionEnumeratorState state) =>

25
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -33,19 +33,34 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -33,19 +33,34 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
PlayoutBuilderState nextState = playoutBuilderState with
{
CurrentTime = firstStart,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
};
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
if (nextState.MultipleRemaining == 0)
{
nextState = nextState with
// playlist count of zero means play all media items in the current playlist item
if (contentEnumerator is PlaylistEnumerator { CurrentEnumeratorPlayAll: true } playlistEnumerator)
{
MultipleRemaining = _collectionItemCount[CollectionKey.ForScheduleItem(scheduleItem)]
};
nextState = nextState with
{
MultipleRemaining = playlistEnumerator
.ChildEnumerators[playlistEnumerator.EnumeratorIndex]
.Enumerator.Count
};
}
else
{
nextState = nextState with
{
MultipleRemaining = _collectionItemCount[CollectionKey.ForScheduleItem(scheduleItem)]
};
}
}
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
while (contentEnumerator.Current.IsSome && nextState.MultipleRemaining > 0 &&
nextState.CurrentTime < hardStop)
{

102
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -1,52 +1,52 @@ @@ -1,52 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NoWarn>VSTHRD200</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.7" />
<PackageReference Include="Jint" Version="4.4.0" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00017" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="8.0.0" />
<PackageReference Include="Refit.Xml" Version="8.0.0" />
<PackageReference Include="Scriban.Signed" Version="6.2.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<!-- transitive; upgrading for vuln -->
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ErsatzTV.Infrastructure.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NoWarn>VSTHRD200</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.7" />
<PackageReference Include="Jint" Version="4.4.0" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00017" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="8.0.0" />
<PackageReference Include="Refit.Xml" Version="8.0.0" />
<PackageReference Include="Scriban.Signed" Version="6.2.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<!-- transitive; upgrading for vuln -->
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ErsatzTV.Infrastructure.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

88
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -1,45 +1,45 @@ @@ -1,45 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug No Sync</Configurations>
<Platforms>AnyCPU</Platforms>
<UserSecretsId>729e6271-c307-43c8-8e36-1b36c39f6de2</UserSecretsId>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure.MySql\ErsatzTV.Infrastructure.MySql.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure.Sqlite\ErsatzTV.Infrastructure.Sqlite.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta6.25358.103" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ErsatzTV.Scanner.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug No Sync</Configurations>
<Platforms>AnyCPU</Platforms>
<UserSecretsId>729e6271-c307-43c8-8e36-1b36c39f6de2</UserSecretsId>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure.MySql\ErsatzTV.Infrastructure.MySql.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure.Sqlite\ErsatzTV.Infrastructure.Sqlite.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta6.25358.103" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ErsatzTV.Scanner.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
Loading…
Cancel
Save