Retain original color space when encoding, cache ffmpeg/ffprobe outputs

This commit is contained in:
N00MKRAD 2021-05-06 12:08:03 +02:00
parent 58e3f3e4f0
commit 55c0f5904c
11 changed files with 274 additions and 46 deletions

92
Code/Data/ColorInfo.cs Normal file
View File

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Flowframes.Data
{
class ColorInfo
{
public string colorSpace;
public string colorRange;
public string colorTransfer;
public string colorPrimaries;
public ColorInfo(string ffprobeOutput)
{
string[] lines = ffprobeOutput.SplitIntoLines();
foreach (string line in lines)
{
if (line.Contains("color_range"))
{
colorRange = line.Split('=').LastOrDefault();
continue;
}
if (line.Contains("color_space"))
{
colorSpace = line.Split('=').LastOrDefault();
continue;
}
if (line.Contains("color_transfer"))
{
colorTransfer = line.Split('=').LastOrDefault();
continue;
}
if (line.Contains("color_primaries"))
{
colorPrimaries = line.Split('=').LastOrDefault();
continue;
}
}
if (colorSpace.Trim() == "unknown")
colorSpace = "";
if (colorRange.Trim() == "unknown")
colorRange = "";
if (colorTransfer.Trim() == "unknown")
colorTransfer = "";
if (colorPrimaries.Trim() == "unknown")
colorPrimaries = "";
}
public bool HasAnyValues ()
{
if (!string.IsNullOrWhiteSpace(colorSpace))
return true;
if (!string.IsNullOrWhiteSpace(colorRange))
return true;
if (!string.IsNullOrWhiteSpace(colorTransfer))
return true;
if (!string.IsNullOrWhiteSpace(colorPrimaries))
return true;
return false;
}
public bool HasAllValues()
{
if (string.IsNullOrWhiteSpace(colorSpace))
return false;
if (string.IsNullOrWhiteSpace(colorRange))
return false;
if (string.IsNullOrWhiteSpace(colorTransfer))
return false;
if (string.IsNullOrWhiteSpace(colorPrimaries))
return false;
return true;
}
}
}

View File

@ -328,6 +328,7 @@
<ItemGroup>
<Compile Include="Data\AI.cs" />
<Compile Include="Data\AudioTrack.cs" />
<Compile Include="Data\ColorInfo.cs" />
<Compile Include="Data\Fraction.cs" />
<Compile Include="Data\InterpSettings.cs" />
<Compile Include="Data\Networks.cs" />
@ -387,8 +388,10 @@
<Compile Include="Media\FFmpegUtils.cs" />
<Compile Include="Media\GetFrameCountCached.cs" />
<Compile Include="Media\GetMediaResolutionCached.cs" />
<Compile Include="Media\GetVideoInfoCached.cs" />
<Compile Include="MiscUtils\Benchmarker.cs" />
<Compile Include="MiscUtils\FrameRename.cs" />
<Compile Include="MiscUtils\NmkdStopwatch.cs" />
<Compile Include="OS\AiProcess.cs" />
<Compile Include="Extensions\ExtensionMethods.cs" />
<Compile Include="Form1.cs">

View File

