Full VFR handling including FPS downsampling to CFR

This commit is contained in:
N00MKRAD
2024-11-28 16:08:04 +01:00
parent 32eeac578d
commit defff4f29c
21 changed files with 189 additions and 194 deletions

View File

@@ -2,6 +2,8 @@
{ {
public class Enums public class Enums
{ {
public enum Round { Near, Up, Down }
public class Output public class Output
{ {
public enum Format { Mp4, Mkv, Webm, Mov, Avi, Gif, Images, Realtime }; public enum Format { Mp4, Mkv, Webm, Mov, Avi, Gif, Images, Realtime };

View File

@@ -2,12 +2,14 @@
namespace Flowframes.Data namespace Flowframes.Data
{ {
public struct Fraction public class Fraction
{ {
public long Numerator; public long Numerator = 0;
public long Denominator; public long Denominator = 1;
public static Fraction Zero = new Fraction(0, 0); public static Fraction Zero = new Fraction(0, 0);
public Fraction() { }
public Fraction(long numerator, long denominator) public Fraction(long numerator, long denominator)
{ {
this.Numerator = numerator; this.Numerator = numerator;
@@ -22,18 +24,6 @@ namespace Flowframes.Data
} }
} }
public Fraction(long numerator, Fraction denominator)
{
//divide the numerator by the denominator fraction
this = new Fraction(numerator, 1) / denominator;
}
public Fraction(Fraction numerator, long denominator)
{
//multiply the numerator fraction by 1 over the denominator
this = numerator * new Fraction(1, denominator);
}
public Fraction(Fraction fraction) public Fraction(Fraction fraction)
{ {
Numerator = fraction.Numerator; Numerator = fraction.Numerator;
@@ -44,7 +34,9 @@ namespace Flowframes.Data
{ {
Numerator = (value * 10000f).RoundToInt(); Numerator = (value * 10000f).RoundToInt();
Denominator = 10000; Denominator = 10000;
this = GetReduced(); var reducedFrac = GetReduced();
Numerator = reducedFrac.Numerator;
Denominator = reducedFrac.Denominator;
} }
public Fraction(string text) public Fraction(string text)
@@ -76,7 +68,9 @@ namespace Flowframes.Data
else else
{ {
// Use float constructor if not a whole number // Use float constructor if not a whole number
this = new Fraction(numFloat); var floatFrac = new Fraction(numFloat);
Numerator = floatFrac.Numerator;
Denominator = floatFrac.Denominator;
} }
return; return;

View File

@@ -4,12 +4,9 @@ using Flowframes.IO;
using Flowframes.Main; using Flowframes.Main;
using Flowframes.MiscUtils; using Flowframes.MiscUtils;
using System; using System;
using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Diagnostics;
namespace Flowframes namespace Flowframes
{ {
@@ -19,10 +16,11 @@ namespace Flowframes
public string outPath; public string outPath;
public string FullOutPath { get; set; } = ""; public string FullOutPath { get; set; } = "";
public AiInfo ai; public AiInfo ai;
public string inPixFmt = "yuv420p";
public Fraction inFps; public Fraction inFps;
public Fraction inFpsDetected; public Fraction inFpsDetected;
public Fraction outFps; public Fraction outFps;
public Fraction outFpsResampled;
public bool FpsResampling => outFpsResampled != null && outFpsResampled.Float > 0.1f && outFpsResampled.Float < outFps.Float;
public float outItsScale; public float outItsScale;
public float interpFactor; public float interpFactor;
public OutputSettings outSettings; public OutputSettings outSettings;
@@ -86,56 +84,23 @@ namespace Flowframes
public InterpSettings() { } public InterpSettings() { }
public InterpSettings(string inPathArg, string outPathArg, AiInfo aiArg, Fraction inFpsDetectedArg, Fraction inFpsArg, float interpFactorArg, float itsScale, OutputSettings outSettingsArg, ModelCollection.ModelInfo modelArg)
{
inPath = inPathArg;
outPath = outPathArg;
ai = aiArg;
inFpsDetected = inFpsDetectedArg;
inFps = inFpsArg;
interpFactor = interpFactorArg;
outItsScale = itsScale;
outSettings = outSettingsArg;
model = modelArg;
InitArgs();
}
public void InitArgs () public void InitArgs ()
{ {
outFps = inFps * (double)interpFactor; outFps = inFps * (double)interpFactor;
outFpsResampled = new Fraction(Config.Get(Config.Key.maxFps));
alpha = false; alpha = false;
stepByStep = false; stepByStep = false;
framesExt = ""; framesExt = "";
interpExt = ""; interpExt = "";
try
{
tempFolder = InterpolateUtils.GetTempFolderLoc(inPath, outPath);
framesFolder = Path.Combine(tempFolder, Paths.framesDir);
interpFolder = Path.Combine(tempFolder, Paths.interpDir);
inputIsFrames = IoUtils.IsPathDirectory(inPath);
}
catch
{
Logger.Log("Tried to create InterpSettings struct without an inpath. Can't set tempFolder, framesFolder and interpFolder.", true);
tempFolder = "";
framesFolder = "";
interpFolder = "";
inputIsFrames = false;
}
_inputResolution = new Size(0, 0); _inputResolution = new Size(0, 0);
SetPaths(inPath);
RefreshExtensions(ai: ai); RefreshExtensions(ai: ai);
} }
public void UpdatePaths (string inPathArg, string outPathArg) private void SetPaths (string inputPath)
{ {
inPath = inPathArg; inPath = inputPath;
outPath = outPathArg; outPath = (Config.GetInt("outFolderLoc") == 0) ? inputPath.GetParentDir() : Config.Get("custOutDir").Trim();
tempFolder = InterpolateUtils.GetTempFolderLoc(inPath, outPath); tempFolder = InterpolateUtils.GetTempFolderLoc(inPath, outPath);
framesFolder = Path.Combine(tempFolder, Paths.framesDir); framesFolder = Path.Combine(tempFolder, Paths.framesDir);
interpFolder = Path.Combine(tempFolder, Paths.interpDir); interpFolder = Path.Combine(tempFolder, Paths.interpDir);

View File

@@ -24,7 +24,7 @@ namespace Flowframes.Data
public string Format; public string Format;
public string Title; public string Title;
public string Language; public string Language;
public Fraction? InputRate = null; public Fraction InputRate;
public long DurationMs; public long DurationMs;
public int StreamCount; public int StreamCount;
public int TotalKbits; public int TotalKbits;
@@ -42,6 +42,8 @@ namespace Flowframes.Data
public bool IsVfr = false; public bool IsVfr = false;
public List<float> InputTimestamps = new List<float>(); public List<float> InputTimestamps = new List<float>();
public List<float> InputTimestampDurations = new List<float>(); public List<float> InputTimestampDurations = new List<float>();
public List<float> OutputTimestamps = new List<float>();
public List<int> OutputFrameIndexes = null;
public int FileCount = 1; public int FileCount = 1;
public int FrameCount { get { return VideoStreams.Count > 0 ? VideoStreams[0].FrameCount : 0; } } public int FrameCount { get { return VideoStreams.Count > 0 ? VideoStreams[0].FrameCount : 0; } }
@@ -140,35 +142,6 @@ namespace Flowframes.Data
TotalKbits = (await GetVideoInfo.GetFfprobeInfoAsync(path, GetVideoInfo.FfprobeMode.ShowFormat, "bit_rate")).GetInt() / 1000; TotalKbits = (await GetVideoInfo.GetFfprobeInfoAsync(path, GetVideoInfo.FfprobeMode.ShowFormat, "bit_rate")).GetInt() / 1000;
} }
public List<float> GetResampledTimestamps(List<float> timestamps, double factor)
{
int originalCount = timestamps.Count;
int newCount = (int)Math.Round(originalCount * factor);
List<float> resampledTimestamps = new List<float>();
for (int i = 0; i < newCount; i++)
{
double x = i / factor;
if (x >= originalCount - 1)
{
resampledTimestamps.Add(timestamps[originalCount - 1]);
}
else
{
int index = (int)Math.Floor(x);
double fraction = x - index;
float startTime = timestamps[index];
float endTime = timestamps[index + 1];
float interpolatedTime = (float)(startTime + (endTime - startTime) * fraction);
resampledTimestamps.Add(interpolatedTime);
}
}
return resampledTimestamps;
}
public string GetName() public string GetName()
{ {
if (IsDirectory) if (IsDirectory)

View File

@@ -494,6 +494,7 @@
<Compile Include="Media\GetMediaResolutionCached.cs" /> <Compile Include="Media\GetMediaResolutionCached.cs" />
<Compile Include="Media\GetVideoInfo.cs" /> <Compile Include="Media\GetVideoInfo.cs" />
<Compile Include="Media\HwEncCheck.cs" /> <Compile Include="Media\HwEncCheck.cs" />
<Compile Include="Media\TimestampUtils.cs" />
<Compile Include="MiscUtils\Benchmarker.cs" /> <Compile Include="MiscUtils\Benchmarker.cs" />
<Compile Include="MiscUtils\FrameRename.cs" /> <Compile Include="MiscUtils\FrameRename.cs" />
<Compile Include="MiscUtils\ModelDownloadFormUtils.cs" /> <Compile Include="MiscUtils\ModelDownloadFormUtils.cs" />

View File

@@ -153,9 +153,6 @@ namespace Flowframes.Forms
Logger.Log($"BatchForm: Dropped path: '{path}'", true); Logger.Log($"BatchForm: Dropped path: '{path}'", true);
InterpSettings current = Program.mainForm.GetCurrentSettings(); InterpSettings current = Program.mainForm.GetCurrentSettings();
string outDir = (Config.GetInt("outFolderLoc") == 0) ? path.GetParentDir() : Config.Get("custOutDir").Trim();
current.UpdatePaths(path, outDir);
current.inFpsDetected = await IoUtils.GetFpsFolderOrVideo(path); current.inFpsDetected = await IoUtils.GetFpsFolderOrVideo(path);
current.inFps = current.inFpsDetected; current.inFps = current.inFpsDetected;

View File

@@ -321,29 +321,6 @@ namespace Flowframes.Forms.Main
return s; return s;
} }
public InterpSettings UpdateCurrentSettings(InterpSettings settings)
{
SetTab(interpOptsTab.Name);
string inPath = inputTbox.Text.Trim();
if (settings.inPath != inPath) // If path changed, get new instance
{
Logger.Log($"settings.inPath ({settings.inPath}) mismatches GUI inPath ({settings.inPath} - Returning fresh instance", true);
return GetCurrentSettings();
}
settings.inPath = inPath;
settings.ai = GetAi();
settings.inFpsDetected = currInFpsDetected;
settings.inFps = currInFps;
settings.interpFactor = interpFactorCombox.GetFloat();
settings.outFps = settings.inFps * settings.interpFactor;
settings.outSettings = GetOutputSettings();
settings.model = GetModel(GetAi());
return settings;
}
public void LoadBatchEntry(InterpSettings entry) public void LoadBatchEntry(InterpSettings entry)
{ {
inputTbox.Text = entry.inPath; inputTbox.Text = entry.inPath;

View File

@@ -597,9 +597,7 @@ namespace Flowframes.IO
public static async Task<string> GetCurrentExportFilename(bool fpsLimit, bool isImgSeq = false, bool includeExt = true) public static async Task<string> GetCurrentExportFilename(bool fpsLimit, bool isImgSeq = false, bool includeExt = true)
{ {
InterpSettings curr = Interpolate.currentSettings; InterpSettings curr = Interpolate.currentSettings;
string max = Config.Get(Config.Key.maxFps); float fps = fpsLimit ? curr.outFpsResampled.Float : curr.outFps.Float;
Fraction maxFps = max.Contains("/") ? new Fraction(max) : new Fraction(max.GetFloat());
float fps = fpsLimit ? maxFps.Float : curr.outFps.Float;
Size outRes = curr.OutputResolution; // TODO: Replace with EncodeResolution once implemented? Size outRes = curr.OutputResolution; // TODO: Replace with EncodeResolution once implemented?
string pattern = Config.Get(Config.Key.exportNamePattern); string pattern = Config.Get(Config.Key.exportNamePattern);

View File

@@ -264,27 +264,21 @@ namespace Flowframes.Magick
public static async Task CreateFramesFileVideo(string videoPath, bool loop) public static async Task CreateFramesFileVideo(string videoPath, bool loop)
{ {
if (!Directory.Exists(Interpolate.currentSettings.tempFolder)) Directory.CreateDirectory(Interpolate.currentSettings.tempFolder);
Directory.CreateDirectory(Interpolate.currentSettings.tempFolder);
Process ffmpeg = OsUtils.NewProcess(true); Process ffmpeg = OsUtils.NewProcess(true);
string baseCmd = $"/C cd /D {Path.Combine(IO.Paths.GetPkgPath(), IO.Paths.audioVideoDir).Wrap()}"; string baseCmd = $"/C cd /D {Path.Combine(IO.Paths.GetPkgPath(), IO.Paths.audioVideoDir).Wrap()}";
string mpDec = FfmpegCommands.GetMpdecimate(wrap: false); // FfmpegCommands.GetMpdecimate((int)FfmpegCommands.MpDecSensitivity.Normal, false); string mpDec = FfmpegCommands.GetMpdecimate(wrap: false); // FfmpegCommands.GetMpdecimate((int)FfmpegCommands.MpDecSensitivity.Normal, false);
ffmpeg.StartInfo.Arguments = $"{baseCmd} & ffmpeg -loglevel debug -y -i {videoPath.Wrap()} -fps_mode vfr -vf {mpDec} -f null NUL 2>&1 | findstr keep_count:"; ffmpeg.StartInfo.Arguments = $"{baseCmd} & ffmpeg -loglevel debug -y -i {videoPath.Wrap()} -fps_mode vfr -vf {mpDec} -f null NUL 2>&1 | findstr keep_count:";
List<string> ffmpegOutputLines = (await Task.Run(() => OsUtils.GetProcStdOut(ffmpeg, true))).SplitIntoLines().Where(l => l.IsNotEmpty()).ToList(); var ffmpegOutputLines = (await Task.Run(() => OsUtils.GetProcStdOut(ffmpeg, true))).SplitIntoLines();
var frames = new Dictionary<int, List<int>>(); var frames = new Dictionary<int, List<int>>();
var frameNums = new List<int>();
int lastKeepFrameNum = 0; int lastKeepFrameNum = 0;
for (int frameIdx = 0; frameIdx < ffmpegOutputLines.Count; frameIdx++) for (int frameIdx = 0; frameIdx < ffmpegOutputLines.Length; frameIdx++)
{ {
string line = ffmpegOutputLines[frameIdx]; string line = ffmpegOutputLines[frameIdx];
bool drop = frameIdx != 0 && line.Contains(" drop ") && !line.Contains(" keep ");
// Console.WriteLine($"[Frame {frameIdx.ToString().PadLeft(6, '0')}] {(drop ? "DROP" : "KEEP")}");
// frameNums.Add(lastKeepFrameNum);
if (!drop) if (line.Contains(" keep pts:"))
{ {
if (!frames.ContainsKey(frameIdx) || frames[frameIdx] == null) if (!frames.ContainsKey(frameIdx) || frames[frameIdx] == null)
{ {
@@ -293,7 +287,7 @@ namespace Flowframes.Magick
lastKeepFrameNum = frameIdx; lastKeepFrameNum = frameIdx;
} }
else else if (line.Contains(" drop pts:"))
{ {
frames[lastKeepFrameNum].Add(frameIdx); frames[lastKeepFrameNum].Add(frameIdx);
} }
@@ -301,6 +295,8 @@ namespace Flowframes.Magick
var inputFrames = new List<int>(frames.Keys); var inputFrames = new List<int>(frames.Keys);
Logger.Log($"Dedupe: Kept {inputFrames.Count}/{ffmpegOutputLines.Length} frames", true);
if (loop) if (loop)
{ {
inputFrames.Add(inputFrames.First()); inputFrames.Add(inputFrames.First());

View File

@@ -19,8 +19,7 @@ namespace Flowframes.Main
{ {
class Export class Export
{ {
private static string MaxFps => Config.Get(Config.Key.maxFps); private static Fraction MaxFpsFrac => I.currentSettings.outFpsResampled;
private static Fraction MaxFpsFrac => new Fraction(MaxFps);
public static async Task ExportFrames(string path, string outFolder, OutputSettings exportSettings, bool stepByStep) public static async Task ExportFrames(string path, string outFolder, OutputSettings exportSettings, bool stepByStep)
{ {
@@ -67,7 +66,7 @@ namespace Flowframes.Main
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Log("FramesToVideo Error: " + e.Message, false); Logger.Log($"{nameof(ExportFrames)} Error: {e.Message}", false);
UiUtils.ShowMessageBox("An error occured while trying to convert the interpolated frames to a video.\nCheck the log for details.", UiUtils.MessageType.Error); UiUtils.ShowMessageBox("An error occured while trying to convert the interpolated frames to a video.\nCheck the log for details.", UiUtils.MessageType.Error);
} }
} }
@@ -79,7 +78,7 @@ namespace Flowframes.Main
bool fpsLimit = MaxFpsFrac.Float > 0f && s.outFps.Float > MaxFpsFrac.Float; bool fpsLimit = MaxFpsFrac.Float > 0f && s.outFps.Float > MaxFpsFrac.Float;
bool gifInput = I.currentMediaFile.Format.Upper() == "GIF"; // If input is GIF, we don't need to check the color space etc bool gifInput = I.currentMediaFile.Format.Upper() == "GIF"; // If input is GIF, we don't need to check the color space etc
VidExtraData extraData = gifInput ? new VidExtraData() : await FfmpegCommands.GetVidExtraInfo(s.inPath); VidExtraData extraData = gifInput ? new VidExtraData() : await FfmpegCommands.GetVidExtraInfo(s.inPath);
string extraArgsIn = await FfmpegEncode.GetFfmpegExportArgsIn(s.outFps, s.outItsScale, extraData.Rotation); string extraArgsIn = await FfmpegEncode.GetFfmpegExportArgsIn(s.FpsResampling ? s.outFpsResampled : s.outFps, s.outItsScale, extraData.Rotation);
string extraArgsOut = await FfmpegEncode.GetFfmpegExportArgsOut(fpsLimit ? MaxFpsFrac : new Fraction(), extraData, s.outSettings); string extraArgsOut = await FfmpegEncode.GetFfmpegExportArgsOut(fpsLimit ? MaxFpsFrac : new Fraction(), extraData, s.outSettings);
// For EXR, force bt709 input flags. Not sure if this really does anything, EXR // For EXR, force bt709 input flags. Not sure if this really does anything, EXR
@@ -121,8 +120,7 @@ namespace Flowframes.Main
Enums.Encoding.Encoder desiredFormat = I.currentSettings.outSettings.Encoder; Enums.Encoding.Encoder desiredFormat = I.currentSettings.outSettings.Encoder;
string availableFormat = Path.GetExtension(IoUtils.GetFilesSorted(framesPath, "*.*")[0]).Remove(".").Upper(); string availableFormat = Path.GetExtension(IoUtils.GetFilesSorted(framesPath, "*.*")[0]).Remove(".").Upper();
Fraction maxFps = new Fraction(MaxFps); bool fpsLimit = MaxFpsFrac.Float > 0f && I.currentSettings.outFps.Float > MaxFpsFrac.Float;
bool fpsLimit = maxFps.Float > 0f && I.currentSettings.outFps.Float > maxFps.Float;
bool encodeFullFpsSeq = !(fpsLimit && Config.GetInt(Config.Key.maxFpsMode) == 0); bool encodeFullFpsSeq = !(fpsLimit && Config.GetInt(Config.Key.maxFpsMode) == 0);
string framesFile = Path.Combine(framesPath.GetParentDir(), Paths.GetFrameOrderFilename(I.currentSettings.interpFactor)); string framesFile = Path.Combine(framesPath.GetParentDir(), Paths.GetFrameOrderFilename(I.currentSettings.interpFactor));
@@ -141,8 +139,8 @@ namespace Flowframes.Main
if (fpsLimit) if (fpsLimit)
{ {
string outputFolderPath = Path.Combine(I.currentSettings.outPath, await IoUtils.GetCurrentExportFilename(fpsLimit: true, isImgSeq: true)); string outputFolderPath = Path.Combine(I.currentSettings.outPath, await IoUtils.GetCurrentExportFilename(fpsLimit: true, isImgSeq: true));
Logger.Log($"Exporting {desiredFormat.ToString().Upper()} frames to '{Path.GetFileName(outputFolderPath)}' (Resampled to {maxFps} FPS)..."); Logger.Log($"Exporting {desiredFormat.ToString().Upper()} frames to '{Path.GetFileName(outputFolderPath)}' (Resampled to {MaxFpsFrac} FPS)...");
await FfmpegEncode.FramesToFrames(framesFile, outputFolderPath, 1, I.currentSettings.outFps, maxFps, desiredFormat, OutputUtils.GetImgSeqQ(I.currentSettings.outSettings)); await FfmpegEncode.FramesToFrames(framesFile, outputFolderPath, 1, I.currentSettings.outFps, MaxFpsFrac, desiredFormat, OutputUtils.GetImgSeqQ(I.currentSettings.outSettings));
} }
if (!stepByStep) if (!stepByStep)
@@ -389,24 +387,19 @@ namespace Flowframes.Main
{ {
if (I.currentSettings.dedupe) if (I.currentSettings.dedupe)
{ {
Logger.Log($"{nameof(MuxTimestamps)}: Dedupe was used; won't mux timestamps for '{vidPath}'", hidden: true); Logger.Log($"{nameof(MuxTimestamps)}: Dedupe was used; won't mux timestamps.", hidden: true);
return;
}
if(I.currentMediaFile.IsVfr && I.currentMediaFile.OutputFrameIndexes != null && I.currentMediaFile.OutputFrameIndexes.Count > 0)
{
Logger.Log($"{nameof(MuxTimestamps)}: CFR conversion due to FPS limit was applied (picked {I.currentMediaFile.OutputFrameIndexes.Count} frames for {I.currentSettings.outFpsResampled} FPS); won't mux timestamps.", hidden: true);
return; return;
} }
Logger.Log($"{nameof(MuxTimestamps)}: Muxing timestamps for '{vidPath}'", hidden: true); Logger.Log($"{nameof(MuxTimestamps)}: Muxing timestamps for '{vidPath}'", hidden: true);
float avgDuration = I.currentMediaFile.InputTimestampDurations.Average(); string tsFile = Path.Combine(Paths.GetSessionDataPath(), "ts.txt");
I.currentMediaFile.InputTimestamps.Add(I.currentMediaFile.InputTimestamps.Last() + avgDuration); // Add extra frame using avg. duration, needed for duration matching or looping TimestampUtils.WriteTsFile(I.currentMediaFile.OutputTimestamps, tsFile);
var resampledTs = I.currentMediaFile.GetResampledTimestamps(I.currentMediaFile.InputTimestamps, I.currentSettings.interpFactor);
var tsFileLines = new List<string>() { "# timecode format v2" };
for (int i = 0; i < (resampledTs.Count - 1); i++)
{
tsFileLines.Add((resampledTs[i] * 1000f).ToString("0.000000"));
}
string tsFile = Path.Combine(Paths.GetSessionDataPath(), "ts.out.txt");
File.WriteAllLines(tsFile, tsFileLines);
string outPath = Path.ChangeExtension(vidPath, ".tmp.mkv"); string outPath = Path.ChangeExtension(vidPath, ".tmp.mkv");
string args = $"mkvmerge --output {outPath.Wrap()} --timestamps \"0:{tsFile}\" {vidPath.Wrap()}"; string args = $"mkvmerge --output {outPath.Wrap()} --timestamps \"0:{tsFile}\" {vidPath.Wrap()}";
var outputMux = NUtilsTemp.OsUtils.RunCommand($"cd /D {Path.Combine(Paths.GetPkgPath(), Paths.audioVideoDir).Wrap()} && {args}"); var outputMux = NUtilsTemp.OsUtils.RunCommand($"cd /D {Path.Combine(Paths.GetPkgPath(), Paths.audioVideoDir).Wrap()} && {args}");
@@ -414,14 +407,14 @@ namespace Flowframes.Main
// Check if file exists and is not too small (min. 80% of input file) // Check if file exists and is not too small (min. 80% of input file)
if (File.Exists(outPath) && ((double)new FileInfo(outPath).Length / (double)new FileInfo(vidPath).Length) > 0.8d) if (File.Exists(outPath) && ((double)new FileInfo(outPath).Length / (double)new FileInfo(vidPath).Length) > 0.8d)
{ {
Logger.Log($"{nameof(MuxTimestamps)}: Deleting '{vidPath}' and moving '{outPath}' to '{vidPath}'", hidden: true); Logger.Log($"{nameof(MuxTimestamps)}: Deleting original '{vidPath}' and moving muxed '{outPath}' to '{vidPath}'", hidden: true);
File.Delete(vidPath); File.Delete(vidPath);
File.Move(outPath, vidPath); File.Move(outPath, vidPath);
} }
else else
{ {
Logger.Log(outputMux, hidden: true);
Logger.Log($"{nameof(MuxTimestamps)}: Timestamp muxing failed, keeping original video file", hidden: true); Logger.Log($"{nameof(MuxTimestamps)}: Timestamp muxing failed, keeping original video file", hidden: true);
Logger.Log(outputMux, hidden: true);
IoUtils.TryDeleteIfExists(outPath); IoUtils.TryDeleteIfExists(outPath);
} }
} }

View File

@@ -17,6 +17,7 @@ using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Padding = Flowframes.Data.Padding; using Padding = Flowframes.Data.Padding;
using Utils = Flowframes.Main.InterpolateUtils; using Utils = Flowframes.Main.InterpolateUtils;
using System.Drawing.Imaging;
namespace Flowframes namespace Flowframes
{ {
@@ -27,7 +28,7 @@ namespace Flowframes
public static MediaFile currentMediaFile; public static MediaFile currentMediaFile;
public static bool canceled = false; public static bool canceled = false;
public static float InterpProgressMultiplier = 1f; public static float InterpProgressMultiplier = 1f;
static Stopwatch sw = new Stopwatch(); private static Stopwatch sw = new Stopwatch();
public static async Task Start() public static async Task Start()
{ {
@@ -45,6 +46,11 @@ namespace Flowframes
Program.mainForm.SetStatus("Starting..."); Program.mainForm.SetStatus("Starting...");
sw.Restart(); sw.Restart();
if (currentMediaFile.IsVfr)
{
TimestampUtils.CalcTimestamps(currentMediaFile, currentSettings);
}
if (!AutoEncodeResume.resumeNextRun && !(currentSettings.ai.Piped && !currentSettings.inputIsFrames /* && Config.GetInt(Config.Key.dedupMode) == 0) */)) if (!AutoEncodeResume.resumeNextRun && !(currentSettings.ai.Piped && !currentSettings.inputIsFrames /* && Config.GetInt(Config.Key.dedupMode) == 0) */))
{ {
await GetFrames(); await GetFrames();

View File

@@ -20,17 +20,7 @@ namespace Flowframes.Main
canceled = false; canceled = false;
Program.mainForm.SetWorking(true); Program.mainForm.SetWorking(true);
if(currentSettings == null) currentSettings = Program.mainForm.GetCurrentSettings();
{
Logger.Log($"[SBS] Getting new current settings", true);
currentSettings = Program.mainForm.GetCurrentSettings();
}
else
{
Logger.Log($"[SBS] Updating current settings", true);
currentSettings = Program.mainForm.UpdateCurrentSettings(currentSettings);
}
currentSettings.RefreshAlpha(); currentSettings.RefreshAlpha();
currentSettings.stepByStep = true; currentSettings.stepByStep = true;

View File

@@ -70,7 +70,7 @@ namespace Flowframes.Main
public static string GetTempFolderLoc(string inPath, string outPath) public static string GetTempFolderLoc(string inPath, string outPath)
{ {
string basePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Temp", "Flowframes"); string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "Flowframes");
int tempFolderLoc = Config.GetInt(Config.Key.tempFolderLoc); int tempFolderLoc = Config.GetInt(Config.Key.tempFolderLoc);
switch (tempFolderLoc) switch (tempFolderLoc)
@@ -96,7 +96,7 @@ namespace Flowframes.Main
break; break;
} }
string folderName = Path.GetFileNameWithoutExtension(inPath).StripBadChars().Remove(" ").Trunc(30, false) + ".tmp"; string folderName = Path.GetFileNameWithoutExtension(inPath).StripBadChars().Remove(" ").Trunc(35, false) + "_tmp";
return Path.Combine(basePath, folderName); return Path.Combine(basePath, folderName);
} }
@@ -131,23 +131,17 @@ namespace Flowframes.Main
passes = false; passes = false;
} }
string fpsLimitValue = Config.Get(Config.Key.maxFps); float fpsLimit = s.outFpsResampled.Float;
float fpsLimit = (fpsLimitValue.Contains("/") ? new Fraction(fpsLimitValue).Float : fpsLimitValue.GetFloat()); int maxEncoderFps = s.outSettings.Encoder.GetInfo().MaxFramerate;
int maxFps = s.outSettings.Encoder.GetInfo().MaxFramerate;
if (passes && s.outFps.Float < 1f || (s.outFps.Float > maxFps && !(fpsLimit > 0 && fpsLimit <= maxFps))) if (passes && s.outFps.Float < 1f || (s.outFps.Float > maxEncoderFps && !(fpsLimit > 0 && fpsLimit <= maxEncoderFps)))
{ {
string imgSeqNote = isFile ? "" : "\n\nWhen using an image sequence as input, you always have to specify the frame rate manually."; string imgSeqNote = isFile ? "" : "\n\nWhen using an image sequence as input, you always have to specify the frame rate manually.";
UiUtils.ShowMessageBox($"Invalid output frame rate ({s.outFps.Float}).\nMust be 1-{maxFps}. Either lower the interpolation factor or use the \"Maximum Output Frame Rate\" option.{imgSeqNote}"); UiUtils.ShowMessageBox($"Invalid output frame rate ({s.outFps.Float}).\nMust be 1-{maxEncoderFps}. Either lower the interpolation factor or use the \"Maximum Output Frame Rate\" option.{imgSeqNote}");
passes = false; passes = false;
} }
float fpsLimitFloat = fpsLimitValue.GetFloat(); I.InterpProgressMultiplier = s.FpsResampling ? s.outFps.Float / fpsLimit : 1f;
if (fpsLimitFloat > 0 && fpsLimitFloat < s.outFps.Float)
Interpolate.InterpProgressMultiplier = s.outFps.Float / fpsLimitFloat;
else
Interpolate.InterpProgressMultiplier = 1f;
if (!passes) if (!passes)
I.Cancel("Invalid settings detected.", true); I.Cancel("Invalid settings detected.", true);

View File

@@ -6,6 +6,7 @@ using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FfmpegUtils; using Utils = Flowframes.Media.FfmpegUtils;
using I = Flowframes.Interpolate; using I = Flowframes.Interpolate;
using Flowframes.Main; using Flowframes.Main;
using System.Linq;
namespace Flowframes.Media namespace Flowframes.Media
{ {
@@ -60,7 +61,7 @@ namespace Flowframes.Media
string metaArg = (isMkv && meta) ? "-map 1:t?" : ""; // https://reddit.com/r/ffmpeg/comments/fw4jnh/how_to_make_ffmpeg_keep_attached_images_in_mkv_as/ string metaArg = (isMkv && meta) ? "-map 1:t?" : ""; // https://reddit.com/r/ffmpeg/comments/fw4jnh/how_to_make_ffmpeg_keep_attached_images_in_mkv_as/
string shortestArg = shortest ? "-shortest" : ""; string shortestArg = shortest ? "-shortest" : "";
if (I.currentMediaFile.IsVfr) if (I.currentMediaFile.IsVfr && I.currentMediaFile.OutputTimestamps.Any())
{ {
Export.MuxTimestamps(tempPath); Export.MuxTimestamps(tempPath);
} }

View File

@@ -70,12 +70,13 @@ namespace Flowframes.Media
{ {
if (Interpolate.currentMediaFile.IsVfr && !Interpolate.currentSettings.dedupe) if (Interpolate.currentMediaFile.IsVfr && !Interpolate.currentSettings.dedupe)
{ {
Logger.Log($"Ignoring {resampleFps.Float} FPS limit as this is currently unsupported for variable framerate videos."); // Logger.Log($"Ignoring {resampleFps.Float} FPS limit as this is currently unsupported for variable framerate videos.");
} }
else else
{ {
filters.Add($"fps={resampleFps}"); filters.Add($"fps={resampleFps}");
} }
// filters.Add($"fps={resampleFps}");
} }
if (Config.GetBool(Config.Key.keepColorSpace) && extraData.HasAllColorValues()) if (Config.GetBool(Config.Key.keepColorSpace) && extraData.HasAllColorValues())

View File

@@ -163,7 +163,7 @@ namespace Flowframes.Media
} }
} }
static bool AreImagesCompatible(string inpath, int maxHeight) private static bool AreImagesCompatible(string inpath, int maxHeight)
{ {
NmkdStopwatch sw = new NmkdStopwatch(); NmkdStopwatch sw = new NmkdStopwatch();
string[] validExtensions = Filetypes.imagesInterpCompat; // = new string[] { ".jpg", ".jpeg", ".png" }; string[] validExtensions = Filetypes.imagesInterpCompat; // = new string[] { ".jpg", ".jpeg", ".png" };
@@ -204,17 +204,6 @@ namespace Flowframes.Media
return false; return false;
} }
Logger.Log($"---> TODO: Modulo check for images? Or just check later and apply padding before interpolation if needed...", hidden: true);
// int div = GetModulo();
// bool allDivBy2 = randomSamples.All(i => (i.Width % div == 0) && (i.Height % div == 0));
//
// if (!allDivBy2)
// {
// Logger.Log($"Sequence not compatible: Not all image dimensions are divisible by {div}.", true);
// return false;
// }
bool allSmallEnough = randomSamples.All(i => (i.Height <= maxHeight)); bool allSmallEnough = randomSamples.All(i => (i.Height <= maxHeight));
if (!allSmallEnough) if (!allSmallEnough)

View File

@@ -37,7 +37,7 @@ namespace Flowframes.Media
return output.SplitIntoLines().Where(x => x.MatchesWildcard("*Stream #0:*: *: *")).Count(); return output.SplitIntoLines().Where(x => x.MatchesWildcard("*Stream #0:*: *: *")).Count();
} }
public static async Task<List<Stream>> GetStreams(string path, bool progressBar, int streamCount, Fraction? defaultFps, bool countFrames, MediaFile mediaFile = null) public static async Task<List<Stream>> GetStreams(string path, bool progressBar, int streamCount, Fraction defaultFps, bool countFrames, MediaFile mediaFile = null)
{ {
List<Stream> streamList = new List<Stream>(); List<Stream> streamList = new List<Stream>();

View File

@@ -0,0 +1,104 @@
using Flowframes.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Flowframes.Media
{
public class TimestampUtils
{
public static void CalcTimestamps(MediaFile media, InterpSettings settings)
{
float avgDuration = media.InputTimestampDurations.Average();
media.InputTimestamps.Add(media.InputTimestamps.Last() + avgDuration); // Add extra frame using avg. duration, needed for duration matching or looping
media.OutputTimestamps = StretchTimestamps(media.InputTimestamps, settings.interpFactor);
if (settings.FpsResampling)
{
List<float[]> timestampsWithInputIdx = ConvertToConstantFrameRate(media.OutputTimestamps, settings.outFpsResampled.Float);
media.OutputTimestamps = timestampsWithInputIdx.Select(x => x[0]).ToList();
media.OutputFrameIndexes = timestampsWithInputIdx.Select(x => (int)x[1]).ToList();
}
}
public static string WriteTsFile(List<float> timestamps, string path)
{
var lines = new List<string>() { "# timecode format v2" };
foreach (var ts in timestamps)
{
lines.Add((ts * 1000f).ToString("0.000000"));
}
File.WriteAllLines(path, lines);
return path;
}
public static List<float> StretchTimestamps(List<float> timestamps, double factor)
{
int originalCount = timestamps.Count;
int newCount = (int)Math.Round(originalCount * factor);
List<float> resampledTimestamps = new List<float>();
for (int i = 0; i < newCount; i++)
{
double x = i / factor;
if (x >= originalCount - 1)
{
resampledTimestamps.Add(timestamps[originalCount - 1]);
}
else
{
int index = (int)Math.Floor(x);
double fraction = x - index;
float startTime = timestamps[index];
float endTime = timestamps[index + 1];
float interpolatedTime = (float)(startTime + (endTime - startTime) * fraction);
resampledTimestamps.Add(interpolatedTime);
}
}
return resampledTimestamps;
}
public static List<float[]> ConvertToConstantFrameRate(List<float> inputTimestamps, float targetFrameRate, Enums.Round roundMethod = Enums.Round.Near)
{
List<float[]> outputTimestamps = new List<float[]>(); // Resulting list of timestamps
float targetFrameInterval = 1.0f / targetFrameRate; // Interval for the target frame rate
float currentTargetTime = 0.0f; // Start time for the target frame rate
int index = 0; // Index for iterating through the input timestamps
Console.WriteLine("--> Converting to constant frame rate...");
while (currentTargetTime <= inputTimestamps.Last()) // Use ^1 to get the last element
{
switch (roundMethod)
{
case Enums.Round.Near: // Find the closest timestamp to the current target time
while (index < inputTimestamps.Count - 1 && Math.Abs(inputTimestamps[index + 1] - currentTargetTime) < Math.Abs(inputTimestamps[index] - currentTargetTime)) index++;
break;
case Enums.Round.Down: // Find the closest timestamp that is <= the current target time
while (index < inputTimestamps.Count - 1 && inputTimestamps[index + 1] <= currentTargetTime) index++;
break;
case Enums.Round.Up: // Find the closest timestamp that is >= the current target time
while (index < inputTimestamps.Count - 1 && inputTimestamps[index] < currentTargetTime) index++;
break;
}
// Debug message to show which frame index was picked
Console.WriteLine($"--> Frame {(outputTimestamps.Count + 1).ToString().PadLeft(3)} | Target Time: {(currentTargetTime * 1000f):F5} | Picked Input Index: {index} | Input TS: {(inputTimestamps[index] * 1000f):F3}");
// Add the closest timestamp to the output list, along with the index of the input timestamp
outputTimestamps.Add(new float[] { inputTimestamps[index], index });
// Move to the next frame time in the target frame rate
currentTargetTime += targetFrameInterval;
}
return outputTimestamps;
}
}
}

View File

@@ -172,6 +172,15 @@ namespace Flowframes.Os
l.Add(Debugger.IsAttached ? $"clip = core.text.FrameNum(clip, alignment=9, scale={txtScale}) # Output frame counter" : ""); l.Add(Debugger.IsAttached ? $"clip = core.text.FrameNum(clip, alignment=9, scale={txtScale}) # Output frame counter" : "");
l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"Frames: {{srcFrames}}/{{preInterpFrames}} -> {{len(clip)}} [{factor.GetString()}x]\", alignment=8, scale={txtScale})" : ""); l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"Frames: {{srcFrames}}/{{preInterpFrames}} -> {{len(clip)}} [{factor.GetString()}x]\", alignment=8, scale={txtScale})" : "");
l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"targetMatchDuration: {targetFrameCountMatchDuration} - targetTrue: {targetFrameCountTrue} - endDupeCount: {endDupeCount}\", alignment=2, scale={txtScale})" : ""); l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"targetMatchDuration: {targetFrameCountMatchDuration} - targetTrue: {targetFrameCountTrue} - endDupeCount: {endDupeCount}\", alignment=2, scale={txtScale})" : "");
if(Interpolate.currentMediaFile.IsVfr && Interpolate.currentMediaFile.OutputFrameIndexes != null && Interpolate.currentMediaFile.OutputFrameIndexes.Count > 0 && s.InterpSettings.outFpsResampled.Float > 0.1f)
{
l.Add($"# Frames picked to resample VFR video to {s.InterpSettings.outFpsResampled.Float} FPS");
l.Add($"frameIndexes = [{string.Join(", ", Interpolate.currentMediaFile.OutputFrameIndexes)}]");
l.Add($"clip = core.std.Splice([clip[i] for i in frameIndexes if i < len(clip)])");
l.Add($"clip = core.std.AssumeFPS(clip, fpsnum={s.InterpSettings.outFpsResampled.Numerator}, fpsden={s.InterpSettings.outFpsResampled.Denominator})");
}
l.Add($"clip.set_output()"); // Set output l.Add($"clip.set_output()"); // Set output
l.Add(""); l.Add("");

View File

@@ -61,5 +61,10 @@ namespace Flowframes.Ui
control.Visible = false; control.Visible = false;
} }
} }
public static void Invoke(this Control control, MethodInvoker action)
{
control.Invoke(action);
}
} }
} }

View File

@@ -136,7 +136,7 @@ namespace Flowframes.Ui
if (I.canceled) return; if (I.canceled) return;
interpolatedInputFramesCount = ((frames / I.currentSettings.interpFactor).RoundToInt() - 1); interpolatedInputFramesCount = ((frames / I.currentSettings.interpFactor).RoundToInt() - 1);
//ResumeUtils.Save(); //ResumeUtils.Save();
target = (target / Interpolate.InterpProgressMultiplier).RoundToInt(); target = (target / I.InterpProgressMultiplier).RoundToInt();
frames = frames.Clamp(0, target); frames = frames.Clamp(0, target);
if (_framesAtTime == null) if (_framesAtTime == null)