Update .NET 5 code

This commit is contained in:
Dankrushen
2021-02-08 15:39:10 -05:00
parent b4251f9f88
commit 8a1c7d3295
47 changed files with 1684 additions and 1061 deletions

189
Code5/Media/AvProcess.cs Normal file
View File

@@ -0,0 +1,189 @@
using Flowframes.IO;
using Flowframes.OS;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Flowframes.MiscUtils;
namespace Flowframes
{
class AvProcess
{
public static Process lastProcess;
public static Stopwatch timeSinceLastOutput = new Stopwatch();
public enum TaskType { ExtractFrames, Encode, GetInfo, Merge, Other };
public static TaskType lastTask = TaskType.Other;
public static string lastOutputFfmpeg;
public static string lastOutputGifski;
public enum LogMode { Visible, OnlyLastLine, Hidden }
static LogMode currentLogMode;
static bool showProgressBar;
public static async Task RunFfmpeg(string args, LogMode logMode, TaskType taskType = TaskType.Other, bool progressBar = false)
{
await RunFfmpeg(args, "", logMode, taskType, progressBar);
}
public static async Task RunFfmpeg(string args, string workingDir, LogMode logMode, TaskType taskType = TaskType.Other, bool progressBar = false)
{
lastOutputFfmpeg = "";
currentLogMode = logMode;
showProgressBar = progressBar;
Process ffmpeg = OSUtils.NewProcess(true);
timeSinceLastOutput.Restart();
lastProcess = ffmpeg;
lastTask = taskType;
if(!string.IsNullOrWhiteSpace(workingDir))
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {workingDir.Wrap()} & {Path.Combine(GetAvDir(), "ffmpeg.exe").Wrap()} -hide_banner -loglevel warning -y -stats {args}";
else
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg.exe -hide_banner -loglevel warning -y -stats {args}";
if (logMode != LogMode.Hidden) Logger.Log("Running ffmpeg...", false);
Logger.Log("cmd.exe " + ffmpeg.StartInfo.Arguments, true, false, "ffmpeg");
ffmpeg.OutputDataReceived += new DataReceivedEventHandler(FfmpegOutputHandler);
ffmpeg.ErrorDataReceived += new DataReceivedEventHandler(FfmpegOutputHandler);
ffmpeg.Start();
ffmpeg.BeginOutputReadLine();
ffmpeg.BeginErrorReadLine();
while (!ffmpeg.HasExited)
await Task.Delay(1);
if(progressBar)
Program.mainForm.SetProgress(0);
}
static void FfmpegOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
timeSinceLastOutput.Restart();
if (outLine == null || outLine.Data == null)
return;
string line = outLine.Data;
lastOutputFfmpeg = lastOutputFfmpeg + "\n" + line;
bool hidden = currentLogMode == LogMode.Hidden;
bool replaceLastLine = currentLogMode == LogMode.OnlyLastLine;
string trimmedLine = line.Remove("q=-0.0").Remove("size=N/A").Remove("bitrate=N/A").TrimWhitespaces();
Logger.Log(trimmedLine, hidden, replaceLastLine, "ffmpeg");
if(line.Contains("Could not open file"))
Interpolate.Cancel($"FFmpeg Error: {line}");
if (line.Contains("No NVENC capable devices found"))
Interpolate.Cancel($"FFmpeg Error: {line}\nMake sure you have an NVENC-capable Nvidia GPU.");
if (!hidden && showProgressBar && line.Contains("time="))
{
Regex timeRegex = new Regex("(?<=time=).*(?= )");
UpdateFfmpegProgress(timeRegex.Match(line).Value);
}
}
static void FfmpegOutputHandlerSilent (object sendingProcess, DataReceivedEventArgs outLine)
{
timeSinceLastOutput.Restart();
if (outLine == null || outLine.Data == null || outLine.Data.Trim().Length < 2)
return;
string line = outLine.Data;
if (!string.IsNullOrWhiteSpace(lastOutputFfmpeg))
lastOutputFfmpeg += "\n";
lastOutputFfmpeg = lastOutputFfmpeg + line;
Logger.Log(line, true, false, "ffmpeg");
if (showProgressBar && line.Contains("time="))
{
Logger.Log($"showProgressBar, contains: {line.Contains("time=")}", true, false, "ffmpeg");
Regex timeRegex = new Regex("(?<=time=).*(?= )");
UpdateFfmpegProgress(timeRegex.Match(line).Value);
}
}
public static string GetFfmpegOutput (string args)
{
Process ffmpeg = OSUtils.NewProcess(true);
lastProcess = ffmpeg;
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg.exe -hide_banner -y -stats {args}";
Logger.Log("cmd.exe " + ffmpeg.StartInfo.Arguments, true, false, "ffmpeg");
ffmpeg.Start();
ffmpeg.WaitForExit();
string output = ffmpeg.StandardOutput.ReadToEnd();
string err = ffmpeg.StandardError.ReadToEnd();
if (!string.IsNullOrWhiteSpace(err)) output += "\n" + err;
return output;
}
public static async Task<string> GetFfmpegOutputAsync(string args, bool setBusy = false, bool progressBar = false)
{
timeSinceLastOutput.Restart();
if (Program.busy) setBusy = false;
lastOutputFfmpeg = "";
showProgressBar = progressBar;
Process ffmpeg = OSUtils.NewProcess(true);
lastProcess = ffmpeg;
ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg.exe -hide_banner -y -stats {args}";
Logger.Log("cmd.exe " + ffmpeg.StartInfo.Arguments, true, false, "ffmpeg");
if (setBusy) Program.mainForm.SetWorking(true);
ffmpeg.OutputDataReceived += new DataReceivedEventHandler(FfmpegOutputHandlerSilent);
ffmpeg.ErrorDataReceived += new DataReceivedEventHandler(FfmpegOutputHandlerSilent);
ffmpeg.Start();
ffmpeg.BeginOutputReadLine();
ffmpeg.BeginErrorReadLine();
while (!ffmpeg.HasExited) await Task.Delay(50);
while(timeSinceLastOutput.ElapsedMilliseconds < 200) await Task.Delay(50);
if (setBusy) Program.mainForm.SetWorking(false);
return lastOutputFfmpeg;
}
public static string GetFfprobeOutput (string args)
{
Process ffprobe = OSUtils.NewProcess(true);
ffprobe.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffprobe.exe {args}";
Logger.Log("cmd.exe " + ffprobe.StartInfo.Arguments, true, false, "ffmpeg");
ffprobe.Start();
ffprobe.WaitForExit();
string output = ffprobe.StandardOutput.ReadToEnd();
string err = ffprobe.StandardError.ReadToEnd();
if (!string.IsNullOrWhiteSpace(err)) output += "\n" + err;
return output;
}
public static void UpdateFfmpegProgress(string ffmpegTime)
{
if (Program.mainForm.currInDuration < 1)
{
Program.mainForm.SetProgress(0);
return;
}
long total = Program.mainForm.currInDuration / 100;
long current = FormatUtils.MsFromTimestamp(ffmpegTime);
int progress = Convert.ToInt32(current / total);
Program.mainForm.SetProgress(progress);
}
static string GetAvDir ()
{
return Path.Combine(Paths.GetPkgPath(), Path.GetFileNameWithoutExtension(Packages.audioVideo.fileName));
}
static string GetCmdArg ()
{
return "/C";
}
public static async Task SetBusyWhileRunning ()
{
if (Program.busy) return;
await Task.Delay(100);
while(!lastProcess.HasExited)
await Task.Delay(10);
}
}
}