@ -81,9 +81,6 @@ namespace Flowframes.Main
unencodedFrameLines.Clear();
//for (int frameLineNum = lastEncodedFrameNum; frameLineNum < interpFramesLines.Length; frameLineNum++)
// unencodedFrameLines.Add(frameLineNum);
bool aiRunning = !AiProcess.lastAiProcess.HasExited;
for (int frameLineNum = lastEncodedFrameNum; frameLineNum < interpFramesLines.Length; frameLineNum++)
@ -103,7 +100,9 @@ namespace Flowframes.Main
if (!File.Exists(lastOfChunk))
{
Logger.Log($"[AutoEnc] Last frame of chunk doesn't exist; skipping loop iteration ({lastOfChunk})", true);
if(debug)
Logger.Log($"[AutoEnc] Last frame of chunk doesn't exist; skipping loop iteration ({lastOfChunk})", true);
await Task.Delay(500);
continue;
}
@ -152,7 +151,9 @@ namespace Flowframes.Main
static async Task DeleteOldFramesAsync (string interpFramesPath, List<int> frameLinesToEncode)
{
Logger.Log("[AutoEnc] Starting DeleteOldFramesAsync.", true, false, "ffmpeg");
if(debug)
Logger.Log("[AutoEnc] Starting DeleteOldFramesAsync.", true, false, "ffmpeg");
Stopwatch sw = new Stopwatch();
sw.Restart();
@ -165,7 +166,8 @@ namespace Flowframes.Main
}
}
Logger.Log("[AutoEnc] DeleteOldFramesAsync finished in " + FormatUtils.TimeSw(sw), true, false, "ffmpeg");
if (debug)
Logger.Log("[AutoEnc] DeleteOldFramesAsync finished in " + FormatUtils.TimeSw(sw), true, false, "ffmpeg");
}
static bool FrameIsStillNeeded (string frameName, int frameIndex)
@ -180,7 +182,7 @@ namespace Flowframes.Main
if (Interpolate.canceled || interpFramesFolder == null) return false;
if(debug)
Logger.Log($"HasWorkToDo - Process Running: {(AiProcess.lastAiProcess != null && !AiProcess.lastAiProcess.HasExited)} - encodedFrameLines.Count: {encodedFrameLines.Count} - interpFramesLines.Length: {interpFramesLines.Length}");
Logger.Log($"HasWorkToDo - Process Running: {(AiProcess.lastAiProcess != null && !AiProcess.lastAiProcess.HasExited)} - encodedFrameLines.Count: {encodedFrameLines.Count} - interpFramesLines.Length: {interpFramesLines.Length}", true);
return ((AiProcess.lastAiProcess != null && !AiProcess.lastAiProcess.HasExited) || encodedFrameLines.Count < interpFramesLines.Length);
}

View File

@ -151,7 +151,8 @@ namespace Flowframes.Main
}
else
{
await FfmpegEncode.FramesToVideo(framesFile, outPath, mode, fps, resampleFps);
ColorInfo colorInfo = await FfmpegCommands.GetColorInfo(I.current.inPath);
await FfmpegEncode.FramesToVideo(framesFile, outPath, mode, fps, resampleFps, colorInfo);
await MuxOutputVideo(I.current.inPath, outPath);
await Loop(currentOutFile, GetLoopTimes());
}
@ -212,18 +213,19 @@ namespace Flowframes.Main
string max = Config.Get("maxFps");
Fraction maxFps = max.Contains("/") ? new Fraction(max) : new Fraction(max.GetFloat());
bool fpsLimit = maxFps.GetFloat() != 0 && I.current.outFps.GetFloat() > maxFps.GetFloat();
ColorInfo colorInfo = await FfmpegCommands.GetColorInfo(I.current.inPath);
bool dontEncodeFullFpsVid = fpsLimit && Config.GetInt("maxFpsMode") == 0;
if (!dontEncodeFullFpsVid)
await FfmpegEncode.FramesToVideoConcat(framesFileChunk, outPath, mode, I.current.outFps, AvProcess.LogMode.Hidden, true); // Encode
await FfmpegEncode.FramesToVideo(framesFileChunk, outPath, mode, I.current.outFps, new Fraction(), colorInfo, AvProcess.LogMode.Hidden, true); // Encode
if (fpsLimit)
{
string filename = Path.GetFileName(outPath);
string newParentDir = outPath.GetParentDir() + Paths.fpsLimitSuffix;
outPath = Path.Combine(newParentDir, filename);
await FfmpegEncode.FramesToVideo(framesFileChunk, outPath, mode, I.current.outFps, maxFps, AvProcess.LogMode.Hidden, true); // Encode with limited fps
await FfmpegEncode.FramesToVideo(framesFileChunk, outPath, mode, I.current.outFps, maxFps, colorInfo, AvProcess.LogMode.Hidden, true); // Encode with limited fps
}
}

