Browse Source

software decoder fixes (#1169)

* fix software decoder pipeline bugs

* tweak nvidia scaling logic

* update changelog

* update dependencies
pull/1171/head
Jason Dove 2 years ago committed by GitHub
parent
commit
1afff11063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs
  3. 5
      ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs
  4. 2
      ErsatzTV.Application/ErsatzTV.Application.csproj
  5. 2
      ErsatzTV.Application/Maintenance/Commands/ReleaseMemory.cs
  6. 8
      ErsatzTV.Application/Maintenance/Commands/ReleaseMemoryHandler.cs
  7. 2
      ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs
  8. 6
      ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs
  9. 2
      ErsatzTV.Application/Search/Commands/ReindexMediaItems.cs
  10. 5
      ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs
  11. 2
      ErsatzTV.Application/Search/Commands/RemoveMediaItems.cs
  12. 5
      ErsatzTV.Application/Search/Commands/RemoveMediaItemsHandler.cs
  13. 2
      ErsatzTV.Core/ErsatzTV.Core.csproj
  14. 8
      ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs
  15. 35
      ErsatzTV.FFmpeg/Filter/Cuda/CudaHardwareDownloadFilter.cs
  16. 5
      ErsatzTV.FFmpeg/Filter/ScaleFilter.cs
  17. 2
      ErsatzTV.FFmpeg/Pipeline/AmfPipelineBuilder.cs
  18. 59
      ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs
  19. 9
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  20. 21
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  21. 16
      ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs
  22. 22
      ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs
  23. 10
      ErsatzTV.FFmpeg/Pipeline/VideoToolboxPipelineBuilder.cs
  24. 6
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  25. 26
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  26. 3
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  27. 2
      ErsatzTV.Scanner/Program.cs
  28. 13
      ErsatzTV/ErsatzTV.csproj
  29. 3
      ErsatzTV/Startup.cs

4
CHANGELOG.md

@ -6,9 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Fixed ### Fixed
- Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/jasongdove/ErsatzTV-ffmpeg) - Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/jasongdove/ErsatzTV-ffmpeg)
- Fix some transcoding pipelines that use software decoders
### Changed ### Changed
- Plex, Jellyfin and Emby libraries now retrieve all metadata and statistics from the media server; ffprobe is no longer used - Plex, Jellyfin and Emby libraries now retrieve all metadata and statistics from the media server
- File systems will no longer be periodically scanned for libraries using these media sources
- Plex, Jellyfin and Emby libraries now direct stream content when files are not found on ErsatzTV's file system - Plex, Jellyfin and Emby libraries now direct stream content when files are not found on ErsatzTV's file system
- Content will still be normalized according to the Channel and FFmpeg Profile settings - Content will still be normalized according to the Channel and FFmpeg Profile settings
- Streaming from disk is preferred, so every playback attempt will first check the local file system - Streaming from disk is preferred, so every playback attempt will first check the local file system

2
ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs

@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Configuration; namespace ErsatzTV.Application.Configuration;
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest<Unit>; public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest;

5
ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs

@ -2,16 +2,15 @@
namespace ErsatzTV.Application.Configuration; namespace ErsatzTV.Application.Configuration;
public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey, Unit> public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{ {
await _configElementRepository.Upsert(request.Key, request.Value); await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
} }
} }

2
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -10,7 +10,7 @@
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.0" /> <PackageReference Include="CliWrap" Version="3.6.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="11.1.0" /> <PackageReference Include="MediatR" Version="12.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

2
ErsatzTV.Application/Maintenance/Commands/ReleaseMemory.cs

@ -1,6 +1,6 @@
namespace ErsatzTV.Application.Maintenance; namespace ErsatzTV.Application.Maintenance;
public record ReleaseMemory(bool ForceAggressive) : IRequest<Unit>, IBackgroundServiceRequest public record ReleaseMemory(bool ForceAggressive) : IRequest, IBackgroundServiceRequest
{ {
public DateTimeOffset RequestTime = DateTimeOffset.Now; public DateTimeOffset RequestTime = DateTimeOffset.Now;
} }

8
ErsatzTV.Application/Maintenance/Commands/ReleaseMemoryHandler.cs

