Browse Source

re-introduce framerate normalization (#610)

pull/611/head
Jason Dove 4 years ago committed by GitHub
parent
commit
f02b0ac345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs
  3. 99
      ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs
  4. 3
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  5. 3
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  6. 3
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  7. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  8. 3
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  9. 3
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  10. 12
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  11. 4
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs
  12. 3
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  13. 2
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  14. 124
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  15. 3
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  16. 1
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  17. 9
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  18. 1
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs
  19. 8
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  20. 15
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  21. 24
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  22. 3
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  23. 3861
      ErsatzTV.Infrastructure/Migrations/20220204172007_Add_FFmpegProfile_NormalizeFramerate.Designer.cs
  24. 26
      ErsatzTV.Infrastructure/Migrations/20220204172007_Add_FFmpegProfile_NormalizeFramerate.cs
  25. 17
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  26. 9
      ErsatzTV/Controllers/InternalController.cs
  27. 3
      ErsatzTV/Pages/FFmpegEditor.razor
  28. 8
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

1
CHANGELOG.md

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Include `Series` category tag for all episodes in XMLTV
- Include movie, episode (show), music video (artist) genres as `category` tags in XMLTV
- Add framerate normalization to `HLS Segmenter` and `MPEG-TS` streaming modes
### Changed
- Intermittent watermarks will now fade in and out

6
ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;

99
ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<GetChannelFramerateHandler> _logger;
public GetChannelFramerateHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<GetChannelFramerateHandler> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
return None;
}
private int ParseFrameRate(string frameRate)
{
if (!int.TryParse(frameRate, out int fr))
{
string[] split = (frameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
}
}
return fr;
}
}

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

@ -24,5 +24,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -24,5 +24,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
}

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

@ -58,7 +58,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -58,7 +58,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio
NormalizeAudio = request.NormalizeAudio,
NormalizeFramerate = request.NormalizeFramerate
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>

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

@ -25,5 +25,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -25,5 +25,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
}

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

@ -50,6 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -50,6 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id);
}

3
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -23,5 +23,6 @@ namespace ErsatzTV.Application.FFmpegProfiles @@ -23,5 +23,6 @@ namespace ErsatzTV.Application.FFmpegProfiles
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio);
bool NormalizeAudio,
bool NormalizeFramerate);
}

3
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -25,7 +25,8 @@ namespace ErsatzTV.Application.FFmpegProfiles @@ -25,7 +25,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
profile.NormalizeLoudness,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeAudio);
profile.NormalizeAudio,
profile.NormalizeVideo && profile.NormalizeFramerate);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);

12
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -5,6 +5,7 @@ using System.Linq; @@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Application.Streaming.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -30,6 +31,7 @@ namespace ErsatzTV.Application.Streaming @@ -30,6 +31,7 @@ namespace ErsatzTV.Application.Streaming
private Timer _timer;
private readonly object _sync = new();
private DateTimeOffset _playlistStart;
private Option<int> _targetFramerate;
public HlsSessionWorker(IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger)
{
@ -66,6 +68,13 @@ namespace ErsatzTV.Application.Streaming @@ -66,6 +68,13 @@ namespace ErsatzTV.Application.Streaming
CancellationToken cancellationToken = cts.Token;
_logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber);
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
_targetFramerate = await mediator.Send(
new GetChannelFramerate(channelNumber),
cancellationToken);
Touch();
_transcodedUntil = DateTimeOffset.Now;
@ -142,7 +151,8 @@ namespace ErsatzTV.Application.Streaming @@ -142,7 +151,8 @@ namespace ErsatzTV.Application.Streaming
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!firstProcess,
realtime,
ptsOffset);
ptsOffset,
_targetFramerate);
// _logger.LogInformation("Request {@Request}", request);

4
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using LanguageExt;
namespace ErsatzTV.Application.Streaming.Queries
{
@ -7,7 +8,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -7,7 +8,8 @@ namespace ErsatzTV.Application.Streaming.Queries
DateTimeOffset Now,
bool StartAtZero,
bool HlsRealtime,
long PtsOffset) : FFmpegProcessRequest(ChannelNumber,
long PtsOffset,
Option<int> TargetFramerate) : FFmpegProcessRequest(ChannelNumber,
Mode,
Now,
StartAtZero,

3
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -159,7 +159,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -159,7 +159,8 @@ namespace ErsatzTV.Application.Streaming.Queries
playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint,
playoutItemWithPath.PlayoutItem.OutPoint,
request.PtsOffset);
request.PtsOffset,
request.TargetFramerate);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);

