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/).
### Added ### Added
- Include `Series` category tag for all episodes in XMLTV - Include `Series` category tag for all episodes in XMLTV
- Include movie, episode (show), music video (artist) genres as `category` tags 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 ### Changed
- Intermittent watermarks will now fade in and out - Intermittent watermarks will now fade in and out

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

@ -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 @@
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
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, 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
NormalizeLoudness = request.NormalizeLoudness, NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels, AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate, AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio NormalizeAudio = request.NormalizeAudio,
NormalizeFramerate = request.NormalizeFramerate
}); });
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) => private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>

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

@ -25,5 +25,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, 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
p.AudioChannels = update.AudioChannels; p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate; p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.Transcode && update.NormalizeAudio; p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id); return new UpdateFFmpegProfileResult(p.Id);
} }

3
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

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

3
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

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

12
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Application.Streaming.Queries; using ErsatzTV.Application.Streaming.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -30,6 +31,7 @@ namespace ErsatzTV.Application.Streaming
private Timer _timer; private Timer _timer;
private readonly object _sync = new(); private readonly object _sync = new();
private DateTimeOffset _playlistStart; private DateTimeOffset _playlistStart;
private Option<int> _targetFramerate;
public HlsSessionWorker(IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger) public HlsSessionWorker(IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger)
{ {
@ -67,6 +69,13 @@ namespace ErsatzTV.Application.Streaming
_logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); _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(); Touch();
_transcodedUntil = DateTimeOffset.Now; _transcodedUntil = DateTimeOffset.Now;
_playlistStart = _transcodedUntil; _playlistStart = _transcodedUntil;
@ -142,7 +151,8 @@ namespace ErsatzTV.Application.Streaming
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!firstProcess, !firstProcess,
realtime, realtime,
ptsOffset); ptsOffset,
_targetFramerate);
// _logger.LogInformation("Request {@Request}", request); // _logger.LogInformation("Request {@Request}", request);

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

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

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

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

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

@ -184,7 +184,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
ChannelWatermarkLocation.TopLeft, ChannelWatermarkLocation.TopLeft,
false, false,
100, 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", "0:1",
"[v]")] "[v]")]
[TestCase( [TestCase(

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

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

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

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

1
ErsatzTV.Core/Domain/FFmpegProfile.cs

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

9
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

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

1
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -28,5 +28,6 @@ namespace ErsatzTV.Core.FFmpeg
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; }
public Option<int> FrameRate { get; set; }
} }
} }

8
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

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

15
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

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

24
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

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

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

@ -28,7 +28,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
FillerKind fillerKind, FillerKind fillerKind,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint, TimeSpan outPoint,
long ptsOffset); long ptsOffset,
Option<int> targetFramerate);
Task<Process> ForError( Task<Process> ForError(
string ffmpegPath, 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 @@
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
b.Property<bool>("NormalizeAudio") b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("NormalizeFramerate")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeLoudness") b.Property<bool>("NormalizeLoudness")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -645,7 +648,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SongMetadataId"); b.HasIndex("SongMetadataId");
b.ToTable("Genre"); b.ToTable("Genre", (string)null);
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b =>
@ -1052,7 +1055,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("ArtistMetadataId"); b.HasIndex("ArtistMetadataId");
b.ToTable("Mood"); b.ToTable("Mood", (string)null);
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b =>
@ -1160,7 +1163,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SmartCollectionId"); b.HasIndex("SmartCollectionId");
b.ToTable("MultiCollectionSmartItem"); b.ToTable("MultiCollectionSmartItem", (string)null);
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b =>
@ -1768,7 +1771,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("ArtistMetadataId"); b.HasIndex("ArtistMetadataId");
b.ToTable("Style"); b.ToTable("Style", (string)null);
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b =>
@ -1822,7 +1825,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("SongMetadataId"); b.HasIndex("SongMetadataId");
b.ToTable("Tag"); b.ToTable("Tag", (string)null);
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b =>
@ -2843,7 +2846,7 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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") b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -2946,7 +2949,7 @@ namespace ErsatzTV.Infrastructure.Migrations
.WithMany() .WithMany()
.HasForeignKey("SmartCollectionId"); .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") b1.Property<int>("PlayoutProgramScheduleAnchorId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

9
ErsatzTV/Controllers/InternalController.cs

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

3
ErsatzTV/Pages/FFmpegEditor.razor

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

8
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

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

Loading…
Cancel
Save