@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Maintenance; namespace ErsatzTV.Application.Maintenance;
public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory, Unit> public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory>
{ {
private static long _lastRelease; private static long _lastRelease;
@ -19,12 +19,12 @@ public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory, Unit>
_logger = logger; _logger = logger;
} }
public Task<Unit> Handle(ReleaseMemory request, CancellationToken cancellationToken) public Task Handle(ReleaseMemory request, CancellationToken cancellationToken)
{ {
if (!request.ForceAggressive && _lastRelease > request.RequestTime.Ticks) if (!request.ForceAggressive && _lastRelease > request.RequestTime.Ticks)
{ {
// we've already released since the request was created, so don't bother // we've already released since the request was created, so don't bother
return Task.FromResult(Unit.Default); return Task.CompletedTask;
} }
bool hasActiveWorkers = _ffmpegSegmenterService.SessionWorkers.Any() || FFmpegProcess.ProcessCount > 0; bool hasActiveWorkers = _ffmpegSegmenterService.SessionWorkers.Any() || FFmpegProcess.ProcessCount > 0;
@ -45,6 +45,6 @@ public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory, Unit>
_logger.LogDebug("Completed garbage collection"); _logger.LogDebug("Completed garbage collection");
Interlocked.Exchange(ref _lastRelease, DateTimeOffset.Now.Ticks); Interlocked.Exchange(ref _lastRelease, DateTimeOffset.Now.Ticks);
return Task.FromResult(Unit.Default); return Task.CompletedTask;
} }
} }

2
ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs

@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public record RebuildSearchIndex : IRequest<Unit>, IBackgroundServiceRequest; public record RebuildSearchIndex : IRequest, IBackgroundServiceRequest;

6
ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs

@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Unit> public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
@ -35,7 +35,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
} }
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken) public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Initializing search index"); _logger.LogInformation("Initializing search index");
@ -63,7 +63,5 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
{ {
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version); _logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
} }
return Unit.Default;
} }
} }

2
ErsatzTV.Application/Search/Commands/ReindexMediaItems.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public record ReindexMediaItems(IReadOnlyCollection<int> MediaItemIds) : IRequest<Unit>, public record ReindexMediaItems(IReadOnlyCollection<int> MediaItemIds) : IRequest,
ISearchIndexBackgroundServiceRequest; ISearchIndexBackgroundServiceRequest;

5
ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs

@ -4,7 +4,7 @@ using ErsatzTV.Core.Interfaces.Search;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public class ReindexMediaItemsHandler : IRequestHandler<ReindexMediaItems, Unit> public class ReindexMediaItemsHandler : IRequestHandler<ReindexMediaItems>
{ {
private readonly ICachingSearchRepository _cachingSearchRepository; private readonly ICachingSearchRepository _cachingSearchRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
@ -20,10 +20,9 @@ public class ReindexMediaItemsHandler : IRequestHandler<ReindexMediaItems, Unit>
_searchIndex = searchIndex; _searchIndex = searchIndex;
} }
public async Task<Unit> Handle(ReindexMediaItems request, CancellationToken cancellationToken) public async Task Handle(ReindexMediaItems request, CancellationToken cancellationToken)
{ {
await _searchIndex.RebuildItems(_cachingSearchRepository, _fallbackMetadataProvider, request.MediaItemIds); await _searchIndex.RebuildItems(_cachingSearchRepository, _fallbackMetadataProvider, request.MediaItemIds);
_searchIndex.Commit(); _searchIndex.Commit();
return Unit.Default;
} }
} }

2
ErsatzTV.Application/Search/Commands/RemoveMediaItems.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public record RemoveMediaItems(IReadOnlyCollection<int> MediaItemIds) : IRequest<Unit>, public record RemoveMediaItems(IReadOnlyCollection<int> MediaItemIds) : IRequest,
ISearchIndexBackgroundServiceRequest; ISearchIndexBackgroundServiceRequest;

5
ErsatzTV.Application/Search/Commands/RemoveMediaItemsHandler.cs