182
Code5/Media/FFmpegUtils.cs Normal file
View File

@@ -0,0 +1,182 @@
using Flowframes.IO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Flowframes.Media
{
class FFmpegUtils
{
public enum Codec { H264, H265, NVENC264, NVENC265, VP9, ProRes, AviRaw }
public static Codec GetCodec(Interpolate.OutMode mode)
{
if (mode == Interpolate.OutMode.VidMp4 || mode == Interpolate.OutMode.VidMkv)
{
int mp4Enc = Config.GetInt("mp4Enc");
if (mp4Enc == 0) return Codec.H264;
if (mp4Enc == 1) return Codec.H265;
if (mp4Enc == 2) return Codec.NVENC264;
if (mp4Enc == 3) return Codec.NVENC265;
}
if (mode == Interpolate.OutMode.VidWebm)
return Codec.VP9;
if (mode == Interpolate.OutMode.VidProRes)
return Codec.ProRes;
if (mode == Interpolate.OutMode.VidAvi)
return Codec.AviRaw;
return Codec.H264;
}
public static string GetEnc(Codec codec)
{
switch (codec)
{
case Codec.H264: return "libx264";
case Codec.H265: return "libx265";
case Codec.NVENC264: return "h264_nvenc";
case Codec.NVENC265: return "hevc_nvenc";
case Codec.VP9: return "libvpx-vp9";
case Codec.ProRes: return "prores_ks";
case Codec.AviRaw: return Config.Get("aviCodec");
}
return "libx264";
}
public static string GetEncArgs (Codec codec)
{
string args = $"-c:v { GetEnc(codec)} ";
if(codec == Codec.H264)
{
args += $"-crf {Config.GetInt("h264Crf")} -preset {Config.Get("ffEncPreset")} -pix_fmt yuv420p";
}
if (codec == Codec.H265)
{
args += $"-crf {Config.GetInt("h265Crf")} -preset {Config.Get("ffEncPreset")} -pix_fmt yuv420p";
}
if (codec == Codec.NVENC264)
{
args += $"-cq {Config.GetInt("h264Crf")} -preset slow -pix_fmt yuv420p";
}
if (codec == Codec.NVENC265)
{
args += $"-cq {Config.GetInt("h265Crf")} -preset slow -pix_fmt yuv420p";
}
if (codec == Codec.VP9)
{
int crf = Config.GetInt("vp9Crf");
string qualityStr = (crf > 0) ? $"-crf {crf}" : "-lossless 1";
string cpuUsed = Config.GetInt("vp9Speed", 3).ToString();
args += $"{qualityStr} -cpu-used {cpuUsed} -tile-columns 2 -tile-rows 2 -row-mt 1 -pix_fmt yuv420p";
}
if(codec == Codec.ProRes)
{
args += $"-profile:v {Config.GetInt("proResProfile")} -pix_fmt yuv420p";
}
if (codec == Codec.AviRaw)
{
args += $"-pix_fmt {Config.Get("aviColors")}";
}
return args;
}
public static string GetAudioEnc(Codec codec)
{
switch (codec)
{
case Codec.VP9: return "libopus";
}
return "aac";
}
public static string GetExt(Interpolate.OutMode outMode, bool dot = true)
{
string ext = dot ? "." : "";
switch (outMode)
{
case Interpolate.OutMode.VidMp4: ext += "mp4"; break;
case Interpolate.OutMode.VidMkv: ext += "mkv"; break;
case Interpolate.OutMode.VidWebm: ext += "webm"; break;
case Interpolate.OutMode.VidProRes: ext += "mov"; break;
case Interpolate.OutMode.VidAvi: ext += "avi"; break;
case Interpolate.OutMode.VidGif: ext += "gif"; break;
}
return ext;
}
public static string GetAudioExt(string videoFile)
{
switch (FfmpegCommands.GetAudioCodec(videoFile))
{
case "vorbis": return "ogg";
case "opus": return "ogg";
case "mp2": return "mp2";
case "aac": return "m4a";
default: return "wav";
}
}
public static string GetAudioFallbackArgs (string containerExt)
{
containerExt = containerExt.Remove(".");
string codec = "aac";
string bitrate = $"{Config.GetInt("aacBitrate", 160)}";
if(containerExt == "webm" || containerExt == "mkv")
{
codec = "libopus";
bitrate = $"{Config.GetInt("opusBitrate", 128)}";
}
return $"-c:a {codec} -b:a {bitrate}k -ac 2";
}
public static string GetAudioExtForContainer(string containerExt)
{
containerExt = containerExt.Remove(".");
string ext = "m4a";
if (containerExt == "webm" || containerExt == "mkv")
ext = "ogg";
return ext;
}
public static string GetSubCodecForContainer(string containerExt)
{
containerExt = containerExt.Remove(".");
if (containerExt == "mp4") return "mov_text";
if (containerExt == "webm") return "webvtt";
return "copy"; // Default: Copy SRT subs
}
public static bool ContainerSupportsSubs(string containerExt, bool showWarningIfNotSupported = true)
{
containerExt = containerExt.Remove(".");
bool supported = (containerExt == "mp4" || containerExt == "mkv" || containerExt == "webm" || containerExt == "mov");
Logger.Log($"Subtitles {(supported ? "are supported" : "not supported")} by {containerExt.ToUpper()}", true);
if (Config.GetBool("keepSubs") && !supported)
Logger.Log($"Warning: Subtitles are enabled, but {containerExt.ToUpper()} does not support them. MKV is recommended instead.");
return supported;
}
}
}

