prioritize container aspect ratio over stream aspect ratio (#930)

* prioritize container aspect ratio over stream aspect ratio

* use setdar filter
This commit is contained in:
Jason Dove
2022-08-16 19:57:38 -05:00
committed by GitHub
parent 36d3d38530
commit d66efa0a1d
10 changed files with 94 additions and 42 deletions
+6 -2
View File
@@ -6,8 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Use MIME Type `application/x-mpegurl` for all playlists instead of `application/vnd.apple.mpegurl`
- Remove `setsar` filter which caused issues scaling between two different aspect ratios
- For example, some 4:3 content would appear stretched when scaled to a 16:9 resolution
- Replace `setsar` filter with `setdar` filter
- `setsar` caused issues scaling between two different aspect ratios
- For example, some 4:3 content would appear stretched when scaled to a 16:9 resolution
- `setdar` is now only used when aspect ratios match
- Prioritize aspect ratio from container when video stream contains conflicting aspect ratio
- This is usually caused by bad authoring, but the change should improve scaling behavior for edge cases
### Added
- Support DSD audio file formats (DFF and DSF) in local song libraries
@@ -140,6 +140,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.DisplayAspectRatio,
videoVersion.RFrameRate,
videoPath != audioPath); // still image when paths are different
@@ -209,6 +210,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
channel.FFmpegProfile.Resolution.Width == 640 ? "4:3" : "16:9",
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
@@ -302,6 +304,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
new PixelFormatYuv420P(),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
desiredResolution.Width == 640 ? "4:3" : "16:9",
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
@@ -329,6 +332,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
VideoFormat.GeneratedImage,
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.DisplayAspectRatio,
None,
true);
@@ -417,7 +421,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{
var videoInputFile = new VideoInputFile(
inputFile,
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, None, true) });
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, string.Empty, None, true) });
var pipelineBuilder = new PipelineBuilder(
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
@@ -497,6 +501,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
"unknown",
new PixelFormatUnknown(),
new FrameSize(1, 1),
string.Empty,
Option<string>.None,
!options.IsAnimated)
},
@@ -3,6 +3,8 @@ using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Bugsnag;
using CliWrap;
using CliWrap.Buffered;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -157,42 +159,48 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
}
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
private async Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
{
var startInfo = new ProcessStartInfo
string[] arguments =
{
FileName = ffprobePath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
"-hide_banner",
"-print_format", "json",
"-show_format",
"-show_streams",
"-show_chapters",
"-i", filePath
};
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add("quiet");
startInfo.ArgumentList.Add("-print_format");
startInfo.ArgumentList.Add("json");
startInfo.ArgumentList.Add("-show_format");
startInfo.ArgumentList.Add("-show_streams");
startInfo.ArgumentList.Add("-show_chapters");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(filePath);
BufferedCommandResult probe = await Cli.Wrap(ffprobePath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
var probe = new Process
if (probe.ExitCode != 0)
{
StartInfo = startInfo
};
return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
}
probe.Start();
return probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, FFprobe>>(
async output =>
FFprobe ffprobe = JsonConvert.DeserializeObject<FFprobe>(probe.StandardOutput);
if (ffprobe != null)
{
const string PATTERN = @"\[SAR\s+([0-9]+:[0-9]+)\s+DAR\s+([0-9]+:[0-9]+)\]";
Match match = Regex.Match(probe.StandardError, PATTERN);
if (match.Success)
{
await probe.WaitForExitAsync();
return probe.ExitCode == 0
? JsonConvert.DeserializeObject<FFprobe>(output)
: BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
});
string sar = match.Groups[1].Value;
string dar = match.Groups[2].Value;
foreach (FFprobeStream stream in ffprobe.streams.Where(s => s.codec_type == "video").ToList())
{
FFprobeStream replacement = stream with { sample_aspect_ratio = sar, display_aspect_ratio = dar };
ffprobe.streams.Remove(stream);
ffprobe.streams.Add(replacement);
}
}
}
return ffprobe;
}
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
@@ -25,7 +25,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -46,6 +46,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -95,7 +96,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -116,6 +117,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -182,7 +184,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -203,6 +205,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -257,7 +260,7 @@ public class PipelineGeneratorTests
"/test/input/file.png",
new List<VideoStream>
{
new(0, string.Empty, Option<IPixelFormat>.None, FrameSize.Unknown, Option<string>.None, true)
new(0, string.Empty, Option<IPixelFormat>.None, FrameSize.Unknown, string.Empty, Option<string>.None, true)
});
var pipelineBuilder = new PipelineBuilder(
+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV.FFmpeg.Filter;
public class SetDarFilter : BaseFilter
{
private readonly string _displayAspectRatio;
public SetDarFilter(string displayAspectRatio)
{
_displayAspectRatio = displayAspectRatio;
}
public override string Filter => $"setdar=dar={_displayAspectRatio.Replace(':', '/')}";
public override FrameState NextState(FrameState currentState) => currentState;
}
-7
View File
@@ -1,7 +0,0 @@
namespace ErsatzTV.FFmpeg.Filter;
public class SetSarFilter : BaseFilter
{
public override string Filter => "setsar=1";
public override FrameState NextState(FrameState currentState) => currentState;
}
+1
View File
@@ -9,6 +9,7 @@ public record FrameState(
Option<IPixelFormat> PixelFormat,
FrameSize ScaledSize,
FrameSize PaddedSize,
string DisplayAspectRatio,
Option<int> FrameRate,
Option<int> VideoBitrate,
Option<int> VideoBufferSize,
+1
View File
@@ -19,6 +19,7 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
string.Empty,
Option<IPixelFormat>.None,
Resolution,
string.Empty,
Option<string>.None,
false)
})
+1
View File
@@ -14,6 +14,7 @@ public record VideoStream(
string Codec,
Option<IPixelFormat> PixelFormat,
FrameSize FrameSize,
string DisplayAspectRatio,
Option<string> FrameRate,
bool StillImage) : MediaStream(
Index,
+22
View File
@@ -188,6 +188,7 @@ public class PipelineBuilder
videoStream.PixelFormat,
videoStream.FrameSize,
videoStream.FrameSize,
videoStream.DisplayAspectRatio,
initialFrameRate,
Option<int>.None,
Option<int>.None,
@@ -379,6 +380,13 @@ public class PipelineBuilder
IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize);
currentState = padStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
}
else if (currentState.ScaledSize != desiredState.ScaledSize)
@@ -398,6 +406,13 @@ public class PipelineBuilder
currentState = padStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
}
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
else if (currentState.PaddedSize != desiredState.PaddedSize)
{
@@ -415,6 +430,13 @@ public class PipelineBuilder
currentState = padStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
}
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
if (hasOverlay && currentState.PixelFormat.Map(pf => pf.FFmpegName) !=