diff --git a/CHANGELOG.md b/CHANGELOG.md index 449282577..1fa0cb7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix schedule start time calculation when daily playout build goes beyond midnight and into a different alternate schedule - Fix compatibility with older NVIDIA devices (compute capability 3.0+) in unified docker image - Fix transitions when using NVIDIA, QSV and VAAPI acceleration +- Fix playback of remote streams on channels where framerate normalization is enabled ### Changed - Always tell ffmpeg to stop encoding with a specific duration diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs index fcfeb33dd..69a8954a1 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs @@ -21,84 +21,100 @@ public class GetChannelFramerateHandler : IRequestHandler> Handle(GetChannelFramerate request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + try + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - FFmpegProfile ffmpegProfile = await dbContext.Channels - .AsNoTracking() - .Filter(c => c.Number == request.ChannelNumber) - .Include(c => c.FFmpegProfile) - .Map(c => c.FFmpegProfile) - .SingleAsync(cancellationToken); + FFmpegProfile ffmpegProfile = await dbContext.Channels + .AsNoTracking() + .Filter(c => c.Number == request.ChannelNumber) + .Include(c => c.FFmpegProfile) + .Map(c => c.FFmpegProfile) + .SingleAsync(cancellationToken); - if (!ffmpegProfile.NormalizeFramerate) - { - return Option.None; - } + if (!ffmpegProfile.NormalizeFramerate) + { + return Option.None; + } - // TODO: expand to check everything in collection rather than what's scheduled? - _logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber); - - List playouts = await dbContext.Playouts - .AsNoTracking() - .Include(p => p.Items) - .ThenInclude(pi => pi.MediaItem) - .ThenInclude(mi => (mi as Movie).MediaVersions) - .Include(p => p.Items) - .ThenInclude(pi => pi.MediaItem) - .ThenInclude(mi => (mi as Episode).MediaVersions) - .Include(p => p.Items) - .ThenInclude(pi => pi.MediaItem) - .ThenInclude(mi => (mi as Song).MediaVersions) - .Include(p => p.Items) - .ThenInclude(pi => pi.MediaItem) - .ThenInclude(mi => (mi as MusicVideo).MediaVersions) - .Include(p => p.Items) - .ThenInclude(pi => pi.MediaItem) - .ThenInclude(mi => (mi as OtherVideo).MediaVersions) - .Filter(p => p.Channel.Number == request.ChannelNumber) - .ToListAsync(cancellationToken); - - var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion())) - .Flatten() - .Map(mv => mv.RFrameRate) - .ToList(); - - var distinct = frameRates.Distinct().ToList(); - if (distinct.Count > 1) - { - // TODO: something more intelligent than minimum framerate? - int result = frameRates.Map(ParseFrameRate).Min(); - if (result < 24) + // TODO: expand to check everything in collection rather than what's scheduled? + _logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber); + + List playouts = await dbContext.Playouts + .AsNoTracking() + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as Movie).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as Episode).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as Song).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as OtherVideo).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as Image).MediaVersions) + .Include(p => p.Items) + .ThenInclude(pi => pi.MediaItem) + .ThenInclude(mi => (mi as RemoteStream).MediaVersions) + .Filter(p => p.Channel.Number == request.ChannelNumber) + .ToListAsync(cancellationToken); + + var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion())) + .Flatten() + .Map(mv => mv.RFrameRate) + .ToList(); + + var distinct = frameRates.Distinct().ToList(); + if (distinct.Count > 1) { + // TODO: something more intelligent than minimum framerate? + int result = frameRates.Map(ParseFrameRate).Min(); + if (result < 24) + { + _logger.LogInformation( + "Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}", + request.ChannelNumber, + distinct, + 24, + result); + + return 24; + } + _logger.LogInformation( - "Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}", + "Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}", request.ChannelNumber, distinct, - 24, result); - - return 24; + return result; } - _logger.LogInformation( - "Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}", - request.ChannelNumber, - distinct, - result); - return result; - } - - if (distinct.Count != 0) - { - _logger.LogInformation( - "All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize", - request.ChannelNumber, - distinct[0]); + if (distinct.Count != 0) + { + _logger.LogInformation( + "All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize", + request.ChannelNumber, + distinct[0]); + } + else + { + _logger.LogInformation( + "No content on channel {ChannelNumber} has frame rate information; will not normalize", + request.ChannelNumber); + } } - else + catch (Exception ex) { - _logger.LogInformation( - "No content on channel {ChannelNumber} has frame rate information; will not normalize", + _logger.LogWarning( + ex, + "Unexpected error checking frame rates on channel {ChannelNumber}", request.ChannelNumber); } diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs index 3428e4d89..450eb5681 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs @@ -81,6 +81,10 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) List collectionItems = enumeratorCache.PlaylistMediaItemsForContent(contentItem.Key, collectionKey); + if (collectionItems.Count == 0) + { + continue; + } foreach (PlayoutHistory h in maybeApplicableHistory) { @@ -131,6 +135,10 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) else { List collectionItems = enumeratorCache.MediaItemsForContent(contentItem.Key); + if (collectionItems.Count == 0) + { + continue; + } // seek to the appropriate place in the collection enumerator foreach (PlayoutHistory h in maybeHistory) diff --git a/ErsatzTV/Resources/yaml-playout.schema.json b/ErsatzTV/Resources/yaml-playout.schema.json index 3b6b985c0..284fe74c2 100644 --- a/ErsatzTV/Resources/yaml-playout.schema.json +++ b/ErsatzTV/Resources/yaml-playout.schema.json @@ -102,9 +102,14 @@ "minItems": 1 } }, - "anyOf": [ - { "required": [ "content", "playout" ] }, - { "required": [ "import", "playout" ] } + "allOf": [ + { + "anyOf": [ + { "required": [ "import" ] }, + { "required": [ "content" ] } + ] + }, + { "required": [ "playout" ] } ], "additionalProperties": false, "$defs": {