View File

@ -167,20 +167,6 @@ namespace Flowframes
}
}
public static string GetFfmpegOutput (string args)
{
Process ffmpeg = OSUtils.NewProcess(true);
lastAvProcess = ffmpeg;
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg.exe -hide_banner -y -stats {args}";
Logger.Log($"ffmpeg {args}", true, false, "ffmpeg");
ffmpeg.Start();
ffmpeg.WaitForExit();
string output = ffmpeg.StandardOutput.ReadToEnd();
string err = ffmpeg.StandardError.ReadToEnd();
if (!string.IsNullOrWhiteSpace(err)) output += "\n" + err;
return output;
}
public static async Task<string> GetFfmpegOutputAsync(string args, bool setBusy = false, bool progressBar = false)
{
timeSinceLastOutput.Restart();
@ -192,11 +178,7 @@ namespace Flowframes
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg.exe -hide_banner -y -stats {args}";
Logger.Log($"ffmpeg {args}", true, false, "ffmpeg");
if (setBusy) Program.mainForm.SetWorking(true);
ffmpeg.OutputDataReceived += FfmpegOutputHandlerSilent;
ffmpeg.ErrorDataReceived += FfmpegOutputHandlerSilent;
ffmpeg.Start();
ffmpeg.BeginOutputReadLine();
ffmpeg.BeginErrorReadLine();
lastOutputFfmpeg = await OSUtils.GetOutputAsync(ffmpeg);
while (!ffmpeg.HasExited) await Task.Delay(50);
while(timeSinceLastOutput.ElapsedMilliseconds < 200) await Task.Delay(50);
if (setBusy) Program.mainForm.SetWorking(false);

View File

@ -64,9 +64,8 @@ namespace Flowframes.Media
public static async Task<List<AudioTrack>> GetAudioTracks(string inputFile)
{
List<AudioTrack> audioTracks = new List<AudioTrack>();
string args = $"-i {inputFile.Wrap()}";
Logger.Log("GetAudioTracks()", true, false, "ffmpeg");
string[] outputLines = (await GetFfmpegOutputAsync(args)).SplitIntoLines();
string[] outputLines = (await GetVideoInfoCached.GetFfmpegInfoAsync(inputFile)).SplitIntoLines();
for (int i = 0; i < outputLines.Length; i++)
{
@ -142,9 +141,8 @@ namespace Flowframes.Media
public static async Task<List<SubtitleTrack>> GetSubtitleTracks(string inputFile)
{
List<SubtitleTrack> subtitleTracks = new List<SubtitleTrack>();
string args = $"-i {inputFile.Wrap()}";
Logger.Log("GetSubtitleTracks()", true, false, "ffmpeg");
string[] outputLines = (await GetFfmpegOutputAsync(args)).SplitIntoLines();
string[] outputLines = (await GetVideoInfoCached.GetFfmpegInfoAsync(inputFile)).SplitIntoLines();
for (int i = 0; i < outputLines.Length; i++)
{

View File

@ -10,7 +10,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualBasic.Logging;
using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FFmpegUtils;
@ -97,9 +96,7 @@ namespace Flowframes
Logger.Log("GetFramerate ffprobe Error: " + ffprobeEx.Message, true, false);
}
string ffmpegArgs = $" -i {inputFile.Wrap()}";
string ffmpegOutput = await GetFfmpegOutputAsync(ffmpegArgs);
string ffmpegOutput = await GetVideoInfoCached.GetFfmpegInfoAsync(inputFile);
string[] entries = ffmpegOutput.Split(',');
foreach (string entry in entries)
@ -190,7 +187,7 @@ namespace Flowframes
static async Task<int> ReadFrameCountFfmpegAsync (string inputFile)
{
string args = $" -loglevel panic -i {inputFile.Wrap()} -map 0:v:0 -c copy -f null - ";
string args = $" -loglevel panic -stats -i {inputFile.Wrap()} -map 0:v:0 -c copy -f null - ";
string info = await GetFfmpegOutputAsync(args, true, true);
try
{
@ -204,6 +201,14 @@ namespace Flowframes
}
}
public static async Task<ColorInfo> GetColorInfo(string inputFile)
{
string ffprobeOutput = await GetVideoInfoCached.GetFfprobeInfoAsync(inputFile, "color");
ColorInfo colorInfo = new ColorInfo(ffprobeOutput);
Logger.Log($"Created ColorInfo - Range: {colorInfo.colorRange} - Space: {colorInfo.colorSpace} - Transer: {colorInfo.colorTransfer} - Primaries: {colorInfo.colorPrimaries}", true, false, "ffmpeg");
return colorInfo;
}
public static async Task<bool> IsEncoderCompatible(string enc)
{
Logger.Log($"IsEncoderCompatible('{enc}')", true, false, "ffmpeg");

View File

@ -17,12 +17,7 @@ namespace Flowframes.Media
{
partial class FfmpegEncode : FfmpegCommands
{
public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, Fraction fps, LogMode logMode = LogMode.OnlyLastLine, bool isChunk = false)
{
await FramesToVideo(framesFile, outPath, outMode, fps, new Fraction(), logMode, isChunk);
}
public static async Task FramesToVideo(string framesFile, string outPath, Interpolate.OutMode outMode, Fraction fps, Fraction resampleFps, LogMode logMode = LogMode.OnlyLastLine, bool isChunk = false)
public static async Task FramesToVideo(string framesFile, string outPath, Interpolate.OutMode outMode, Fraction fps, Fraction resampleFps, ColorInfo colors, LogMode logMode = LogMode.OnlyLastLine, bool isChunk = false)
{
if (logMode != LogMode.Hidden)
Logger.Log((resampleFps.GetFloat() <= 0) ? "Encoding video..." : $"Encoding video resampled to {resampleFps.GetString()} FPS...");
@ -39,9 +34,22 @@ namespace Flowframes.Media
inArg = $"-i {Path.GetFileName(framesFile) + Paths.symlinksSuffix}/%{Padding.interpFrames}d.png";
}
string rate = fps.ToString().Replace(",", ".");
string vf = (resampleFps.GetFloat() < 0.1f) ? "" : $"-vf fps=fps={resampleFps}";
string extraArgs = Config.Get("ffEncArgs");
string rate = fps.ToString().Replace(",", ".");
List<string> filters = new List<string>();
if (resampleFps.GetFloat() >= 0.1f)
filters.Add($"fps=fps={resampleFps}");
if (colors.HasAllValues())
{
Logger.Log($"Applying color transfer ({colors.colorSpace}).", true, false, "ffmpeg");
filters.Add($"scale=out_color_matrix={colors.colorSpace}");
extraArgs += $" -colorspace {colors.colorSpace} -color_primaries {colors.colorPrimaries} -color_trc {colors.colorTransfer} -color_range:v \"{colors.colorRange}\"";
}
string vf = filters.Count > 0 ? $"-vf {string.Join(",", filters)}" : "";
string args = $"-vsync 0 -r {rate} {inArg} {encArgs} {vf} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
await RunFfmpeg(args, framesFile.GetParentDir(), logMode, "error", TaskType.Encode, !isChunk);
IOUtils.TryDeleteIfExists(linksDir);

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.OS;
namespace Flowframes.Media
{
class GetVideoInfoCached
{
enum InfoType { Ffmpeg, Ffprobe };
static Dictionary<PseudoUniqueFile, string> ffmpegCache = new Dictionary<PseudoUniqueFile, string>();
static Dictionary<PseudoUniqueFile, string> ffprobeCache = new Dictionary<PseudoUniqueFile, string>();
public static async Task<string> GetFfmpegInfoAsync(string path, string lineFilter = "")
{
return await GetInfoAsync(path, InfoType.Ffmpeg, lineFilter);
}
public static async Task<string> GetFfprobeInfoAsync(string path, string lineFilter = "")
{
return await GetInfoAsync(path, InfoType.Ffprobe, lineFilter);
}
static async Task<string> GetInfoAsync(string path, InfoType type, string lineFilter)
{
Logger.Log($"Get{type}InfoAsync({path})", true);
Dictionary<PseudoUniqueFile, string> cacheDict = new Dictionary<PseudoUniqueFile, string>(type == InfoType.Ffmpeg ? ffmpegCache : ffprobeCache);
long filesize = IOUtils.GetFilesize(path);
PseudoUniqueFile hash = new PseudoUniqueFile(path, filesize);
if (filesize > 0 && CacheContains(hash, ref cacheDict))
{
Logger.Log($"Returning cached {type} info.", true);
return GetFromCache(hash, ref cacheDict);
}
Process process = OSUtils.NewProcess(true);
string avPath = Path.Combine(Paths.GetPkgPath(), Paths.audioVideoDir);
if(type == InfoType.Ffmpeg)
process.StartInfo.Arguments = $"/C cd /D {avPath.Wrap()} & ffmpeg.exe -hide_banner -y -stats -i {path.Wrap()}";
if (type == InfoType.Ffprobe)
process.StartInfo.Arguments = $"/C cd /D {avPath.Wrap()} & ffprobe -v quiet -show_format -show_streams {path.Wrap()}";
string output = await OSUtils.GetOutputAsync(process);
if (type == InfoType.Ffmpeg)
ffmpegCache.Add(hash, output);
if (type == InfoType.Ffprobe)
ffprobeCache.Add(hash, output);
if (!string.IsNullOrWhiteSpace(lineFilter.Trim()))
output = string.Join("\n", output.SplitIntoLines().Where(x => x.Contains(lineFilter)).ToArray());
return output;
}
private static bool CacheContains(PseudoUniqueFile hash, ref Dictionary<PseudoUniqueFile, string> cacheDict)
{
foreach (KeyValuePair<PseudoUniqueFile, string> entry in cacheDict)
if (entry.Key.path == hash.path && entry.Key.filesize == hash.filesize)
return true;
return false;
}
private static string GetFromCache(PseudoUniqueFile hash, ref Dictionary<PseudoUniqueFile, string> cacheDict)
{
foreach (KeyValuePair<PseudoUniqueFile, string> entry in cacheDict)
if (entry.Key.path == hash.path && entry.Key.filesize == hash.filesize)
return entry.Value;
return "";
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Diagnostics;
using System.Linq;
namespace Flowframes.MiscUtils
{
class NmkdStopwatch
{
public Stopwatch sw = new Stopwatch();
public NmkdStopwatch (bool startOnCreation = true)
{
if (startOnCreation)
sw.Restart();
}
public string GetElapsedStr ()
{
return FormatUtils.TimeSw(sw);
}
}
}

View File

@ -12,6 +12,9 @@ using DiskDetector;
using DiskDetector.Models;
using Microsoft.VisualBasic.Devices;
using Flowframes.Extensions;
using Flowframes.MiscUtils;
using System.Threading;
using System.Linq;
namespace Flowframes.OS
{
@ -176,5 +179,32 @@ namespace Flowframes.OS
return children;
}
public static async Task<string> GetOutputAsync(Process process, bool onlyLastLine = false)
{
Logger.Log($"Getting output for {process.StartInfo.FileName} {process.StartInfo.Arguments}", true);
NmkdStopwatch sw = new NmkdStopwatch();
Stopwatch timeSinceLastOutput = new Stopwatch();
timeSinceLastOutput.Restart();
string output = "";
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => output += $"{e.Data}\n";
process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => output += $"{e.Data}\n";
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
while (!process.HasExited) await Task.Delay(50);
while (timeSinceLastOutput.ElapsedMilliseconds < 100) await Task.Delay(50);
output = output.Trim('\r', '\n');
Logger.Log($"Output (after {sw.GetElapsedStr()}): " + output.Replace("\r", " / ").Replace("\n", " / "), true);
if (onlyLastLine)
output = output.SplitIntoLines().LastOrDefault();
return output;
}
}
}