@ -2,16 +2,15 @@ using ErsatzTV.Core.Interfaces.Search;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public class RemoveMediaItemsHandler : IRequestHandler<RemoveMediaItems, Unit> public class RemoveMediaItemsHandler : IRequestHandler<RemoveMediaItems>
{ {
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
public RemoveMediaItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex; public RemoveMediaItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
public async Task<Unit> Handle(RemoveMediaItems request, CancellationToken cancellationToken) public async Task Handle(RemoveMediaItems request, CancellationToken cancellationToken)
{ {
await _searchIndex.RemoveItems(request.MediaItemIds); await _searchIndex.RemoveItems(request.MediaItemIds);
_searchIndex.Commit(); _searchIndex.Commit();
return Unit.Default;
} }
} }

2
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -12,7 +12,7 @@
<PackageReference Include="Flurl" Version="3.0.7" /> <PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="LanguageExt.Core" Version="4.4.2" /> <PackageReference Include="LanguageExt.Core" Version="4.4.2" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.2" /> <PackageReference Include="LanguageExt.Transformers" Version="4.4.2" />
<PackageReference Include="MediatR" Version="11.1.0" /> <PackageReference Include="MediatR" Version="12.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />

8
ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs

@ -32,10 +32,16 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
// no hardware decoding of 10-bit h264 // no hardware decoding of 10-bit h264
VideoFormat.H264 when bitDepth == 10 => false, VideoFormat.H264 when bitDepth == 10 => false,
VideoFormat.Mpeg2Video => true,
VideoFormat.Vc1 => true,
VideoFormat.Mpeg4 => true,
// generated images are decoded into software // generated images are decoded into software
VideoFormat.GeneratedImage => false, VideoFormat.GeneratedImage => false,
_ => true _ => false
}; };
} }

35
ErsatzTV.FFmpeg/Filter/Cuda/CudaHardwareDownloadFilter.cs

