diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c90da1..190f305b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - This allows these endpoints to be accessed through port `ETV_STREAMING_PORT` (default `8409`) - This only matters if you configured `ETV_UI_PORT` to be a different value, which makes UI endpoints inaccessible on the streaming port - Update Plex movie/other video plot ("summary") during library deep scan +- Fix compatibility with ffmpeg 7.2+ when using NVIDIA accel and 10-bit source content ## [25.2.0] - 2025-06-24 ### Added diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs index 1aeab4cd..b334f8dd 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs @@ -28,7 +28,8 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) { - var random = new Random(context.Playout.Seed + context.InstructionIndex); + int seed = context.Playout.Seed + context.InstructionIndex + context.CurrentTime.DayOfYear; + var random = new Random(seed); int enumeratorCount = enumerator is PlaylistEnumerator playlistEnumerator ? playlistEnumerator.CountForRandom : enumerator.Count; diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs index 623779c5..5c6356a6 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs @@ -27,7 +27,8 @@ public class YamlPlayoutSkipItemsHandler(EnumeratorCache enumeratorCache) : IYam foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) { - var random = new Random(context.Playout.Seed + context.InstructionIndex); + int seed = context.Playout.Seed + context.InstructionIndex + context.CurrentTime.DayOfYear; + var random = new Random(seed); int enumeratorCount = enumerator is PlaylistEnumerator playlistEnumerator ? playlistEnumerator.CountForRandom : enumerator.Count; diff --git a/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs b/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs index 4e6035d1..dc8ea04d 100644 --- a/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs @@ -7,6 +7,7 @@ public class ScaleCudaFilter : BaseFilter private readonly Option _croppedSize; private readonly FrameState _currentState; private readonly bool _isAnamorphicEdgeCase; + private readonly bool _passthrough; private readonly FrameSize _paddedSize; private readonly FrameSize _scaledSize; @@ -15,13 +16,15 @@ public class ScaleCudaFilter : BaseFilter FrameSize scaledSize, FrameSize paddedSize, Option croppedSize, - bool isAnamorphicEdgeCase) + bool isAnamorphicEdgeCase, + bool passthrough) { _currentState = currentState; _scaledSize = scaledSize; _paddedSize = paddedSize; _croppedSize = croppedSize; _isAnamorphicEdgeCase = isAnamorphicEdgeCase; + _passthrough = passthrough; } public bool IsFormatOnly => _currentState.ScaledSize == _scaledSize; @@ -33,10 +36,11 @@ public class ScaleCudaFilter : BaseFilter string scale = string.Empty; if (_currentState.ScaledSize == _scaledSize) { + // don't need scaling, but still need pixel format foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) { - // don't need scaling, but still need pixel format - scale = $"scale_cuda=format={pixelFormat.FFmpegName}"; + string passthrough = _passthrough ? ":passthrough=1" : string.Empty; + scale = $"scale_cuda=format={pixelFormat.FFmpegName}{passthrough}"; } } else diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index f143ab83..adba490d 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -163,6 +163,20 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder foreach (IDecoder decoder in maybeDecoder) { currentState = decoder.NextState(currentState); + + // ffmpeg 7.2+ uses p016 internally for cuda, so convert to p010 for compatibility until min ver is 7.2 + if (decoder is DecoderImplicitCuda && videoStream.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10) + { + var filter = new ScaleCudaFilter( + currentState with { PixelFormat = new PixelFormatP010() }, + videoStream.FrameSize, + videoStream.FrameSize, + Option.None, + false, + passthrough: true); + currentState = filter.NextState(currentState); + videoInputFile.FilterSteps.Add(filter); + } } // if (context.HasSubtitleOverlay || context.HasWatermark) @@ -214,6 +228,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder currentState.ScaledSize, currentState.PaddedSize, Option.None, + false, false); currentState = filter.NextState(currentState); videoInputFile.FilterSteps.Add(filter); @@ -693,7 +708,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder desiredState.ScaledSize, desiredState.PaddedSize, desiredState.CroppedSize, - VideoStream.IsAnamorphicEdgeCase); + VideoStream.IsAnamorphicEdgeCase, + false); } if (!string.IsNullOrWhiteSpace(scaleStep.Filter)) diff --git a/docker/ffmpeg-tests/Dockerfile b/docker/ffmpeg-tests/Dockerfile index 5a97828f..5811e0ca 100644 --- a/docker/ffmpeg-tests/Dockerfile +++ b/docker/ffmpeg-tests/Dockerfile @@ -1,5 +1,10 @@ FROM jasongdove/ersatztv-ffmpeg:7.1.1 -RUN apt-get update && apt-get install -y ca-certificates gnupg dotnet9 dotnet-sdk-9.0 aspnetcore-runtime-9.0 mkvtoolnix +RUN apt-get update && apt-get install -y ca-certificates gnupg mkvtoolnix && \ + curl -L https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh && \ + chmod +x ./dotnet-install.sh && \ + ./dotnet-install.sh --channel 9.0 +ENV DOTNET_ROOT="/root/.dotnet" +ENV PATH="$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools" WORKDIR /source # copy csproj and restore as distinct layers