View File

@@ -0,0 +1,52 @@
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
namespace Flowframes.Media
{
class FfmpegAlpha
{
public static async Task ExtractAlphaDir(string rgbDir, string alphaDir)
{
Directory.CreateDirectory(alphaDir);
foreach (FileInfo file in IOUtils.GetFileInfosSorted(rgbDir))
{
string args = $"-i {file.FullName.Wrap()} -vf format=yuva444p16le,alphaextract,format=yuv420p {Path.Combine(alphaDir, file.Name).Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
}
}
public static async Task RemoveAlpha(string inputDir, string outputDir, string fillColor = "black")
{
Directory.CreateDirectory(outputDir);
foreach (FileInfo file in IOUtils.GetFileInfosSorted(inputDir))
{
string outFilename = Path.Combine(outputDir, "_" + file.Name);
Size res = IOUtils.GetImage(file.FullName).Size;
string args = $" -f lavfi -i color={fillColor}:s={res.Width}x{res.Height} -i {file.FullName.Wrap()} " +
$"-filter_complex overlay=0:0:shortest=1 -pix_fmt rgb24 {outFilename.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
file.Delete();
File.Move(outFilename, file.FullName);
}
}
public static async Task MergeAlphaIntoRgb(string rgbDir, int rgbPad, string alphaDir, int aPad, bool deleteAlphaDir)
{
string filter = "-filter_complex [0:v:0][1:v:0]alphamerge[out] -map [out]";
string args = $"-i \"{rgbDir}/%{rgbPad}d.png\" -i \"{alphaDir}/%{aPad}d.png\" {filter} \"{rgbDir}/%{rgbPad}d.png\"";
await RunFfmpeg(args, LogMode.Hidden);
if (deleteAlphaDir)
IOUtils.TryDeleteIfExists(alphaDir);
}
}
}

