Browse Source

use video and audio format instead of video and audio codec (#696)

pull/700/head
Jason Dove 4 years ago committed by GitHub
parent
commit
d5a03963c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  3. 8
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  4. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  5. 8
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  6. 4
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  7. 4
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  8. 84
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  9. 63
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  10. 8
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  11. 11
      ErsatzTV.Core/Domain/FFmpegProfileAudioFormat.cs
  12. 12
      ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs
  13. 20
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  14. 23
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  15. 4
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs
  16. 35
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  17. 28
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  18. 174
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  19. 4
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  20. 3892
      ErsatzTV.Infrastructure/Migrations/20220311195632_Add_FFmpegProfileFormats.Designer.cs
  21. 37
      ErsatzTV.Infrastructure/Migrations/20220311195632_Add_FFmpegProfileFormats.cs
  22. 3892
      ErsatzTV.Infrastructure/Migrations/20220311204554_Convert_FFmpegProfileFormats.Designer.cs
  23. 38
      ErsatzTV.Infrastructure/Migrations/20220311204554_Convert_FFmpegProfileFormats.cs
  24. 8
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  25. 15
      ErsatzTV/Pages/FFmpeg.razor
  26. 11
      ErsatzTV/Pages/FFmpegEditor.razor
  27. 59
      ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs
  28. 16
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

4
CHANGELOG.md

@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Perform additional duration analysis on files with missing duration metadata - Perform additional duration analysis on files with missing duration metadata
- Add `nouveau` VAAPI driver option - Add `nouveau` VAAPI driver option
### Changed
- Change FFmpeg Profile video codec and audio codec text fields to select fields
- The appropriate video encoder will be determined based on the video format and hardware acceleration selections
## [0.4.3-alpha] - 2022-03-05 ## [0.4.3-alpha] - 2022-03-05
### Fixed ### Fixed
- Fix song sorting with `Chronological` and `Shuffle In Order` playback orders - Fix song sorting with `Chronological` and `Shuffle In Order` playback orders

4
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs

@ -13,10 +13,10 @@ public record CreateFFmpegProfile(
string VaapiDevice, string VaapiDevice,
int ResolutionId, int ResolutionId,
bool NormalizeVideo, bool NormalizeVideo,
string VideoCodec, FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
string AudioCodec, FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate, int AudioBitrate,
int AudioBufferSize, int AudioBufferSize,
bool NormalizeLoudness, bool NormalizeLoudness,

8
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -18,9 +18,9 @@ public class CreateFFmpegProfileHandler :
CreateFFmpegProfile request, CreateFFmpegProfile request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request); Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, profile => PersistFFmpegProfile(dbContext, profile)); return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
} }
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile( private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
@ -45,10 +45,10 @@ public class CreateFFmpegProfileHandler :
VaapiDevice = request.VaapiDevice, VaapiDevice = request.VaapiDevice,
ResolutionId = resolutionId, ResolutionId = resolutionId,
NormalizeVideo = request.NormalizeVideo, NormalizeVideo = request.NormalizeVideo,
VideoCodec = request.VideoCodec, VideoFormat = request.VideoFormat,
VideoBitrate = request.VideoBitrate, VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize, VideoBufferSize = request.VideoBufferSize,
AudioCodec = request.AudioCodec, AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate, AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize, AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness, NormalizeLoudness = request.NormalizeLoudness,

4
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs

@ -14,10 +14,10 @@ public record UpdateFFmpegProfile(
string VaapiDevice, string VaapiDevice,
int ResolutionId, int ResolutionId,
bool NormalizeVideo, bool NormalizeVideo,
string VideoCodec, FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
string AudioCodec, FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate, int AudioBitrate,
int AudioBufferSize, int AudioBufferSize,
bool NormalizeLoudness, bool NormalizeLoudness,

8
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs

@ -18,9 +18,9 @@ public class
UpdateFFmpegProfile request, UpdateFFmpegProfile request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request); Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request)); return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
} }
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest( private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
@ -36,10 +36,10 @@ public class
p.VaapiDevice = update.VaapiDevice; p.VaapiDevice = update.VaapiDevice;
p.ResolutionId = update.ResolutionId; p.ResolutionId = update.ResolutionId;
p.NormalizeVideo = update.Transcode && update.NormalizeVideo; p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
p.VideoCodec = update.VideoCodec; p.VideoFormat = update.VideoFormat;
p.VideoBitrate = update.VideoBitrate; p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize; p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec; p.AudioFormat = update.AudioFormat;
p.AudioBitrate = update.AudioBitrate; p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize; p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness; p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;

4
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -14,10 +14,10 @@ public record FFmpegProfileViewModel(
string VaapiDevice, string VaapiDevice,
ResolutionViewModel Resolution, ResolutionViewModel Resolution,
bool NormalizeVideo, bool NormalizeVideo,
string VideoCodec, FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
string AudioCodec, FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate, int AudioBitrate,
int AudioBufferSize, int AudioBufferSize,
bool NormalizeLoudness, bool NormalizeLoudness,

4
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -16,10 +16,10 @@ internal static class Mapper
profile.VaapiDevice, profile.VaapiDevice,
Project(profile.Resolution), Project(profile.Resolution),
profile.NormalizeVideo, profile.NormalizeVideo,
profile.VideoCodec, profile.VideoFormat,
profile.VideoBitrate, profile.VideoBitrate,
profile.VideoBufferSize, profile.VideoBufferSize,
profile.AudioCodec, profile.AudioFormat,
profile.AudioBitrate, profile.AudioBitrate,
profile.AudioBufferSize, profile.AudioBufferSize,
profile.NormalizeLoudness, profile.NormalizeLoudness,

84
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -445,14 +445,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetDesiredVideoCodec_When_ContentIsPadded_ForTransportStream() public void Should_SetDesiredVideoFormat_When_ContentIsPadded_ForTransportStream()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
Transcode = true, Transcode = true,
NormalizeVideo = true, NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -473,19 +473,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
actual.VideoCodec.Should().Be("testCodec"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
} }
[Test] [Test]
public void public void
Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() Should_SetDesiredVideoFormat_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
Transcode = true, Transcode = true,
NormalizeVideo = true, NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -507,19 +507,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("testCodec"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
} }
[Test] [Test]
public void public void
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming() Should_SetCopyVideoFormat_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
Transcode = true, Transcode = true,
NormalizeVideo = true, NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -541,18 +541,18 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
} }
[Test] [Test]
public void Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_ForTransportStream() public void Should_NotSetCopyVideoFormat_When_ContentIsCorrectSize_And_CorrectFormat_ForTransportStream()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
Transcode = true, Transcode = true,
NormalizeVideo = true, NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -563,7 +563,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
version, version,
new MediaStream { Codec = "libx264" }, new MediaStream { Codec = "h264" },
new MediaStream(), new MediaStream(),
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
@ -574,19 +574,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
} }
[Test] [Test]
public void public void
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream() Should_SetCopyVideoFormat_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
Transcode = true, Transcode = true,
NormalizeVideo = false, NormalizeVideo = false,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -608,12 +608,12 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
} }
[Test] [Test]
public void public void
Should_SetCopyVideoCodec_AndCopyAudioCodec_When_NotTranscoding_ForTransportStream() Should_SetCopyVideoFormat_AndCopyAudioFormat_When_NotTranscoding_ForTransportStream()
{ {
var ffmpegProfile = new FFmpegProfile var ffmpegProfile = new FFmpegProfile
{ {
@ -622,7 +622,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
NormalizeAudio = true, NormalizeAudio = true,
NormalizeLoudness = true, NormalizeLoudness = true,
Resolution = new Resolution { Width = 1920, Height = 1080 }, Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264" VideoFormat = FFmpegProfileVideoFormat.H264
}; };
// not anamorphic // not anamorphic
@ -644,9 +644,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy"); actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
actual.NormalizeLoudness.Should().BeFalse(); actual.NormalizeLoudness.Should().BeFalse();
actual.AudioCodec.Should().Be("copy"); actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
} }
[Test] [Test]
@ -781,13 +781,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetDesiredAudioFormat_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioCodec = "aac" AudioFormat = FFmpegProfileAudioFormat.Aac
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -805,16 +805,16 @@ public class FFmpegPlaybackSettingsCalculatorTests
false, false,
None); None);
actual.AudioCodec.Should().Be("aac"); actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
} }
[Test] [Test]
public void Should_SetCopyAudioCodec_When_NotNormalizingAudio_ForTransportStream() public void Should_SetCopyAudioFormat_When_NotNormalizingAudio_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
NormalizeAudio = false, NormalizeAudio = false,
AudioCodec = "aac" AudioFormat = FFmpegProfileAudioFormat.Aac
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -832,17 +832,17 @@ public class FFmpegPlaybackSettingsCalculatorTests
false, false,
None); None);
actual.AudioCodec.Should().Be("copy"); actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
} }
[Test] [Test]
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_ForTransportStream() public void Should_SetDesiredAudioFormat_When_NormalizingAudio_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioCodec = "aac" AudioFormat = FFmpegProfileAudioFormat.Aac
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -860,17 +860,17 @@ public class FFmpegPlaybackSettingsCalculatorTests
false, false,
None); None);
actual.AudioCodec.Should().Be("aac"); actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
} }
[Test] [Test]
public void Should_SetCopyAudioCodec_When_NormalizingAudio_ForHttpLiveStreaming() public void Should_SetCopyAudioFormat_When_NormalizingAudio_ForHttpLiveStreaming()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioCodec = "aac" AudioFormat = FFmpegProfileAudioFormat.Aac
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -888,18 +888,18 @@ public class FFmpegPlaybackSettingsCalculatorTests
false, false,
None); None);
actual.AudioCodec.Should().Be("copy"); actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
} }
[Test] [Test]
public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioBitrate = 2424, AudioBitrate = 2424,
AudioCodec = "ac3" AudioFormat = FFmpegProfileAudioFormat.Ac3
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -921,14 +921,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioBufferSize = 2424, AudioBufferSize = 2424,
AudioCodec = "ac3" AudioFormat = FFmpegProfileAudioFormat.Ac3
}; };
var version = new MediaVersion(); var version = new MediaVersion();
@ -950,13 +950,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioCodec = "ac3", AudioFormat = FFmpegProfileAudioFormat.Ac3,
AudioChannels = 6 AudioChannels = 6
}; };
@ -979,13 +979,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioCodec = "ac3", AudioFormat = FFmpegProfileAudioFormat.Ac3,
AudioSampleRate = 48 AudioSampleRate = 48
}; };
@ -1064,14 +1064,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
} }
[Test] [Test]
public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with FFmpegProfile ffmpegProfile = TestProfile() with
{ {
Transcode = true, Transcode = true,
NormalizeAudio = true, NormalizeAudio = true,
AudioSampleRate = 48, AudioSampleRate = 48,
AudioCodec = "ac3" AudioFormat = FFmpegProfileAudioFormat.Ac3
}; };
var version = new MediaVersion { Duration = TimeSpan.FromMinutes(5) }; // not pulled from here var version = new MediaVersion { Duration = TimeSpan.FromMinutes(5) }; // not pulled from here

