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/). @@ -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
- 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
### Fixed
- 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( @@ -13,10 +13,10 @@ public record CreateFFmpegProfile(
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,

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

@ -18,9 +18,9 @@ public class CreateFFmpegProfileHandler : @@ -18,9 +18,9 @@ public class CreateFFmpegProfileHandler :
CreateFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
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(
@ -45,10 +45,10 @@ public class CreateFFmpegProfileHandler : @@ -45,10 +45,10 @@ public class CreateFFmpegProfileHandler :
VaapiDevice = request.VaapiDevice,
ResolutionId = resolutionId,
NormalizeVideo = request.NormalizeVideo,
VideoCodec = request.VideoCodec,
VideoFormat = request.VideoFormat,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioCodec = request.AudioCodec,
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness,

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

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

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

@ -18,9 +18,9 @@ public class @@ -18,9 +18,9 @@ public class
UpdateFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
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(
@ -36,10 +36,10 @@ public class @@ -36,10 +36,10 @@ public class
p.VaapiDevice = update.VaapiDevice;
p.ResolutionId = update.ResolutionId;
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
p.VideoCodec = update.VideoCodec;
p.VideoFormat = update.VideoFormat;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec;
p.AudioFormat = update.AudioFormat;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;

4
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

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

4
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

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

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

@ -445,14 +445,14 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -445,14 +445,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetDesiredVideoCodec_When_ContentIsPadded_ForTransportStream()
public void Should_SetDesiredVideoFormat_When_ContentIsPadded_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
Transcode = true,
NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -473,19 +473,19 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -473,19 +473,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
actual.VideoCodec.Should().Be("testCodec");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
}
[Test]
public void
Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
Should_SetDesiredVideoFormat_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
Transcode = true,
NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -507,19 +507,19 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -507,19 +507,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("testCodec");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
}
[Test]
public void
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming()
Should_SetCopyVideoFormat_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming()
{
var ffmpegProfile = new FFmpegProfile
{
Transcode = true,
NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "testCodec"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -541,18 +541,18 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -541,18 +541,18 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
}
[Test]
public void Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_ForTransportStream()
public void Should_NotSetCopyVideoFormat_When_ContentIsCorrectSize_And_CorrectFormat_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
Transcode = true,
NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -563,7 +563,7 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -563,7 +563,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
StreamingMode.TransportStream,
ffmpegProfile,
version,
new MediaStream { Codec = "libx264" },
new MediaStream { Codec = "h264" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now,
@ -574,19 +574,19 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -574,19 +574,19 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
}
[Test]
public void
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
Should_SetCopyVideoFormat_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
Transcode = true,
NormalizeVideo = false,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -608,12 +608,12 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -608,12 +608,12 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
}
[Test]
public void
Should_SetCopyVideoCodec_AndCopyAudioCodec_When_NotTranscoding_ForTransportStream()
Should_SetCopyVideoFormat_AndCopyAudioFormat_When_NotTranscoding_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
@ -622,7 +622,7 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -622,7 +622,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
NormalizeAudio = true,
NormalizeLoudness = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264"
VideoFormat = FFmpegProfileVideoFormat.H264
};
// not anamorphic
@ -644,9 +644,9 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -644,9 +644,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy");
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
actual.NormalizeLoudness.Should().BeFalse();
actual.AudioCodec.Should().Be("copy");
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
}
[Test]
@ -781,13 +781,13 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -781,13 +781,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetDesiredAudioFormat_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioCodec = "aac"
AudioFormat = FFmpegProfileAudioFormat.Aac
};
var version = new MediaVersion();
@ -805,16 +805,16 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -805,16 +805,16 @@ public class FFmpegPlaybackSettingsCalculatorTests
false,
None);
actual.AudioCodec.Should().Be("aac");
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
}
[Test]
public void Should_SetCopyAudioCodec_When_NotNormalizingAudio_ForTransportStream()
public void Should_SetCopyAudioFormat_When_NotNormalizingAudio_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
NormalizeAudio = false,
AudioCodec = "aac"
AudioFormat = FFmpegProfileAudioFormat.Aac
};
var version = new MediaVersion();
@ -832,17 +832,17 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -832,17 +832,17 @@ public class FFmpegPlaybackSettingsCalculatorTests
false,
None);
actual.AudioCodec.Should().Be("copy");
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
}
[Test]
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_ForTransportStream()
public void Should_SetDesiredAudioFormat_When_NormalizingAudio_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioCodec = "aac"
AudioFormat = FFmpegProfileAudioFormat.Aac
};
var version = new MediaVersion();
@ -860,17 +860,17 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -860,17 +860,17 @@ public class FFmpegPlaybackSettingsCalculatorTests
false,
None);
actual.AudioCodec.Should().Be("aac");
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
}
[Test]
public void Should_SetCopyAudioCodec_When_NormalizingAudio_ForHttpLiveStreaming()
public void Should_SetCopyAudioFormat_When_NormalizingAudio_ForHttpLiveStreaming()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioCodec = "aac"
AudioFormat = FFmpegProfileAudioFormat.Aac
};
var version = new MediaVersion();
@ -888,18 +888,18 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -888,18 +888,18 @@ public class FFmpegPlaybackSettingsCalculatorTests
false,
None);
actual.AudioCodec.Should().Be("copy");
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
}
[Test]
public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioBitrate = 2424,
AudioCodec = "ac3"
AudioFormat = FFmpegProfileAudioFormat.Ac3
};
var version = new MediaVersion();
@ -921,14 +921,14 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -921,14 +921,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioBufferSize = 2424,
AudioCodec = "ac3"
AudioFormat = FFmpegProfileAudioFormat.Ac3
};
var version = new MediaVersion();
@ -950,13 +950,13 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -950,13 +950,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioCodec = "ac3",
AudioFormat = FFmpegProfileAudioFormat.Ac3,
AudioChannels = 6
};
@ -979,13 +979,13 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -979,13 +979,13 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioCodec = "ac3",
AudioFormat = FFmpegProfileAudioFormat.Ac3,
AudioSampleRate = 48
};
@ -1064,14 +1064,14 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -1064,14 +1064,14 @@ public class FFmpegPlaybackSettingsCalculatorTests
}
[Test]
public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectFormat_ForTransportStream()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
Transcode = true,
NormalizeAudio = true,
AudioSampleRate = 48,
AudioCodec = "ac3"
AudioFormat = FFmpegProfileAudioFormat.Ac3
};
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 @@ -118,21 +118,15 @@ public class TranscodingTests
new() { Width = 1280, Height = 720 }
};
public static string[] SoftwareCodecs =
{
"libx264",
"libx265"
};
public static HardwareAccelerationKind[] NoAcceleration =
{
HardwareAccelerationKind.None
};
public static string[] NvidiaCodecs =
public static FFmpegProfileVideoFormat[] VideoFormats =
{
"h264_nvenc",
"hevc_nvenc"
FFmpegProfileVideoFormat.H264,
FFmpegProfileVideoFormat.Hevc
};
public static HardwareAccelerationKind[] NvidiaAcceleration =
@ -140,34 +134,16 @@ public class TranscodingTests @@ -140,34 +134,16 @@ public class TranscodingTests
HardwareAccelerationKind.Nvenc
};
public static string[] VaapiCodecs =
{
"h264_vaapi",
"hevc_vaapi"
};
public static HardwareAccelerationKind[] VaapiAcceleration =
{
HardwareAccelerationKind.Vaapi
};
public static string[] VideoToolboxCodecs =
{
"h264_videotoolbox",
"hevc_videotoolbox"
};
public static HardwareAccelerationKind[] VideoToolboxAcceleration =
{
HardwareAccelerationKind.VideoToolbox
};
public static string[] QsvCodecs =
{
"h264_qsv",
"hevc_qsv"
};
public static HardwareAccelerationKind[] QsvAcceleration =
{
HardwareAccelerationKind.Qsv
@ -176,27 +152,16 @@ public class TranscodingTests @@ -176,27 +152,16 @@ public class TranscodingTests
[Test, Combinatorial]
public async Task Transcode(
[ValueSource(typeof(TestData), nameof(TestData.InputFormats))]
InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))]
Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
VideoScanKind videoScanKind,
[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.InputFormats))] InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))] Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] VideoScanKind videoScanKind,
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark,
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] 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.VideoToolboxCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
{
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
@ -209,7 +174,7 @@ public class TranscodingTests @@ -209,7 +174,7 @@ public class TranscodingTests
}
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");
if (!File.Exists(file))
@ -360,8 +325,8 @@ public class TranscodingTests @@ -360,8 +325,8 @@ public class TranscodingTests
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
{
HardwareAcceleration = profileAcceleration,
VideoCodec = profileCodec,
AudioCodec = "aac"
VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac
},
StreamingMode = StreamingMode.TransportStream
},