View File

@@ -0,0 +1,178 @@
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FFmpegUtils;
namespace Flowframes.Media
{
partial class FfmpegAudioAndMetadata : FfmpegCommands
{
public static async Task ExtractAudio(string inputFile, string outFile) // https://stackoverflow.com/a/27413824/14274419
{
try
{
string audioExt = Utils.GetAudioExt(inputFile);
outFile = Path.ChangeExtension(outFile, audioExt);
Logger.Log($"[FFCmds] Extracting audio from {inputFile} to {outFile}", true);
string args = $" -loglevel panic -i {inputFile.Wrap()} -vn -c:a copy {outFile.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
if (File.Exists(outFile) && IOUtils.GetFilesize(outFile) < 512)
{
Logger.Log("Failed to extract audio losslessly! Trying to re-encode.");
File.Delete(outFile);
outFile = Path.ChangeExtension(outFile, Utils.GetAudioExtForContainer(Path.GetExtension(inputFile)));
args = $" -loglevel panic -i {inputFile.Wrap()} -vn {Utils.GetAudioFallbackArgs(Path.GetExtension(inputFile))} {outFile.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
if ((File.Exists(outFile) && IOUtils.GetFilesize(outFile) < 512) || lastOutputFfmpeg.Contains("Invalid data"))
{
Logger.Log("Failed to extract audio, even with re-encoding. Output will not have audio.");
IOUtils.TryDeleteIfExists(outFile);
return;
}
Logger.Log($"Source audio has been re-encoded as it can't be extracted losslessly. This may decrease the quality slightly.", false, true);
}
}
catch (Exception e)
{
Logger.Log("Error extracting audio: " + e.Message);
}
}
public static async Task ExtractSubtitles(string inputFile, string outFolder, Interpolate.OutMode outMode,bool showMsg = true)
{
try
{
string msg = "Extracting subtitles from video...";
if (showMsg) Logger.Log(msg);
List<SubtitleTrack> subtitleTracks = await GetSubtitleTracks(inputFile);
int counter = 1;
foreach (SubtitleTrack subTrack in subtitleTracks)
{
string outPath = Path.Combine(outFolder, $"{subTrack.streamIndex}_{subTrack.langFriendly}_{subTrack.encoding}.srt");
string args = $" -loglevel error -sub_charenc {subTrack.encoding} -i {inputFile.Wrap()} -map 0:s:{subTrack.streamIndex} {outPath.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
if (subtitleTracks.Count > 4) Program.mainForm.SetProgress(FormatUtils.RatioInt(counter, subtitleTracks.Count));
Logger.Log($"[FFCmds] Extracted subtitle track {subTrack.streamIndex} to {outPath} ({FormatUtils.Bytes(IOUtils.GetFilesize(outPath))})", true, false, "ffmpeg");
counter++;
}
if (subtitleTracks.Count > 0)
Utils.ContainerSupportsSubs(Utils.GetExt(outMode), true);
Logger.Log($"Extracted {subtitleTracks.Count} subtitle tracks from the input video.".Replace(" 0 ", " no "), false, Logger.GetLastLine().Contains(msg));
}
catch (Exception e)
{
Logger.Log("Error extracting subtitles: " + e.Message);
}
Program.mainForm.SetProgress(0);
}
public static async Task<List<SubtitleTrack>> GetSubtitleTracks(string inputFile)
{
List<SubtitleTrack> subtitleTracks = new List<SubtitleTrack>();
string args = $"-i {inputFile.Wrap()}";
Logger.Log("GetSubtitleTracks()", true, false, "ffmpeg");
string[] outputLines = (await GetFfmpegOutputAsync(args)).SplitIntoLines();
int idx = 0;
for (int i = 0; i < outputLines.Length; i++)
{
string line = outputLines[i];
if (!line.Contains(" Subtitle: ")) continue;
string lang = "unknown";
string subEnc = "UTF-8";
if (line.Contains("(") && line.Contains("): Subtitle: ")) // Lang code in stream name, like "Stream #0:2(eng): Subtitle: ..."
lang = line.Split('(')[1].Split(')')[0];
if ((i + 2 < outputLines.Length) && outputLines[i + 1].Contains("Metadata:") && outputLines[i + 2].Contains("SUB_CHARENC")) // Subtitle encoding is in metadata!
subEnc = outputLines[i + 2].Remove("SUB_CHARENC").Remove(":").TrimWhitespaces();
Logger.Log($"Found subtitle track #{idx} with language '{lang}' and encoding '{subEnc}'", true, false, "ffmpeg");
subtitleTracks.Add(new SubtitleTrack(idx, lang, subEnc));
idx++;
}
return subtitleTracks;
}
public static async Task MergeAudioAndSubs(string inputFile, string audioPath, string tempFolder, int looptimes = -1) // https://superuser.com/a/277667
{
Logger.Log($"[FFCmds] Merging audio from {audioPath} into {inputFile}", true);
string containerExt = Path.GetExtension(inputFile);
string tempPath = Path.Combine(tempFolder, $"vid{containerExt}"); // inputFile + "-temp" + Path.GetExtension(inputFile);
string outPath = Path.Combine(tempFolder, $"muxed{containerExt}"); // inputFile + "-temp" + Path.GetExtension(inputFile);
File.Move(inputFile, tempPath);
string inName = Path.GetFileName(tempPath);
string audioName = Path.GetFileName(audioPath);
string outName = Path.GetFileName(outPath);
bool subs = Utils.ContainerSupportsSubs(containerExt, false) && Config.GetBool("keepSubs");
string subInputArgs = "";
string subMapArgs = "";
string subMetaArgs = "";
string[] subTracks = subs ? IOUtils.GetFilesSorted(tempFolder, false, "*.srt") : new string[0];
for (int subTrack = 0; subTrack < subTracks.Length; subTrack++)
{
subInputArgs += $" -i {Path.GetFileName(subTracks[subTrack])}";
subMapArgs += $" -map {subTrack + 2}";
subMetaArgs += $" -metadata:s:s:{subTrack} language={Path.GetFileNameWithoutExtension(subTracks[subTrack]).Split('_')[1]}";
}
string subCodec = Utils.GetSubCodecForContainer(containerExt);
string args = $" -i {inName} -stream_loop {looptimes} -i {audioName.Wrap()}" +
$"{subInputArgs} -map 0:v -map 1:a {subMapArgs} -c:v copy -c:a copy -c:s {subCodec} {subMetaArgs} -shortest {outName}";
await RunFfmpeg(args, tempFolder, LogMode.Hidden);
if ((File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 1024) || lastOutputFfmpeg.Contains("Invalid data") || lastOutputFfmpeg.Contains("Error initializing output stream"))
{
Logger.Log("Failed to merge audio losslessly! Trying to re-encode.", false, false, "ffmpeg");
args = $" -i {inName} -stream_loop {looptimes} -i {audioName.Wrap()}" +
$"{subInputArgs} -map 0:v -map 1:a {subMapArgs} -c:v copy {Utils.GetAudioFallbackArgs(Path.GetExtension(inputFile))} -c:s {subCodec} {subMetaArgs} -shortest {outName}";
await RunFfmpeg(args, tempFolder, LogMode.Hidden);
if ((File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 1024) || lastOutputFfmpeg.Contains("Invalid data") || lastOutputFfmpeg.Contains("Error initializing output stream"))
{
Logger.Log("Failed to merge audio, even with re-encoding. Output will not have audio.", false, false, "ffmpeg");
IOUtils.TryMove(tempPath, inputFile); // Move temp file back
IOUtils.TryDeleteIfExists(tempPath);
return;
}
string audioExt = Path.GetExtension(audioPath).Remove(".").ToUpper();
Logger.Log($"Source audio ({audioExt}) has been re-encoded to fit into the target container ({containerExt.Remove(".").ToUpper()}). This may decrease the quality slightly.", false, true, "ffmpeg");
}
if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) > 512)
{
File.Delete(tempPath);
File.Move(outPath, inputFile);
}
else
{
File.Move(tempPath, inputFile);
}
}
}
}