63
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -118,21 +118,15 @@ public class TranscodingTests
new() { Width = 1280, Height = 720 } new() { Width = 1280, Height = 720 }
}; };
public static string[] SoftwareCodecs =
{
"libx264",
"libx265"
};
public static HardwareAccelerationKind[] NoAcceleration = public static HardwareAccelerationKind[] NoAcceleration =
{ {
HardwareAccelerationKind.None HardwareAccelerationKind.None
}; };
public static string[] NvidiaCodecs = public static FFmpegProfileVideoFormat[] VideoFormats =
{ {
"h264_nvenc", FFmpegProfileVideoFormat.H264,
"hevc_nvenc" FFmpegProfileVideoFormat.Hevc
}; };
public static HardwareAccelerationKind[] NvidiaAcceleration = public static HardwareAccelerationKind[] NvidiaAcceleration =
@ -140,34 +134,16 @@ public class TranscodingTests
HardwareAccelerationKind.Nvenc HardwareAccelerationKind.Nvenc
}; };
public static string[] VaapiCodecs =
{
"h264_vaapi",
"hevc_vaapi"
};
public static HardwareAccelerationKind[] VaapiAcceleration = public static HardwareAccelerationKind[] VaapiAcceleration =
{ {
HardwareAccelerationKind.Vaapi HardwareAccelerationKind.Vaapi
}; };
public static string[] VideoToolboxCodecs =
{
"h264_videotoolbox",
"hevc_videotoolbox"
};
public static HardwareAccelerationKind[] VideoToolboxAcceleration = public static HardwareAccelerationKind[] VideoToolboxAcceleration =
{ {
HardwareAccelerationKind.VideoToolbox HardwareAccelerationKind.VideoToolbox
}; };
public static string[] QsvCodecs =
{
"h264_qsv",
"hevc_qsv"
};
public static HardwareAccelerationKind[] QsvAcceleration = public static HardwareAccelerationKind[] QsvAcceleration =
{ {
HardwareAccelerationKind.Qsv HardwareAccelerationKind.Qsv
@ -176,27 +152,16 @@ public class TranscodingTests
[Test, Combinatorial] [Test, Combinatorial]
public async Task Transcode( public async Task Transcode(
[ValueSource(typeof(TestData), nameof(TestData.InputFormats))] [ValueSource(typeof(TestData), nameof(TestData.InputFormats))] InputFormat inputFormat,
InputFormat inputFormat, [ValueSource(typeof(TestData), nameof(TestData.Resolutions))] Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] [ValueSource(typeof(TestData), nameof(TestData.Paddings))] Padding padding,
Resolution profileResolution, [ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] VideoScanKind videoScanKind,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))] [ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark,
Padding padding, [ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
VideoScanKind videoScanKind, [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))]
Watermark watermark,
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))]
string profileCodec,
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))]
HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
{ {
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3") if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
@ -209,7 +174,7 @@ public class TranscodingTests
} }
string name = GetStringSha256Hash( string name = GetStringSha256Hash(
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}"); $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}");
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file)) if (!File.Exists(file))
@ -360,8 +325,8 @@ public class TranscodingTests
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
{ {
HardwareAcceleration = profileAcceleration, HardwareAcceleration = profileAcceleration,
VideoCodec = profileCodec, VideoFormat = profileVideoFormat,
AudioCodec = "aac" AudioFormat = FFmpegProfileAudioFormat.Aac
}, },
StreamingMode = StreamingMode.TransportStream StreamingMode = StreamingMode.TransportStream
}, },