8
ErsatzTV.Core/Domain/FFmpegProfile.cs

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

11
ErsatzTV.Core/Domain/FFmpegProfileAudioFormat.cs

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

23
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

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

4
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

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

35
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

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

28
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

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

174
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -35,7 +35,7 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -35,7 +35,7 @@ public class FFmpegProcessService : IFFmpegProcessService
_logger = logger;
}
public async Task<Process> ForPlayoutItem(
public Task<Process> ForPlayoutItem(
string ffmpegPath,
bool saveReports,
Channel channel,
@ -56,144 +56,7 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -56,144 +56,7 @@ public class FFmpegProcessService : IFFmpegProcessService
long ptsOffset,
Option<int> targetFramerate)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
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()
};
throw new NotSupportedException();
}
public async Task<Process> ForError(
@ -225,6 +88,19 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -225,6 +88,19 @@ public class FFmpegProcessService : IFFmpegProcessService
var videoStream = 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)
.WithThreads(1)
.WithQuiet()
@ -239,9 +115,9 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -239,9 +115,9 @@ public class FFmpegProcessService : IFFmpegProcessService
audioStream,
Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"),
"fake-audio-path",
playbackSettings.VideoCodec)
playbackSettings.VideoFormat)
.WithPixfmt("yuv420p")
.WithPlaybackArgs(playbackSettings)
.WithPlaybackArgs(playbackSettings, videoCodec, audioCodec)
.WithMetadata(channel, None);
await duration.IfSomeAsync(d => builder = builder.WithDuration(d));
@ -266,19 +142,7 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -266,19 +142,7 @@ public class FFmpegProcessService : IFFmpegProcessService
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
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();
throw new NotSupportedException();
}
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
@ -398,7 +262,7 @@ public class FFmpegProcessService : IFFmpegProcessService @@ -398,7 +262,7 @@ public class FFmpegProcessService : IFFmpegProcessService
None,
videoPath,
None,
playbackSettings.VideoCodec)
playbackSettings.VideoFormat)
.WithOutputFormat("apng", outputFile)
.Build();