View File

@@ -0,0 +1,196 @@
using Flowframes.Media;
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FFmpegUtils;
namespace Flowframes
{
class FfmpegCommands
{
public static string divisionFilter = "pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2:color=black@0";
public static string pngComprArg = "-compression_level 3";
public static string mpDecDef = "\"mpdecimate\"";
public static string mpDecAggr = "\"mpdecimate=hi=64*32:lo=64*32:frac=0.1\"";
public static async Task ConcatVideos(string concatFile, string outPath, int looptimes = -1)
{
Logger.Log($"Merging videos...", false, Logger.GetLastLine().Contains("frame"));
string loopStr = (looptimes > 0) ? $"-stream_loop {looptimes}" : "";
string vfrFilename = Path.GetFileName(concatFile);
string args = $" {loopStr} -vsync 1 -f concat -i {vfrFilename} -c copy -movflags +faststart {outPath.Wrap()}";
await RunFfmpeg(args, concatFile.GetParentDir(), LogMode.Hidden, TaskType.Merge);
}
public static async Task LoopVideo(string inputFile, int times, bool delSrc = false)
{
string pathNoExt = Path.ChangeExtension(inputFile, null);
string ext = Path.GetExtension(inputFile);
string args = $" -stream_loop {times} -i {inputFile.Wrap()} -c copy \"{pathNoExt}-Loop{times}{ext}\"";
await RunFfmpeg(args, LogMode.Hidden);
if (delSrc)
DeleteSource(inputFile);
}
public static async Task ChangeSpeed(string inputFile, float newSpeedPercent, bool delSrc = false)
{
string pathNoExt = Path.ChangeExtension(inputFile, null);
string ext = Path.GetExtension(inputFile);
float val = newSpeedPercent / 100f;
string speedVal = (1f / val).ToString("0.0000").Replace(",", ".");
string args = " -itsscale " + speedVal + " -i \"" + inputFile + "\" -c copy \"" + pathNoExt + "-" + newSpeedPercent + "pcSpeed" + ext + "\"";
await RunFfmpeg(args, LogMode.OnlyLastLine);
if (delSrc)
DeleteSource(inputFile);
}
public static long GetDuration(string inputFile)
{
Logger.Log($"GetDuration({inputFile}) - Reading Duration using ffprobe.", true, false, "ffmpeg");
string args = $" -v panic -select_streams v:0 -show_entries format=duration -of csv=s=x:p=0 -sexagesimal {inputFile.Wrap()}";
string info = GetFfprobeOutput(args);
return FormatUtils.MsFromTimestamp(info);
}
public static async Task<float> GetFramerate(string inputFile)
{
Logger.Log($"GetFramerate('{inputFile}')", true, false, "ffmpeg");
try
{
string args = $" -i {inputFile.Wrap()}";
string output = await GetFfmpegOutputAsync(args);
string[] entries = output.Split(',');
foreach (string entry in entries)
{
if (entry.Contains(" fps") && !entry.Contains("Input ")) // Avoid reading FPS from the filename, in case filename contains "fps"
{
string num = entry.Replace(" fps", "").Trim().Replace(",", ".");
float value;
float.TryParse(num, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
return value;
}
}
}
catch(Exception e)
{
Logger.Log("GetFramerate Error: " + e.Message, true, false);
}
return 0f;
}
public static Size GetSize(string inputFile)
{
string args = $" -v panic -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 {inputFile.Wrap()}";
string output = GetFfprobeOutput(args);
if (output.Length > 4 && output.Contains("x"))
{
string[] numbers = output.Split('x');
return new Size(numbers[0].GetInt(), numbers[1].GetInt());
}
return new Size(0, 0);
}
public static async Task<int> GetFrameCountAsync(string inputFile)
{
Logger.Log($"GetFrameCountAsync('{inputFile}') - Trying ffprobe first.", true, false, "ffmpeg");
int frames = 0;
frames = await ReadFrameCountFfprobeAsync(inputFile, Config.GetBool("ffprobeCountFrames")); // Try reading frame count with ffprobe
if (frames > 0) return frames;
Logger.Log($"Failed to get frame count using ffprobe (frames = {frames}). Trying to calculate with duration * fps.", true, false, "ffmpeg");
frames = await ReadFrameCountFfmpegAsync(inputFile); // Try reading frame count with ffmpeg
if (frames > 0) return frames;
Logger.Log("Failed to get total frame count of video.");
return 0;
}
static int ReadFrameCountFromDuration (string inputFile, long durationMs, float fps)
{
float durationSeconds = durationMs / 1000f;
float frameCount = durationSeconds * fps;
int frameCountRounded = frameCount.RoundToInt();
Logger.Log($"ReadFrameCountFromDuration: Got frame count of {frameCount}, rounded to {frameCountRounded}");
return frameCountRounded;
}
static async Task<int> ReadFrameCountFfprobeAsync(string inputFile, bool readFramesSlow)
{
string args = $" -v panic -select_streams v:0 -show_entries stream=nb_frames -of default=noprint_wrappers=1 {inputFile.Wrap()}";
if (readFramesSlow)
{
Logger.Log("Counting total frames using FFprobe. This can take a moment...");
await Task.Delay(10);
args = $" -v panic -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 {inputFile.Wrap()}";
}
string info = GetFfprobeOutput(args);
string[] entries = info.SplitIntoLines();
try
{
if (readFramesSlow)
return info.GetInt();
foreach (string entry in entries)
{
if (entry.Contains("nb_frames="))
return entry.GetInt();
}
}
catch { }
return -1;
}
static async Task<int> ReadFrameCountFfmpegAsync (string inputFile)
{
string args = $" -loglevel panic -i {inputFile.Wrap()} -map 0:v:0 -c copy -f null - ";
string info = await GetFfmpegOutputAsync(args, true, true);
try
{
string[] lines = info.SplitIntoLines();
string lastLine = lines.Last();
return lastLine.Substring(0, lastLine.IndexOf("fps")).GetInt();
}
catch
{
return -1;
}
}
public static string GetAudioCodec(string path)
{
string args = $" -v panic -show_streams -select_streams a -show_entries stream=codec_name {path.Wrap()}";
string info = GetFfprobeOutput(args);
string[] entries = info.SplitIntoLines();
foreach (string entry in entries)
{
if (entry.Contains("codec_name="))
return entry.Split('=')[1];
}
return "";
}
public static void DeleteSource(string path)
{
Logger.Log("[FFCmds] Deleting input file/dir: " + path, true);
if (IOUtils.IsPathDirectory(path) && Directory.Exists(path))
Directory.Delete(path, true);
if (!IOUtils.IsPathDirectory(path) && File.Exists(path))
File.Delete(path);
}
}
}

View File

@@ -0,0 +1,65 @@
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FFmpegUtils;
namespace Flowframes.Media
{
partial class FfmpegEncode : FfmpegCommands
{
public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, LogMode logMode = LogMode.OnlyLastLine, bool isChunk = false)
{
await FramesToVideoConcat(framesFile, outPath, outMode, fps, 0, logMode, isChunk);
}
public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, float resampleFps, LogMode logMode = LogMode.OnlyLastLine, bool isChunk = false)
{
if (logMode != LogMode.Hidden)
Logger.Log((resampleFps <= 0) ? $"Encoding video..." : $"Encoding video resampled to {resampleFps.ToString().Replace(",", ".")} FPS...");
Directory.CreateDirectory(outPath.GetParentDir());
string encArgs = Utils.GetEncArgs(Utils.GetCodec(outMode));
if (!isChunk) encArgs += $" -movflags +faststart";
string vfrFilename = Path.GetFileName(framesFile);
string rate = fps.ToString().Replace(",", ".");
string vf = (resampleFps <= 0) ? "" : $"-vf fps=fps={resampleFps.ToStringDot()}";
string extraArgs = Config.Get("ffEncArgs");
string args = $"-loglevel error -vsync 0 -f concat -r {rate} -i {vfrFilename} {encArgs} {vf} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
await RunFfmpeg(args, framesFile.GetParentDir(), logMode, TaskType.Encode, !isChunk);
}
public static async Task FramesToGifConcat(string framesFile, string outPath, float fps, bool palette, int colors = 64, float resampleFps = -1, LogMode logMode = LogMode.OnlyLastLine)
{
if (logMode != LogMode.Hidden)
Logger.Log((resampleFps <= 0) ? $"Encoding GIF..." : $"Encoding GIF resampled to {resampleFps.ToString().Replace(",", ".")} FPS...");
string vfrFilename = Path.GetFileName(framesFile);
string paletteFilter = palette ? $"-vf \"split[s0][s1];[s0]palettegen={colors}[p];[s1][p]paletteuse=dither=floyd_steinberg:diff_mode=rectangle\"" : "";
string fpsFilter = (resampleFps <= 0) ? "" : $"fps=fps={resampleFps.ToStringDot()}";
string vf = FormatUtils.ConcatStrings(new string[] { paletteFilter, fpsFilter });
string rate = fps.ToStringDot();
string args = $"-loglevel error -f concat -r {rate} -i {vfrFilename.Wrap()} -f gif {vf} {outPath.Wrap()}";
await RunFfmpeg(args, framesFile.GetParentDir(), LogMode.OnlyLastLine, TaskType.Encode);
}
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 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))
args = args.Replace("-c:a", "-an");
if (audioKbps < 0)
args = args.Replace($" -b:a {audioKbps}", "");
await RunFfmpeg(args, LogMode.OnlyLastLine, TaskType.Encode, true);
if (delSrc)
DeleteSource(inputFile);
}
}
}

