I have now created a script to fix aspect ratio and padding. I have not experimented which is necessary and which is not ye, but I want to give the script to help others in the same situation:
First I have the bat file that acts as ffmpeg.exe:
- Code:
@echo off
echo Args^: %* >> C:\Tools\FFMPEGWrapper\ffmpegwrap.log
cscs /nl C:\Tools\FFMPEGWrapper\ffmpegwrap.cs %*
rem "C:\Program Files (x86)\Serviio\lib\ffmpeg.exe" %*
echo ErrorLevel^: %errorlevel% >> C:\Tools\FFMPEGWrapper\ffmpegwrap.log
The bat file is loaded by Serviio by modifying the ServiioService.exe.vmoptions file by adding this line:
-Dffmpeg.location=C:\Tools\FFMPEGWrapper\ffmpegwrap.bat
As can be seen, I am using cs-script (cscs) which means I programmed it in C# and can avoid having to compile an exe file for each change. It allows for easy modifications, like scripts usually allow for.
Here is the script that performs all the work. Please excuse the less than pretty code, but it was written with Notepad++ and it had a tendency to work against me:
- Code:
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System;
using System.Linq;
// Written by: galmok@gmail.com
public class Program {
public static int Main(string[] argv) {
//Console.WriteLine("Hello World");
//foreach(var arg in argv)
// Console.WriteLine(arg);
// find the argument after the "-i" argument
var iIndex = Array.FindIndex(argv, e => e == "-i");
if (iIndex >= 0) {
string inputFile = argv[iIndex + 1]; // slightly weak point here. Should check out of bounds here.
//Console.WriteLine("Input file: {0}", inputFile);
string ffmpegInfo = Program.GetFFMPEGFileInformation(inputFile);
//Console.WriteLine("FFMPEG Information: {0}", ffmpegInfo);
string stream = ffmpegInfo
.Split('\n')
.Where(x => x.Trim().StartsWith("Stream"))
.Where(x => x.Contains("Video:"))
.Where(x => x.Contains(" hevc "))
.Select(x => x)
.FirstOrDefault();
//Console.WriteLine("stream line: {0}", stream);
if (!string.IsNullOrEmpty(stream))
{
// this is a h265 file that needs further checking.
var parts = Regex.Matches(stream, @"[^,]+\(.+?\)|[^,]+")
.Cast<Match>()
.Select(m => m.Value)
.Where(x => x.Contains("SAR"))
.Where(x => x.Contains("DAR"))
.ToList();
foreach(var part in parts)
{
// Look for this: 1976x1076 [SAR 1:1 DAR 479:269]
var m = Regex.Match(part, @"([0-9]+)x([0-9]+) \[SAR ([0-9]+):([0-9]+) DAR ([0-9]+):([0-9]+)\]");
//Console.WriteLine("part: {0} {1}", part, m.Success);
if (m.Success) {
var width = int.Parse(m.Groups[1].Value);
var height = int.Parse(m.Groups[2].Value);
var SARnum = double.Parse(m.Groups[3].Value);
var SARdenom = double.Parse(m.Groups[4].Value);
var DARnum = double.Parse(m.Groups[5].Value);
var DARdenom = double.Parse(m.Groups[6].Value);
//Console.WriteLine("width: {0} height: {1}", width, height);
//Console.WriteLine("SAR: {0}:{1} = {2} [{3}]", SARnum, SARdenom, SARnum/SARdenom, GetClosestAspectRatio(SARnum, SARdenom));
//Console.WriteLine("DAR: {0}:{1} = {2} [{3}]", DARnum, DARdenom, DARnum/DARdenom, GetClosestAspectRatio(DARnum, DARdenom));
int widthPad = (width % 16)==0? width:((width/16)+1)*16;
int heightPad = (height % 8)==0? height:((height/8)+1)*8;
//Console.WriteLine("widthPad: {0} heightPad: {1}", widthPad, heightPad);
var padLeft = (widthPad-width)/2;
var padTop = (heightPad-height)/2;
var cfIndex = Array.FindIndex(argv, e => e == "-filter_complex");
if (cfIndex >= 0) {
// -vf argument conflicts with -filter_complex argument
// filter_complex example argument: [0:v]scale=1282:720[v]
// result like: [0:v]pad:1920x1080:2:2:black,setsar=ratio=1:1,setdar=ratio:16:9,scale=1282:720[v]
string cfParam = string.Format("pad={0}:{1}:{2}:{3}:black,setsar=ratio={4},setdar=ratio={5}", widthPad, heightPad, padLeft, padTop, GetClosestAspectRatio(SARnum, SARdenom), GetClosestAspectRatio(DARnum, DARdenom));
string oldCfParam = argv[cfIndex+1];
var tmp = argv.ToList();
m = Regex.Match(oldCfParam,@"(\[0:v\])(.*)");
if (m.Success) {
// video stream is already to be modified. Prepend my changes.
var part1 = m.Groups[1].Value;
var part2 = m.Groups[2].Value;
var newCfParam = string.Format("{0}{1},{2}", part1, cfParam, part2);
argv[cfIndex+1] = newCfParam;
} else {
// no video stream changes: Just add my changes.
argv[cfIndex+1] = string.Format("{0};[0:v] {1} [v]", oldCfParam, cfParam);
}
} else {
// construct: -vf "pad=1920:1080:2:2:black,setsar=1/1,setdar=16/9"
string vfArg = string.Format("pad={0}:{1}:{2}:{3}:black,setsar={4},setdar={5}", widthPad, heightPad, padLeft, padTop, GetClosestAspectRatio(SARnum, SARdenom), GetClosestAspectRatio(DARnum, DARdenom));
//Console.WriteLine("vfArg: {0}", vfArg);
var tmp = argv.ToList();
tmp.Insert(iIndex+2,"-vf");
tmp.Insert(iIndex+3,vfArg);
argv=tmp.ToArray();
}
foreach(var arg in argv)
Console.WriteLine("{0} => {1}", arg, EncodeParameterArgument(arg));
}
}
}
}
// now run ffmpeg
return RunFFMPEG(argv);
}
static string GetClosestAspectRatio(double num, double demon) {
List<KeyValuePair<double,string>> knownAspects = new List<KeyValuePair<double,string>>{
new KeyValuePair<double,string>((double)1, "1/1"),
new KeyValuePair<double,string>((double)4/(double)3, "4/3"),
new KeyValuePair<double,string>((double)16/(double)9, "16/9")
};
string result = null;
double? dist = null;
foreach(var aspect in knownAspects) {
if (!dist.HasValue || Math.Abs(aspect.Key - (num/demon)) < (double)dist) {
dist = Math.Abs(aspect.Key - (num/demon));
result = aspect.Value;
}
}
return result;
}
static string GetFFMPEGFileInformation(string filename) {
var processStartInfo = new ProcessStartInfo
{
FileName = @"C:\Program Files (x86)\Serviio\lib\ffmpeg.exe",
Arguments = "-i " + EncodeParameterArgument(filename),
RedirectStandardOutput = false,
RedirectStandardError = true,
UseShellExecute = false
};
var process = Process.Start(processStartInfo);
var output = process.StandardError.ReadToEnd();
process.WaitForExit();
return output;
}
static int RunFFMPEG(string[] argv)
{
var arguments = string.Join(" ", argv.Select(x => EncodeParameterArgument(x)));
//Console.WriteLine("args: {0}", arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = @"C:\Program Files (x86)\Serviio\lib\ffmpeg.exe",
Arguments = arguments,
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false
};
var process = new Process();
process.StartInfo = processStartInfo;
process.Start();
process.WaitForExit();
return process.ExitCode;
}
/// From StackOverflow.com:
/// <summary>
/// Encodes an argument for passing into a program
/// </summary>
/// <param name="original">The value that should be received by the program</param>
/// <returns>The value which needs to be passed to the program for the original value
/// to come through</returns>
public static string EncodeParameterArgument(string original)
{
if( string.IsNullOrEmpty(original))
return original;
string value = Regex.Replace(original, @"(\\*)" + "\"", @"$1\$0");
value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");
return value;
}
}
It will only modify arguments if there is a hevc (h265) video stream in the file that has SAR and DAR defined (I guess all files have them, but I check for it anyway). It will either add video filter (-vf) if -complex_filter is missing, will add video filter in complex_filter if there is only audio or will modify the existing video filter given with -complex_filter. It will lock aspect ratios to 1:1, 4:3 or 16:9 for both SAR and DAR. This may show not to be enough, but more aspect ratios are easily added.
At least I can have my hevc files with bad sizes/aspect ratios converted to mpeg2 in proper sizes/aspect ratios (at least good enough to allow my TV to show them properly).