@page "/ffmpeg/{Id:int}" @page "/ffmpeg/add" @using System.Runtime.InteropServices @using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.Resolutions @using ErsatzTV.Core.FFmpeg @using ErsatzTV.FFmpeg.Format @using ErsatzTV.Validators @using FluentValidation.Results @using Microsoft.Extensions.Caching.Memory @implements IDisposable @inject NavigationManager NavigationManager @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @inject IMemoryCache MemoryCache @inject PersistentComponentState ApplicationState @(IsEdit ? "Save Profile" : "Add Profile")
General
Name
Thread Count
Normalize Audio
Normalize Video
Preferred Resolution
@foreach (ResolutionViewModel resolution in _resolutions) { @resolution.Name }
@if (_model.NormalizeVideo) {
Scaling Behavior
Scale and Pad Stretch Crop
Pad Mode
Hardware If Possible Software
} Video @if (_model.NormalizeVideo) {
Format
h264 hevc mpeg-2 @if (_model.HardwareAcceleration is (HardwareAccelerationKind.Nvenc or HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)) { av1 }
Profile
main high @if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc) { high444p } else { high10 }
Preset
@{ ICollection presets = FFmpegLibraryHelper.PresetsForFFmpegProfile( _model.HardwareAcceleration, _model.VideoFormat, _model.BitDepth); } @foreach (string preset in presets) { if (!string.IsNullOrWhiteSpace(preset)) { @preset } }
Allow B-Frames
Bit Depth
8-bit 10-bit
Bitrate
Buffer Size
Hardware Acceleration
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds) { @hwAccel }
@if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi) {
VAAPI Driver
@foreach (VaapiDriver driver in Enum.GetValues()) { @driver }
VAAPI Display
@foreach (string display in _vaapiDisplays) { @display }
} @if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv) {
@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")
@foreach (string device in _vaapiDevices) { @device }
} } @if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv) {
QSV Extra Hardware Frames
} else {
Tonemap Algorithm
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues()) { @algorithm }
}
Normalize Frame Rate
Normalize Colors
Auto Deinterlace Video
} else { Video will be copied as-is, including timestamps, which *will* cause issues with most clients } Audio @if (_model.NormalizeAudio) {
Format
aac ac3 aac (latm)
Bitrate
Buffer Size
Channels
Sample Rate
Normalize Loudness
Off loudnorm
@if (_model.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm) {
Target Loudness
} } else { Audio will be copied as-is, including timestamps, which *will* cause issues with most clients }
@code { private CancellationTokenSource _cts; [Parameter] public int Id { get; set; } private FFmpegProfileEditViewModel _model = new(); private readonly FFmpegProfileEditViewModelValidator _validator = new(); private MudForm _form; private List _resolutions = []; private List _hardwareAccelerationKinds = []; private List _vaapiDisplays = []; private List _vaapiDevices = []; private PersistingComponentStateSubscription _persistingSubscription; public void Dispose() { _persistingSubscription.Dispose(); _cts?.Cancel(); _cts?.Dispose(); } protected override Task OnInitializedAsync() { _persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData); return base.OnInitializedAsync(); } protected override async Task OnParametersSetAsync() { _cts?.Cancel(); _cts?.Dispose(); _cts = new CancellationTokenSource(); var token = _cts.Token; try { if (!ApplicationState.TryTakeFromJson("_resolutions", out List restoredResolutions)) { _resolutions = await Mediator.Send(new GetAllResolutions(), token); } else { _resolutions = restoredResolutions; } if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List restoredHardwareAccelerationKinds)) { _hardwareAccelerationKinds = await Mediator.Send(new GetSupportedHardwareAccelerationKinds(), token); } else { _hardwareAccelerationKinds = restoredHardwareAccelerationKinds; } if (IsEdit) { if (!ApplicationState.TryTakeFromJson("_model", out FFmpegProfileEditViewModel restoredProfile)) { Option maybeProfile = await Mediator.Send(new GetFFmpegProfileById(Id), token); foreach (FFmpegProfileViewModel profile in maybeProfile) { _model = new FFmpegProfileEditViewModel(profile); } if (maybeProfile.IsNone) { NavigationManager.NavigateTo("404"); } } else { _model = restoredProfile; } } else { _model = new FFmpegProfileEditViewModel(await Mediator.Send(new NewFFmpegProfile(), token)); } if (!_hardwareAccelerationKinds.Contains(_model.HardwareAcceleration)) { _model.HardwareAcceleration = HardwareAccelerationKind.None; } if (!MemoryCache.TryGetValue("ffmpeg.render_devices", out List vaapiDevices)) { vaapiDevices = ["/dev/dri/renderD128"]; } _vaapiDevices = vaapiDevices.OrderBy(s => s).ToList(); if (!MemoryCache.TryGetValue("ffmpeg.vaapi_displays", out List vaapiDisplays)) { vaapiDisplays = ["drm"]; } _vaapiDisplays = vaapiDisplays.OrderBy(s => s).ToList(); } catch (OperationCanceledException) { // do nothing } } private bool HardwareAccelSupportsH264Profile(HardwareAccelerationKind accel) => accel switch { HardwareAccelerationKind.None => true, HardwareAccelerationKind.Nvenc => true, HardwareAccelerationKind.Qsv => true, HardwareAccelerationKind.Vaapi => true, HardwareAccelerationKind.VideoToolbox => true, _ => false, }; private Task PersistData() { ApplicationState.PersistAsJson("_model", _model); ApplicationState.PersistAsJson("_resolutions", _resolutions); ApplicationState.PersistAsJson("_hardwareAccelerationKinds", _hardwareAccelerationKinds); return Task.CompletedTask; } private bool IsEdit => Id != 0; private async Task HandleSubmitAsync() { await _form.Validate(); if (!_form.IsValid) { return; } using var cts = new CancellationTokenSource(); try { ValidationResult result = await _validator.ValidateAsync(_model, cts.Token); if (result.IsValid) { Seq errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), cts.Token)).LeftToSeq(); errorMessage.HeadOrNone().Match( error => { Snackbar.Add("Unexpected error saving ffmpeg profile"); Logger.LogError("Unexpected error saving ffmpeg profile: {Error}", error.Value); }, () => NavigationManager.NavigateTo("ffmpeg")); } } catch (OperationCanceledException) { // do nothing } } }