@ -4,16 +4,23 @@ namespace ErsatzTV.FFmpeg.Filter.Cuda;
public class CudaHardwareDownloadFilter : BaseFilter public class CudaHardwareDownloadFilter : BaseFilter
{ {
private readonly Option<IPixelFormat> _maybePixelFormat; private readonly Option<IPixelFormat> _maybeCurrentPixelFormat;
private readonly Option<IPixelFormat> _maybeTargetPixelFormat;
public CudaHardwareDownloadFilter(Option<IPixelFormat> maybePixelFormat) => _maybePixelFormat = maybePixelFormat; public CudaHardwareDownloadFilter(
Option<IPixelFormat> maybeCurrentPixelFormat,
Option<IPixelFormat> maybeTargetPixelFormat)
{
_maybeCurrentPixelFormat = maybeCurrentPixelFormat;
_maybeTargetPixelFormat = maybeTargetPixelFormat;
}
public override string Filter public override string Filter
{ {
get get
{ {
var hwdownload = "hwdownload"; var hwdownload = "hwdownload";
foreach (IPixelFormat pixelFormat in _maybePixelFormat) foreach (IPixelFormat pixelFormat in _maybeCurrentPixelFormat)
{ {
if (!string.IsNullOrWhiteSpace(pixelFormat.FFmpegName)) if (!string.IsNullOrWhiteSpace(pixelFormat.FFmpegName))
{ {
@ -21,9 +28,19 @@ public class CudaHardwareDownloadFilter : BaseFilter
if (pixelFormat is PixelFormatNv12 nv12) if (pixelFormat is PixelFormatNv12 nv12)
{ {
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) if (_maybeTargetPixelFormat.IsNone)
{
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null))
{
hwdownload += $",format={pf.FFmpegName}";
}
}
else
{ {
hwdownload += $",format={pf.FFmpegName}"; foreach (IPixelFormat pf in _maybeTargetPixelFormat)
{
hwdownload += $",format={pf.FFmpegName}";
}
} }
} }
} }
@ -40,7 +57,13 @@ public class CudaHardwareDownloadFilter : BaseFilter
FrameDataLocation = FrameDataLocation.Software FrameDataLocation = FrameDataLocation.Software
}; };
foreach (IPixelFormat pixelFormat in _maybePixelFormat) foreach (IPixelFormat pixelFormat in _maybeTargetPixelFormat)
{
result = result with { PixelFormat = Some(pixelFormat) };
return result;
}
foreach (IPixelFormat pixelFormat in _maybeCurrentPixelFormat)
{ {
if (pixelFormat is PixelFormatNv12 nv12) if (pixelFormat is PixelFormatNv12 nv12)
{ {

5
ErsatzTV.FFmpeg/Filter/ScaleFilter.cs

@ -21,6 +21,11 @@ public class ScaleFilter : BaseFilter
{ {
get get
{ {
if (_currentState.ScaledSize == _scaledSize)
{
return string.Empty;
}
string aspectRatio = string.Empty; string aspectRatio = string.Empty;
if (_scaledSize != _paddedSize) if (_scaledSize != _paddedSize)
{ {

2
ErsatzTV.FFmpeg/Pipeline/AmfPipelineBuilder.cs

@ -92,7 +92,7 @@ public class AmfPipelineBuilder : SoftwarePipelineBuilder
{ {
if (!videoStream.ColorParams.IsBt709) if (!videoStream.ColorParams.IsBt709)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat); var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);
currentState = colorspace.NextState(currentState); currentState = colorspace.NextState(currentState);
result.Add(colorspace); result.Add(colorspace);

59
ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs

@ -80,12 +80,11 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
}; };
} }
protected override void SetDecoder( protected override Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context)
ICollection<IPipelineStep> pipelineSteps)
{ {
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch
{ {
@ -105,7 +104,10 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
foreach (IDecoder decoder in maybeDecoder) foreach (IDecoder decoder in maybeDecoder)
{ {
videoInputFile.AddOption(decoder); videoInputFile.AddOption(decoder);
return Some(decoder);
} }
return None;
} }
protected override FilterChain SetVideoFilters( protected override FilterChain SetVideoFilters(
@ -114,6 +116,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
Option<WatermarkInputFile> watermarkInputFile, Option<WatermarkInputFile> watermarkInputFile,
Option<SubtitleInputFile> subtitleInputFile, Option<SubtitleInputFile> subtitleInputFile,
PipelineContext context, PipelineContext context,
Option<IDecoder> maybeDecoder,
FFmpegState ffmpegState, FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,
@ -132,11 +135,13 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
: videoStream.PixelFormat, : videoStream.PixelFormat,
IsAnamorphic = videoStream.IsAnamorphic, IsAnamorphic = videoStream.IsAnamorphic,
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc
? FrameDataLocation.Hardware
: FrameDataLocation.Software
}; };
foreach (IDecoder decoder in maybeDecoder)
{
currentState = decoder.NextState(currentState);
}
// if (context.HasSubtitleOverlay || context.HasWatermark) // if (context.HasSubtitleOverlay || context.HasWatermark)
// { // {
// IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone( // IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone(
@ -275,7 +280,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
if (!videoStream.ColorParams.IsBt709) if (!videoStream.ColorParams.IsBt709)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, format, false); var colorspace = new ColorspaceFilter(currentState, videoStream, format, false);
currentState = colorspace.NextState(currentState); currentState = colorspace.NextState(currentState);
@ -292,12 +297,20 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
_logger.LogDebug( _logger.LogDebug(
"HasSubtitleOverlay || HasWatermark && FrameDataLocation == FrameDataLocation.Hardware"); "HasSubtitleOverlay || HasWatermark && FrameDataLocation == FrameDataLocation.Hardware");
var hardwareDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat); var hardwareDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat, None);
currentState = hardwareDownload.NextState(currentState); currentState = hardwareDownload.NextState(currentState);
result.Add(hardwareDownload); result.Add(hardwareDownload);
} }
} }
if (currentState.FrameDataLocation == FrameDataLocation.Hardware &&
ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None)
{
var hardwareDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat, Some(format));
currentState = hardwareDownload.NextState(currentState);
result.Add(hardwareDownload);
}
if (currentState.PixelFormat.Map(f => f.FFmpegName) != format.FFmpegName) if (currentState.PixelFormat.Map(f => f.FFmpegName) != format.FFmpegName)
{ {
_logger.LogDebug( _logger.LogDebug(
@ -332,14 +345,6 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
pipelineSteps.Add(new PixelFormatOutputOption(format)); pipelineSteps.Add(new PixelFormatOutputOption(format));
} }
} }
if (currentState.FrameDataLocation == FrameDataLocation.Hardware &&
ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None)
{
var hardwareDownload = new CudaHardwareDownloadFilter(Some(format));
currentState = hardwareDownload.NextState(currentState);
result.Add(hardwareDownload);
}
} }
return result; return result;
@ -398,6 +403,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
videoStream.SquarePixelFrameSize(currentState.PaddedSize), videoStream.SquarePixelFrameSize(currentState.PaddedSize),
_logger); _logger);
watermarkOverlayFilterSteps.Add(watermarkFilter); watermarkOverlayFilterSteps.Add(watermarkFilter);
currentState = watermarkFilter.NextState(currentState);
} }
return currentState; return currentState;
@ -473,7 +480,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
{ {
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) if (currentState.FrameDataLocation == FrameDataLocation.Hardware)
{ {
var cudaDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat); var cudaDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat, None);
currentState = cudaDownload.NextState(currentState); currentState = cudaDownload.NextState(currentState);
videoInputFile.FilterSteps.Add(cudaDownload); videoInputFile.FilterSteps.Add(cudaDownload);
} }
@ -527,11 +534,19 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
{ {
IPipelineFilterStep scaleStep; IPipelineFilterStep scaleStep;
if (currentState.ScaledSize != desiredState.ScaledSize && ffmpegState is bool needsToScale = currentState.ScaledSize != desiredState.ScaledSize;
{ if (!needsToScale)
DecoderHardwareAccelerationMode: HardwareAccelerationMode.None, {
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None return currentState;
} && context is { HasWatermark: false, HasSubtitleOverlay: false, ShouldDeinterlace: false }) }
bool decodedToSoftware = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.None;
bool softwareEncoder = ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None;
bool noHardwareFilters = context is
{ HasWatermark: false, HasSubtitleOverlay: false, ShouldDeinterlace: false };
bool needsToPad = currentState.PaddedSize != desiredState.PaddedSize;
if (decodedToSoftware && (needsToPad || noHardwareFilters && softwareEncoder))
{ {
scaleStep = new ScaleFilter( scaleStep = new ScaleFilter(
currentState, currentState,

9
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -395,7 +395,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
ffmpegState = SetAccelState(videoStream, ffmpegState, desiredState, context, pipelineSteps); ffmpegState = SetAccelState(videoStream, ffmpegState, desiredState, context, pipelineSteps);
SetDecoder(videoInputFile, videoStream, ffmpegState, context, pipelineSteps); Option<IDecoder> maybeDecoder = SetDecoder(videoInputFile, videoStream, ffmpegState, context);
SetStillImageInfiniteLoop(videoInputFile, videoStream, ffmpegState); SetStillImageInfiniteLoop(videoInputFile, videoStream, ffmpegState);
SetRealtimeInput(videoInputFile, desiredState); SetRealtimeInput(videoInputFile, desiredState);
@ -411,6 +411,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
_watermarkInputFile, _watermarkInputFile,
_subtitleInputFile, _subtitleInputFile,
context, context,
maybeDecoder,
ffmpegState, ffmpegState,
desiredState, desiredState,
_fontsFolder, _fontsFolder,
@ -421,12 +422,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
return filterChain; return filterChain;
} }
protected abstract void SetDecoder( protected abstract Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context);
ICollection<IPipelineStep> pipelineSteps);
protected Option<IDecoder> GetSoftwareDecoder(VideoStream videoStream) => protected Option<IDecoder> GetSoftwareDecoder(VideoStream videoStream) =>
videoStream.Codec switch videoStream.Codec switch
@ -471,6 +471,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
Option<WatermarkInputFile> watermarkInputFile, Option<WatermarkInputFile> watermarkInputFile,
Option<SubtitleInputFile> subtitleInputFile, Option<SubtitleInputFile> subtitleInputFile,
PipelineContext context, PipelineContext context,
Option<IDecoder> maybeDecoder,
FFmpegState ffmpegState, FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,

21
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -78,12 +78,11 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
}; };
} }
protected override void SetDecoder( protected override Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context)
ICollection<IPipelineStep> pipelineSteps)
{ {
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch
{ {
@ -99,7 +98,10 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
foreach (IDecoder decoder in maybeDecoder) foreach (IDecoder decoder in maybeDecoder)
{ {
videoInputFile.AddOption(decoder); videoInputFile.AddOption(decoder);
return Some(decoder);
} }
return None;
} }
protected override FilterChain SetVideoFilters( protected override FilterChain SetVideoFilters(
@ -108,6 +110,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
Option<WatermarkInputFile> watermarkInputFile, Option<WatermarkInputFile> watermarkInputFile,
Option<SubtitleInputFile> subtitleInputFile, Option<SubtitleInputFile> subtitleInputFile,
PipelineContext context, PipelineContext context,
Option<IDecoder> maybeDecoder,
FFmpegState ffmpegState, FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,
@ -126,12 +129,14 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
? videoStream.PixelFormat.Map(pf => pf.BitDepth == 8 ? new PixelFormatNv12(pf.Name) : pf) ? videoStream.PixelFormat.Map(pf => pf.BitDepth == 8 ? new PixelFormatNv12(pf.Name) : pf)
: videoStream.PixelFormat, : videoStream.PixelFormat,
IsAnamorphic = videoStream.IsAnamorphic, IsAnamorphic = videoStream.IsAnamorphic
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv
? FrameDataLocation.Hardware
: FrameDataLocation.Software
}; };
foreach (IDecoder decoder in maybeDecoder)
{
currentState = decoder.NextState(currentState);
}
// easier to use nv12 for overlay // easier to use nv12 for overlay
if (context.HasSubtitleOverlay || context.HasWatermark) if (context.HasSubtitleOverlay || context.HasWatermark)
{ {
@ -294,7 +299,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
if (!videoStream.ColorParams.IsBt709 || usesVppQsv) if (!videoStream.ColorParams.IsBt709 || usesVppQsv)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
// force p010/nv12 if we're still in hardware // force p010/nv12 if we're still in hardware
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) if (currentState.FrameDataLocation == FrameDataLocation.Hardware)

16
ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs

@ -44,17 +44,19 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
EncoderHardwareAccelerationMode = HardwareAccelerationMode.None EncoderHardwareAccelerationMode = HardwareAccelerationMode.None
}; };
protected override void SetDecoder( protected override Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context)
ICollection<IPipelineStep> pipelineSteps)
{ {
foreach (IDecoder decoder in GetSoftwareDecoder(videoStream)) foreach (IDecoder decoder in GetSoftwareDecoder(videoStream))
{ {
videoInputFile.AddOption(decoder); videoInputFile.AddOption(decoder);
return Some(decoder);
} }
return None;
} }
protected virtual Option<IEncoder> GetEncoder( protected virtual Option<IEncoder> GetEncoder(
@ -71,6 +73,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
Option<WatermarkInputFile> watermarkInputFile, Option<WatermarkInputFile> watermarkInputFile,
Option<SubtitleInputFile> subtitleInputFile, Option<SubtitleInputFile> subtitleInputFile,
PipelineContext context, PipelineContext context,
Option<IDecoder> maybeDecoder,
FFmpegState ffmpegState, FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,
@ -88,6 +91,11 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
PaddedSize = videoStream.FrameSize PaddedSize = videoStream.FrameSize
}; };
foreach (IDecoder decoder in maybeDecoder)
{
currentState = decoder.NextState(currentState);
}
SetDeinterlace(videoInputFile, context, currentState); SetDeinterlace(videoInputFile, context, currentState);
currentState = SetScale(videoInputFile, videoStream, desiredState, currentState); currentState = SetScale(videoInputFile, videoStream, desiredState, currentState);
@ -139,7 +147,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
{ {
if (!videoStream.ColorParams.IsBt709) if (!videoStream.ColorParams.IsBt709)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat); var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);
currentState = colorspace.NextState(currentState); currentState = colorspace.NextState(currentState);
result.Add(colorspace); result.Add(colorspace);