2
ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs

@ -184,7 +184,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -184,7 +184,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
ChannelWatermarkLocation.TopLeft,
false,
100,
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=yuv420p[v]",
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]",
"0:1",
"[v]")]
[TestCase(

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

@ -4,6 +4,7 @@ using ErsatzTV.Core.FFmpeg; @@ -4,6 +4,7 @@ using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using FluentAssertions;
using NUnit.Framework;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.FFmpeg
{
@ -32,7 +33,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -32,7 +33,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.FormatFlags.Should().NotContain("+genpts");
}
@ -54,7 +56,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -54,7 +56,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ThreadCount.Should().Be(1);
}
@ -74,7 +77,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -74,7 +77,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ThreadCount.Should().Be(7);
}
@ -94,7 +98,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -94,7 +98,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length);
@ -116,7 +121,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -116,7 +121,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length);
@ -138,7 +144,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -138,7 +144,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.RealtimeOutput.Should().BeTrue();
}
@ -158,7 +165,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -158,7 +165,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.RealtimeOutput.Should().BeTrue();
}
@ -180,7 +188,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -180,7 +188,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now.AddMinutes(5),
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@ -203,7 +212,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -203,7 +212,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now.AddMinutes(5),
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@ -224,7 +234,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -224,7 +234,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -251,7 +262,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -251,7 +262,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -278,7 +290,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -278,7 +290,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -305,7 +318,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -305,7 +318,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -334,7 +348,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -334,7 +348,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -362,7 +377,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -362,7 +377,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
scaledSize.Width.Should().Be(1280);
@ -393,7 +409,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -393,7 +409,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -422,7 +439,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -422,7 +439,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -452,7 +470,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -452,7 +470,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -485,7 +504,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -485,7 +504,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -518,7 +538,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -518,7 +538,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -550,7 +571,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -550,7 +571,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -583,7 +605,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -583,7 +605,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -618,7 +641,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -618,7 +641,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -651,7 +675,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -651,7 +675,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -683,7 +708,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -683,7 +708,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -714,7 +740,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -714,7 +740,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -747,7 +774,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -747,7 +774,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -776,7 +804,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -776,7 +804,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioCodec.Should().Be("aac");
}
@ -802,7 +831,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -802,7 +831,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioCodec.Should().Be("copy");
}
@ -829,7 +859,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -829,7 +859,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioCodec.Should().Be("aac");
}
@ -856,7 +887,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -856,7 +887,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioCodec.Should().Be("copy");
}
@ -884,7 +916,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -884,7 +916,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioBitrate.IfNone(0).Should().Be(2424);
}
@ -912,7 +945,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -912,7 +945,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioBufferSize.IfNone(0).Should().Be(2424);
}
@ -940,7 +974,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -940,7 +974,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioChannels.IfNone(0).Should().Be(6);
}
@ -968,7 +1003,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -968,7 +1003,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioSampleRate.IfNone(0).Should().Be(48);
}
@ -995,7 +1031,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -995,7 +1031,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioChannels.IfNone(0).Should().Be(6);
}
@ -1022,7 +1059,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -1022,7 +1059,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.AudioSampleRate.IfNone(0).Should().Be(48);
}
@ -1050,7 +1088,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -1050,7 +1088,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.FromMinutes(2),
false);
false,
None);
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
}
@ -1077,7 +1116,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -1077,7 +1116,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.NormalizeLoudness.Should().BeTrue();
}
@ -1104,7 +1144,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -1104,7 +1144,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.NormalizeLoudness.Should().BeFalse();
}
@ -1133,7 +1174,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -1133,7 +1174,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
None);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
}

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

@ -215,7 +215,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -215,7 +215,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FillerKind.None,
TimeSpan.Zero,
TimeSpan.FromSeconds(5),
0);
0,
None);
process.StartInfo.RedirectStandardError = true;

1
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -24,6 +24,7 @@ namespace ErsatzTV.Core.Domain @@ -24,6 +24,7 @@ namespace ErsatzTV.Core.Domain
public int AudioChannels { get; set; }
public int AudioSampleRate { get; set; }
public bool NormalizeAudio { get; set; }
public bool NormalizeFramerate { get; set; }
public static FFmpegProfile New(string name, Resolution resolution) =>
new()