8
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -13,11 +13,11 @@ public record FFmpegProfile
public string VaapiDevice { get; set; } public string VaapiDevice { get; set; }
public int ResolutionId { get; set; } public int ResolutionId { get; set; }
public Resolution Resolution { get; set; } public Resolution Resolution { get; set; }
public string VideoCodec { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public bool NormalizeVideo { get; set; } public bool NormalizeVideo { get; set; }
public int VideoBitrate { get; set; } public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; } public int VideoBufferSize { get; set; }
public string AudioCodec { get; set; } public FFmpegProfileAudioFormat AudioFormat { get; set; }
public int AudioBitrate { get; set; } public int AudioBitrate { get; set; }
public int AudioBufferSize { get; set; } public int AudioBufferSize { get; set; }
public bool NormalizeLoudness { get; set; } public bool NormalizeLoudness { get; set; }
@ -34,8 +34,8 @@ public record FFmpegProfile
Transcode = true, Transcode = true,
ResolutionId = resolution.Id, ResolutionId = resolution.Id,
Resolution = resolution, Resolution = resolution,
VideoCodec = "libx264", VideoFormat = FFmpegProfileVideoFormat.H264,
AudioCodec = "ac3", AudioFormat = FFmpegProfileAudioFormat.Ac3,
VideoBitrate = 2000, VideoBitrate = 2000,
VideoBufferSize = 4000, VideoBufferSize = 4000,
AudioBitrate = 192, AudioBitrate = 192,

11
ErsatzTV.Core/Domain/FFmpegProfileAudioFormat.cs

@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain;
public enum FFmpegProfileAudioFormat
{
None = 0,
Aac = 1,
Ac3 = 2,
Copy = 99
}

12
ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs

@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain;
public enum FFmpegProfileVideoFormat
{
None = 0,
H264 = 1,
Hevc = 2,
Mpeg2Video = 3,
Copy = 99
}