4
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -44,8 +44,8 @@ public class ChannelPlaylist @@ -44,8 +44,8 @@ public class ChannelPlaylist
_ => "ts?mode=ts-legacy"
};
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();
string acodec = channel.FFmpegProfile.AudioCodec;
string vcodec = channel.FFmpegProfile.VideoFormat.ToString().ToLowerInvariant();
string acodec = channel.FFmpegProfile.AudioFormat.ToString().ToLowerInvariant();
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}");

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

15
ErsatzTV/Pages/FFmpeg.razor

@ -40,14 +40,21 @@ @@ -40,14 +40,21 @@
@context.Resolution.Name
</MudText>
</MudTd>
<MudTd DataLabel="Video Codec">
<MudTd DataLabel="Video Format">
<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>
</MudTd>
<MudTd DataLabel="Audio Codec">
<MudTd DataLabel="Audio Format">
<MudText Color="@(context.Transcode && context.NormalizeAudio ? Color.Tertiary : Color.Inherit)">
@context.AudioCodec
@context.AudioFormat.ToString().ToLowerInvariant()
</MudText>
</MudTd>
<MudTd>

11
ErsatzTV/Pages/FFmpegEditor.razor

@ -44,7 +44,11 @@ @@ -44,7 +44,11 @@
</MudItem>
<MudItem>
<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">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement>
@ -84,7 +88,10 @@ @@ -84,7 +88,10 @@
</MudItem>
<MudItem>
<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">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement>

59
ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs

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

16
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

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

Loading…
Cancel
Save