9
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -28,6 +28,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -28,6 +28,7 @@ namespace ErsatzTV.Core.FFmpeg
private string _videoEncoder;
private Option<string> _subtitle;
private bool _boxBlur;
private Option<int> _frameRate;
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
{
@ -85,6 +86,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -85,6 +86,12 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegComplexFilterBuilder WithFrameRate(Option<int> frameRate)
{
_frameRate = frameRate;
return this;
}
public FFmpegComplexFilterBuilder WithWatermark(
Option<ChannelWatermark> watermark,
Option<List<FadePoint>> maybeFadePoints,
@ -240,6 +247,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -240,6 +247,8 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add("format=nv12|vaapi,hwupload");
}
videoFilterQueue.AddRange(_frameRate.Select(frameRate => $"fps=fps={frameRate}"));
_scaleToSize.IfSome(
size =>
{

1
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -28,5 +28,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -28,5 +28,6 @@ namespace ErsatzTV.Core.FFmpeg
public bool Deinterlace { get; set; }
public Option<int> VideoTrackTimeScale { get; set; }
public bool NormalizeLoudness { get; set; }
public Option<int> FrameRate { get; set; }
}
}

8
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -58,7 +58,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -58,7 +58,8 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset now,
TimeSpan inPoint,
TimeSpan outPoint,
bool hlsRealtime)
bool hlsRealtime,
Option<int> targetFramerate)
{
var result = new FFmpegPlaybackSettings
{
@ -113,6 +114,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -113,6 +114,11 @@ namespace ErsatzTV.Core.FFmpeg
if (ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo)
{
if (ffmpegProfile.NormalizeFramerate)
{
result.FrameRate = targetFramerate;
}
result.VideoTrackTimeScale = 90000;
}

15
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -214,6 +214,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -214,6 +214,12 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithFrameRate(Option<int> frameRate)
{
_complexFilterBuilder = _complexFilterBuilder.WithFrameRate(frameRate);
return this;
}
public FFmpegProcessBuilder WithWatermark(
Option<WatermarkOptions> watermarkOptions,
Option<List<FadePoint>> maybeFadePoints,
@ -406,10 +412,15 @@ namespace ErsatzTV.Core.FFmpeg @@ -406,10 +412,15 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion, long ptsOffset, Option<int> maybeTimeScale)
public FFmpegProcessBuilder WithHls(
string channelNumber,
Option<MediaVersion> mediaVersion,
long ptsOffset,
Option<int> maybeTimeScale,
Option<int> maybeFrameRate)
{
const int SEGMENT_SECONDS = 4;
int frameRate = GetFrameRateFromMediaVersion(mediaVersion);
int frameRate = maybeFrameRate.IfNone(GetFrameRateFromMediaVersion(mediaVersion));
foreach (int timescale in maybeTimeScale)
{

24
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -53,7 +53,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -53,7 +53,8 @@ namespace ErsatzTV.Core.FFmpeg
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset)
long ptsOffset,
Option<int> targetFramerate)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
@ -68,7 +69,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -68,7 +69,8 @@ namespace ErsatzTV.Core.FFmpeg
now,
inPoint,
outPoint,
hlsRealtime);
hlsRealtime,
targetFramerate);
Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
@ -114,6 +116,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -114,6 +116,7 @@ namespace ErsatzTV.Core.FFmpeg
videoStream.Codec,
videoStream.PixelFormat)
.WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution)
.WithFrameRate(playbackSettings.FrameRate)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
@ -182,7 +185,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -182,7 +185,12 @@ namespace ErsatzTV.Core.FFmpeg
{
// HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, videoVersion, ptsOffset, playbackSettings.VideoTrackTimeScale)
return builder.WithHls(
channel.Number,
videoVersion,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build();
default:
return builder.WithFormat("mpegts")
@ -246,7 +254,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -246,7 +254,12 @@ namespace ErsatzTV.Core.FFmpeg
{
// HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, None, ptsOffset, playbackSettings.VideoTrackTimeScale)
return builder.WithHls(
channel.Number,
None,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build();
default:
return builder.WithFormat("mpegts")
@ -361,7 +374,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -361,7 +374,8 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset.UnixEpoch,
TimeSpan.Zero,
TimeSpan.Zero,
false);
false,
Option<int>.None);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1)

3
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -28,7 +28,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg @@ -28,7 +28,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset);
long ptsOffset,
Option<int> targetFramerate);
Task<Process> ForError(
string ffmpegPath,

3861
ErsatzTV.Infrastructure/Migrations/20220204172007_Add_FFmpegProfile_NormalizeFramerate.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20220204172007_Add_FFmpegProfile_NormalizeFramerate.cs

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

17
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -505,6 +505,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -505,6 +505,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeFramerate")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeLoudness")
.HasColumnType("INTEGER");
@ -645,7 +648,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -645,7 +648,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SongMetadataId");
b.ToTable("Genre");
b.ToTable("Genre", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b =>
@ -1052,7 +1055,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1052,7 +1055,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("ArtistMetadataId");
b.ToTable("Mood");
b.ToTable("Mood", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b =>
@ -1160,7 +1163,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1160,7 +1163,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SmartCollectionId");
b.ToTable("MultiCollectionSmartItem");
b.ToTable("MultiCollectionSmartItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b =>
@ -1768,7 +1771,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1768,7 +1771,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("ArtistMetadataId");
b.ToTable("Style");
b.ToTable("Style", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b =>
@ -1822,7 +1825,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1822,7 +1825,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SongMetadataId");
b.ToTable("Tag");
b.ToTable("Tag", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b =>
@ -2843,7 +2846,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2843,7 +2846,7 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
b.OwnsOne("ErsatzTV.Core.Domain.Playout.Anchor#ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
{
b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
@ -2946,7 +2949,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2946,7 +2949,7 @@ namespace ErsatzTV.Infrastructure.Migrations
.WithMany()
.HasForeignKey("SmartCollectionId");
b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor.EnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
{
b1.Property<int>("PlayoutProgramScheduleAnchorId")
.HasColumnType("INTEGER");

9
ErsatzTV/Controllers/InternalController.cs

@ -34,7 +34,14 @@ namespace ErsatzTV.Controllers @@ -34,7 +34,14 @@ namespace ErsatzTV.Controllers
[FromQuery]
string mode = "mixed") =>
_mediator.Send(
new GetPlayoutItemProcessByChannelNumber(channelNumber, mode, DateTimeOffset.Now, false, true, 0))
new GetPlayoutItemProcessByChannelNumber(
channelNumber,
mode,
DateTimeOffset.Now,
false,
true,
0,
Option<int>.None))
.Map(
result =>
result.Match<IActionResult>(

3
ErsatzTV/Pages/FFmpegEditor.razor

@ -81,6 +81,9 @@ @@ -81,6 +81,9 @@
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Frame Rate" @bind-Checked="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)"/>
</MudElement>
</MudItem>
<MudItem>
<MudText Typo="Typo.h6">Audio</MudText>

8
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -24,6 +24,7 @@ namespace ErsatzTV.ViewModels @@ -24,6 +24,7 @@ namespace ErsatzTV.ViewModels
Name = viewModel.Name;
NormalizeAudio = viewModel.NormalizeAudio;
NormalizeVideo = viewModel.NormalizeVideo;
NormalizeFramerate = viewModel.NormalizeFramerate;
Resolution = viewModel.Resolution;
ThreadCount = viewModel.ThreadCount;
Transcode = viewModel.Transcode;
@ -45,6 +46,7 @@ namespace ErsatzTV.ViewModels @@ -45,6 +46,7 @@ namespace ErsatzTV.ViewModels
public string Name { get; set; }
public bool NormalizeAudio { get; set; }
public bool NormalizeVideo { get; set; }
public bool NormalizeFramerate { get; set; }
public ResolutionViewModel Resolution { get; set; }
public int ThreadCount { get; set; }
public bool Transcode { get; set; }
@ -74,7 +76,8 @@ namespace ErsatzTV.ViewModels @@ -74,7 +76,8 @@ namespace ErsatzTV.ViewModels
NormalizeLoudness,
AudioChannels,
AudioSampleRate,
NormalizeAudio
NormalizeAudio,
NormalizeFramerate
);
public UpdateFFmpegProfile ToUpdate() =>
@ -97,7 +100,8 @@ namespace ErsatzTV.ViewModels @@ -97,7 +100,8 @@ namespace ErsatzTV.ViewModels
NormalizeLoudness,
AudioChannels,
AudioSampleRate,
NormalizeAudio
NormalizeAudio,
NormalizeFramerate
);
}
}

Loading…
Cancel
Save