20
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -22,7 +22,7 @@ public class FFmpegComplexFilterBuilder
private Option<int> _watermarkIndex; private Option<int> _watermarkIndex;
private string _pixelFormat; private string _pixelFormat;
private string _videoDecoder; private string _videoDecoder;
private string _videoEncoder; private FFmpegProfileVideoFormat _videoFormat;
private Option<string> _subtitle; private Option<string> _subtitle;
private bool _boxBlur; private bool _boxBlur;
@ -131,9 +131,9 @@ public class FFmpegComplexFilterBuilder
return this; return this;
} }
public FFmpegComplexFilterBuilder WithVideoEncoder(string videoEncoder) public FFmpegComplexFilterBuilder WithVideoFormat(FFmpegProfileVideoFormat videoFormat)
{ {
_videoEncoder = videoEncoder; _videoFormat = videoFormat;
return this; return this;
} }
@ -379,21 +379,21 @@ public class FFmpegComplexFilterBuilder
string outputPixelFormat = null; string outputPixelFormat = null;
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay)) if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
{ {
switch (acceleration, _videoEncoder, _pixelFormat) switch (acceleration, _videoFormat, _pixelFormat)
{ {
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"): case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le"):
outputPixelFormat = "yuv420p"; outputPixelFormat = "yuv420p";
break; break;
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"): case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv444p10le"):
outputPixelFormat = "yuv444p"; outputPixelFormat = "yuv444p";
break; break;
} }
} }
string outputFormat = (_videoEncoder, _pixelFormat) switch string outputFormat = (acceleration, _videoFormat, _pixelFormat) switch
{ {
("hevc_nvenc", "yuv420p10le") => "p010le", (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.Hevc, "yuv420p10le") => "p010le",
("h264_nvenc", "yuv420p10le") => "p010le", (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le") => "p010le",
_ => null _ => null
}; };
@ -485,7 +485,7 @@ public class FFmpegComplexFilterBuilder
// vaapi downsample 10bit hevc to 8bit h264 // vaapi downsample 10bit hevc to 8bit h264
if (acceleration == HardwareAccelerationKind.Vaapi && !videoFilterQueue.Any() && if (acceleration == HardwareAccelerationKind.Vaapi && !videoFilterQueue.Any() &&
_pixelFormat == "yuv420p10le" && _videoEncoder.StartsWith("h264")) _pixelFormat == "yuv420p10le" && _videoFormat == FFmpegProfileVideoFormat.H264)
{ {
videoFilterQueue.Add("scale_vaapi=format=nv12"); videoFilterQueue.Add("scale_vaapi=format=nv12");
} }

23
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -85,9 +85,17 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.StreamSeek, playbackSettings.StreamSeek,
wm.FrequencyMinutes, wm.FrequencyMinutes,
wm.DurationSeconds)); wm.DurationSeconds));
string audioFormat = playbackSettings.AudioFormat switch
{
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
FFmpegProfileAudioFormat.Copy => AudioFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}")
};
var audioState = new AudioState( var audioState = new AudioState(
playbackSettings.AudioCodec, audioFormat,
playbackSettings.AudioChannels, playbackSettings.AudioChannels,
playbackSettings.AudioBitrate, playbackSettings.AudioBitrate,
playbackSettings.AudioBufferSize, playbackSettings.AudioBufferSize,
@ -114,14 +122,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints); var watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
// TODO: need formats for these codecs string videoFormat = playbackSettings.VideoFormat switch
string videoFormat = playbackSettings.VideoCodec switch
{ {
"libx265" or "hevc_nvenc" or "hevc_qsv" or "hevc_vaapi" or "hevc_videotoolbox" => VideoFormat.Hevc, FFmpegProfileVideoFormat.Hevc => VideoFormat.Hevc,
"libx264" or "h264_nvenc" or "h264_qsv" or "h264_vaapi" or "h264_videotoolbox" => VideoFormat.H264, FFmpegProfileVideoFormat.H264 => VideoFormat.H264,
"mpeg2video" => VideoFormat.Mpeg2Video, FFmpegProfileVideoFormat.Mpeg2Video => VideoFormat.Mpeg2Video,
"copy" => VideoFormat.Copy, FFmpegProfileVideoFormat.Copy => VideoFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected video codec {playbackSettings.VideoCodec}") _ => throw new ArgumentOutOfRangeException($"unexpected video format {playbackSettings.VideoFormat}")
}; };
HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch

4
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -13,7 +13,7 @@ public class FFmpegPlaybackSettings
public Option<TimeSpan> StreamSeek { get; set; } public Option<TimeSpan> StreamSeek { get; set; }
public Option<IDisplaySize> ScaledSize { get; set; } public Option<IDisplaySize> ScaledSize { get; set; }
public bool PadToDesiredResolution { get; set; } public bool PadToDesiredResolution { get; set; }
public string VideoCodec { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public Option<int> VideoBitrate { get; set; } public Option<int> VideoBitrate { get; set; }
public Option<int> VideoBufferSize { get; set; } public Option<int> VideoBufferSize { get; set; }
public Option<int> AudioBitrate { get; set; } public Option<int> AudioBitrate { get; set; }
@ -21,7 +21,7 @@ public class FFmpegPlaybackSettings
public Option<int> AudioChannels { get; set; } public Option<int> AudioChannels { get; set; }
public Option<int> AudioSampleRate { get; set; } public Option<int> AudioSampleRate { get; set; }
public Option<TimeSpan> AudioDuration { get; set; } public Option<TimeSpan> AudioDuration { get; set; }
public string AudioCodec { get; set; } public FFmpegProfileAudioFormat AudioFormat { get; set; }
public bool Deinterlace { get; set; } public bool Deinterlace { get; set; }
public Option<int> VideoTrackTimeScale { get; set; } public Option<int> VideoTrackTimeScale { get; set; }
public bool NormalizeLoudness { get; set; } public bool NormalizeLoudness { get; set; }

35
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -82,8 +82,8 @@ public class FFmpegPlaybackSettingsCalculator
switch (streamingMode) switch (streamingMode)
{ {
case StreamingMode.HttpLiveStreamingDirect: case StreamingMode.HttpLiveStreamingDirect:
result.AudioCodec = "copy"; result.AudioFormat = FFmpegProfileAudioFormat.Copy;
result.VideoCodec = "copy"; result.VideoFormat = FFmpegProfileVideoFormat.Copy;
result.Deinterlace = false; result.Deinterlace = false;
break; break;
case StreamingMode.TransportStreamHybrid: case StreamingMode.TransportStreamHybrid:
@ -121,9 +121,9 @@ public class FFmpegPlaybackSettingsCalculator
foreach (MediaStream stream in videoStream.Where(s => s.AttachedPic == false)) foreach (MediaStream stream in videoStream.Where(s => s.AttachedPic == false))
{ {
if (result.ScaledSize.IsSome || result.PadToDesiredResolution || if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
NeedToNormalizeVideoCodec(ffmpegProfile, stream)) NeedToNormalizeVideo(ffmpegProfile))
{ {
result.VideoCodec = ffmpegProfile.VideoCodec; result.VideoFormat = ffmpegProfile.VideoFormat;
result.VideoBitrate = ffmpegProfile.VideoBitrate; result.VideoBitrate = ffmpegProfile.VideoBitrate;
result.VideoBufferSize = ffmpegProfile.VideoBufferSize; result.VideoBufferSize = ffmpegProfile.VideoBufferSize;
@ -141,23 +141,23 @@ public class FFmpegPlaybackSettingsCalculator
(HardwareAccelerationKind.Qsv, "h264", _) => "h264_qsv", (HardwareAccelerationKind.Qsv, "h264", _) => "h264_qsv",
(HardwareAccelerationKind.Qsv, "hevc", _) => "hevc_qsv", (HardwareAccelerationKind.Qsv, "hevc", _) => "hevc_qsv",
(HardwareAccelerationKind.Qsv, "mpeg2video", _) => "mpeg2_qsv", (HardwareAccelerationKind.Qsv, "mpeg2video", _) => "mpeg2_qsv",
// temp disable mpeg4 hardware decoding for all vaapi // temp disable mpeg4 hardware decoding for all vaapi
// TODO: check for codec support // TODO: check for codec support
(HardwareAccelerationKind.Vaapi, "mpeg4", _) => "mpeg4", (HardwareAccelerationKind.Vaapi, "mpeg4", _) => "mpeg4",
_ => null _ => null
}; };
} }
else else
{ {
result.VideoCodec = "copy"; result.VideoFormat = FFmpegProfileVideoFormat.Copy;
} }
} }
if (ffmpegProfile.Transcode && ffmpegProfile.NormalizeAudio) if (ffmpegProfile.Transcode && ffmpegProfile.NormalizeAudio)
{ {
result.AudioCodec = ffmpegProfile.AudioCodec; result.AudioFormat = ffmpegProfile.AudioFormat;
result.AudioBitrate = ffmpegProfile.AudioBitrate; result.AudioBitrate = ffmpegProfile.AudioBitrate;
result.AudioBufferSize = ffmpegProfile.AudioBufferSize; result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
@ -176,7 +176,7 @@ public class FFmpegPlaybackSettingsCalculator
} }
else else
{ {
result.AudioCodec = "copy"; result.AudioFormat = FFmpegProfileAudioFormat.Copy;
} }
if (videoVersion.VideoScanKind == VideoScanKind.Interlaced) if (videoVersion.VideoScanKind == VideoScanKind.Interlaced)
@ -192,20 +192,13 @@ public class FFmpegPlaybackSettingsCalculator
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile) public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile)
{ {
string softwareCodec = ffmpegProfile.VideoCodec switch
{
{ } c when c.Contains("hevc") || c.Contains("265") => "libx265",
{ } c when c.Contains("264") => "libx264",
{ } c when c.Contains("mpeg2") => "mpeg2video",
_ => "libx264"
};
return new FFmpegPlaybackSettings return new FFmpegPlaybackSettings
{ {
HardwareAcceleration = HardwareAccelerationKind.None,
ThreadCount = ffmpegProfile.ThreadCount, ThreadCount = ffmpegProfile.ThreadCount,
FormatFlags = CommonFormatFlags, FormatFlags = CommonFormatFlags,
VideoCodec = softwareCodec, VideoFormat = ffmpegProfile.VideoFormat,
AudioCodec = ffmpegProfile.AudioCodec, AudioFormat = ffmpegProfile.AudioFormat
}; };
} }
@ -227,8 +220,8 @@ public class FFmpegPlaybackSettingsCalculator
private static bool IsOddSize(MediaVersion version) => private static bool IsOddSize(MediaVersion version) =>
version.Height % 2 == 1 || version.Width % 2 == 1; version.Height % 2 == 1 || version.Width % 2 == 1;
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) => private static bool NeedToNormalizeVideo(FFmpegProfile ffmpegProfile) =>
ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo && ffmpegProfile.VideoCodec != videoStream.Codec; ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo;
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version) private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
{ {

28
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -71,7 +71,10 @@ internal class FFmpegProcessBuilder
return this; return this;
} }
public FFmpegProcessBuilder WithHardwareAcceleration(HardwareAccelerationKind hwAccel, Option<string> pixelFormat, string encoder) public FFmpegProcessBuilder WithHardwareAcceleration(
HardwareAccelerationKind hwAccel,
Option<string> pixelFormat,
FFmpegProfileVideoFormat videoFormat)
{ {
_hwAccel = hwAccel; _hwAccel = hwAccel;
@ -84,14 +87,14 @@ internal class FFmpegProcessBuilder
_arguments.Add("qsv=qsv:MFX_IMPL_hw_any"); _arguments.Add("qsv=qsv:MFX_IMPL_hw_any");
break; break;
case HardwareAccelerationKind.Nvenc: case HardwareAccelerationKind.Nvenc:
string outputFormat = (encoder, pixelFormat.IfNone("")) switch string outputFormat = (videoFormat, pixelFormat.IfNone("")) switch
{ {
("hevc_nvenc", "yuv420p10le") => "p010le", (FFmpegProfileVideoFormat.Hevc, "yuv420p10le") => "p010le",
("h264_nvenc", "yuv420p10le") => "p010le", (FFmpegProfileVideoFormat.H264, "yuv420p10le") => "p010le",
// ("hevc_nvenc", "yuv444p10le") => "p016le", // ("hevc_nvenc", "yuv444p10le") => "p016le",
_ => "cuda" _ => "cuda"
}; };
_arguments.Add("-hwaccel"); _arguments.Add("-hwaccel");
_arguments.Add("cuda"); _arguments.Add("cuda");
_arguments.Add("-hwaccel_output_format"); _arguments.Add("-hwaccel_output_format");
@ -462,14 +465,17 @@ internal class FFmpegProcessBuilder
return this; return this;
} }
public FFmpegProcessBuilder WithPlaybackArgs(FFmpegPlaybackSettings playbackSettings) public FFmpegProcessBuilder WithPlaybackArgs(
FFmpegPlaybackSettings playbackSettings,
string videoCodec,
string audioCodec)
{ {
var arguments = new List<string> var arguments = new List<string>
{ {
"-c:v", playbackSettings.VideoCodec, "-c:v", videoCodec,
"-flags", "cgop", "-flags", "cgop",
// disable scene change detection except with mpeg2video // disable scene change detection except with mpeg2video
"-sc_threshold", playbackSettings.VideoCodec == "mpeg2video" ? "1000000000" : "0" "-sc_threshold", playbackSettings.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video ? "1000000000" : "0"
}; };
if (!string.IsNullOrWhiteSpace(_outputPixelFormat)) if (!string.IsNullOrWhiteSpace(_outputPixelFormat))
@ -512,7 +518,7 @@ internal class FFmpegProcessBuilder
arguments.AddRange( arguments.AddRange(
new[] new[]
{ {
"-c:a", playbackSettings.AudioCodec, "-c:a", audioCodec,
"-movflags", "+faststart", "-movflags", "+faststart",
"-muxdelay", "0", "-muxdelay", "0",
"-muxpreload", "0" "-muxpreload", "0"
@ -591,9 +597,9 @@ internal class FFmpegProcessBuilder
Option<MediaStream> maybeAudioStream, Option<MediaStream> maybeAudioStream,
string videoPath, string videoPath,
Option<string> audioPath, Option<string> audioPath,
string videoCodec) FFmpegProfileVideoFormat videoFormat)
{ {
_complexFilterBuilder = _complexFilterBuilder.WithVideoEncoder(videoCodec); _complexFilterBuilder = _complexFilterBuilder.WithVideoFormat(videoFormat);
int videoStreamIndex = videoStream.Index; int videoStreamIndex = videoStream.Index;
Option<int> maybeIndex = maybeAudioStream.Map(ms => ms.Index); Option<int> maybeIndex = maybeAudioStream.Map(ms => ms.Index);

174
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -35,7 +35,7 @@ public class FFmpegProcessService : IFFmpegProcessService
_logger = logger; _logger = logger;
} }
public async Task<Process> ForPlayoutItem( public Task<Process> ForPlayoutItem(
string ffmpegPath, string ffmpegPath,
bool saveReports, bool saveReports,
Channel channel, Channel channel,
@ -56,144 +56,7 @@ public class FFmpegProcessService : IFFmpegProcessService
long ptsOffset, long ptsOffset,
Option<int> targetFramerate) Option<int> targetFramerate)
{ {
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); throw new NotSupportedException();
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode,
channel.FFmpegProfile,
videoVersion,
videoStream,
maybeAudioStream,
start,
now,
inPoint,
outPoint,
hlsRealtime,
targetFramerate);
Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
.Map(o => o.Watermark)
.Flatten()
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
.Map(
wm =>
WatermarkCalculator.CalculateFadePoints(
start,
inPoint,
outPoint,
playbackSettings.StreamSeek,
wm.FrequencyMinutes,
wm.DurationSeconds));
// foreach (List<FadePoint> fadePoints in maybeFadePoints)
// {
// foreach (FadePoint fadePoint in fadePoints)
// {
// _logger.LogDebug("Fade point filter: {FadePointFilter}", fadePoint.ToFilter());
// }
// }
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(playbackSettings.ThreadCount)
.WithVaapiDriver(vaapiDriver, vaapiDevice)
.WithHardwareAcceleration(
playbackSettings.HardwareAcceleration,
videoStream.PixelFormat,
playbackSettings.VideoCodec)
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithInputCodec(
playbackSettings.StreamSeek,
fillerKind == FillerKind.Fallback,
videoPath,
audioPath,
playbackSettings.VideoDecoder,
videoStream.Codec,
videoStream.PixelFormat,
playbackSettings.Deinterlace)
.WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution)
.WithFrameRate(playbackSettings.FrameRate)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
playbackSettings.ScaledSize.Match(
scaledSize =>
{
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
.WithScaling(scaledSize);
if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize))
{
builder = builder.WithBlackBars(channel.FFmpegProfile.Resolution);
}
builder = builder
.WithFilterComplex(
videoStream,
maybeAudioStream,
videoPath,
audioPath,
channel.FFmpegProfile.VideoCodec);
},
() =>
{
if (playbackSettings.PadToDesiredResolution)
{
builder = builder
.WithDeinterlace(playbackSettings.Deinterlace)
.WithBlackBars(channel.FFmpegProfile.Resolution)
.WithFilterComplex(
videoStream,
maybeAudioStream,
videoPath,
audioPath,
channel.FFmpegProfile.VideoCodec);
}
else if (playbackSettings.Deinterlace)
{
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
.WithAlignedAudio(playbackSettings.AudioDuration)
.WithFilterComplex(
videoStream,
maybeAudioStream,
videoPath,
audioPath,
channel.FFmpegProfile.VideoCodec);
}
else
{
builder = builder
.WithFilterComplex(
videoStream,
maybeAudioStream,
videoPath,
audioPath,
channel.FFmpegProfile.VideoCodec);
}
});
builder = builder.WithPlaybackArgs(playbackSettings)
.WithMetadata(channel, maybeAudioStream)
.WithDuration(finish - now);
return channel.StreamingMode switch
{
// HLS needs to segment and generate playlist
StreamingMode.HttpLiveStreamingSegmenter => builder.WithHls(
channel.Number,
videoVersion,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build(),
_ => builder.WithFormat("mpegts").WithInitialDiscontinuity().WithPipe().Build()
};
} }
public async Task<Process> ForError( public async Task<Process> ForError(
@ -225,6 +88,19 @@ public class FFmpegProcessService : IFFmpegProcessService
var videoStream = new MediaStream { Index = 0 }; var videoStream = new MediaStream { Index = 0 };
var audioStream = new MediaStream { Index = 0 }; var audioStream = new MediaStream { Index = 0 };
string videoCodec = playbackSettings.VideoFormat switch
{
FFmpegProfileVideoFormat.Hevc => "libx265",
FFmpegProfileVideoFormat.Mpeg2Video => "mpeg2video",
_ => "libx264"
};
string audioCodec = playbackSettings.AudioFormat switch
{
FFmpegProfileAudioFormat.Ac3 => "ac3",
_ => "aac"
};
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
@ -239,9 +115,9 @@ public class FFmpegProcessService : IFFmpegProcessService
audioStream, audioStream,
Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"), Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"),
"fake-audio-path", "fake-audio-path",
playbackSettings.VideoCodec) playbackSettings.VideoFormat)
.WithPixfmt("yuv420p") .WithPixfmt("yuv420p")
.WithPlaybackArgs(playbackSettings) .WithPlaybackArgs(playbackSettings, videoCodec, audioCodec)
.WithMetadata(channel, None); .WithMetadata(channel, None);
await duration.IfSomeAsync(d => builder = builder.WithDuration(d)); await duration.IfSomeAsync(d => builder = builder.WithDuration(d));
@ -266,19 +142,7 @@ public class FFmpegProcessService : IFFmpegProcessService
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{ {
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; throw new NotSupportedException();
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(1)
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithInfiniteLoop()
.WithConcat($"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}")
.WithMetadata(channel, None)
.WithFormat("mpegts")
.WithPipe()
.Build();
} }
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
@ -398,7 +262,7 @@ public class FFmpegProcessService : IFFmpegProcessService
None, None,
videoPath, videoPath,
None, None,
playbackSettings.VideoCodec) playbackSettings.VideoFormat)
.WithOutputFormat("apng", outputFile) .WithOutputFormat("apng", outputFile)
.Build(); .Build();