View File

@@ -0,0 +1,107 @@
using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
namespace Flowframes.Media
{
partial class FfmpegExtract : FfmpegCommands
{
public static async Task ExtractSceneChanges(string inputFile, string frameFolderPath, float rate)
{
Logger.Log("Extracting scene changes...");
await VideoToFrames(inputFile, frameFolderPath, false, rate, false, false, new Size(320, 180), true);
bool hiddenLog = Interpolate.currentInputFrameCount <= 50;
int amount = IOUtils.GetAmountOfFiles(frameFolderPath, false);
Logger.Log($"Detected {amount} scene {(amount == 1 ? "change" : "changes")}.".Replace(" 0 ", " no "), false, !hiddenLog);
}
public static async Task VideoToFrames(string inputFile, string framesDir, bool alpha, float rate, bool deDupe, bool delSrc)
{
await VideoToFrames(inputFile, framesDir, alpha, rate, deDupe, delSrc, new Size());
}
public static async Task VideoToFrames(string inputFile, string framesDir, bool alpha, float rate, bool deDupe, bool delSrc, Size size, bool sceneDetect = false)
{
if (!sceneDetect) Logger.Log("Extracting video frames from input video...");
string sizeStr = (size.Width > 1 && size.Height > 1) ? $"-s {size.Width}x{size.Height}" : "";
IOUtils.CreateDir(framesDir);
string timecodeStr = /* timecodes ? $"-copyts -r {FrameOrder.timebase} -frame_pts true" : */ "-copyts -frame_pts true";
string scnDetect = sceneDetect ? $"\"select='gt(scene,{Config.GetFloatString("scnDetectValue")})'\"" : "";
string mpStr = deDupe ? ((Config.GetInt("mpdecimateMode") == 0) ? mpDecDef : mpDecAggr) : "";
string filters = FormatUtils.ConcatStrings(new string[] { divisionFilter, scnDetect, mpStr });
string vf = filters.Length > 2 ? $"-vf {filters}" : "";
string rateArg = (rate > 0) ? $" -r {rate.ToStringDot()}" : "";
string pixFmt = alpha ? "-pix_fmt rgba" : "-pix_fmt rgb24"; // Use RGBA for GIF for alpha support
string args = $"{rateArg} -i {inputFile.Wrap()} {pngComprArg} -vsync 0 {pixFmt} {timecodeStr} {vf} {sizeStr} \"{framesDir}/%{Padding.inputFrames}d.png\"";
LogMode logMode = Interpolate.currentInputFrameCount > 50 ? LogMode.OnlyLastLine : LogMode.Hidden;
await AvProcess.RunFfmpeg(args, logMode, TaskType.ExtractFrames, true);
int amount = IOUtils.GetAmountOfFiles(framesDir, false, "*.png");
if (!sceneDetect) Logger.Log($"Extracted {amount} {(amount == 1 ? "frame" : "frames")} from input.", false, true);
await Task.Delay(1);
if (delSrc)
DeleteSource(inputFile);
}
public static async Task ImportImages(string inpath, string outpath, bool alpha, Size size, bool delSrc = false, bool showLog = true)
{
if (showLog) Logger.Log("Importing images...");
Logger.Log($"Importing images from {inpath} to {outpath}.", true);
IOUtils.CreateDir(outpath);
string concatFile = Path.Combine(Paths.GetDataPath(), "png-concat-temp.ini");
string concatFileContent = "";
string[] files = IOUtils.GetFilesSorted(inpath);
foreach (string img in files)
concatFileContent += $"file '{img.Replace(@"\", "/")}'\n";
File.WriteAllText(concatFile, concatFileContent);
string sizeStr = (size.Width > 1 && size.Height > 1) ? $"-s {size.Width}x{size.Height}" : "";
string pixFmt = alpha ? "-pix_fmt rgba" : "-pix_fmt rgb24"; // Use RGBA for GIF for alpha support
string vf = alpha ? $"-filter_complex \"[0:v]{divisionFilter},split[a][b];[a]palettegen=reserve_transparent=on:transparency_color=ffffff[p];[b][p]paletteuse\"" : $"-vf {divisionFilter}";
string args = $" -loglevel panic -f concat -safe 0 -i {concatFile.Wrap()} {pngComprArg} {sizeStr} {pixFmt} -vsync 0 {vf} \"{outpath}/%{Padding.inputFrames}d.png\"";
LogMode logMode = IOUtils.GetAmountOfFiles(inpath, false) > 50 ? LogMode.OnlyLastLine : LogMode.Hidden;
await AvProcess.RunFfmpeg(args, logMode, TaskType.ExtractFrames);
if (delSrc)
DeleteSource(inpath);
}
public static async Task ImportSingleImage(string inputFile, string outPath, Size size)
{
string sizeStr = (size.Width > 1 && size.Height > 1) ? $"-s {size.Width}x{size.Height}" : "";
bool isPng = (Path.GetExtension(outPath).ToLower() == ".png");
string comprArg = isPng ? pngComprArg : "";
string pixFmt = "-pix_fmt " + (isPng ? $"rgb24 {comprArg}" : "yuvj420p");
string args = $"-i {inputFile.Wrap()} {comprArg} {sizeStr} {pixFmt} -vf {divisionFilter} {outPath.Wrap()}";
await AvProcess.RunFfmpeg(args, LogMode.Hidden, TaskType.ExtractFrames);
}
public static async Task ExtractSingleFrame(string inputFile, string outputPath, int frameNum)
{
bool isPng = (Path.GetExtension(outputPath).ToLower() == ".png");
string comprArg = isPng ? pngComprArg : "";
string pixFmt = "-pix_fmt " + (isPng ? $"rgb24 {comprArg}" : "yuvj420p");
string args = $"-i {inputFile.Wrap()} -vf \"select=eq(n\\,{frameNum})\" -vframes 1 {pixFmt} {outputPath.Wrap()}";
await AvProcess.RunFfmpeg(args, LogMode.Hidden, TaskType.ExtractFrames);
}
public static async Task ExtractLastFrame(string inputFile, string outputPath, Size size)
{
if (IOUtils.IsPathDirectory(outputPath))
outputPath = Path.Combine(outputPath, "last.png");
bool isPng = (Path.GetExtension(outputPath).ToLower() == ".png");
string comprArg = isPng ? pngComprArg : "";
string pixFmt = "-pix_fmt " + (isPng ? $"rgb24 {comprArg}" : "yuvj420p");
string sizeStr = (size.Width > 1 && size.Height > 1) ? $"-s {size.Width}x{size.Height}" : "";
string args = $"-sseof -1 -i {inputFile.Wrap()} -update 1 {pixFmt} {sizeStr} {outputPath.Wrap()}";
await AvProcess.RunFfmpeg(args, LogMode.Hidden, TaskType.ExtractFrames);
}
}
}