add normalization options (#2802)

* add new fields to database

* update editor

* audio and video normalization settings appear to work

* implement optional color normalization

* fix transcoding tests

* update changelog
This commit is contained in:
Jason Dove
2026-01-26 23:43:56 -06:00
committed by GitHub
parent cd2bb0f2e0
commit e10b28bc0b
34 changed files with 14592 additions and 332 deletions
+25 -5
View File
@@ -98,11 +98,25 @@
<div style="align-items: center; display: flex;">
@if (_channelsThatCanPreview.Contains(context.Id))
{
<MudTooltip Text="Preview Channel">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))">
</MudIconButton>
</MudTooltip>
Option<FFmpegProfileViewModel> maybeProfile = Optional(_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId));
bool notNormalizing = maybeProfile.Any(p => !p.NormalizeVideo || !p.NormalizeAudio);
if (notNormalizing)
{
<MudTooltip Text="Preview Channel; audio and/or video may not work without normalization">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))"
Style="color: var(--mud-palette-warning-lighten);">
</MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="Preview Channel">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))">
</MudIconButton>
</MudTooltip>
}
}
else if (CanPreviewChannel(context) && _ffmpegProfilesThatCanPreview.ContainsKey(context.FFmpegProfileId) && !_ffmpegProfilesThatCanPreview[context.FFmpegProfileId])
{
@@ -272,6 +286,12 @@
//Console.WriteLine($"Checking video format {videoCodec} and audio format {audioCodec}");
// good luck
if (profile.VideoFormat == FFmpegProfileVideoFormat.Copy || profile.AudioFormat == FFmpegProfileAudioFormat.Copy)
{
return true;
}
if (string.IsNullOrWhiteSpace(videoCodec) || string.IsNullOrWhiteSpace(audioCodec))
{
return false;
+271 -235
View File
@@ -36,6 +36,18 @@
</div>
<MudTextField @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Audio</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Video</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Resolution</MudText>
@@ -47,257 +59,281 @@
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Scaling Behavior</MudText>
</div>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
<MudSelectItem Value="@ScalingBehavior.ScaleAndPad">Scale and Pad</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Stretch">Stretch</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Crop">Crop</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Pad Mode</MudText>
</div>
<MudSelect @bind-Value="_model.PadMode"
For="@(() => _model.PadMode)"
Disabled="_model.HardwareAcceleration is not HardwareAccelerationKind.Vaapi"
HelperText="If hardware padding is green, software padding should be used instead">
<MudSelectItem Value="@FilterMode.HardwareIfPossible">Hardware If Possible</MudSelectItem>
<MudSelectItem Value="@FilterMode.Software">Software</MudSelectItem>
</MudSelect>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
<MudSelectItem Value="@FFmpegProfileVideoFormat.H264">h264</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
@if (_model.HardwareAcceleration is (HardwareAccelerationKind.Nvenc or HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv))
{
<MudSelectItem Value="@FFmpegProfileVideoFormat.Av1">av1</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Profile</MudText>
</div>
<MudSelect @bind-Value="_model.VideoProfile"
For="@(() => _model.VideoProfile)"
Disabled="@(_model.VideoFormat != FFmpegProfileVideoFormat.H264 || !HardwareAccelSupportsH264Profile(_model.HardwareAcceleration))"
Clearable="true">
<MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High">high</MudSelectItem>
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc)
{
<MudSelectItem Value="@VideoProfile.High444p">high444p</MudSelectItem>
}
else
{
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preset</MudText>
</div>
@{
ICollection<string> presets = FFmpegLibraryHelper.PresetsForFFmpegProfile(
_model.HardwareAcceleration,
_model.VideoFormat,
_model.BitDepth);
}
<MudSelect @bind-Value="_model.VideoPreset"
For="@(() => _model.VideoPreset)"
Disabled="@(presets.Count == 0)"
Clearable="true">
@foreach (string preset in presets)
{
if (!string.IsNullOrWhiteSpace(preset))
{
<MudSelectItem Value="@preset">@preset</MudSelectItem>
}
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Allow B-Frames</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bit Depth</MudText>
</div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
<MudSelectItem Value="@FFmpegProfileBitDepth.EightBit">8-bit</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileBitDepth.TenBit">10-bit</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Hardware Acceleration</MudText>
</div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds)
{
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
}
</MudSelect>
</MudStack>
@if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Driver</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
{
<MudSelectItem Value="@driver">@driver</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Display</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@foreach (string display in _vaapiDisplays)
{
<MudSelectItem Value="@display">@display</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@foreach (string device in _vaapiDevices)
{
<MudSelectItem Value="@device">@device</MudSelectItem>
}
</MudSelect>
</MudStack>
}
}
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
@if (_model.NormalizeVideo)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>QSV Extra Hardware Frames</MudText>
<MudText>Scaling Behavior</MudText>
</div>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
<MudSelectItem Value="@ScalingBehavior.ScaleAndPad">Scale and Pad</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Stretch">Stretch</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Crop">Crop</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Pad Mode</MudText>
</div>
<MudSelect @bind-Value="_model.PadMode"
For="@(() => _model.PadMode)"
Disabled="_model.HardwareAcceleration is not HardwareAccelerationKind.Vaapi"
HelperText="If hardware padding is green, software padding should be used instead">
<MudSelectItem Value="@FilterMode.HardwareIfPossible">Hardware If Possible</MudSelectItem>
<MudSelectItem Value="@FilterMode.Software">Software</MudSelectItem>
</MudSelect>
</MudStack>
}
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
@if (_model.NormalizeVideo)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
<MudSelectItem Value="@FFmpegProfileVideoFormat.H264">h264</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
@if (_model.HardwareAcceleration is (HardwareAccelerationKind.Nvenc or HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv))
{
<MudSelectItem Value="@FFmpegProfileVideoFormat.Av1">av1</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Profile</MudText>
</div>
<MudSelect @bind-Value="_model.VideoProfile"
For="@(() => _model.VideoProfile)"
Disabled="@(_model.VideoFormat != FFmpegProfileVideoFormat.H264 || !HardwareAccelSupportsH264Profile(_model.HardwareAcceleration))"
Clearable="true">
<MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High">high</MudSelectItem>
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc)
{
<MudSelectItem Value="@VideoProfile.High444p">high444p</MudSelectItem>
}
else
{
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preset</MudText>
</div>
@{
ICollection<string> presets = FFmpegLibraryHelper.PresetsForFFmpegProfile(
_model.HardwareAcceleration,
_model.VideoFormat,
_model.BitDepth);
}
<MudSelect @bind-Value="_model.VideoPreset"
For="@(() => _model.VideoPreset)"
Disabled="@(presets.Count == 0)"
Clearable="true">
@foreach (string preset in presets)
{
if (!string.IsNullOrWhiteSpace(preset))
{
<MudSelectItem Value="@preset">@preset</MudSelectItem>
}
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Allow B-Frames</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bit Depth</MudText>
</div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
<MudSelectItem Value="@FFmpegProfileBitDepth.EightBit">8-bit</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileBitDepth.TenBit">10-bit</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Hardware Acceleration</MudText>
</div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds)
{
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
}
</MudSelect>
</MudStack>
@if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Driver</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
{
<MudSelectItem Value="@driver">@driver</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Display</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@foreach (string display in _vaapiDisplays)
{
<MudSelectItem Value="@display">@display</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@foreach (string device in _vaapiDevices)
{
<MudSelectItem Value="@device">@device</MudSelectItem>
}
</MudSelect>
</MudStack>
}
}
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>QSV Extra Hardware Frames</MudText>
</div>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Tonemap Algorithm</MudText>
</div>
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues<FFmpegProfileTonemapAlgorithm>())
{
<MudSelectItem Value="@algorithm">@algorithm</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Frame Rate</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Colors</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeColors" For="@(() => _model.NormalizeColors)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Auto Deinterlace Video</MudText>
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Tonemap Algorithm</MudText>
</div>
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues<FFmpegProfileTonemapAlgorithm>())
{
<MudSelectItem Value="@algorithm">@algorithm</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudText>Video will be copied as-is, including timestamps, which *will* cause issues with most clients</MudText>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Frame Rate</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Auto Deinterlace Video</MudText>
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Audio</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.AacLatm">aac (latm)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Channels</MudText>
</div>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Sample Rate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Loudness</MudText>
</div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">
<MudSelectItem Value="@NormalizeLoudnessMode.Off">Off</MudSelectItem>
<MudSelectItem Value="@NormalizeLoudnessMode.LoudNorm">loudnorm</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm)
@if (_model.NormalizeAudio)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Target Loudness</MudText>
<MudText>Format</MudText>
</div>
<MudTextField @bind-Value="_model.TargetLoudness" For="@(() => _model.TargetLoudness)" Adornment="Adornment.End" AdornmentText="LUFS"/>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.AacLatm">aac (latm)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Channels</MudText>
</div>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Sample Rate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Loudness</MudText>
</div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">
<MudSelectItem Value="@NormalizeLoudnessMode.Off">Off</MudSelectItem>
<MudSelectItem Value="@NormalizeLoudnessMode.LoudNorm">loudnorm</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Target Loudness</MudText>
</div>
<MudTextField @bind-Value="_model.TargetLoudness" For="@(() => _model.TargetLoudness)" Adornment="Adornment.End" AdornmentText="LUFS"/>
</MudStack>
}
}
else
{
<MudText>Audio will be copied as-is, including timestamps, which *will* cause issues with most clients</MudText>
}
</MudContainer>
</div>