4
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -44,8 +44,8 @@ public class ChannelPlaylist
_ => "ts?mode=ts-legacy" _ => "ts?mode=ts-legacy"
}; };
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head(); string vcodec = channel.FFmpegProfile.VideoFormat.ToString().ToLowerInvariant();
string acodec = channel.FFmpegProfile.AudioCodec; string acodec = channel.FFmpegProfile.AudioFormat.ToString().ToLowerInvariant();
sb.AppendLine( sb.AppendLine(
$"#EXTINF:0 tvg-id=\"{channel.Number}.etv\" channel-id=\"{shortUniqueId}\" channel-number=\"{channel.Number}\" CUID=\"{shortUniqueId}\" tvg-chno=\"{channel.Number}\" tvg-name=\"{channel.Name}\" tvg-logo=\"{logo}\" group-title=\"{channel.Group}\" tvc-stream-vcodec=\"{vcodec}\" tvc-stream-acodec=\"{acodec}\", {channel.Name}"); $"#EXTINF:0 tvg-id=\"{channel.Number}.etv\" channel-id=\"{shortUniqueId}\" channel-number=\"{channel.Number}\" CUID=\"{shortUniqueId}\" tvg-chno=\"{channel.Number}\" tvg-name=\"{channel.Name}\" tvg-logo=\"{logo}\" group-title=\"{channel.Group}\" tvc-stream-vcodec=\"{vcodec}\" tvc-stream-acodec=\"{acodec}\", {channel.Name}");

