mirror of
https://github.com/n00mkrad/flowframes.git
synced 2025-12-22 11:19:25 +01:00
Better CFR-based encoding, fixed dupe frame when using loop
This commit is contained in:
@@ -92,17 +92,17 @@ namespace Flowframes
|
|||||||
DeleteSource(inputFile);
|
DeleteSource(inputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task FramesToVideoVfr(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false)
|
public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false)
|
||||||
{
|
{
|
||||||
if (logMode != AvProcess.LogMode.Hidden)
|
if (logMode != AvProcess.LogMode.Hidden)
|
||||||
Logger.Log($"Encoding video...");
|
Logger.Log($"Encoding video...");
|
||||||
string encArgs = Utils.GetEncArgs(Utils.GetCodec(outMode)) + " -pix_fmt yuv420p ";
|
string encArgs = Utils.GetEncArgs(Utils.GetCodec(outMode)) + " -pix_fmt yuv420p ";
|
||||||
if (!isChunk) encArgs += $"-movflags +faststart";
|
if (!isChunk) encArgs += $"-movflags +faststart";
|
||||||
string vfrFilename = Path.GetFileName(framesFile);
|
string vfrFilename = Path.GetFileName(framesFile);
|
||||||
string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2";
|
//string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2";
|
||||||
string rate = fps.ToString().Replace(",", ".");
|
string rate = fps.ToString().Replace(",", ".");
|
||||||
string extraArgs = Config.Get("ffEncArgs");
|
string extraArgs = Config.Get("ffEncArgs");
|
||||||
string args = $"{vsync} -f concat -i {vfrFilename} -r {rate} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
string args = $"-loglevel error -vsync 0 -f concat -r {rate} -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
||||||
//string args = $"-vsync 0 -f concat -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
//string args = $"-vsync 0 -f concat -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
||||||
await AvProcess.RunFfmpeg(args, framesFile.GetParentDir(), logMode);
|
await AvProcess.RunFfmpeg(args, framesFile.GetParentDir(), logMode);
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ namespace Flowframes
|
|||||||
public static async Task Encode(string inputFile, string vcodec, string acodec, int crf, int audioKbps = 0, bool delSrc = false)
|
public static async Task Encode(string inputFile, string vcodec, string acodec, int crf, int audioKbps = 0, bool delSrc = false)
|
||||||
{
|
{
|
||||||
string outPath = Path.ChangeExtension(inputFile, null) + "-convert.mp4";
|
string outPath = Path.ChangeExtension(inputFile, null) + "-convert.mp4";
|
||||||
string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k {outPath.Wrap()}";
|
string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k -vf {divisionFilter} {outPath.Wrap()}";
|
||||||
if (string.IsNullOrWhiteSpace(acodec))
|
if (string.IsNullOrWhiteSpace(acodec))
|
||||||
args = args.Replace("-c:a", "-an");
|
args = args.Replace("-c:a", "-an");
|
||||||
if (audioKbps < 0)
|
if (audioKbps < 0)
|
||||||
|
|||||||
@@ -155,5 +155,15 @@ namespace Flowframes
|
|||||||
|
|
||||||
return str.Remove(place, stringToReplace.Length).Insert(place, replaceWith);
|
return str.Remove(place, stringToReplace.Length).Insert(place, replaceWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string[] SplitBy (this string str, string splitBy)
|
||||||
|
{
|
||||||
|
return str.Split(new string[] { splitBy }, StringSplitOptions.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RemoveComments (this string str)
|
||||||
|
{
|
||||||
|
return str.Split('#')[0].SplitBy("//")[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ namespace Flowframes.Magick
|
|||||||
|
|
||||||
bool hasReachedEnd = false;
|
bool hasReachedEnd = false;
|
||||||
|
|
||||||
|
string infoFile = Path.Combine(path.GetParentDir(), $"dupes.ini");
|
||||||
|
string fileContent = "";
|
||||||
|
|
||||||
for (int i = 0; i < framePaths.Length; i++) // Loop through frames
|
for (int i = 0; i < framePaths.Length; i++) // Loop through frames
|
||||||
{
|
{
|
||||||
if (hasReachedEnd)
|
if (hasReachedEnd)
|
||||||
@@ -133,16 +136,17 @@ namespace Flowframes.Magick
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
fileContent += $"{Path.GetFileNameWithoutExtension(framePaths[i].Name)}:{currentDupeCount}\n";
|
||||||
statsFramesKept++;
|
statsFramesKept++;
|
||||||
currentOutFrame++;
|
currentOutFrame++;
|
||||||
currentDupeCount = 0;
|
currentDupeCount = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length)
|
if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length) // Print every 1s (or when done)
|
||||||
{
|
{
|
||||||
sw.Restart();
|
sw.Restart();
|
||||||
Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}. Total: {framePaths.Length - statsFramesDeleted} kept / {statsFramesDeleted} deleted.", false, true);
|
Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}.", false, true);
|
||||||
Program.mainForm.SetProgress((int)Math.Round(((float)i / framePaths.Length) * 100f));
|
Program.mainForm.SetProgress((int)Math.Round(((float)i / framePaths.Length) * 100f));
|
||||||
if (imageCache.Count > bufferSize || (imageCache.Count > 50 && OSUtils.GetFreeRamMb() < 2500))
|
if (imageCache.Count > bufferSize || (imageCache.Count > 50 && OSUtils.GetFreeRamMb() < 2500))
|
||||||
ClearCache();
|
ClearCache();
|
||||||
@@ -169,6 +173,8 @@ namespace Flowframes.Magick
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(infoFile, fileContent);
|
||||||
|
|
||||||
foreach (string frame in framesToDelete)
|
foreach (string frame in framesToDelete)
|
||||||
IOUtils.TryDeleteIfExists(frame);
|
IOUtils.TryDeleteIfExists(frame);
|
||||||
|
|
||||||
@@ -176,10 +182,15 @@ namespace Flowframes.Magick
|
|||||||
|
|
||||||
if (Interpolate.canceled) return;
|
if (Interpolate.canceled) return;
|
||||||
|
|
||||||
|
int framesLeft = IOUtils.GetAmountOfFiles(path, false, $"*.png");
|
||||||
|
int framesDeleted = framePaths.Length - framesLeft;
|
||||||
|
float percentDeleted = ((float)framesDeleted / framePaths.Length) * 100f;
|
||||||
|
string keptPercent = $"{(100f - percentDeleted).ToString("0.0")}%";
|
||||||
|
|
||||||
if (skipped)
|
if (skipped)
|
||||||
Logger.Log($"[FrameDedup] First {skipAfterNoDupesFrames} frames did not have any duplicates - Skipping the rest!", false, true);
|
Logger.Log($"[FrameDedup] First {skipAfterNoDupesFrames} frames did not have any duplicates - Skipping the rest!", false, true);
|
||||||
else
|
else
|
||||||
Logger.Log($"[FrameDedup]{testStr} Done. Kept {statsFramesKept} frames, deleted {statsFramesDeleted} frames.", false, true);
|
Logger.Log($"[FrameDedup]{testStr} Done. Kept {framesLeft} ({keptPercent}) frames, deleted {framesDeleted} frames.", false, true);
|
||||||
|
|
||||||
if (statsFramesKept <= 0)
|
if (statsFramesKept <= 0)
|
||||||
Interpolate.Cancel("No frames were left after de-duplication!\n\nTry decreasing the de-duplication threshold.");
|
Interpolate.Cancel("No frames were left after de-duplication!\n\nTry decreasing the de-duplication threshold.");
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ namespace Flowframes.Main
|
|||||||
bool h265 = Config.GetInt("mp4Enc") == 1;
|
bool h265 = Config.GetInt("mp4Enc") == 1;
|
||||||
int crf = h265 ? Config.GetInt("h265Crf") : Config.GetInt("h264Crf");
|
int crf = h265 ? Config.GetInt("h265Crf") : Config.GetInt("h264Crf");
|
||||||
|
|
||||||
await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, fps);
|
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, fps);
|
||||||
await MergeAudio(i.current.inPath, outPath);
|
await MergeAudio(i.current.inPath, outPath);
|
||||||
|
|
||||||
if (changeFps > 0)
|
if (changeFps > 0)
|
||||||
@@ -185,7 +185,7 @@ namespace Flowframes.Main
|
|||||||
string vfrFile = Path.Combine(i.current.tempFolder, $"vfr-chunk-{firstFrameNum}-{firstFrameNum + framesAmount}.ini");
|
string vfrFile = Path.Combine(i.current.tempFolder, $"vfr-chunk-{firstFrameNum}-{firstFrameNum + framesAmount}.ini");
|
||||||
File.WriteAllLines(vfrFile, IOUtils.ReadLines(vfrFileOriginal).Skip(firstFrameNum * 2).Take(framesAmount * 2));
|
File.WriteAllLines(vfrFile, IOUtils.ReadLines(vfrFileOriginal).Skip(firstFrameNum * 2).Take(framesAmount * 2));
|
||||||
|
|
||||||
await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true);
|
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task Loop(string outPath, int looptimes)
|
static async Task Loop(string outPath, int looptimes)
|
||||||
|
|||||||
@@ -14,14 +14,25 @@ namespace Flowframes.Main
|
|||||||
{
|
{
|
||||||
class FrameTiming
|
class FrameTiming
|
||||||
{
|
{
|
||||||
|
public enum Mode { CFR, VFR }
|
||||||
public static int timebase = 10000;
|
public static int timebase = 10000;
|
||||||
|
|
||||||
public static async Task CreateTimecodeFiles(string framesPath, bool loopEnabled, int times, bool noTimestamps)
|
public static async Task CreateTimecodeFiles(string framesPath, Mode mode, bool loopEnabled, int times, bool noTimestamps)
|
||||||
{
|
{
|
||||||
Logger.Log("Generating timecodes...");
|
Logger.Log("Generating timecodes...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mode == Mode.VFR)
|
||||||
await CreateTimecodeFile(framesPath, loopEnabled, times, false, noTimestamps);
|
await CreateTimecodeFile(framesPath, loopEnabled, times, false, noTimestamps);
|
||||||
|
if (mode == Mode.CFR)
|
||||||
|
await CreateEncFile(framesPath, loopEnabled, times, false);
|
||||||
Logger.Log($"Generating timecodes... Done.", false, true);
|
Logger.Log($"Generating timecodes... Done.", false, true);
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Log($"Error generating timecodes: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task CreateTimecodeFile(string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun, bool noTimestamps)
|
public static async Task CreateTimecodeFile(string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun, bool noTimestamps)
|
||||||
{
|
{
|
||||||
@@ -138,5 +149,124 @@ namespace Flowframes.Main
|
|||||||
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, int> dupesDict = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
static void LoadDupesFile (string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
dupesDict.Clear();
|
||||||
|
string[] dupesFileLines = IOUtils.ReadLines(path);
|
||||||
|
foreach(string line in dupesFileLines)
|
||||||
|
{
|
||||||
|
string[] values = line.Split(':');
|
||||||
|
dupesDict.Add(values[0], values[1].GetInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CreateEncFile (string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun)
|
||||||
|
{
|
||||||
|
if (Interpolate.canceled) return;
|
||||||
|
Logger.Log($"Generating timecodes for {interpFactor}x...", false, true);
|
||||||
|
|
||||||
|
bool loop = Config.GetBool("enableLoop");
|
||||||
|
bool sceneDetection = true;
|
||||||
|
string ext = InterpolateUtils.GetOutExt();
|
||||||
|
|
||||||
|
FileInfo[] frameFiles = new DirectoryInfo(framesPath).GetFiles($"*.png");
|
||||||
|
string vfrFile = Path.Combine(framesPath.GetParentDir(), $"vfr-{interpFactor}x.ini");
|
||||||
|
string fileContent = "";
|
||||||
|
string dupesFile = Path.Combine(framesPath.GetParentDir(), $"dupes.ini");
|
||||||
|
LoadDupesFile(dupesFile);
|
||||||
|
|
||||||
|
string scnFramesPath = Path.Combine(framesPath.GetParentDir(), Paths.scenesDir);
|
||||||
|
string interpPath = Paths.interpDir;
|
||||||
|
|
||||||
|
List<string> sceneFrames = new List<string>();
|
||||||
|
if (Directory.Exists(scnFramesPath))
|
||||||
|
sceneFrames = Directory.GetFiles(scnFramesPath).Select(file => Path.GetFileNameWithoutExtension(file)).ToList();
|
||||||
|
|
||||||
|
int totalFileCount = 1;
|
||||||
|
for (int i = 0; i < (frameFiles.Length - 1); i++)
|
||||||
|
{
|
||||||
|
if (Interpolate.canceled) return;
|
||||||
|
|
||||||
|
int interpFramesAmount = interpFactor;
|
||||||
|
string inputFilenameNoExt = Path.GetFileNameWithoutExtension(frameFiles[i].Name);
|
||||||
|
int dupesAmount = dupesDict.ContainsKey(inputFilenameNoExt) ? dupesDict[inputFilenameNoExt] : 0;
|
||||||
|
//Logger.Log($"{Path.GetFileNameWithoutExtension(frameFiles[i].Name)} has {dupesAmount} dupes", true);
|
||||||
|
|
||||||
|
bool discardThisFrame = (sceneDetection && (i + 2) < frameFiles.Length && sceneFrames.Contains(Path.GetFileNameWithoutExtension(frameFiles[i + 1].Name))); // i+2 is in scene detection folder, means i+1 is ugly interp frame
|
||||||
|
|
||||||
|
// TODO: Check if this is needed
|
||||||
|
// If loop is enabled, account for the extra frame added to the end for loop continuity
|
||||||
|
if (loopEnabled && i == (frameFiles.Length - 2))
|
||||||
|
interpFramesAmount = interpFramesAmount * 2;
|
||||||
|
|
||||||
|
//Logger.Log($"Writing out frames for in frame {i} which has {dupesAmount} dupes", true);
|
||||||
|
// Generate frames file lines
|
||||||
|
for (int frm = 0; frm < interpFramesAmount; frm++)
|
||||||
|
{
|
||||||
|
//Logger.Log($"Writing out frame {frm+1}/{interpFramesAmount}", true);
|
||||||
|
|
||||||
|
|
||||||
|
if (discardThisFrame && totalFileCount > 1) // If frame is scene cut frame
|
||||||
|
{
|
||||||
|
int lastNum = totalFileCount;
|
||||||
|
|
||||||
|
//Logger.Log($"Writing frame {totalFileCount} [Discarding Next]", true);
|
||||||
|
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||||
|
totalFileCount++;
|
||||||
|
|
||||||
|
//Logger.Log("Discarding interp frames with out num " + totalFileCount);
|
||||||
|
for (int dupeCount = 1; dupeCount < interpFramesAmount; dupeCount++)
|
||||||
|
{
|
||||||
|
//Logger.Log($"Writing frame {totalFileCount} which is actually repeated frame {lastNum}");
|
||||||
|
fileContent += $"file '{interpPath}/{lastNum.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||||
|
totalFileCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
frm = interpFramesAmount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for(int writtenDupes = -1; writtenDupes < dupesAmount; writtenDupes++) // Write duplicates
|
||||||
|
{
|
||||||
|
//Logger.Log($"Writing frame {totalFileCount}", true, false);
|
||||||
|
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||||
|
}
|
||||||
|
totalFileCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((i + 1) % 100 == 0)
|
||||||
|
await Task.Delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use average frame duration for last frame - TODO: Use real duration??
|
||||||
|
//string durationStrLast = ((totalDuration / (totalFileCount - 1)) / timebase).ToString("0.0000000", CultureInfo.InvariantCulture);
|
||||||
|
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||||
|
totalFileCount++;
|
||||||
|
|
||||||
|
string finalFileContent = fileContent.Trim();
|
||||||
|
if(loop)
|
||||||
|
finalFileContent = finalFileContent.Remove(finalFileContent.LastIndexOf("\n"));
|
||||||
|
File.WriteAllText(vfrFile, finalFileContent);
|
||||||
|
|
||||||
|
if (notFirstRun) return; // Skip all steps that only need to be done once
|
||||||
|
|
||||||
|
if (loop)
|
||||||
|
{
|
||||||
|
int lastFileNumber = frameFiles.Last().Name.GetInt() + 1;
|
||||||
|
string loopFrameTargetPath = Path.Combine(frameFiles.First().FullName.GetParentDir(), lastFileNumber.ToString().PadLeft(Padding.inputFrames, '0') + $".png");
|
||||||
|
if (File.Exists(loopFrameTargetPath))
|
||||||
|
{
|
||||||
|
Logger.Log($"Won't copy loop frame - {Path.GetFileName(loopFrameTargetPath)} already exists.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
||||||
|
Logger.Log($"Copied loop frame to {loopFrameTargetPath}.", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ namespace Flowframes
|
|||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
bool useTimestamps = Config.GetInt("timingMode") == 1; // TODO: Auto-Disable timestamps if input frames are sequential, not timestamped
|
bool useTimestamps = Config.GetInt("timingMode") == 1; // TODO: Auto-Disable timestamps if input frames are sequential, not timestamped
|
||||||
await FrameTiming.CreateTimecodeFiles(current.framesFolder, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps);
|
await FrameTiming.CreateTimecodeFiles(current.framesFolder, FrameTiming.Mode.CFR, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps);
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace Flowframes.Main
|
|||||||
public static int currentFactor;
|
public static int currentFactor;
|
||||||
public static async void GetProgressByFrameAmount(string outdir, int target)
|
public static async void GetProgressByFrameAmount(string outdir, int target)
|
||||||
{
|
{
|
||||||
|
Logger.Log($"Starting GetProgressByFrameAmount() loop for outdir '{outdir}', target is {target} frames", true);
|
||||||
bool firstProgUpd = true;
|
bool firstProgUpd = true;
|
||||||
Program.mainForm.SetProgress(0);
|
Program.mainForm.SetProgress(0);
|
||||||
targetFrames = target;
|
targetFrames = target;
|
||||||
|
|||||||
Reference in New Issue
Block a user