Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be7311e620 | ||
|
|
03be372070 | ||
|
|
d196308ee9 | ||
|
|
3d68b0f055 | ||
|
|
37e32f06ad | ||
|
|
c43ca2837d | ||
|
|
992121f308 | ||
|
|
04adbfeffa | ||
|
|
1fc905c6ad | ||
|
|
4b5dff2159 | ||
|
|
2a5edf8214 | ||
|
|
69912c8cae | ||
|
|
fd3de2d82a | ||
|
|
6ba9404752 |
+24
-1
@@ -5,6 +5,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.2-alpha] - 2021-10-12
|
||||
### Added
|
||||
- Include more cuda (nvidia) filters in docker image
|
||||
- Enable deinterlacing with nvidia using new `yadif_cuda` filter
|
||||
- Add two HLS Segmenter settings: idle timeout and work-ahead limit
|
||||
- `HLS Segmenter Idle Timeout` - the number of seconds to keep transcoding a channel while no requests have been received from any client
|
||||
- This setting must be greater than or equal to 30 (seconds)
|
||||
- `Work-Ahead HLS Segmenter Limit` - the number of segmenters (channels) that will work-ahead simultaneously (if multiple channels are being watched)
|
||||
- "working ahead" means transcoding at full speed, which can take a lot of resources
|
||||
- This setting must be greater than or equal to 0
|
||||
- Add more watermark locations ("middle" of each side)
|
||||
- Add `VAAPI Device` setting to ffmpeg profile to support installations with multiple video cards
|
||||
- Add *experimental* `RadeonSI` option for `VAAPI Driver` and include mesa drivers in vaapi docker image
|
||||
|
||||
### Changed
|
||||
- Upgrade ffmpeg from 4.3 to 4.4 in all docker images
|
||||
- Upgrading from 4.3 to 4.4 is recommended for all installations
|
||||
- Move `VAAPI Driver` from settings page to ffmpeg profile to support installations with multiple video cards
|
||||
|
||||
### Fixed
|
||||
- Fix some transcoding edge cases with nvidia and pixel format `yuv420p10le`
|
||||
|
||||
## [0.1.1-alpha] - 2021-10-10
|
||||
### Added
|
||||
- Add music video album to search index
|
||||
@@ -669,7 +691,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...HEAD
|
||||
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||
[0.1.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
|
||||
[0.1.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.62-alpha...v0.1.0-alpha
|
||||
[0.0.62-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.61-alpha...v0.0.62-alpha
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -10,6 +11,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
int ThreadCount,
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
int ResolutionId,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
ThreadCount = threadCount,
|
||||
Transcode = request.Transcode,
|
||||
HardwareAcceleration = request.HardwareAcceleration,
|
||||
VaapiDriver = request.VaapiDriver,
|
||||
VaapiDevice = request.VaapiDevice,
|
||||
ResolutionId = resolutionId,
|
||||
NormalizeVideo = request.NormalizeVideo,
|
||||
VideoCodec = request.VideoCodec,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -11,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
int ThreadCount,
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
int ResolutionId,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
p.ThreadCount = update.ThreadCount;
|
||||
p.Transcode = update.Transcode;
|
||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||
p.VaapiDriver = update.VaapiDriver;
|
||||
p.VaapiDevice = update.VaapiDevice;
|
||||
p.ResolutionId = update.ResolutionId;
|
||||
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
|
||||
p.VideoCodec = update.VideoCodec;
|
||||
|
||||
@@ -116,8 +116,12 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
}
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegVaapiDriver,
|
||||
(int)request.Settings.VaapiDriver);
|
||||
ConfigElementKey.FFmpegSegmenterTimeout,
|
||||
request.Settings.HlsSegmenterIdleTimeout);
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegWorkAheadSegmenters,
|
||||
request.Settings.WorkAheadSegmenterLimit);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Application.Resolutions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles
|
||||
{
|
||||
@@ -9,6 +10,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
int ThreadCount,
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
ResolutionViewModel Resolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles
|
||||
namespace ErsatzTV.Application.FFmpegProfiles
|
||||
{
|
||||
public class FFmpegSettingsViewModel
|
||||
{
|
||||
@@ -10,6 +8,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
public bool SaveReports { get; set; }
|
||||
public int? GlobalWatermarkId { get; set; }
|
||||
public VaapiDriver VaapiDriver { get; set; }
|
||||
public int HlsSegmenterIdleTimeout { get; set; }
|
||||
public int WorkAheadSegmenterLimit { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
profile.ThreadCount,
|
||||
profile.Transcode,
|
||||
profile.HardwareAcceleration,
|
||||
profile.VaapiDriver,
|
||||
profile.VaapiDevice,
|
||||
Project(profile.Resolution),
|
||||
profile.NormalizeVideo,
|
||||
profile.VideoCodec,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
@@ -29,8 +28,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
Option<int> watermark =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||
Option<int> vaapiDriver =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegVaapiDriver);
|
||||
Option<int> hlsSegmenterIdleTimeout =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
|
||||
Option<int> workAheadSegmenterLimit =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
|
||||
|
||||
var result = new FFmpegSettingsViewModel
|
||||
{
|
||||
@@ -39,7 +40,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
||||
SaveReports = await saveReports.IfNoneAsync(false),
|
||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
|
||||
VaapiDriver = (VaapiDriver)await vaapiDriver.IfNoneAsync(0)
|
||||
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
|
||||
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
|
||||
};
|
||||
|
||||
foreach (int watermarkId in watermark)
|
||||
|
||||
@@ -3,9 +3,11 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -18,18 +20,21 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
private readonly ILogger<StartFFmpegSessionHandler> _logger;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
|
||||
public StartFFmpegSessionHandler(
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<StartFFmpegSessionHandler> logger,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService)
|
||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
|
||||
@@ -42,12 +47,16 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
|
||||
private async Task<Unit> StartProcess(StartFFmpegSession request)
|
||||
{
|
||||
TimeSpan idleTimeout = await _configElementRepository
|
||||
.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout)
|
||||
.Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1)));
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService<HlsSessionWorker>();
|
||||
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
|
||||
|
||||
// fire and forget worker
|
||||
_ = worker.Run(request.ChannelNumber)
|
||||
_ = worker.Run(request.ChannelNumber, idleTimeout)
|
||||
.ContinueWith(
|
||||
_ => _ffmpegSegmenterService.SessionWorkers.TryRemove(
|
||||
request.ChannelNumber,
|
||||
|
||||
@@ -6,23 +6,27 @@ using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using ErsatzTV.Application.Streaming.Queries;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Timer = System.Timers.Timer;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming
|
||||
{
|
||||
public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
private static int _workAheadCount;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<HlsSessionWorker> _logger;
|
||||
private DateTimeOffset _lastAccess;
|
||||
private DateTimeOffset _transcodedUntil;
|
||||
private readonly Timer _timer = new(TimeSpan.FromMinutes(2).TotalMilliseconds) { AutoReset = false };
|
||||
private Timer _timer;
|
||||
private readonly object _sync = new();
|
||||
private DateTimeOffset _playlistStart;
|
||||
|
||||
@@ -40,19 +44,23 @@ namespace ErsatzTV.Application.Streaming
|
||||
{
|
||||
_lastAccess = DateTimeOffset.Now;
|
||||
|
||||
_timer.Stop();
|
||||
_timer.Start();
|
||||
_timer?.Stop();
|
||||
_timer?.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Run(string channelNumber)
|
||||
public async Task Run(string channelNumber, TimeSpan idleTimeout)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
void Cancel(object o, ElapsedEventArgs e) => cts.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_timer.Elapsed += Cancel;
|
||||
lock (_sync)
|
||||
{
|
||||
_timer = new Timer(idleTimeout.TotalMilliseconds) { AutoReset = false };
|
||||
_timer.Elapsed += Cancel;
|
||||
}
|
||||
|
||||
CancellationToken cancellationToken = cts.Token;
|
||||
|
||||
@@ -62,16 +70,15 @@ namespace ErsatzTV.Application.Streaming
|
||||
_transcodedUntil = DateTimeOffset.Now;
|
||||
_playlistStart = _transcodedUntil;
|
||||
|
||||
// start initial transcode WITHOUT realtime throttle
|
||||
if (!await Transcode(channelNumber, true, false, cancellationToken))
|
||||
bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
|
||||
if (!await Transcode(channelNumber, true, !initialWorkAhead, cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// TODO: configurable? 5 minutes?
|
||||
if (DateTimeOffset.Now - _lastAccess > TimeSpan.FromMinutes(2))
|
||||
if (DateTimeOffset.Now - _lastAccess > idleTimeout)
|
||||
{
|
||||
_logger.LogInformation("Stopping idle HLS session for channel {Channel}", channelNumber);
|
||||
return;
|
||||
@@ -83,7 +90,9 @@ namespace ErsatzTV.Application.Streaming
|
||||
{
|
||||
// only use realtime encoding when we're at least 30 seconds ahead
|
||||
bool realtime = transcodedBuffer >= TimeSpan.FromSeconds(30);
|
||||
if (!await Transcode(channelNumber, false, realtime, cancellationToken))
|
||||
bool subsequentWorkAhead =
|
||||
!realtime && Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
|
||||
if (!await Transcode(channelNumber, false, !subsequentWorkAhead, cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -97,7 +106,10 @@ namespace ErsatzTV.Application.Streaming
|
||||
}
|
||||
finally
|
||||
{
|
||||
_timer.Elapsed -= Cancel;
|
||||
lock (_sync)
|
||||
{
|
||||
_timer.Elapsed -= Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +117,18 @@ namespace ErsatzTV.Application.Streaming
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!realtime)
|
||||
{
|
||||
Interlocked.Increment(ref _workAheadCount);
|
||||
_logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", channelNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"HLS segmenter will NOT work ahead for channel {Channel}",
|
||||
channelNumber);
|
||||
}
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
@@ -134,7 +158,7 @@ namespace ErsatzTV.Application.Streaming
|
||||
foreach (PlayoutItemProcessModel processModel in result.RightAsEnumerable())
|
||||
{
|
||||
await TrimAndDelete(channelNumber, cancellationToken);
|
||||
|
||||
|
||||
Process process = processModel.Process;
|
||||
|
||||
_logger.LogDebug(
|
||||
@@ -166,6 +190,10 @@ namespace ErsatzTV.Application.Streaming
|
||||
_logger.LogError(ex, "Error transcoding channel {Channel}", channelNumber);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _workAheadCount);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -203,5 +231,13 @@ namespace ErsatzTV.Application.Streaming
|
||||
_playlistStart = trimResult.PlaylistStart;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> GetWorkAheadLimit()
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
|
||||
return await repo.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
|
||||
.Map(maybeCount => maybeCount.Match(identity, () => 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -100,10 +100,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
watermarkId => dbContext.ChannelWatermarks
|
||||
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
|
||||
|
||||
Option<VaapiDriver> maybeVaapiDriver = await dbContext.ConfigElements
|
||||
.GetValue<int>(ConfigElementKey.FFmpegVaapiDriver)
|
||||
.MapT(i => (VaapiDriver)i);
|
||||
|
||||
Process process = await _ffmpegProcessService.ForPlayoutItem(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
@@ -113,7 +109,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
playoutItemWithPath.PlayoutItem.StartOffset,
|
||||
request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now,
|
||||
maybeGlobalWatermark,
|
||||
maybeVaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
request.StartAtZero,
|
||||
request.HlsRealtime);
|
||||
|
||||
|
||||
@@ -341,43 +341,24 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
// TODO: get yadif_cuda working in docker
|
||||
// [TestCase(true, false, false, "[0:V]yadif_cuda[v]", "[v]")]
|
||||
// [TestCase(
|
||||
// true,
|
||||
// true,
|
||||
// false,
|
||||
// "[0:V]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]",
|
||||
// "[v]")]
|
||||
// [TestCase(
|
||||
// true,
|
||||
// false,
|
||||
// true,
|
||||
// "[0:V]yadif_cuda,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
// "[v]")]
|
||||
// [TestCase(
|
||||
// true,
|
||||
// true,
|
||||
// true,
|
||||
// "[0:V]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
// "[v]")]
|
||||
[TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[0:0]yadif_cuda,scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]yadif_cuda,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]yadif_cuda,scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
|
||||
@@ -128,6 +128,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
p1.Start();
|
||||
await p1.WaitForExitAsync();
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
p1.WaitForExit();
|
||||
p1.ExitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
@@ -184,7 +186,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
now,
|
||||
now,
|
||||
None,
|
||||
None,
|
||||
VaapiDriver.Default,
|
||||
"/dev/dri/renderD128",
|
||||
false,
|
||||
false);
|
||||
|
||||
@@ -192,9 +195,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
process.Start().Should().BeTrue();
|
||||
|
||||
await process.StandardOutput.ReadToEndAsync();
|
||||
process.BeginOutputReadLine();
|
||||
string error = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
process.WaitForExit();
|
||||
|
||||
string[] unsupportedMessages =
|
||||
{
|
||||
@@ -205,9 +210,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
if (profileAcceleration != HardwareAccelerationKind.None && unsupportedMessages.Any(error.Contains))
|
||||
{
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
|
||||
process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}");
|
||||
Assert.Warn("Unsupported on this hardware");
|
||||
Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}");
|
||||
}
|
||||
else if (error.Contains("Impossible to convert between"))
|
||||
{
|
||||
@@ -216,7 +221,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
process.ExitCode.Should().Be(0, error);
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
process.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
BottomRight = 0,
|
||||
BottomLeft = 1,
|
||||
TopRight = 2,
|
||||
TopLeft = 3
|
||||
TopLeft = 3,
|
||||
TopMiddle = 4,
|
||||
RightMiddle = 5,
|
||||
BottomMiddle = 6,
|
||||
LeftMiddle = 7
|
||||
}
|
||||
|
||||
public enum ChannelWatermarkSize
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||
public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id");
|
||||
public static ConfigElementKey FFmpegVaapiDriver => new("ffmpeg.vaapi_driver");
|
||||
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
|
||||
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
|
||||
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public record FFmpegProfile
|
||||
{
|
||||
@@ -7,6 +9,8 @@
|
||||
public int ThreadCount { get; set; }
|
||||
public bool Transcode { get; set; }
|
||||
public HardwareAccelerationKind HardwareAcceleration { get; set; }
|
||||
public VaapiDriver VaapiDriver { get; set; }
|
||||
public string VaapiDevice { get; set; }
|
||||
public int ResolutionId { get; set; }
|
||||
public Resolution Resolution { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
@@ -39,7 +43,8 @@
|
||||
AudioChannels = 2,
|
||||
AudioSampleRate = 48,
|
||||
NormalizeVideo = true,
|
||||
NormalizeAudio = true
|
||||
NormalizeAudio = true,
|
||||
HardwareAcceleration = HardwareAccelerationKind.None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public record FFmpegComplexFilter(string ComplexFilter, string VideoLabel, string AudioLabel);
|
||||
public record FFmpegComplexFilter(string ComplexFilter, string VideoLabel, string AudioLabel, string PixelFormat);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string filter = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Qsv => "deinterlace_qsv",
|
||||
HardwareAccelerationKind.Nvenc => "", // TODO: yadif_cuda support in docker
|
||||
HardwareAccelerationKind.Nvenc => "yadif_cuda",
|
||||
HardwareAccelerationKind.Vaapi => "deinterlace_vaapi",
|
||||
_ => "yadif=1"
|
||||
};
|
||||
@@ -156,8 +156,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string filter = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
|
||||
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
|
||||
$"hwdownload,format=p010le,format=nv12,hwupload,scale_npp={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" =>
|
||||
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Nvenc => $"scale_npp={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}",
|
||||
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
||||
@@ -180,7 +180,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string format = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
|
||||
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _pixelFormat == "yuv420p10le" =>
|
||||
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
|
||||
"format=p010le,format=nv12",
|
||||
_ => "format=nv12"
|
||||
};
|
||||
@@ -206,6 +206,10 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
ChannelWatermarkLocation.BottomLeft => $"x={horizontalMargin}:y=H-h-{verticalMargin}",
|
||||
ChannelWatermarkLocation.TopLeft => $"x={horizontalMargin}:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.TopRight => $"x=W-w-{horizontalMargin}:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.TopMiddle => $"x=(W-w)/2:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.RightMiddle => $"x=W-w-{horizontalMargin}:y=(H-h)/2",
|
||||
ChannelWatermarkLocation.BottomMiddle => $"x=(W-w)/2:y=H-h-{verticalMargin}",
|
||||
ChannelWatermarkLocation.LeftMiddle => $"x={horizontalMargin}:y=(H-h)/2",
|
||||
_ => $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}"
|
||||
};
|
||||
|
||||
@@ -234,6 +238,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
_padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2"));
|
||||
|
||||
string outputPixelFormat = null;
|
||||
|
||||
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None &&
|
||||
string.IsNullOrWhiteSpace(watermarkOverlay))
|
||||
{
|
||||
@@ -245,6 +251,19 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
videoFilterQueue.Add(upload);
|
||||
}
|
||||
|
||||
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
|
||||
{
|
||||
switch (acceleration, _videoEncoder, _pixelFormat)
|
||||
{
|
||||
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"):
|
||||
outputPixelFormat = "yuv420p";
|
||||
break;
|
||||
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"):
|
||||
outputPixelFormat = "yuv444p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasAudioFilters = audioFilterQueue.Any();
|
||||
if (hasAudioFilters)
|
||||
{
|
||||
@@ -307,7 +326,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
var filterResult = complexFilter.ToString();
|
||||
return string.IsNullOrWhiteSpace(filterResult)
|
||||
? Option<FFmpegComplexFilter>.None
|
||||
: new FFmpegComplexFilter(filterResult, videoLabel, audioLabel);
|
||||
: new FFmpegComplexFilter(filterResult, videoLabel, audioLabel, outputPixelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,22 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
result.VideoCodec = ffmpegProfile.VideoCodec;
|
||||
result.VideoBitrate = ffmpegProfile.VideoBitrate;
|
||||
result.VideoBufferSize = ffmpegProfile.VideoBufferSize;
|
||||
|
||||
result.VideoDecoder =
|
||||
(result.HardwareAcceleration, videoStream.Codec, videoStream.PixelFormat) switch
|
||||
{
|
||||
(HardwareAccelerationKind.Nvenc, "h264", "yuv420p10le" or "yuv444p" or "yuv444p10le") =>
|
||||
"h264",
|
||||
(HardwareAccelerationKind.Nvenc, "hevc", "yuv444p" or "yuv444p10le") => "hevc",
|
||||
(HardwareAccelerationKind.Nvenc, "h264", _) => "h264_cuvid",
|
||||
(HardwareAccelerationKind.Nvenc, "hevc", _) => "hevc_cuvid",
|
||||
(HardwareAccelerationKind.Nvenc, "mpeg2video", _) => "mpeg2_cuvid",
|
||||
(HardwareAccelerationKind.Nvenc, "mpeg4", _) => "mpeg4_cuvid",
|
||||
(HardwareAccelerationKind.Qsv, "h264", _) => "h264_qsv",
|
||||
(HardwareAccelerationKind.Qsv, "hevc", _) => "hevc_qsv",
|
||||
(HardwareAccelerationKind.Qsv, "mpeg2video", _) => "mpeg2_qsv",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,13 +32,6 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
internal class FFmpegProcessBuilder
|
||||
{
|
||||
private static readonly Dictionary<string, string> QsvMap = new()
|
||||
{
|
||||
{ "h264", "h264_qsv" },
|
||||
{ "hevc", "hevc_qsv" },
|
||||
{ "mpeg2video", "mpeg2_qsv" }
|
||||
};
|
||||
|
||||
private readonly List<string> _arguments = new();
|
||||
private readonly string _ffmpegPath;
|
||||
private readonly bool _saveReports;
|
||||
@@ -46,7 +39,9 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
|
||||
private bool _isConcat;
|
||||
private VaapiDriver _vaapiDriver;
|
||||
private string _vaapiDevice;
|
||||
private HardwareAccelerationKind _hwAccel;
|
||||
private string _outputPixelFormat;
|
||||
|
||||
public FFmpegProcessBuilder(string ffmpegPath, bool saveReports, ILogger logger)
|
||||
{
|
||||
@@ -55,13 +50,17 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithVaapiDriver(Option<VaapiDriver> maybeVaapiDriver)
|
||||
public FFmpegProcessBuilder WithVaapiDriver(VaapiDriver vaapiDriver, string vaapiDevice)
|
||||
{
|
||||
foreach (VaapiDriver vaapiDriver in maybeVaapiDriver)
|
||||
if (vaapiDriver != VaapiDriver.Default)
|
||||
{
|
||||
_vaapiDriver = vaapiDriver;
|
||||
}
|
||||
|
||||
_vaapiDevice = string.IsNullOrWhiteSpace(vaapiDevice)
|
||||
? "/dev/dri/renderD128"
|
||||
: vaapiDevice;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithHardwareAcceleration(HardwareAccelerationKind hwAccel)
|
||||
public FFmpegProcessBuilder WithHardwareAcceleration(HardwareAccelerationKind hwAccel, string pixelFormat, string encoder)
|
||||
{
|
||||
_hwAccel = hwAccel;
|
||||
|
||||
@@ -85,16 +84,24 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
_arguments.Add("qsv=qsv:MFX_IMPL_hw_any");
|
||||
break;
|
||||
case HardwareAccelerationKind.Nvenc:
|
||||
string outputFormat = (encoder, pixelFormat) switch
|
||||
{
|
||||
("hevc_nvenc", "yuv420p10le") => "p010le",
|
||||
("h264_nvenc", "yuv420p10le") => "p010le",
|
||||
// ("hevc_nvenc", "yuv444p10le") => "p016le",
|
||||
_ => "cuda"
|
||||
};
|
||||
|
||||
_arguments.Add("-hwaccel");
|
||||
_arguments.Add("cuda");
|
||||
_arguments.Add("-hwaccel_output_format");
|
||||
_arguments.Add("cuda");
|
||||
_arguments.Add(outputFormat);
|
||||
break;
|
||||
case HardwareAccelerationKind.Vaapi:
|
||||
_arguments.Add("-hwaccel");
|
||||
_arguments.Add("vaapi");
|
||||
_arguments.Add("-vaapi_device");
|
||||
_arguments.Add("/dev/dri/renderD128");
|
||||
_arguments.Add(_vaapiDevice);
|
||||
_arguments.Add("-hwaccel_output_format");
|
||||
_arguments.Add("vaapi");
|
||||
break;
|
||||
@@ -199,12 +206,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithInputCodec(string input, HardwareAccelerationKind hwAccel, string codec, string pixelFormat)
|
||||
public FFmpegProcessBuilder WithInputCodec(string input, string decoder, string codec, string pixelFormat)
|
||||
{
|
||||
if (hwAccel == HardwareAccelerationKind.Qsv && QsvMap.TryGetValue(codec, out string qsvCodec))
|
||||
if (!string.IsNullOrWhiteSpace(decoder))
|
||||
{
|
||||
_arguments.Add("-c:v");
|
||||
_arguments.Add(qsvCodec);
|
||||
_arguments.Add(decoder);
|
||||
}
|
||||
|
||||
_complexFilterBuilder = _complexFilterBuilder
|
||||
@@ -364,6 +371,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"-sc_threshold", "0" // disable scene change detection
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_outputPixelFormat))
|
||||
{
|
||||
arguments.AddRange(new[] { "-pix_fmt", _outputPixelFormat });
|
||||
}
|
||||
|
||||
string[] videoBitrateArgs = playbackSettings.VideoBitrate.Match(
|
||||
bitrate =>
|
||||
new[]
|
||||
@@ -471,6 +483,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
_arguments.Add(filter.ComplexFilter);
|
||||
videoLabel = filter.VideoLabel;
|
||||
audioLabel = filter.AudioLabel;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PixelFormat))
|
||||
{
|
||||
_outputPixelFormat = filter.PixelFormat;
|
||||
}
|
||||
});
|
||||
|
||||
_arguments.Add("-map");
|
||||
@@ -510,6 +527,9 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
case VaapiDriver.iHD:
|
||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "iHD");
|
||||
break;
|
||||
case VaapiDriver.RadeonSI:
|
||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "radeonsi");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
Option<VaapiDriver> maybeVaapiDriver,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
bool startAtZero,
|
||||
bool hlsRealtime)
|
||||
{
|
||||
@@ -64,13 +65,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||
.WithThreads(playbackSettings.ThreadCount)
|
||||
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
|
||||
.WithVaapiDriver(maybeVaapiDriver)
|
||||
.WithVaapiDriver(vaapiDriver, vaapiDevice)
|
||||
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration, videoStream.PixelFormat, playbackSettings.VideoCodec)
|
||||
.WithQuiet()
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithSeek(playbackSettings.StreamSeek)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec, videoStream.PixelFormat)
|
||||
.WithInputCodec(path, playbackSettings.VideoDecoder, videoStream.Codec, videoStream.PixelFormat)
|
||||
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
|
||||
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
Default = 0,
|
||||
iHD = 1,
|
||||
i965 = 2
|
||||
i965 = 2,
|
||||
RadeonSI = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
||||
{
|
||||
public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck
|
||||
{
|
||||
private const string BundledVersion = "N-104321-ga742ba60f1";
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository)
|
||||
@@ -72,17 +73,17 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
||||
{
|
||||
if (version.StartsWith("3."))
|
||||
{
|
||||
return FailResult($"{app} version {version} is too old; please install 4.3!");
|
||||
return FailResult($"{app} version {version} is too old; please install 4.4!");
|
||||
}
|
||||
|
||||
if (version.StartsWith("4.4"))
|
||||
if (version.StartsWith("4.3"))
|
||||
{
|
||||
return FailResult($"{app} version 4.4 is known to have issues; please install 4.3!");
|
||||
return WarningResult($"{app} version 4.4 is now supported, please upgrade from 4.3!");
|
||||
}
|
||||
|
||||
if (!version.StartsWith("4.3"))
|
||||
if (!version.StartsWith("4.4") && version != BundledVersion)
|
||||
{
|
||||
return WarningResult($"{app} version {version} is unexpected and may have problems; please install 4.3!");
|
||||
return WarningResult($"{app} version {version} is unexpected and may have problems; please install 4.4!");
|
||||
}
|
||||
|
||||
return None;
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Health;
|
||||
using ErsatzTV.Core.Health.Checks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Health.Checks
|
||||
{
|
||||
public class VaapiDriverHealthCheck : BaseHealthCheck, IVaapiDriverHealthCheck
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public VaapiDriverHealthCheck(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
public VaapiDriverHealthCheck(IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
protected override string Title => "VAAPI Driver";
|
||||
|
||||
public async Task<HealthCheckResult> Check()
|
||||
{
|
||||
string version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion ?? "unknown";
|
||||
|
||||
if (!version.Contains("docker", StringComparison.OrdinalIgnoreCase) ||
|
||||
!version.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
List<FFmpegProfile> profiles = await dbContext.FFmpegProfiles
|
||||
.Filter(p => p.HardwareAcceleration == HardwareAccelerationKind.Vaapi)
|
||||
.ToListAsync();
|
||||
|
||||
if (profiles.Count == 0)
|
||||
{
|
||||
return NotApplicableResult();
|
||||
}
|
||||
|
||||
Option<int> maybeVaapiDriver =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegVaapiDriver);
|
||||
var vaapiDriver = (VaapiDriver)await maybeVaapiDriver.IfNoneAsync(0);
|
||||
if (vaapiDriver == VaapiDriver.Default)
|
||||
{
|
||||
return InfoResult(
|
||||
"Settings > FFmpeg Settings > VAAPI Driver is set to Default; selecting iHD (Gen 8+) or i965 (up to Gen 9) may offer better performance");
|
||||
}
|
||||
|
||||
return OkResult();
|
||||
|
||||
var defaultProfiles = profiles
|
||||
.Filter(p => p.VaapiDriver == VaapiDriver.Default)
|
||||
.ToList();
|
||||
|
||||
return defaultProfiles.Any()
|
||||
? InfoResult(
|
||||
$"{defaultProfiles.Count} FFmpeg Profile{(defaultProfiles.Count > 1 ? "s are" : " is")} set to use Default VAAPI Driver; selecting iHD (Gen 8+) or i965 (up to Gen 9) may offer better performance with Intel iGPU")
|
||||
: OkResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3293
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_FFmpegProfileVaapiDriverVaapiDevice : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "VaapiDevice",
|
||||
table: "FFmpegProfile",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "VaapiDriver",
|
||||
table: "FFmpegProfile",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VaapiDevice",
|
||||
table: "FFmpegProfile");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VaapiDriver",
|
||||
table: "FFmpegProfile");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3293
File diff suppressed because it is too large
Load Diff
+19
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Update_FFmpegProfileVaapiDriverVaapiDevice : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE FFmpegProfile SET VaapiDevice = '/dev/dri/renderD128'");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE FFmpegProfile SET VaapiDriver = (SELECT IFNULL((SELECT Value AS A FROM ConfigElement WHERE Key = 'ffmpeg.vaapi_driver'), 0))");
|
||||
migrationBuilder.Sql("DELETE FROM ConfigElement WHERE Key = 'ffmpeg.vaapi_driver'");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +482,12 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<bool>("Transcode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("VaapiDevice")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VaapiDriver")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VideoBitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=playout/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playouts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=probesize/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Radeon/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Segmenter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=showtitle/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
@using ErsatzTV.Application.FFmpegProfiles
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
@using ErsatzTV.Core.FFmpeg
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.Extensions.Caching.Memory
|
||||
@inject NavigationManager _navigationManager
|
||||
@inject ILogger<FFmpegEditor> _logger
|
||||
@inject ISnackbar _snackbar
|
||||
@inject IMediator _mediator
|
||||
@inject IMemoryCache _memoryCache
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
|
||||
@@ -58,6 +62,22 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSelect Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Vaapi)" Label="VAAPI Driver" @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
|
||||
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
|
||||
{
|
||||
<MudSelectItem Value="@driver">@driver</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSelect Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Vaapi)" Label="VAAPI Device" @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
|
||||
@foreach (string device in _vaapiDevices)
|
||||
{
|
||||
<MudSelectItem Value="@device">@device</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
|
||||
</MudElement>
|
||||
@@ -107,6 +127,7 @@
|
||||
private ValidationMessageStore _messageStore;
|
||||
|
||||
private List<ResolutionViewModel> _resolutions;
|
||||
private List<string> _vaapiDevices;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
@@ -126,6 +147,13 @@
|
||||
|
||||
_editContext = new EditContext(_model);
|
||||
_messageStore = new ValidationMessageStore(_editContext);
|
||||
|
||||
if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List<string> vaapiDevices))
|
||||
{
|
||||
vaapiDevices = new List<string> { "/dev/dri/renderD128" };
|
||||
}
|
||||
|
||||
_vaapiDevices = vaapiDevices.OrderBy(s => s).ToList();
|
||||
}
|
||||
|
||||
private bool IsEdit => Id != 0;
|
||||
|
||||
@@ -58,12 +58,24 @@
|
||||
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3" Label="VAAPI Driver" @bind-Value="_ffmpegSettings.VaapiDriver" For="@(() => _ffmpegSettings.VaapiDriver)">
|
||||
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
|
||||
{
|
||||
<MudSelectItem Value="@driver">@driver</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudTextField T="int"
|
||||
Label="HLS Segmenter Idle Timeout"
|
||||
@bind-Value="_ffmpegSettings.HlsSegmenterIdleTimeout"
|
||||
Validation="@(new Func<int, string>(ValidateHlsSegmenterIdleTimeout))"
|
||||
Required="true"
|
||||
RequiredError="HLS Segmenter idle timeout is required!"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentText="Seconds"/>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudTextField T="int"
|
||||
Label="Work-Ahead HLS Segmenter Limit"
|
||||
@bind-Value="_ffmpegSettings.WorkAheadSegmenterLimit"
|
||||
Validation="@(new Func<int, string>(ValidateWorkAheadSegmenterLimit))"
|
||||
Required="true"
|
||||
RequiredError="Work-ahead HLS Segmenter limit is required!"/>
|
||||
</MudElement>
|
||||
</MudForm>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
@@ -169,6 +181,10 @@
|
||||
|
||||
private static string ValidatePlayoutDaysToBuild(int daysToBuild) => daysToBuild <= 0 ? "Days to build must be greater than zero" : null;
|
||||
|
||||
private static string ValidateHlsSegmenterIdleTimeout(int idleTimeout) => idleTimeout < 30 ? "HLS Segmenter idle timeout must be greater than or equal to 30" : null;
|
||||
|
||||
private static string ValidateWorkAheadSegmenterLimit(int limit) => limit < 0 ? "Work-Ahead HLS Segmenter limit must be greater than or equal to 0" : null;
|
||||
|
||||
private async Task LoadFFmpegProfilesAsync() =>
|
||||
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles());
|
||||
|
||||
|
||||
@@ -56,9 +56,13 @@
|
||||
For="@(() => _model.Location)"
|
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.BottomRight)">Bottom Right</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.BottomMiddle)">Bottom Middle</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.BottomLeft)">Bottom Left</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopRight)">Top Right</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.LeftMiddle)">Left Middle</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopLeft)">Top Left</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopMiddle)">Top Middle</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopRight)">Top Right</MudSelectItem>
|
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.RightMiddle)">Right Middle</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center">
|
||||
<MudItem xs="6">
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -36,6 +40,18 @@ namespace ErsatzTV.Services.RunOnce
|
||||
IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
|
||||
await repo.Upsert(ConfigElementKey.FFmpegSaveReports, false);
|
||||
}
|
||||
|
||||
if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
|
||||
IMemoryCache memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
|
||||
|
||||
var devices = localFileSystem.ListFiles("/dev/dri")
|
||||
.Filter(s => s.StartsWith("/dev/dri/render"))
|
||||
.ToList();
|
||||
|
||||
memoryCache.Set("ffmpeg.render_devices", devices);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using ErsatzTV.Application.FFmpegProfiles.Commands;
|
||||
using ErsatzTV.Application.Resolutions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
|
||||
namespace ErsatzTV.ViewModels
|
||||
{
|
||||
@@ -27,6 +28,8 @@ namespace ErsatzTV.ViewModels
|
||||
ThreadCount = viewModel.ThreadCount;
|
||||
Transcode = viewModel.Transcode;
|
||||
HardwareAcceleration = viewModel.HardwareAcceleration;
|
||||
VaapiDriver = viewModel.VaapiDriver;
|
||||
VaapiDevice = viewModel.VaapiDevice;
|
||||
VideoBitrate = viewModel.VideoBitrate;
|
||||
VideoBufferSize = viewModel.VideoBufferSize;
|
||||
VideoCodec = viewModel.VideoCodec;
|
||||
@@ -46,6 +49,8 @@ namespace ErsatzTV.ViewModels
|
||||
public int ThreadCount { get; set; }
|
||||
public bool Transcode { get; set; }
|
||||
public HardwareAccelerationKind HardwareAcceleration { get; set; }
|
||||
public VaapiDriver VaapiDriver { get; set; }
|
||||
public string VaapiDevice { get; set; }
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBufferSize { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
@@ -56,6 +61,8 @@ namespace ErsatzTV.ViewModels
|
||||
ThreadCount,
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
VaapiDriver,
|
||||
VaapiDevice,
|
||||
Resolution.Id,
|
||||
NormalizeVideo,
|
||||
VideoCodec,
|
||||
@@ -77,6 +84,8 @@ namespace ErsatzTV.ViewModels
|
||||
ThreadCount,
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
VaapiDriver,
|
||||
VaapiDevice,
|
||||
Resolution.Id,
|
||||
NormalizeVideo,
|
||||
VideoCodec,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime
|
||||
|
||||
FROM jrottenberg/ffmpeg:4.3-ubuntu2004 AS runtime-base
|
||||
FROM jasongdove/ffmpeg:4.4-ubuntu2004 AS runtime-base
|
||||
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
|
||||
RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ ARG INFO_VERSION="unknown"
|
||||
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION}
|
||||
|
||||
# final stage/image
|
||||
FROM jasongdove/ffmpeg:4.3-nvidia2004 AS runtime-base
|
||||
FROM jasongdove/ffmpeg:4.4-nvidia2004 AS runtime-base
|
||||
WORKDIR /app
|
||||
EXPOSE 8409
|
||||
COPY --from=build /app ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime
|
||||
|
||||
FROM jrottenberg/ffmpeg:4.3-nvidia2004 AS runtime-base
|
||||
FROM jasongdove/ffmpeg-base:4.4-nvidia2004 AS runtime-base
|
||||
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata \
|
||||
|
||||
@@ -23,7 +23,7 @@ ARG INFO_VERSION="unknown"
|
||||
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION}
|
||||
|
||||
# final stage/image
|
||||
FROM jasongdove/ffmpeg:4.3-vaapi2004 AS runtime-base
|
||||
FROM jasongdove/ffmpeg:4.4-vaapi2004 AS runtime-base
|
||||
WORKDIR /app
|
||||
EXPOSE 8409
|
||||
COPY --from=build /app ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime
|
||||
|
||||
FROM jrottenberg/ffmpeg:4.3-vaapi2004 AS runtime-base
|
||||
FROM jasongdove/ffmpeg-base:4.4-vaapi2004 AS runtime-base
|
||||
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
|
||||
RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev \
|
||||
tzdata \
|
||||
@@ -12,6 +12,7 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu
|
||||
build-essential \
|
||||
cmake \
|
||||
wget \
|
||||
mesa-va-drivers \
|
||||
&& mkdir /tmp/intel && cd /tmp/intel \
|
||||
&& wget -O - https://github.com/intel/libva/archive/refs/tags/2.12.0.tar.gz | tar zxf - \
|
||||
&& cd libva-2.12.0 \
|
||||
@@ -36,4 +37,4 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu
|
||||
&& apt autoremove -y \
|
||||
&& rm -rf /tmp/intel \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mv /usr/lib/x86_64-linux-gnu/dri/i* /usr/local/lib/dri/
|
||||
&& mv /usr/lib/x86_64-linux-gnu/dri/* /usr/local/lib/dri/
|
||||
|
||||
Reference in New Issue
Block a user