3892
ErsatzTV.Infrastructure/Migrations/20220311195632_Add_FFmpegProfileFormats.Designer.cs generated

File diff suppressed because it is too large Load Diff

37
ErsatzTV.Infrastructure/Migrations/20220311195632_Add_FFmpegProfileFormats.cs

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_FFmpegProfileFormats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AudioFormat",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "VideoFormat",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AudioFormat",
table: "FFmpegProfile");
migrationBuilder.DropColumn(
name: "VideoFormat",
table: "FFmpegProfile");
}
}
}

3892
ErsatzTV.Infrastructure/Migrations/20220311204554_Convert_FFmpegProfileFormats.Designer.cs generated

File diff suppressed because it is too large Load Diff

38
ErsatzTV.Infrastructure/Migrations/20220311204554_Convert_FFmpegProfileFormats.cs

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Convert_FFmpegProfileFormats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// h264
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET VideoFormat = 1 WHERE VideoCodec LIKE '%264%'");
// hevc
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET VideoFormat = 2 WHERE VideoCodec LIKE '%265%'");
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET VideoFormat = 2 WHERE VideoCodec LIKE '%hevc%'");
// mpeg-2
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET VideoFormat = 3 WHERE VideoCodec LIKE '%mpeg2%'");
// anything else => h264
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET VideoFormat = 1 WHERE VideoFormat = 0");
// aac
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET AudioFormat = 1 WHERE AudioCodec LIKE '%AAC%'");
// ac3
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET AudioFormat = 2 WHERE AudioCodec LIKE '%AC3%'");
// anything else => aac
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET AudioFormat = 1 WHERE AudioFormat = 0");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

