mirror of
https://github.com/n00mkrad/flowframes.git
synced 2025-12-25 12:49:26 +01:00
Port to .NET 5
This commit is contained in:
178
Code5/Main/AutoEncode.cs
Normal file
178
Code5/Main/AutoEncode.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using Flowframes.AudioVideo;
|
||||
using Flowframes.Data;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.MiscUtils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
class AutoEncode
|
||||
{
|
||||
static string interpFramesFolder;
|
||||
static string videoChunksFolder;
|
||||
public static int chunkSize = 125; // Encode every n frames
|
||||
public static int safetyBufferFrames = 90; // Ignore latest n frames to avoid using images that haven't been fully encoded yet
|
||||
public static string[] interpFramesLines;
|
||||
public static List<int> encodedFrameLines = new List<int>();
|
||||
public static List<int> unencodedFrameLines = new List<int>();
|
||||
|
||||
public static bool busy;
|
||||
|
||||
public static bool paused;
|
||||
|
||||
public static void UpdateChunkAndBufferSizes ()
|
||||
{
|
||||
chunkSize = GetChunkSize(IOUtils.GetAmountOfFiles(Interpolate.current.framesFolder, false, "*.png") * Interpolate.current.interpFactor);
|
||||
bool isNcnn = Interpolate.current.ai.aiName.ToUpper().Contains("NCNN");
|
||||
safetyBufferFrames = isNcnn ? Config.GetInt("autoEncSafeBufferNcnn", 90) : Config.GetInt("autoEncSafeBufferCuda", 30); // Use bigger safety buffer for NCNN
|
||||
}
|
||||
|
||||
public static async Task MainLoop(string interpFramesPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateChunkAndBufferSizes();
|
||||
|
||||
interpFramesFolder = interpFramesPath;
|
||||
videoChunksFolder = Path.Combine(interpFramesPath.GetParentDir(), Paths.chunksDir);
|
||||
if (Interpolate.currentlyUsingAutoEnc)
|
||||
Directory.CreateDirectory(videoChunksFolder);
|
||||
|
||||
encodedFrameLines.Clear();
|
||||
unencodedFrameLines.Clear();
|
||||
|
||||
Logger.Log($"[AutoEnc] Starting AutoEncode MainLoop - Chunk Size: {chunkSize} Frames - Safety Buffer: {safetyBufferFrames} Frames", true);
|
||||
int videoIndex = 1;
|
||||
string encFile = Path.Combine(interpFramesPath.GetParentDir(), $"vfr-{Interpolate.current.interpFactor}x.ini");
|
||||
interpFramesLines = IOUtils.ReadLines(encFile).Select(x => x.Split('/').Last().Remove("'").Split('#').First()).ToArray(); // Array with frame filenames
|
||||
|
||||
while (!Interpolate.canceled && GetInterpFramesAmount() < 2)
|
||||
await Task.Delay(2000);
|
||||
|
||||
int lastEncodedFrameNum = 0;
|
||||
|
||||
while (HasWorkToDo()) // Loop while proc is running and not all frames have been encoded
|
||||
{
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
if (paused)
|
||||
{
|
||||
//Logger.Log("autoenc paused");
|
||||
await Task.Delay(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
unencodedFrameLines.Clear();
|
||||
|
||||
for (int vfrLine = lastEncodedFrameNum; vfrLine < interpFramesLines.Length; vfrLine++)
|
||||
unencodedFrameLines.Add(vfrLine);
|
||||
|
||||
bool aiRunning = !AiProcess.currentAiProcess.HasExited;
|
||||
|
||||
if (unencodedFrameLines.Count > 0 && (unencodedFrameLines.Count >= (chunkSize + safetyBufferFrames) || !aiRunning)) // Encode every n frames, or after process has exited
|
||||
{
|
||||
|
||||
List<int> frameLinesToEncode = aiRunning ? unencodedFrameLines.Take(chunkSize).ToList() : unencodedFrameLines; // Take all remaining frames if process is done
|
||||
|
||||
string lastOfChunk = Path.Combine(interpFramesPath, interpFramesLines[frameLinesToEncode.Last()]);
|
||||
|
||||
if (!File.Exists(lastOfChunk))
|
||||
{
|
||||
await Task.Delay(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
string outpath = Path.Combine(videoChunksFolder, "chunks", $"{videoIndex.ToString().PadLeft(4, '0')}{FFmpegUtils.GetExt(Interpolate.current.outMode)}");
|
||||
//int firstFrameNum = frameLinesToEncode[0];
|
||||
int firstLineNum = frameLinesToEncode.First();
|
||||
int lastLineNum = frameLinesToEncode.Last();
|
||||
Logger.Log($"[AutoEnc] Encoding Chunk #{videoIndex} to '{outpath}' using line {firstLineNum} ({Path.GetFileName(interpFramesLines[firstLineNum])}) through {lastLineNum} ({Path.GetFileName(Path.GetFileName(interpFramesLines[frameLinesToEncode.Last()]))})", true, false, "ffmpeg");
|
||||
|
||||
await CreateVideo.EncodeChunk(outpath, Interpolate.current.outMode, firstLineNum, frameLinesToEncode.Count);
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
if (Config.GetInt("autoEncMode") == 2)
|
||||
Task.Run(() => DeleteOldFramesAsync(interpFramesPath, frameLinesToEncode));
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
encodedFrameLines.AddRange(frameLinesToEncode);
|
||||
|
||||
Logger.Log("Done Encoding Chunk #" + videoIndex, true, false, "ffmpeg");
|
||||
lastEncodedFrameNum = (frameLinesToEncode.Last() + 1 );
|
||||
|
||||
videoIndex++;
|
||||
busy = false;
|
||||
}
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
IOUtils.ReverseRenaming(AiProcess.filenameMap, true); // Get timestamps back
|
||||
await CreateVideo.ChunksToVideos(Interpolate.current.tempFolder, videoChunksFolder, Interpolate.current.outFilename);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"AutoEnc Error: {e.Message}. Stack Trace:\n{e.StackTrace}");
|
||||
Interpolate.Cancel("Auto-Encode encountered an error.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task DeleteOldFramesAsync (string interpFramesPath, List<int> frameLinesToEncode)
|
||||
{
|
||||
Logger.Log("[AutoEnc] Starting DeleteOldFramesAsync.", true, false, "ffmpeg");
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
|
||||
foreach (int frame in frameLinesToEncode)
|
||||
{
|
||||
bool delete = !FrameIsStillNeeded(interpFramesLines[frame], frame);
|
||||
if (delete) // Make sure frames are no longer needed (e.g. for dupes) before deleting!
|
||||
{
|
||||
string framePath = Path.Combine(interpFramesPath, interpFramesLines[frame]);
|
||||
File.WriteAllText(framePath, "THIS IS A DUMMY FILE - DO NOT DELETE ME"); // Overwrite to save space without breaking progress counter
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log("[AutoEnc] DeleteOldFramesAsync finished in " + FormatUtils.TimeSw(sw), true, false, "ffmpeg");
|
||||
}
|
||||
|
||||
static bool FrameIsStillNeeded (string frameName, int frameIndex)
|
||||
{
|
||||
if ((frameIndex + 1) < interpFramesLines.Length && interpFramesLines[frameIndex+1].Contains(frameName))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasWorkToDo ()
|
||||
{
|
||||
if (Interpolate.canceled || interpFramesFolder == null) return false;
|
||||
// Logger.Log($"HasWorkToDo - Process Running: {(AiProcess.currentAiProcess != null && !AiProcess.currentAiProcess.HasExited)} - encodedFrameLines.Count: {encodedFrameLines.Count} - interpFramesLines.Length: {interpFramesLines.Length}");
|
||||
return ((AiProcess.currentAiProcess != null && !AiProcess.currentAiProcess.HasExited) || encodedFrameLines.Count < interpFramesLines.Length);
|
||||
}
|
||||
|
||||
static int GetChunkSize(int targetFramesAmount)
|
||||
{
|
||||
if (targetFramesAmount > 50000) return 2400;
|
||||
if (targetFramesAmount > 20000) return 1200;
|
||||
if (targetFramesAmount > 5000) return 600;
|
||||
if (targetFramesAmount > 1000) return 300;
|
||||
return 150;
|
||||
}
|
||||
|
||||
static int GetInterpFramesAmount()
|
||||
{
|
||||
return IOUtils.GetAmountOfFiles(interpFramesFolder, false, $"*.{InterpolateUtils.GetOutExt()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Code5/Main/BatchProcessing.cs
Normal file
110
Code5/Main/BatchProcessing.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Flowframes.Forms;
|
||||
using Flowframes.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
class BatchProcessing
|
||||
{
|
||||
public static bool stopped = false;
|
||||
|
||||
public static BatchForm currentBatchForm;
|
||||
public static bool busy = false;
|
||||
|
||||
public static async void Start()
|
||||
{
|
||||
if (Config.GetBool("clearLogOnInput"))
|
||||
Logger.ClearLogBox();
|
||||
|
||||
stopped = false;
|
||||
Program.mainForm.SetTab("preview");
|
||||
int initTaskCount = Program.batchQueue.Count;
|
||||
|
||||
for (int i = 0; i < initTaskCount; i++)
|
||||
{
|
||||
if (!stopped && Program.batchQueue.Count > 0)
|
||||
{
|
||||
Logger.Log($"[Queue] Running queue task {i + 1}/{initTaskCount}, {Program.batchQueue.Count} tasks left.");
|
||||
await RunEntry(Program.batchQueue.Peek());
|
||||
if (currentBatchForm != null)
|
||||
currentBatchForm.RefreshGui();
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
Logger.Log("[Queue] Finished queue processing.");
|
||||
SetBusy(false);
|
||||
Program.mainForm.SetTab("interpolation");
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
static async Task RunEntry(InterpSettings entry)
|
||||
{
|
||||
if (!EntryIsValid(entry))
|
||||
{
|
||||
Logger.Log("[Queue] Skipping entry because it's invalid.");
|
||||
Program.batchQueue.Dequeue();
|
||||
return;
|
||||
}
|
||||
|
||||
string fname = Path.GetFileName(entry.inPath);
|
||||
if (IOUtils.IsPathDirectory(entry.inPath)) fname = Path.GetDirectoryName(entry.inPath);
|
||||
Logger.Log($"[Queue] Processing {fname} ({entry.interpFactor}x {entry.ai.aiNameShort}).");
|
||||
|
||||
SetBusy(true);
|
||||
Program.mainForm.LoadBatchEntry(entry); // Load entry into GUI
|
||||
Interpolate.current = entry;
|
||||
Program.mainForm.runBtn_Click(null, null);
|
||||
|
||||
await Task.Delay(2000);
|
||||
while (Program.busy)
|
||||
await Task.Delay(1000);
|
||||
|
||||
SetBusy(false);
|
||||
|
||||
Program.batchQueue.Dequeue();
|
||||
Logger.Log($"[Queue] Done processing {fname} ({entry.interpFactor}x {entry.ai.aiNameShort}).");
|
||||
}
|
||||
|
||||
static void SetBusy(bool state)
|
||||
{
|
||||
busy = state;
|
||||
if (currentBatchForm != null)
|
||||
currentBatchForm.SetWorking(state);
|
||||
Program.mainForm.SetWorking(state);
|
||||
Program.mainForm.GetMainTabControl().Enabled = !state; // Lock GUI
|
||||
}
|
||||
|
||||
static bool EntryIsValid(InterpSettings entry)
|
||||
{
|
||||
|
||||
if (entry.inPath == null || (IOUtils.IsPathDirectory(entry.inPath) && !Directory.Exists(entry.inPath)) || (!IOUtils.IsPathDirectory(entry.inPath) && !File.Exists(entry.inPath)))
|
||||
{
|
||||
Logger.Log("[Queue] Can't process queue entry: Input path is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.outPath == null || !Directory.Exists(entry.outPath))
|
||||
{
|
||||
Logger.Log("[Queue] Can't process queue entry: Output path is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PkgUtils.IsAiAvailable(entry.ai))
|
||||
{
|
||||
Logger.Log("[Queue] Can't process queue entry: Selected AI is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
233
Code5/Main/CreateVideo.cs
Normal file
233
Code5/Main/CreateVideo.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using Flowframes;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.Magick;
|
||||
using Flowframes.Main;
|
||||
using Flowframes.OS;
|
||||
using Flowframes.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Padding = Flowframes.Data.Padding;
|
||||
using i = Flowframes.Interpolate;
|
||||
using System.Diagnostics;
|
||||
using Flowframes.AudioVideo;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
class CreateVideo
|
||||
{
|
||||
static string currentOutFile; // Keeps track of the out file, in case it gets renamed (FPS limiting, looping, etc) before finishing export
|
||||
|
||||
public static async Task Export(string path, string outPath, i.OutMode mode)
|
||||
{
|
||||
if (!mode.ToString().ToLower().Contains("vid")) // Copy interp frames out of temp folder and skip video export for image seq export
|
||||
{
|
||||
try
|
||||
{
|
||||
await CopyOutputFrames(path, Path.GetFileNameWithoutExtension(outPath));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Logger.Log("Failed to move interp frames folder: " + e.Message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (IOUtils.GetAmountOfFiles(path, false, $"*.{InterpolateUtils.GetOutExt()}") <= 1)
|
||||
{
|
||||
i.Cancel("Output folder does not contain frames - An error must have occured during interpolation!", AiProcess.hasShownError);
|
||||
return;
|
||||
}
|
||||
await Task.Delay(10);
|
||||
Program.mainForm.SetStatus("Creating output video from frames...");
|
||||
try
|
||||
{
|
||||
float maxFps = Config.GetFloat("maxFps");
|
||||
bool fpsLimit = maxFps != 0 && i.current.outFps > maxFps;
|
||||
|
||||
bool dontEncodeFullFpsVid = fpsLimit && Config.GetInt("maxFpsMode") == 0;
|
||||
|
||||
if(!dontEncodeFullFpsVid)
|
||||
await Encode(mode, path, outPath, i.current.outFps);
|
||||
|
||||
if (fpsLimit)
|
||||
await Encode(mode, path, outPath.FilenameSuffix($"-{maxFps.ToStringDot("0.00")}fps"), i.current.outFps, maxFps);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("FramesToVideo Error: " + e.Message, false);
|
||||
MessageBox.Show("An error occured while trying to convert the interpolated frames to a video.\nCheck the log for details.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task CopyOutputFrames (string framesPath, string folderName)
|
||||
{
|
||||
Program.mainForm.SetStatus("Copying output frames...");
|
||||
string copyPath = Path.Combine(i.current.outPath, folderName);
|
||||
Logger.Log($"Moving output frames to '{copyPath}'");
|
||||
IOUtils.TryDeleteIfExists(copyPath);
|
||||
IOUtils.CreateDir(copyPath);
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
|
||||
string vfrFile = Path.Combine(framesPath.GetParentDir(), $"vfr-{i.current.interpFactor}x.ini");
|
||||
string[] vfrLines = IOUtils.ReadLines(vfrFile);
|
||||
|
||||
for (int idx = 1; idx <= vfrLines.Length; idx++)
|
||||
{
|
||||
string line = vfrLines[idx-1];
|
||||
string inFilename = line.Split('/').Last().Remove("'").RemoveComments();
|
||||
string framePath = Path.Combine(framesPath, inFilename);
|
||||
string outFilename = Path.Combine(copyPath, idx.ToString().PadLeft(Padding.interpFrames, '0')) + Path.GetExtension(framePath);
|
||||
|
||||
if ((idx < vfrLines.Length) && vfrLines[idx].Contains(inFilename)) // If file is re-used in the next line, copy instead of move
|
||||
File.Copy(framePath, outFilename);
|
||||
else
|
||||
File.Move(framePath, outFilename);
|
||||
|
||||
if (sw.ElapsedMilliseconds >= 500 || idx == vfrLines.Length)
|
||||
{
|
||||
sw.Restart();
|
||||
Logger.Log($"Moving output frames to '{Path.GetFileName(copyPath)}' - {idx}/{vfrLines.Length}", false, true);
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async Task Encode(i.OutMode mode, string framesPath, string outPath, float fps, float resampleFps = -1)
|
||||
{
|
||||
currentOutFile = outPath;
|
||||
string vfrFile = Path.Combine(framesPath.GetParentDir(), $"vfr-{i.current.interpFactor}x.ini");
|
||||
|
||||
if (mode == i.OutMode.VidGif)
|
||||
{
|
||||
await FFmpegCommands.FramesToGifConcat(vfrFile, outPath, fps, true, Config.GetInt("gifColors"), resampleFps);
|
||||
}
|
||||
else
|
||||
{
|
||||
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, fps, resampleFps);
|
||||
await MergeAudio(i.current.inPath, outPath);
|
||||
|
||||
int looptimes = GetLoopTimes();
|
||||
if (looptimes > 0)
|
||||
await Loop(currentOutFile, looptimes);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ChunksToVideos(string tempFolder, string chunksFolder, string baseOutPath)
|
||||
{
|
||||
if (IOUtils.GetAmountOfFiles(chunksFolder, true, $"*{FFmpegUtils.GetExt(i.current.outMode)}") < 1)
|
||||
{
|
||||
i.Cancel("No video chunks found - An error must have occured during chunk encoding!", AiProcess.hasShownError);
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(10);
|
||||
Program.mainForm.SetStatus("Merging video chunks...");
|
||||
try
|
||||
{
|
||||
DirectoryInfo chunksDir = new DirectoryInfo(chunksFolder);
|
||||
foreach(DirectoryInfo dir in chunksDir.GetDirectories())
|
||||
{
|
||||
string suffix = dir.Name.Replace("chunks", "");
|
||||
string tempConcatFile = Path.Combine(tempFolder, $"chunks-concat{suffix}.ini");
|
||||
string concatFileContent = "";
|
||||
foreach (string vid in IOUtils.GetFilesSorted(dir.FullName))
|
||||
concatFileContent += $"file '{Paths.chunksDir}/{dir.Name}/{Path.GetFileName(vid)}'\n";
|
||||
File.WriteAllText(tempConcatFile, concatFileContent);
|
||||
|
||||
Logger.Log($"CreateVideo: Running MergeChunks() for vfrFile '{Path.GetFileName(tempConcatFile)}'", true);
|
||||
await MergeChunks(tempConcatFile, baseOutPath.FilenameSuffix(suffix));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("ChunksToVideo Error: " + e.Message, false);
|
||||
MessageBox.Show("An error occured while trying to merge the video chunks.\nCheck the log for details.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task MergeChunks(string vfrFile, string outPath)
|
||||
{
|
||||
await FFmpegCommands.ConcatVideos(vfrFile, outPath, -1);
|
||||
await MergeAudio(i.current.inPath, outPath);
|
||||
|
||||
int looptimes = GetLoopTimes();
|
||||
if (looptimes > 0)
|
||||
await Loop(outPath, looptimes);
|
||||
}
|
||||
|
||||
public static async Task EncodeChunk(string outPath, i.OutMode mode, int firstFrameNum, int framesAmount)
|
||||
{
|
||||
string vfrFileOriginal = Path.Combine(i.current.tempFolder, $"vfr-{i.current.interpFactor}x.ini");
|
||||
string vfrFile = Path.Combine(i.current.tempFolder, $"vfr-chunk-{firstFrameNum}-{firstFrameNum + framesAmount}.ini");
|
||||
File.WriteAllLines(vfrFile, IOUtils.ReadLines(vfrFileOriginal).Skip(firstFrameNum).Take(framesAmount));
|
||||
|
||||
float maxFps = Config.GetFloat("maxFps");
|
||||
bool fpsLimit = maxFps != 0 && i.current.outFps > maxFps;
|
||||
|
||||
bool dontEncodeFullFpsVid = fpsLimit && Config.GetInt("maxFpsMode") == 0;
|
||||
|
||||
if(!dontEncodeFullFpsVid)
|
||||
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true); // Encode
|
||||
|
||||
if (fpsLimit)
|
||||
{
|
||||
string filename = Path.GetFileName(outPath);
|
||||
string newParentDir = outPath.GetParentDir() + "-" + maxFps.ToStringDot("0.00") + "fps";
|
||||
outPath = Path.Combine(newParentDir, filename);
|
||||
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, i.current.outFps, maxFps, AvProcess.LogMode.Hidden, true); // Encode with limited fps
|
||||
}
|
||||
}
|
||||
|
||||
static async Task Loop(string outPath, int looptimes)
|
||||
{
|
||||
Logger.Log($"Looping {looptimes} {(looptimes == 1 ? "time" : "times")} to reach target length of {Config.GetInt("minOutVidLength")}s...");
|
||||
await FFmpegCommands.LoopVideo(outPath, looptimes, Config.GetInt("loopMode") == 0);
|
||||
}
|
||||
|
||||
static int GetLoopTimes()
|
||||
{
|
||||
int times = -1;
|
||||
int minLength = Config.GetInt("minOutVidLength");
|
||||
int minFrameCount = (minLength * i.current.outFps).RoundToInt();
|
||||
int outFrames = i.currentInputFrameCount * i.current.interpFactor;
|
||||
if (outFrames / i.current.outFps < minLength)
|
||||
times = (int)Math.Ceiling((double)minFrameCount / (double)outFrames);
|
||||
times--; // Not counting the 1st play (0 loops)
|
||||
if (times <= 0) return -1; // Never try to loop 0 times, idk what would happen, probably nothing
|
||||
return times;
|
||||
}
|
||||
|
||||
public static async Task MergeAudio(string inputPath, string outVideo, int looptimes = -1)
|
||||
{
|
||||
if (!Config.GetBool("keepAudio")) return;
|
||||
try
|
||||
{
|
||||
string audioFileBasePath = Path.Combine(i.current.tempFolder, "audio");
|
||||
|
||||
if (inputPath != null && IOUtils.IsPathDirectory(inputPath) && !File.Exists(IOUtils.GetAudioFile(audioFileBasePath))) // Try loading out of same folder as input if input is a folder
|
||||
audioFileBasePath = Path.Combine(i.current.tempFolder.GetParentDir(), "audio");
|
||||
|
||||
if (!File.Exists(IOUtils.GetAudioFile(audioFileBasePath)))
|
||||
await FFmpegCommands.ExtractAudio(inputPath, audioFileBasePath); // Extract from sourceVideo to audioFile unless it already exists
|
||||
|
||||
if (!File.Exists(IOUtils.GetAudioFile(audioFileBasePath)) || new FileInfo(IOUtils.GetAudioFile(audioFileBasePath)).Length < 4096)
|
||||
{
|
||||
Logger.Log("No compatible audio stream found.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await FFmpegCommands.MergeAudioAndSubs(outVideo, IOUtils.GetAudioFile(audioFileBasePath), i.current.tempFolder); // Merge from audioFile into outVideo
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("Failed to copy audio!");
|
||||
Logger.Log("MergeAudio() Exception: " + e.Message, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
Code5/Main/FrameOrder.cs
Normal file
160
Code5/Main/FrameOrder.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Flowframes.Data;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
class FrameOrder
|
||||
{
|
||||
public enum Mode { CFR, VFR }
|
||||
public static int timebase = 10000;
|
||||
|
||||
public static async Task CreateFrameOrderFile(string framesPath, bool loopEnabled, int times)
|
||||
{
|
||||
Logger.Log("Generating frame order information...");
|
||||
try
|
||||
{
|
||||
await CreateEncFile(framesPath, loopEnabled, times, false);
|
||||
Logger.Log($"Generating frame order information... Done.", false, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Error generating frame order information: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, int> dupesDict = new Dictionary<string, int>();
|
||||
|
||||
static void LoadDupesFile (string path)
|
||||
{
|
||||
dupesDict.Clear();
|
||||
if (!File.Exists(path)) return;
|
||||
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 frame order information 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();
|
||||
|
||||
bool debug = Config.GetBool("frameOrderDebug", false);
|
||||
|
||||
int totalFileCount = 0;
|
||||
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;
|
||||
|
||||
if(debug) 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
|
||||
|
||||
// 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;
|
||||
|
||||
if (debug) 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++)
|
||||
{
|
||||
//if (debug) Logger.Log($"Writing out frame {frm+1}/{interpFramesAmount}", true);
|
||||
|
||||
if (discardThisFrame) // If frame is scene cut frame
|
||||
{
|
||||
//if (debug) Logger.Log($"Writing frame {totalFileCount} [Discarding Next]", true);
|
||||
totalFileCount++;
|
||||
int lastNum = totalFileCount;
|
||||
fileContent = WriteFrameWithDupes(dupesAmount, fileContent, totalFileCount, interpPath, ext, debug, $"[In: {inputFilenameNoExt}] [{((frm == 0) ? " Source " : $"Interp {frm}")}] [DiscardNext]");
|
||||
|
||||
//if (debug) Logger.Log("Discarding interp frames with out num " + totalFileCount);
|
||||
for (int dupeCount = 1; dupeCount < interpFramesAmount; dupeCount++)
|
||||
{
|
||||
totalFileCount++;
|
||||
if (debug) Logger.Log($"Writing frame {totalFileCount} which is actually repeated frame {lastNum}", true);
|
||||
fileContent = WriteFrameWithDupes(dupesAmount, fileContent, lastNum, interpPath, ext, debug, $"[In: {inputFilenameNoExt}] [DISCARDED]");
|
||||
}
|
||||
|
||||
frm = interpFramesAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalFileCount++;
|
||||
fileContent = WriteFrameWithDupes(dupesAmount, fileContent, totalFileCount, interpPath, ext, debug, $"[In: {inputFilenameNoExt}] [{((frm == 0) ? " Source " : $"Interp {frm}")}]");
|
||||
}
|
||||
}
|
||||
|
||||
if ((i + 1) % 100 == 0)
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
// if(debug) Logger.Log("target: " + ((frameFiles.Length * interpFactor) - (interpFactor - 1)), true);
|
||||
// if(debug) Logger.Log("totalFileCount: " + totalFileCount, true);
|
||||
|
||||
totalFileCount++;
|
||||
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||
|
||||
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))
|
||||
{
|
||||
if (debug) Logger.Log($"Won't copy loop frame - {Path.GetFileName(loopFrameTargetPath)} already exists.", true);
|
||||
return;
|
||||
}
|
||||
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
||||
if (debug) Logger.Log($"Copied loop frame to {loopFrameTargetPath}.", true);
|
||||
}
|
||||
}
|
||||
|
||||
static string WriteFrameWithDupes (int dupesAmount, string fileContent, int frameNum, string interpPath, string ext, bool debug, string note = "")
|
||||
{
|
||||
for (int writtenDupes = -1; writtenDupes < dupesAmount; writtenDupes++) // Write duplicates
|
||||
{
|
||||
if (debug) Logger.Log($"Writing frame {frameNum} (writtenDupes {writtenDupes})", true, false);
|
||||
fileContent += $"file '{interpPath}/{frameNum.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'{(debug ? ($" # Dupe {(writtenDupes+1).ToString("000")} {note}").Replace("Dupe 000", " ") : "" )}\n";
|
||||
}
|
||||
return fileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
216
Code5/Main/Interpolate.cs
Normal file
216
Code5/Main/Interpolate.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using Flowframes;
|
||||
using Flowframes.Data;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.Magick;
|
||||
using Flowframes.Main;
|
||||
using Flowframes.MiscUtils;
|
||||
using Flowframes.OS;
|
||||
using Flowframes.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Padding = Flowframes.Data.Padding;
|
||||
using Utils = Flowframes.Main.InterpolateUtils;
|
||||
|
||||
namespace Flowframes
|
||||
{
|
||||
public class Interpolate
|
||||
{
|
||||
public enum OutMode { VidMp4, VidMkv, VidWebm, VidProRes, VidAvi, VidGif, ImgPng }
|
||||
|
||||
public static int currentInputFrameCount;
|
||||
public static bool currentlyUsingAutoEnc;
|
||||
|
||||
public static InterpSettings current;
|
||||
|
||||
public static bool canceled = false;
|
||||
|
||||
static Stopwatch sw = new Stopwatch();
|
||||
|
||||
public static async Task Start()
|
||||
{
|
||||
canceled = false;
|
||||
if (!Utils.InputIsValid(current.inPath, current.outPath, current.outFps, current.interpFactor, current.outMode)) return; // General input checks
|
||||
if (!Utils.CheckAiAvailable(current.ai)) return; // Check if selected AI pkg is installed
|
||||
if (!Utils.CheckDeleteOldTempFolder()) return; // Try to delete temp folder if an old one exists
|
||||
if(!Utils.CheckPathValid(current.inPath)) return; // Check if input path/file is valid
|
||||
Utils.PathAsciiCheck(current.inPath, current.outPath);
|
||||
currentInputFrameCount = await Utils.GetInputFrameCountAsync(current.inPath);
|
||||
Program.mainForm.SetStatus("Starting...");
|
||||
Program.mainForm.SetWorking(true);
|
||||
await GetFrames();
|
||||
if (canceled) return;
|
||||
sw.Restart();
|
||||
await PostProcessFrames();
|
||||
if (canceled) return;
|
||||
await RunAi(current.interpFolder, current.ai);
|
||||
if (canceled) return;
|
||||
Program.mainForm.SetProgress(100);
|
||||
if(!currentlyUsingAutoEnc)
|
||||
await CreateVideo.Export(current.interpFolder, current.outFilename, current.outMode);
|
||||
IOUtils.ReverseRenaming(AiProcess.filenameMap, true); // Get timestamps back
|
||||
Cleanup(current.interpFolder);
|
||||
Program.mainForm.SetWorking(false);
|
||||
Logger.Log("Total processing time: " + FormatUtils.Time(sw.Elapsed));
|
||||
sw.Stop();
|
||||
Program.mainForm.SetStatus("Done interpolating!");
|
||||
}
|
||||
|
||||
public static async Task GetFrames ()
|
||||
{
|
||||
if (!current.inputIsFrames) // Input is video - extract frames first
|
||||
{
|
||||
current.alpha = (Config.GetBool("enableAlpha", false) && (Path.GetExtension(current.inPath).ToLower() == ".gif"));
|
||||
await ExtractFrames(current.inPath, current.framesFolder, current.alpha);
|
||||
}
|
||||
else
|
||||
{
|
||||
current.alpha = (Config.GetBool("enableAlpha", false) && Path.GetExtension(IOUtils.GetFilesSorted(current.inPath).First()).ToLower() == ".gif");
|
||||
await FFmpegCommands.ImportImages(current.inPath, current.framesFolder, current.alpha, await Utils.GetOutputResolution(current.inPath, true));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExtractFrames(string inPath, string outPath, bool alpha, bool allowSceneDetect = true, bool extractAudio = true)
|
||||
{
|
||||
if (Config.GetBool("scnDetect"))
|
||||
{
|
||||
Program.mainForm.SetStatus("Extracting scenes from video...");
|
||||
await FFmpegCommands.ExtractSceneChanges(inPath, Path.Combine(current.tempFolder, Paths.scenesDir), current.inFps);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
Program.mainForm.SetStatus("Extracting frames from video...");
|
||||
bool mpdecimate = Config.GetInt("dedupMode") == 2;
|
||||
await FFmpegCommands.VideoToFrames(inPath, outPath, alpha, current.inFps, mpdecimate, false, await Utils.GetOutputResolution(inPath, true), false);
|
||||
|
||||
if (mpdecimate)
|
||||
{
|
||||
int framesLeft = IOUtils.GetAmountOfFiles(outPath, false, $"*.png");
|
||||
int framesDeleted = currentInputFrameCount - framesLeft;
|
||||
float percentDeleted = ((float)framesDeleted / currentInputFrameCount) * 100f;
|
||||
string keptPercent = $"{(100f - percentDeleted).ToString("0.0")}%";
|
||||
Logger.Log($"[Deduplication] Kept {framesLeft} ({keptPercent}) frames, deleted {framesDeleted} frames.");
|
||||
}
|
||||
|
||||
if(!Config.GetBool("allowConsecutiveSceneChanges", true))
|
||||
Utils.FixConsecutiveSceneFrames(Path.Combine(current.tempFolder, Paths.scenesDir), current.framesFolder);
|
||||
|
||||
if (extractAudio)
|
||||
{
|
||||
string audioFile = Path.Combine(current.tempFolder, "audio");
|
||||
if (audioFile != null && !File.Exists(audioFile))
|
||||
await FFmpegCommands.ExtractAudio(inPath, audioFile);
|
||||
}
|
||||
|
||||
await FFmpegCommands.ExtractSubtitles(inPath, current.tempFolder, current.outMode);
|
||||
}
|
||||
|
||||
public static async Task PostProcessFrames (bool sbsMode = false)
|
||||
{
|
||||
if (canceled) return;
|
||||
|
||||
int extractedFrames = IOUtils.GetAmountOfFiles(current.framesFolder, false, "*.png");
|
||||
if (!Directory.Exists(current.framesFolder) || currentInputFrameCount <= 0 || extractedFrames < 2)
|
||||
{
|
||||
if(extractedFrames == 1)
|
||||
Cancel("Only a single frame was extracted from your input file!\n\nPossibly your input is an image, not a video?");
|
||||
else
|
||||
Cancel("Frame extraction failed!\n\nYour input file might be incompatible.");
|
||||
}
|
||||
|
||||
if (Config.GetInt("dedupMode") == 1)
|
||||
await Dedupe.Run(current.framesFolder);
|
||||
else
|
||||
Dedupe.ClearCache();
|
||||
|
||||
if(!Config.GetBool("enableLoop"))
|
||||
await Utils.CopyLastFrame(currentInputFrameCount);
|
||||
|
||||
if (Config.GetInt("dedupMode") > 0)
|
||||
await Dedupe.CreateDupesFile(current.framesFolder, currentInputFrameCount);
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
if(current.alpha)
|
||||
await Converter.ExtractAlpha(current.framesFolder, current.framesFolder + "-a");
|
||||
|
||||
await FrameOrder.CreateFrameOrderFile(current.framesFolder, Config.GetBool("enableLoop"), current.interpFactor);
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
AiProcess.filenameMap = IOUtils.RenameCounterDirReversible(current.framesFolder, "png", 1, 8);
|
||||
}
|
||||
|
||||
public static async Task RunAi(string outpath, AI ai, bool stepByStep = false)
|
||||
{
|
||||
Program.mainForm.SetStatus("Downloading models...");
|
||||
await ModelDownloader.DownloadModelFiles(Path.GetFileNameWithoutExtension(ai.pkg.fileName), current.model);
|
||||
if (canceled) return;
|
||||
|
||||
currentlyUsingAutoEnc = Utils.CanUseAutoEnc(stepByStep, current);
|
||||
|
||||
IOUtils.CreateDir(outpath);
|
||||
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
if (ai.aiName == Networks.rifeCuda.aiName)
|
||||
tasks.Add(AiProcess.RunRifeCuda(current.framesFolder, current.interpFactor, current.model));
|
||||
|
||||
if (ai.aiName == Networks.rifeNcnn.aiName)
|
||||
tasks.Add(AiProcess.RunRifeNcnn(current.framesFolder, outpath, current.interpFactor, current.model));
|
||||
|
||||
if (ai.aiName == Networks.dainNcnn.aiName)
|
||||
tasks.Add(AiProcess.RunDainNcnn(current.framesFolder, outpath, current.interpFactor, current.model, Config.GetInt("dainNcnnTilesize", 512)));
|
||||
|
||||
if (currentlyUsingAutoEnc)
|
||||
{
|
||||
Logger.Log($"{Logger.GetLastLine()} (Using Auto-Encode)", true);
|
||||
tasks.Add(AutoEncode.MainLoop(outpath));
|
||||
}
|
||||
|
||||
Program.mainForm.SetStatus("Running AI...");
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static void Cancel(string reason = "", bool noMsgBox = false)
|
||||
{
|
||||
if (AiProcess.currentAiProcess != null && !AiProcess.currentAiProcess.HasExited)
|
||||
OSUtils.KillProcessTree(AiProcess.currentAiProcess.Id);
|
||||
if (AvProcess.lastProcess != null && !AvProcess.lastProcess.HasExited)
|
||||
OSUtils.KillProcessTree(AvProcess.lastProcess.Id);
|
||||
canceled = true;
|
||||
Program.mainForm.SetStatus("Canceled.");
|
||||
Program.mainForm.SetProgress(0);
|
||||
if (Config.GetInt("processingMode") == 0 && !Config.GetBool("keepTempFolder"))
|
||||
IOUtils.TryDeleteIfExists(current.tempFolder);
|
||||
AutoEncode.busy = false;
|
||||
Program.mainForm.SetWorking(false);
|
||||
Program.mainForm.SetTab("interpolation");
|
||||
if(!Logger.GetLastLine().Contains("Canceled interpolation."))
|
||||
Logger.Log("Canceled interpolation.");
|
||||
if (!string.IsNullOrWhiteSpace(reason) && !noMsgBox)
|
||||
Utils.ShowMessage($"Canceled:\n\n{reason}");
|
||||
}
|
||||
|
||||
public static void Cleanup(string interpFramesDir, bool ignoreKeepSetting = false)
|
||||
{
|
||||
if (!ignoreKeepSetting && Config.GetBool("keepTempFolder")) return;
|
||||
Logger.Log("Deleting temporary files...");
|
||||
try
|
||||
{
|
||||
if (Config.GetBool("keepFrames"))
|
||||
IOUtils.Copy(interpFramesDir, Path.Combine(current.tempFolder.GetParentDir(), Path.GetFileName(current.tempFolder).Replace("-temp", "-interpframes")));
|
||||
Directory.Delete(current.tempFolder, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("Cleanup Error: " + e.Message, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
Code5/Main/InterpolateSteps.cs
Normal file
142
Code5/Main/InterpolateSteps.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using Flowframes.AudioVideo;
|
||||
using Flowframes.Data;
|
||||
using Flowframes.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
using static Interpolate;
|
||||
|
||||
class InterpolateSteps
|
||||
{
|
||||
public enum Step { ExtractScnChanges, ExtractFrames, Interpolate, CreateVid, Reset }
|
||||
|
||||
//public static string current.inPath;
|
||||
//public static string currentOutPath;
|
||||
//public static string current.interpFolder;
|
||||
//public static AI currentAi;
|
||||
//public static OutMode currentOutMode;
|
||||
|
||||
public static async Task Run(string step)
|
||||
{
|
||||
Logger.Log($"[SBS] Running step '{step}'", true);
|
||||
canceled = false;
|
||||
Program.mainForm.SetWorking(true);
|
||||
current = Program.mainForm.GetCurrentSettings();
|
||||
|
||||
if (!InterpolateUtils.InputIsValid(current.inPath, current.outPath, current.outFps, current.interpFactor, current.outMode)) return; // General input checks
|
||||
|
||||
if (step.Contains("Extract Scene Changes"))
|
||||
{
|
||||
if (!current.inputIsFrames) // Input is video - extract frames first
|
||||
await ExtractSceneChanges();
|
||||
else
|
||||
InterpolateUtils.ShowMessage("Scene changes can only be extracted from videos, not frames!", "Error");
|
||||
}
|
||||
|
||||
if (step.Contains("Extract Frames"))
|
||||
{
|
||||
await GetFrames();
|
||||
}
|
||||
|
||||
if (step.Contains("Run Interpolation"))
|
||||
await DoInterpolate();
|
||||
|
||||
if (step.Contains("Export"))
|
||||
await CreateOutputVid();
|
||||
|
||||
if (step.Contains("Cleanup"))
|
||||
await Reset();
|
||||
|
||||
Program.mainForm.SetWorking(false);
|
||||
Logger.Log("Done running this step.");
|
||||
}
|
||||
|
||||
public static async Task ExtractSceneChanges()
|
||||
{
|
||||
string scenesPath = Path.Combine(current.tempFolder, Paths.scenesDir);
|
||||
if (!IOUtils.TryDeleteIfExists(scenesPath))
|
||||
{
|
||||
InterpolateUtils.ShowMessage("Failed to delete existing scenes folder - Make sure no file is opened in another program!", "Error");
|
||||
return;
|
||||
}
|
||||
Program.mainForm.SetStatus("Extracting scenes from video...");
|
||||
await FFmpegCommands.ExtractSceneChanges(current.inPath, scenesPath, current.inFps);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
public static async Task ExtractVideoFrames()
|
||||
{
|
||||
if (!IOUtils.TryDeleteIfExists(current.framesFolder))
|
||||
{
|
||||
InterpolateUtils.ShowMessage("Failed to delete existing frames folder - Make sure no file is opened in another program!", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
currentInputFrameCount = await InterpolateUtils.GetInputFrameCountAsync(current.inPath);
|
||||
AiProcess.filenameMap.Clear();
|
||||
|
||||
await ExtractFrames(current.inPath, current.framesFolder, false, true);
|
||||
}
|
||||
|
||||
public static async Task DoInterpolate()
|
||||
{
|
||||
current.framesFolder = Path.Combine(current.tempFolder, Paths.framesDir);
|
||||
if (!Directory.Exists(current.framesFolder) || IOUtils.GetAmountOfFiles(current.framesFolder, false, "*.png") < 2)
|
||||
{
|
||||
InterpolateUtils.ShowMessage("There are no extracted frames that can be interpolated!\nDid you run the extraction step?", "Error");
|
||||
return;
|
||||
}
|
||||
if (!IOUtils.TryDeleteIfExists(current.interpFolder))
|
||||
{
|
||||
InterpolateUtils.ShowMessage("Failed to delete existing frames folder - Make sure no file is opened in another program!", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
currentInputFrameCount = await InterpolateUtils.GetInputFrameCountAsync(current.inPath);
|
||||
|
||||
foreach (string ini in Directory.GetFiles(current.tempFolder, "*.ini", SearchOption.TopDirectoryOnly))
|
||||
IOUtils.TryDeleteIfExists(ini);
|
||||
|
||||
IOUtils.ReverseRenaming(AiProcess.filenameMap, true); // Get timestamps back
|
||||
|
||||
// TODO: Check if this works lol, remove if it does
|
||||
//if (Config.GetBool("sbsAllowAutoEnc"))
|
||||
// nextOutPath = Path.Combine(currentOutPath, Path.GetFileNameWithoutExtension(current.inPath) + IOUtils.GetAiSuffix(current.ai, current.interpFactor) + InterpolateUtils.GetExt(current.outMode));
|
||||
|
||||
await PostProcessFrames(true);
|
||||
|
||||
int frames = IOUtils.GetAmountOfFiles(current.framesFolder, false, "*.png");
|
||||
int targetFrameCount = frames * current.interpFactor;
|
||||
if (canceled) return;
|
||||
Program.mainForm.SetStatus("Running AI...");
|
||||
await RunAi(current.interpFolder, current.ai, true);
|
||||
Program.mainForm.SetProgress(0);
|
||||
}
|
||||
|
||||
public static async Task CreateOutputVid()
|
||||
{
|
||||
string[] outFrames = IOUtils.GetFilesSorted(current.interpFolder, $"*.{InterpolateUtils.GetOutExt()}");
|
||||
if (outFrames.Length > 0 && !IOUtils.CheckImageValid(outFrames[0]))
|
||||
{
|
||||
InterpolateUtils.ShowMessage("Invalid frame files detected!\n\nIf you used Auto-Encode, this is normal, and you don't need to run " +
|
||||
"this step as the video was already created in the \"Interpolate\" step.", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(current.outPath, Path.GetFileNameWithoutExtension(current.inPath) + IOUtils.GetCurrentExportSuffix() + FFmpegUtils.GetExt(current.outMode));
|
||||
await CreateVideo.Export(current.interpFolder, outPath, current.outMode);
|
||||
}
|
||||
|
||||
public static async Task Reset()
|
||||
{
|
||||
Cleanup(current.interpFolder, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
444
Code5/Main/InterpolateUtils.cs
Normal file
444
Code5/Main/InterpolateUtils.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
using Flowframes.Data;
|
||||
using Flowframes.Forms;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.MiscUtils;
|
||||
using Flowframes.OS;
|
||||
using Flowframes.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using i = Flowframes.Interpolate;
|
||||
using Padding = Flowframes.Data.Padding;
|
||||
|
||||
namespace Flowframes.Main
|
||||
{
|
||||
class InterpolateUtils
|
||||
{
|
||||
public static PictureBox preview;
|
||||
public static BigPreviewForm bigPreviewForm;
|
||||
|
||||
public static async Task CopyLastFrame (int lastFrameNum)
|
||||
{
|
||||
try
|
||||
{
|
||||
lastFrameNum--; // We have to do this as extracted frames start at 0, not 1
|
||||
bool frameFolderInput = IOUtils.IsPathDirectory(i.current.inPath);
|
||||
string targetPath = Path.Combine(i.current.framesFolder, lastFrameNum.ToString().PadLeft(Padding.inputFrames, '0') + ".png");
|
||||
if (File.Exists(targetPath)) return;
|
||||
|
||||
Size res = IOUtils.GetImage(IOUtils.GetFilesSorted(i.current.framesFolder, false).First()).Size;
|
||||
|
||||
if (frameFolderInput)
|
||||
{
|
||||
string lastFramePath = IOUtils.GetFilesSorted(i.current.inPath, false).Last();
|
||||
await FFmpegCommands.ExtractLastFrame(lastFramePath, targetPath, res);
|
||||
}
|
||||
else
|
||||
{
|
||||
await FFmpegCommands.ExtractLastFrame(i.current.inPath, targetPath, res);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("CopyLastFrame Error: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetOutExt (bool withDot = false)
|
||||
{
|
||||
string dotStr = withDot ? "." : "";
|
||||
if (Config.GetBool("jpegInterp"))
|
||||
return dotStr + "jpg";
|
||||
return dotStr + "png";
|
||||
}
|
||||
|
||||
public static int targetFrames;
|
||||
public static string currentOutdir;
|
||||
public static int currentFactor;
|
||||
public static bool progressPaused = false;
|
||||
public static bool progCheckRunning = false;
|
||||
public static async void GetProgressByFrameAmount(string outdir, int target)
|
||||
{
|
||||
progCheckRunning = true;
|
||||
targetFrames = target;
|
||||
currentOutdir = outdir;
|
||||
Logger.Log($"Starting GetProgressByFrameAmount() loop for outdir '{currentOutdir}', target is {target} frames", true);
|
||||
bool firstProgUpd = true;
|
||||
Program.mainForm.SetProgress(0);
|
||||
while (Program.busy)
|
||||
{
|
||||
if (!progressPaused && AiProcess.processTime.IsRunning && Directory.Exists(currentOutdir))
|
||||
{
|
||||
if (firstProgUpd && Program.mainForm.IsInFocus())
|
||||
Program.mainForm.SetTab("preview");
|
||||
firstProgUpd = false;
|
||||
string[] frames = IOUtils.GetFilesSorted(currentOutdir, $"*.{GetOutExt()}");
|
||||
if (frames.Length > 1)
|
||||
UpdateInterpProgress(frames.Length, targetFrames, frames[frames.Length - 1]);
|
||||
if (frames.Length >= targetFrames)
|
||||
break;
|
||||
await Task.Delay(GetProgressWaitTime(frames.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
progCheckRunning = false;
|
||||
if (i.canceled)
|
||||
Program.mainForm.SetProgress(0);
|
||||
}
|
||||
|
||||
public static void UpdateInterpProgress(int frames, int target, string latestFramePath = "")
|
||||
{
|
||||
if (i.canceled) return;
|
||||
|
||||
frames = frames.Clamp(0, target);
|
||||
int percent = (int)Math.Round(((float)frames / target) * 100f);
|
||||
Program.mainForm.SetProgress(percent);
|
||||
|
||||
float generousTime = ((AiProcess.processTime.ElapsedMilliseconds - AiProcess.lastStartupTimeMs) / 1000f);
|
||||
float fps = (float)frames / generousTime;
|
||||
string fpsIn = (fps / currentFactor).ToString("0.00");
|
||||
string fpsOut = fps.ToString("0.00");
|
||||
|
||||
float secondsPerFrame = generousTime / (float)frames;
|
||||
int framesLeft = target - frames;
|
||||
float eta = framesLeft * secondsPerFrame;
|
||||
string etaStr = FormatUtils.Time(new TimeSpan(0, 0, eta.RoundToInt()), false);
|
||||
|
||||
bool replaceLine = Regex.Split(Logger.textbox.Text, "\r\n|\r|\n").Last().Contains("Average Speed: ");
|
||||
|
||||
string logStr = $"Interpolated {frames}/{target} frames ({percent}%) - Average Speed: {fpsIn} FPS In / {fpsOut} FPS Out - ";
|
||||
logStr += $"Time: {FormatUtils.Time(AiProcess.processTime.Elapsed)} - ETA: {etaStr}";
|
||||
if (AutoEncode.busy) logStr += " - Encoding...";
|
||||
Logger.Log(logStr, false, replaceLine);
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(latestFramePath) && frames > currentFactor)
|
||||
{
|
||||
if (bigPreviewForm == null && !preview.Visible /* ||Program.mainForm.WindowState != FormWindowState.Minimized */ /* || !Program.mainForm.IsInFocus()*/) return; // Skip if the preview is not visible or the form is not in focus
|
||||
Image img = IOUtils.GetImage(latestFramePath);
|
||||
SetPreviewImg(img);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void SetPreviewImg (Image img)
|
||||
{
|
||||
if (img == null)
|
||||
return;
|
||||
preview.Image = img;
|
||||
if (bigPreviewForm != null)
|
||||
bigPreviewForm.SetImage(img);
|
||||
}
|
||||
|
||||
public static Dictionary<string, int> frameCountCache = new Dictionary<string, int>();
|
||||
public static async Task<int> GetInputFrameCountAsync (string path)
|
||||
{
|
||||
string hash = await IOUtils.GetHashAsync(path, IOUtils.Hash.xxHash); // Get checksum for caching
|
||||
if (hash.Length > 1 && frameCountCache.ContainsKey(hash))
|
||||
{
|
||||
Logger.Log($"FrameCountCache contains this hash ({hash}), using cached frame count.", true);
|
||||
return frameCountCache[hash];
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log($"Hash ({hash}) not cached, reading frame count.", true);
|
||||
}
|
||||
|
||||
int frameCount = 0;
|
||||
if (IOUtils.IsPathDirectory(path))
|
||||
frameCount = IOUtils.GetAmountOfFiles(path, false);
|
||||
else
|
||||
frameCount = await FFmpegCommands.GetFrameCountAsync(path);
|
||||
|
||||
if (hash.Length > 1 && frameCount > 5000) // Cache if >5k frames to avoid re-reading it every single time
|
||||
{
|
||||
Logger.Log($"Adding hash ({hash}) with frame count {frameCount} to cache.", true);
|
||||
frameCountCache[hash] = frameCount; // Use CRC32 instead of path to avoid using cached value if file was changed
|
||||
}
|
||||
return frameCount;
|
||||
}
|
||||
|
||||
public static int GetProgressWaitTime(int numFrames)
|
||||
{
|
||||
float hddMultiplier = !Program.lastInputPathIsSsd ? 2f : 1f;
|
||||
|
||||
int waitMs = 200;
|
||||
|
||||
if (numFrames > 100)
|
||||
waitMs = 500;
|
||||
|
||||
if (numFrames > 1000)
|
||||
waitMs = 1000;
|
||||
|
||||
if (numFrames > 2500)
|
||||
waitMs = 1500;
|
||||
|
||||
if (numFrames > 5000)
|
||||
waitMs = 2500;
|
||||
|
||||
return (waitMs * hddMultiplier).RoundToInt();
|
||||
}
|
||||
|
||||
public static string GetTempFolderLoc (string inPath, string outPath)
|
||||
{
|
||||
string basePath = inPath.GetParentDir();
|
||||
|
||||
if(Config.GetInt("tempFolderLoc") == 1)
|
||||
basePath = outPath.GetParentDir();
|
||||
|
||||
if (Config.GetInt("tempFolderLoc") == 2)
|
||||
basePath = outPath;
|
||||
|
||||
if (Config.GetInt("tempFolderLoc") == 3)
|
||||
basePath = IOUtils.GetExeDir();
|
||||
|
||||
if (Config.GetInt("tempFolderLoc") == 4)
|
||||
{
|
||||
string custPath = Config.Get("tempDirCustom");
|
||||
if(IOUtils.IsDirValid(custPath))
|
||||
basePath = custPath;
|
||||
}
|
||||
|
||||
return Path.Combine(basePath, Path.GetFileNameWithoutExtension(inPath).StripBadChars().Remove(" ").Trunc(30, false) + "-temp");
|
||||
}
|
||||
|
||||
public static bool InputIsValid(string inDir, string outDir, float fpsOut, int interp, Interpolate.OutMode outMode)
|
||||
{
|
||||
bool passes = true;
|
||||
|
||||
bool isFile = !IOUtils.IsPathDirectory(inDir);
|
||||
|
||||
if ((passes && isFile && !IOUtils.IsFileValid(inDir)) || (!isFile && !IOUtils.IsDirValid(inDir)))
|
||||
{
|
||||
ShowMessage("Input path is not valid!");
|
||||
passes = false;
|
||||
}
|
||||
if (passes && !IOUtils.IsDirValid(outDir))
|
||||
{
|
||||
ShowMessage("Output path is not valid!");
|
||||
passes = false;
|
||||
}
|
||||
if (passes && interp != 2 && interp != 4 && interp != 8)
|
||||
{
|
||||
ShowMessage("Interpolation factor is not valid!");
|
||||
passes = false;
|
||||
}
|
||||
if (passes && outMode == i.OutMode.VidGif && fpsOut > 50)
|
||||
{
|
||||
ShowMessage("Invalid output frame rate!\nGIF does not properly support frame rates above 40 FPS.\nPlease use MP4, WEBM or another video format.");
|
||||
passes = false;
|
||||
}
|
||||
if (passes && fpsOut < 1 || fpsOut > 500)
|
||||
{
|
||||
ShowMessage("Invalid output frame rate - Must be 1-500.");
|
||||
passes = false;
|
||||
}
|
||||
if (!passes)
|
||||
i.Cancel("Invalid settings detected.", true);
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static void PathAsciiCheck (string inpath, string outpath)
|
||||
{
|
||||
bool shownMsg = false;
|
||||
|
||||
if (OSUtils.HasNonAsciiChars(inpath))
|
||||
{
|
||||
ShowMessage("Warning: Input path includes non-ASCII characters. This might cause problems.");
|
||||
shownMsg = true;
|
||||
}
|
||||
|
||||
if (!shownMsg && OSUtils.HasNonAsciiChars(outpath))
|
||||
ShowMessage("Warning: Output path includes non-ASCII characters. This might cause problems.");
|
||||
}
|
||||
|
||||
public static void GifCompatCheck (Interpolate.OutMode outMode, float fpsOut, int targetFrameCount)
|
||||
{
|
||||
if (outMode != Interpolate.OutMode.VidGif)
|
||||
return;
|
||||
|
||||
if(fpsOut >= 50f)
|
||||
Logger.Log("Warning: GIFs above 50 FPS might play slower on certain software/hardware! MP4 is recommended for higher frame rates.");
|
||||
|
||||
int maxGifFrames = 200;
|
||||
if (targetFrameCount > maxGifFrames)
|
||||
{
|
||||
ShowMessage($"You can't use GIF with more than {maxGifFrames} output frames!\nPlease use MP4 for this.", "Error");
|
||||
i.Cancel($"Can't use GIF encoding with more than {maxGifFrames} frames!");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CheckAiAvailable (AI ai)
|
||||
{
|
||||
if (!PkgUtils.IsAiAvailable(ai))
|
||||
{
|
||||
ShowMessage("The selected AI is not installed!\nYou can download it from the Package Installer.", "Error");
|
||||
i.Cancel("Selected AI not available.", true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CheckDeleteOldTempFolder ()
|
||||
{
|
||||
if (!IOUtils.TryDeleteIfExists(i.current.tempFolder))
|
||||
{
|
||||
ShowMessage("Failed to remove an existing temp folder of this video!\nMake sure you didn't open any frames in an editor.", "Error");
|
||||
i.Cancel();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CheckPathValid (string path)
|
||||
{
|
||||
if (IOUtils.IsPathDirectory(path))
|
||||
{
|
||||
if (!IOUtils.IsDirValid(path))
|
||||
{
|
||||
ShowMessage("Input directory is not valid.");
|
||||
i.Cancel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsVideoValid(path))
|
||||
{
|
||||
ShowMessage("Input video file is not valid.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsVideoValid(string videoPath)
|
||||
{
|
||||
if (videoPath == null || !IOUtils.IsFileValid(videoPath))
|
||||
return false;
|
||||
// string ext = Path.GetExtension(videoPath).ToLower();
|
||||
// if (!Formats.supported.Contains(ext))
|
||||
// return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ShowMessage(string msg, string title = "Message")
|
||||
{
|
||||
if (!BatchProcessing.busy)
|
||||
MessageBox.Show(msg, title);
|
||||
Logger.Log("Message: " + msg, true);
|
||||
}
|
||||
|
||||
public static async Task<Size> GetOutputResolution (string inputPath, bool print)
|
||||
{
|
||||
Size resolution = await IOUtils.GetVideoOrFramesRes(inputPath);
|
||||
return GetOutputResolution(resolution, print);
|
||||
}
|
||||
|
||||
public static Size GetOutputResolution(Size inputRes, bool print = false)
|
||||
{
|
||||
int maxHeight = RoundDiv2(Config.GetInt("maxVidHeight"));
|
||||
if (inputRes.Height > maxHeight)
|
||||
{
|
||||
float factor = (float)maxHeight / inputRes.Height;
|
||||
Logger.Log($"Un-rounded downscaled size: {(inputRes.Width * factor).ToString("0.00")}x{Config.GetInt("maxVidHeight")}", true);
|
||||
int width = RoundDiv2((inputRes.Width * factor).RoundToInt());
|
||||
if (print)
|
||||
Logger.Log($"Video is bigger than the maximum - Downscaling to {width}x{maxHeight}.");
|
||||
return new Size(width, maxHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Size(RoundDiv2(inputRes.Width), RoundDiv2(inputRes.Height));
|
||||
}
|
||||
}
|
||||
|
||||
public static int RoundDiv2(int n) // Round to a number that's divisible by 2 (for h264 etc)
|
||||
{
|
||||
int a = (n / 2) * 2; // Smaller multiple
|
||||
int b = a + 2; // Larger multiple
|
||||
return (n - a > b - n) ? b : a; // Return of closest of two
|
||||
}
|
||||
|
||||
public static bool CanUseAutoEnc (bool stepByStep, InterpSettings current)
|
||||
{
|
||||
AutoEncode.UpdateChunkAndBufferSizes();
|
||||
|
||||
if (current.alpha)
|
||||
{
|
||||
Logger.Log($"Not Using AutoEnc: Alpha mode is enabled.", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current.outMode.ToString().ToLower().Contains("vid") || current.outMode.ToString().ToLower().Contains("gif"))
|
||||
{
|
||||
Logger.Log($"Not Using AutoEnc: Out Mode is not video ({current.outMode.ToString()})", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(stepByStep && !Config.GetBool("sbsAllowAutoEnc"))
|
||||
{
|
||||
Logger.Log($"Not Using AutoEnc: Using step-by-step mode, but 'sbsAllowAutoEnc' is false.", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stepByStep && Config.GetInt("autoEncMode") == 0)
|
||||
{
|
||||
Logger.Log($"Not Using AutoEnc: 'autoEncMode' is 0.", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
int inFrames = IOUtils.GetAmountOfFiles(current.framesFolder, false);
|
||||
if (inFrames * current.interpFactor < (AutoEncode.chunkSize + AutoEncode.safetyBufferFrames) * 1.2f)
|
||||
{
|
||||
Logger.Log($"Not Using AutoEnc: Input frames ({inFrames}) * factor ({current.interpFactor}) is smaller than (chunkSize ({AutoEncode.chunkSize}) + safetyBufferFrames ({AutoEncode.safetyBufferFrames}) * 1.2f)", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> UseUHD ()
|
||||
{
|
||||
return (await GetOutputResolution(i.current.inPath, false)).Height >= Config.GetInt("uhdThresh");
|
||||
}
|
||||
|
||||
public static void FixConsecutiveSceneFrames (string sceneFramesPath, string sourceFramesPath)
|
||||
{
|
||||
if (!Directory.Exists(sceneFramesPath) || IOUtils.GetAmountOfFiles(sceneFramesPath, false) < 1)
|
||||
return;
|
||||
|
||||
List<string> sceneFrames = IOUtils.GetFilesSorted(sceneFramesPath).Select(x => Path.GetFileNameWithoutExtension(x)).ToList();
|
||||
List<string> sourceFrames = IOUtils.GetFilesSorted(sourceFramesPath).Select(x => Path.GetFileNameWithoutExtension(x)).ToList();
|
||||
List<string> sceneFramesToDelete = new List<string>();
|
||||
|
||||
foreach(string scnFrame in sceneFrames)
|
||||
{
|
||||
if (sceneFramesToDelete.Contains(scnFrame))
|
||||
continue;
|
||||
|
||||
int sourceIndexForScnFrame = sourceFrames.IndexOf(scnFrame); // Get source index of scene frame
|
||||
if ((sourceIndexForScnFrame + 1) == sourceFrames.Count)
|
||||
continue;
|
||||
string followingFrame = sourceFrames[sourceIndexForScnFrame + 1]; // Get filename/timestamp of the next source frame
|
||||
|
||||
if (sceneFrames.Contains(followingFrame)) // If next source frame is in scene folder, add to deletion list
|
||||
sceneFramesToDelete.Add(followingFrame);
|
||||
}
|
||||
|
||||
foreach (string frame in sceneFramesToDelete)
|
||||
IOUtils.TryDeleteIfExists(Path.Combine(sceneFramesPath, frame + ".png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user