22
ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs

@ -85,12 +85,11 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
}; };
} }
protected override void SetDecoder( protected override Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context)
ICollection<IPipelineStep> pipelineSteps)
{ {
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch
{ {
@ -101,7 +100,10 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
foreach (IDecoder decoder in maybeDecoder) foreach (IDecoder decoder in maybeDecoder)
{ {
videoInputFile.AddOption(decoder); videoInputFile.AddOption(decoder);
return Some(decoder);
} }
return None;
} }
protected override FilterChain SetVideoFilters( protected override FilterChain SetVideoFilters(
@ -110,6 +112,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
Option<WatermarkInputFile> watermarkInputFile, Option<WatermarkInputFile> watermarkInputFile,
Option<SubtitleInputFile> subtitleInputFile, Option<SubtitleInputFile> subtitleInputFile,
PipelineContext context, PipelineContext context,
Option<IDecoder> maybeDecoder,
FFmpegState ffmpegState, FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,
@ -122,16 +125,15 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
{ {
ScaledSize = videoStream.FrameSize, ScaledSize = videoStream.FrameSize,
PaddedSize = videoStream.FrameSize, PaddedSize = videoStream.FrameSize,
PixelFormat = videoStream.PixelFormat, PixelFormat = videoStream.PixelFormat,
IsAnamorphic = videoStream.IsAnamorphic, IsAnamorphic = videoStream.IsAnamorphic,
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi
? FrameDataLocation.Hardware
: FrameDataLocation.Software
}; };
foreach (IDecoder decoder in maybeDecoder)
{
currentState = decoder.NextState(currentState);
}
// easier to use nv12 for overlay // easier to use nv12 for overlay
if (context.HasSubtitleOverlay || context.HasWatermark) if (context.HasSubtitleOverlay || context.HasWatermark)
{ {
@ -246,7 +248,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
if (!videoStream.ColorParams.IsBt709) if (!videoStream.ColorParams.IsBt709)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter( var colorspace = new ColorspaceFilter(
currentState, currentState,
videoStream, videoStream,

10
ErsatzTV.FFmpeg/Pipeline/VideoToolboxPipelineBuilder.cs

@ -68,12 +68,11 @@ public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder
}; };
} }
protected override void SetDecoder( protected override Option<IDecoder> SetDecoder(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState, FFmpegState ffmpegState,
PipelineContext context, PipelineContext context)
ICollection<IPipelineStep> pipelineSteps)
{ {
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch
{ {
@ -85,7 +84,10 @@ public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder
foreach (IDecoder decoder in maybeDecoder) foreach (IDecoder decoder in maybeDecoder)
{ {
videoInputFile.AddOption(decoder); videoInputFile.AddOption(decoder);
return Some(decoder);
} }
return None;
} }
protected override Option<IEncoder> GetEncoder(FFmpegState ffmpegState, FrameState currentState, FrameState desiredState) protected override Option<IEncoder> GetEncoder(FFmpegState ffmpegState, FrameState currentState, FrameState desiredState)
@ -113,7 +115,7 @@ public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder
{ {
if (!videoStream.ColorParams.IsBt709) if (!videoStream.ColorParams.IsBt709)
{ {
_logger.LogDebug("Adding colorspace filter"); // _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat); var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);
currentState = colorspace.NextState(currentState); currentState = colorspace.NextState(currentState);
result.Add(colorspace); result.Add(colorspace);

6
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -15,12 +15,12 @@
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

26
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -125,38 +125,38 @@ public class TranscodingTests
public static VideoScanKind[] VideoScanKinds = public static VideoScanKind[] VideoScanKinds =
{ {
VideoScanKind.Progressive, VideoScanKind.Progressive,
// VideoScanKind.Interlaced VideoScanKind.Interlaced
}; };
public static InputFormat[] InputFormats = public static InputFormat[] InputFormats =
{ {
// // // example format that requires colorspace filter // // // example format that requires colorspace filter
// new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"),
// // // //
// // // example format that requires setparams filter // // // example format that requires setparams filter
// new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty),
// // // //
// // // new("libx264", "yuvj420p"), // // // new("libx264", "yuvj420p"),
// new("libx264", "yuv420p10le"), new("libx264", "yuv420p10le"),
// // // new("libx264", "yuv444p10le"), // // // new("libx264", "yuv444p10le"),
// // // //
// // // new("mpeg1video", "yuv420p"), // // // new("mpeg1video", "yuv420p"),
// // // // // //
// // // new("mpeg2video", "yuv420p"), new("mpeg2video", "yuv420p"),
// // // //
// new("libx265", "yuv420p"), new("libx265", "yuv420p"),
// new("libx265", "yuv420p10le"), new("libx265", "yuv420p10le"),
// //
// // new("mpeg4", "yuv420p"), // new("mpeg4", "yuv420p"),
// // //
new("libvpx-vp9", "yuv420p"), // new("libvpx-vp9", "yuv420p"),
new("libvpx-vp9", "yuv420p10le"), // new("libvpx-vp9", "yuv420p10le"),
// //
// // // new("libaom-av1", "yuv420p") // // // new("libaom-av1", "yuv420p")
// // // av1 yuv420p10le 51 // // // av1 yuv420p10le 51
// // // //
// // new("msmpeg4v2", "yuv420p"), // new("msmpeg4v2", "yuv420p"),
// // new("msmpeg4v3", "yuv420p") new("msmpeg4v3", "yuv420p")
// //
// // wmv3 yuv420p 1 // // wmv3 yuv420p 1
}; };

3
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -18,8 +18,7 @@
<PackageReference Include="CliWrap" Version="3.6.0" /> <PackageReference Include="CliWrap" Version="3.6.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.2" /> <PackageReference Include="LanguageExt.Core" Version="4.4.2" />
<PackageReference Include="MediatR" Version="11.1.0" /> <PackageReference Include="MediatR" Version="12.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />

2
ErsatzTV.Scanner/Program.cs

@ -167,7 +167,7 @@ public class Program
// TODO: real bugsnag? // TODO: real bugsnag?
services.AddSingleton<IClient>(_ => new BugsnagNoopClient()); services.AddSingleton<IClient>(_ => new BugsnagNoopClient());
services.AddMediatR(typeof(Worker).Assembly); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>());
services.AddMemoryCache(); services.AddMemoryCache();
services.AddHostedService<Worker>(); services.AddHostedService<Worker>();

13
ErsatzTV/ErsatzTV.csproj

@ -54,17 +54,16 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" /> <PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
<PackageReference Include="FluentValidation" Version="11.4.0" /> <PackageReference Include="FluentValidation" Version="11.5.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageReference Include="HtmlSanitizer" Version="8.0.645" /> <PackageReference Include="HtmlSanitizer" Version="8.0.645" />
<PackageReference Include="LanguageExt.Core" Version="4.4.2" /> <PackageReference Include="LanguageExt.Core" Version="4.4.2" />
<PackageReference Include="Markdig" Version="0.30.4" /> <PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" /> <PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -72,7 +71,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="6.1.8" /> <PackageReference Include="MudBlazor" Version="6.1.9" />
<PackageReference Include="NaturalSort.Extension" Version="4.0.0" /> <PackageReference Include="NaturalSort.Extension" Version="4.0.0" />
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="11.1.0" /> <PackageReference Include="PPioli.FluentValidation.Blazor" Version="11.1.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.3.2" /> <PackageReference Include="Refit.HttpClientFactory" Version="6.3.2" />

3
ErsatzTV/Startup.cs

@ -58,7 +58,6 @@ using ErsatzTV.Services.RunOnce;
using FluentValidation; using FluentValidation;
using FluentValidation.AspNetCore; using FluentValidation.AspNetCore;
using Ganss.Xss; using Ganss.Xss;
using MediatR;
using MediatR.Courier.DependencyInjection; using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@ -329,7 +328,7 @@ public class Startup
SqlMapper.AddTypeHandler(new GuidHandler()); SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler()); SqlMapper.AddTypeHandler(new TimeSpanHandler());
services.AddMediatR(typeof(GetAllChannels).Assembly); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<GetAllChannels>());
services.AddRefitClient<IPlexTvApi>() services.AddRefitClient<IPlexTvApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2")); .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2"));

Loading…
Cancel
Save