8
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.2"); modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{ {
@ -502,6 +502,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("AudioCodec") b.Property<string>("AudioCodec")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("AudioFormat")
.HasColumnType("INTEGER");
b.Property<int>("AudioSampleRate") b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -549,6 +552,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("VideoCodec") b.Property<string>("VideoCodec")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("VideoFormat")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ResolutionId"); b.HasIndex("ResolutionId");

15
ErsatzTV/Pages/FFmpeg.razor

@ -40,14 +40,21 @@
@context.Resolution.Name @context.Resolution.Name
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd DataLabel="Video Codec"> <MudTd DataLabel="Video Format">
<MudText Color="@(context.Transcode && context.NormalizeVideo ? Color.Tertiary : Color.Inherit)"> <MudText Color="@(context.Transcode && context.NormalizeVideo ? Color.Tertiary : Color.Inherit)">
@context.VideoCodec @if (context.HardwareAcceleration != HardwareAccelerationKind.None)
{
@($"{context.VideoFormat.ToString().ToLowerInvariant()} / {context.HardwareAcceleration.ToString().ToLowerInvariant()}")
}
else
{
@context.VideoFormat.ToString().ToLowerInvariant()
}
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd DataLabel="Audio Codec"> <MudTd DataLabel="Audio Format">
<MudText Color="@(context.Transcode && context.NormalizeAudio ? Color.Tertiary : Color.Inherit)"> <MudText Color="@(context.Transcode && context.NormalizeAudio ? Color.Tertiary : Color.Inherit)">
@context.AudioCodec @context.AudioFormat.ToString().ToLowerInvariant()
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd> <MudTd>

11
ErsatzTV/Pages/FFmpegEditor.razor

@ -44,7 +44,11 @@
</MudItem> </MudItem>
<MudItem> <MudItem>
<MudText Typo="Typo.h6">Video</MudText> <MudText Typo="Typo.h6">Video</MudText>
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.VideoCodec" For="@(() => _model.VideoCodec)"/> <MudSelect Disabled="@(!_model.Transcode)" Label="Format" @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>
</MudSelect>
<MudElement HtmlTag="div" Class="mt-3"> <MudElement HtmlTag="div" Class="mt-3">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement> </MudElement>
@ -84,7 +88,10 @@
</MudItem> </MudItem>
<MudItem> <MudItem>
<MudText Typo="Typo.h6">Audio</MudText> <MudText Typo="Typo.h6">Audio</MudText>
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/> <MudSelect Disabled="@(!_model.Transcode)" Label="Format" @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
</MudSelect>
<MudElement HtmlTag="div" Class="mt-3"> <MudElement HtmlTag="div" Class="mt-3">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement> </MudElement>

59
ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs

@ -6,10 +6,31 @@ namespace ErsatzTV.Validators;
public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfileEditViewModel> public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfileEditViewModel>
{ {
private static readonly List<string> QsvEncoders = new() { "h264_qsv", "hevc_qsv", "mpeg2_qsv" }; private static readonly List<FFmpegProfileVideoFormat> QsvFormats = new()
private static readonly List<string> NvencEncoders = new() { "h264_nvenc", "hevc_nvenc" }; {
private static readonly List<string> VaapiEncoders = new() { "h264_vaapi", "hevc_vaapi", "mpeg2_vaapi" }; FFmpegProfileVideoFormat.H264,
private static readonly List<string> VideoToolboxEncoders = new() { "h264_videotoolbox", "hevc_videotoolbox" }; FFmpegProfileVideoFormat.Hevc,
FFmpegProfileVideoFormat.Mpeg2Video
};
private static readonly List<FFmpegProfileVideoFormat> NvencFormats = new()
{
FFmpegProfileVideoFormat.H264,
FFmpegProfileVideoFormat.Hevc
};
private static readonly List<FFmpegProfileVideoFormat> VaapiFormats = new()
{
FFmpegProfileVideoFormat.H264,
FFmpegProfileVideoFormat.Hevc,
FFmpegProfileVideoFormat.Mpeg2Video
};
private static readonly List<FFmpegProfileVideoFormat> VideoToolboxFormats = new()
{
FFmpegProfileVideoFormat.H264,
FFmpegProfileVideoFormat.Hevc
};
public FFmpegProfileEditViewModelValidator() public FFmpegProfileEditViewModelValidator()
{ {
@ -20,11 +41,11 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfi
x => x.Transcode, x => x.Transcode,
() => () =>
{ {
RuleFor(x => x.VideoCodec).NotEmpty(); RuleFor(x => x.VideoFormat).NotEqual(FFmpegProfileVideoFormat.None);
RuleFor(x => x.VideoBitrate).GreaterThan(0); RuleFor(x => x.VideoBitrate).GreaterThan(0);
RuleFor(x => x.VideoBufferSize).GreaterThan(0); RuleFor(x => x.VideoBufferSize).GreaterThan(0);
RuleFor(x => x.AudioCodec).NotEmpty(); RuleFor(x => x.AudioFormat).NotEqual(FFmpegProfileAudioFormat.None);
RuleFor(x => x.AudioBitrate).GreaterThan(0); RuleFor(x => x.AudioBitrate).GreaterThan(0);
RuleFor(x => x.AudioChannels).GreaterThan(0); RuleFor(x => x.AudioChannels).GreaterThan(0);
}); });
@ -33,42 +54,32 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfi
x => x.HardwareAcceleration == HardwareAccelerationKind.Qsv, x => x.HardwareAcceleration == HardwareAccelerationKind.Qsv,
() => () =>
{ {
RuleFor(x => x.VideoCodec).Must(c => QsvEncoders.Contains(c)) RuleFor(x => x.VideoFormat).Must(c => QsvFormats.Contains(c))
.WithMessage("QSV codec is required (h264_qsv, hevc_qsv, mpeg2_qsv)"); .WithMessage("QSV supports formats (h264, hevc, mpeg2video)");
}); });
When( When(
x => x.HardwareAcceleration == HardwareAccelerationKind.Nvenc, x => x.HardwareAcceleration == HardwareAccelerationKind.Nvenc,
() => () =>
{ {
RuleFor(x => x.VideoCodec).Must(c => NvencEncoders.Contains(c)) RuleFor(x => x.VideoFormat).Must(c => NvencFormats.Contains(c))
.WithMessage("NVENC codec is required (h264_nvenc, hevc_nvenc)"); .WithMessage("NVENC supports formats (h264, hevc)");
}); });
When( When(
x => x.HardwareAcceleration == HardwareAccelerationKind.Vaapi, x => x.HardwareAcceleration == HardwareAccelerationKind.Vaapi,
() => () =>
{ {
RuleFor(x => x.VideoCodec).Must(c => VaapiEncoders.Contains(c)) RuleFor(x => x.VideoFormat).Must(c => VaapiFormats.Contains(c))
.WithMessage("VAAPI codec is required (h264_vaapi, hevc_vaapi, mpeg2_vaapi)"); .WithMessage("VAAPI supports formats (h264, hevc, mpeg2video)");
}); });
When( When(
x => x.HardwareAcceleration == HardwareAccelerationKind.VideoToolbox, x => x.HardwareAcceleration == HardwareAccelerationKind.VideoToolbox,
() => () =>
{ {
RuleFor(x => x.VideoCodec).Must(c => VideoToolboxEncoders.Contains(c)) RuleFor(x => x.VideoFormat).Must(c => VideoToolboxFormats.Contains(c))
.WithMessage("VideoToolbox codec is required (h264_videotoolbox, hevc_videotoolbox)"); .WithMessage("VideoToolbox supports formats (h264, hevc)");
});
When(
x => x.HardwareAcceleration == HardwareAccelerationKind.None,
() =>
{
RuleFor(x => x.VideoCodec).Must(
c => !QsvEncoders.Contains(c) && !NvencEncoders.Contains(c) && !VaapiEncoders.Contains(c) &&
!VideoToolboxEncoders.Contains(c))
.WithMessage("Hardware acceleration is required for this codec");
}); });
} }
} }

16
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -16,7 +16,7 @@ public class FFmpegProfileEditViewModel
AudioBitrate = viewModel.AudioBitrate; AudioBitrate = viewModel.AudioBitrate;
AudioBufferSize = viewModel.AudioBufferSize; AudioBufferSize = viewModel.AudioBufferSize;
AudioChannels = viewModel.AudioChannels; AudioChannels = viewModel.AudioChannels;
AudioCodec = viewModel.AudioCodec; AudioFormat = viewModel.AudioFormat;
AudioSampleRate = viewModel.AudioSampleRate; AudioSampleRate = viewModel.AudioSampleRate;
NormalizeLoudness = viewModel.NormalizeLoudness; NormalizeLoudness = viewModel.NormalizeLoudness;
Id = viewModel.Id; Id = viewModel.Id;
@ -32,13 +32,13 @@ public class FFmpegProfileEditViewModel
VaapiDevice = viewModel.VaapiDevice; VaapiDevice = viewModel.VaapiDevice;
VideoBitrate = viewModel.VideoBitrate; VideoBitrate = viewModel.VideoBitrate;
VideoBufferSize = viewModel.VideoBufferSize; VideoBufferSize = viewModel.VideoBufferSize;
VideoCodec = viewModel.VideoCodec; VideoFormat = viewModel.VideoFormat;
} }
public int AudioBitrate { get; set; } public int AudioBitrate { get; set; }
public int AudioBufferSize { get; set; } public int AudioBufferSize { get; set; }
public int AudioChannels { get; set; } public int AudioChannels { get; set; }
public string AudioCodec { get; set; } public FFmpegProfileAudioFormat AudioFormat { get; set; }
public int AudioSampleRate { get; set; } public int AudioSampleRate { get; set; }
public bool NormalizeLoudness { get; set; } public bool NormalizeLoudness { get; set; }
public int Id { get; set; } public int Id { get; set; }
@ -54,7 +54,7 @@ public class FFmpegProfileEditViewModel
public string VaapiDevice { get; set; } public string VaapiDevice { get; set; }
public int VideoBitrate { get; set; } public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; } public int VideoBufferSize { get; set; }
public string VideoCodec { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public CreateFFmpegProfile ToCreate() => public CreateFFmpegProfile ToCreate() =>
new( new(
@ -66,10 +66,10 @@ public class FFmpegProfileEditViewModel
VaapiDevice, VaapiDevice,
Resolution.Id, Resolution.Id,
NormalizeVideo, NormalizeVideo,
VideoCodec, VideoFormat,
VideoBitrate, VideoBitrate,
VideoBufferSize, VideoBufferSize,
AudioCodec, AudioFormat,
AudioBitrate, AudioBitrate,
AudioBufferSize, AudioBufferSize,
NormalizeLoudness, NormalizeLoudness,
@ -90,10 +90,10 @@ public class FFmpegProfileEditViewModel
VaapiDevice, VaapiDevice,
Resolution.Id, Resolution.Id,
NormalizeVideo, NormalizeVideo,
VideoCodec, VideoFormat,
VideoBitrate, VideoBitrate,
VideoBufferSize, VideoBufferSize,
AudioCodec, AudioFormat,
AudioBitrate, AudioBitrate,
AudioBufferSize, AudioBufferSize,
NormalizeLoudness, NormalizeLoudness,

Loading…
Cancel
Save