diff --git a/Code/Data/Enums.cs b/Code/Data/Enums.cs index 625bc24..0270508 100644 --- a/Code/Data/Enums.cs +++ b/Code/Data/Enums.cs @@ -13,7 +13,7 @@ { public enum Codec { H264, H265, AV1, VP9, ProRes, Gif, Png, Jpeg, Webp, Ffv1, Huffyuv, Magicyuv, Rawvideo } public enum Encoder { X264, X265, SvtAv1, VpxVp9, Nvenc264, Nvenc265, NvencAv1, ProResKs, Gif, Png, Jpeg, Webp, Ffv1, Huffyuv, Magicyuv, Rawvideo } - public enum PixelFormat { Yuv420P, Yuva420P, Yuv420P10Le, Yuv422P, Yuv422P10Le, Yuv444P, Yuv444P10Le, Yuva444P10Le, Rgb24, Rgba, Rgb8 }; + public enum PixelFormat { Yuv420P, Yuva420P, Yuv420P10Le, Yuv422P, Yuv422P10Le, Yuv444P, Yuv444P10Le, Yuva444P10Le, Rgb24, Rgba, Pal8 }; public class Quality { diff --git a/Code/Data/Strings.cs b/Code/Data/Strings.cs index 29ebe61..850d739 100644 --- a/Code/Data/Strings.cs +++ b/Code/Data/Strings.cs @@ -26,7 +26,7 @@ namespace Flowframes.Data { Enums.Encoding.Encoder.Nvenc264.ToString(), "h264 NVENC" }, { Enums.Encoding.Encoder.Nvenc265.ToString(), "h265 NVENC" }, { Enums.Encoding.Encoder.NvencAv1.ToString(), "AV1 NVENC" }, - { Enums.Encoding.Encoder.Gif.ToString(), "Animation" }, + { Enums.Encoding.Encoder.Gif.ToString(), "GIF" }, { Enums.Encoding.Encoder.Png.ToString(), "PNG" }, { Enums.Encoding.Encoder.Jpeg.ToString(), "JPEG" }, { Enums.Encoding.Encoder.Webp.ToString(), "WEBP" }, @@ -47,7 +47,7 @@ namespace Flowframes.Data { Enums.Encoding.PixelFormat.Yuv444P10Le.ToString(), "YUV 4:4:4 10-bit" }, { Enums.Encoding.PixelFormat.Yuva444P10Le.ToString(), "YUVA 4:4:4 10-bit" }, { Enums.Encoding.PixelFormat.Rgb24.ToString(), "RGB 8-bit" }, - { Enums.Encoding.PixelFormat.Rgb8.ToString(), "RGB 256-color" }, + { Enums.Encoding.PixelFormat.Pal8.ToString(), "256-color Palette" }, { Enums.Encoding.PixelFormat.Rgba.ToString(), "RGBA 8-bit" }, }; diff --git a/Code/IO/Config.cs b/Code/IO/Config.cs index c6d008c..54b032b 100644 --- a/Code/IO/Config.cs +++ b/Code/IO/Config.cs @@ -268,7 +268,7 @@ namespace Flowframes.IO if (key == Key.imgSeqFormat) return WriteDefault(key, "PNG"); if (key == Key.aviColors) return WriteDefault(key, "yuv420p"); if (key == Key.gifColors) return WriteDefault(key, "128 (High)"); - if (key == Key.gifDitherType) return WriteDefault(key, "bayer (Recommended)"); + if (key == Key.gifDitherType) return WriteDefault(key, "bayer"); if (key == Key.minVidLength) return WriteDefault(key, "5"); // AI if (key == Key.uhdThresh) return WriteDefault(key, "1600"); diff --git a/Code/Main/Export.cs b/Code/Main/Export.cs index 16bae58..aa02fff 100644 --- a/Code/Main/Export.cs +++ b/Code/Main/Export.cs @@ -83,8 +83,8 @@ namespace Flowframes.Main bool fpsLimit = maxFps.GetFloat() > 0f && s.outFps.GetFloat() > maxFps.GetFloat(); VidExtraData extraData = await FfmpegCommands.GetVidExtraInfo(s.inPath); - string extraArgsIn = FfmpegEncode.GetFfmpegExportArgsIn(s.outFps, s.outItsScale); - string extraArgsOut = FfmpegEncode.GetFfmpegExportArgsOut(fpsLimit ? maxFps : new Fraction(), extraData, s.outSettings); + string extraArgsIn = await FfmpegEncode.GetFfmpegExportArgsIn(s.outFps, s.outItsScale); + string extraArgsOut = await FfmpegEncode.GetFfmpegExportArgsOut(fpsLimit ? maxFps : new Fraction(), extraData, s.outSettings); if (ffplay) { @@ -99,7 +99,7 @@ namespace Flowframes.Main { s.FullOutPath = Path.Combine(s.outPath, await IoUtils.GetCurrentExportFilename(fpsLimit, true)); IoUtils.RenameExistingFile(s.FullOutPath); - return $"{extraArgsIn} -i pipe: {encArgs} {extraArgsOut} {s.FullOutPath.Wrap()}"; + return $"{extraArgsIn} -i pipe: {extraArgsOut} {encArgs} {s.FullOutPath.Wrap()}"; } } diff --git a/Code/Media/AvProcess.cs b/Code/Media/AvProcess.cs index 2069463..ca11bd8 100644 --- a/Code/Media/AvProcess.cs +++ b/Code/Media/AvProcess.cs @@ -102,6 +102,33 @@ namespace Flowframes return processOutput; } + public static string RunFfmpegSync(string args, string workingDir = "", LogMode logMode = LogMode.Hidden, string loglevel = "warning") + { + Process ffmpeg = OsUtils.NewProcess(true); + lastAvProcess = ffmpeg; + + if (string.IsNullOrWhiteSpace(loglevel)) + loglevel = defLogLevel; + + string beforeArgs = $"-hide_banner -stats -loglevel {loglevel} -y"; + + if (!string.IsNullOrWhiteSpace(workingDir)) + ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {workingDir.Wrap()} & {Path.Combine(GetAvDir(), "ffmpeg.exe").Wrap()} {beforeArgs} {args}"; + else + ffmpeg.StartInfo.Arguments = $"{GetCmdArg()} cd /D {GetAvDir().Wrap()} & ffmpeg {beforeArgs} {args}"; + + if (logMode != LogMode.Hidden) Logger.Log("Running FFmpeg...", false); + Logger.Log($"ffmpeg {beforeArgs} {args}", true, false, "ffmpeg"); + + ffmpeg.StartInfo.Arguments += " 2>&1"; + ffmpeg.Start(); + ffmpeg.PriorityClass = ProcessPriorityClass.BelowNormal; + string output = ffmpeg.StandardOutput.ReadToEnd(); + ffmpeg.WaitForExit(); + Logger.Log($"Synchronous ffmpeg output:\n{output}", true, false, "ffmpeg"); + return output; + } + public static string GetFfmpegDefaultArgs(string loglevel = "warning") { return $"-hide_banner -stats -loglevel {loglevel} -y"; diff --git a/Code/Media/FfmpegAudioAndMetadata.cs b/Code/Media/FfmpegAudioAndMetadata.cs index 52b764a..cc13164 100644 --- a/Code/Media/FfmpegAudioAndMetadata.cs +++ b/Code/Media/FfmpegAudioAndMetadata.cs @@ -19,6 +19,14 @@ namespace Flowframes.Media return; } + Data.Enums.Output.Format format = I.currentSettings.outSettings.Format; + + if (format == Data.Enums.Output.Format.Gif || format == Data.Enums.Output.Format.Images) + { + Logger.Log("Warning: Output format does not support audio."); + return; + } + string containerExt = Path.GetExtension(interpVideo); string tempPath = Path.Combine(tempFolder, $"vid{containerExt}"); string outPath = Path.Combine(tempFolder, $"muxed{containerExt}"); diff --git a/Code/Media/FfmpegEncode.cs b/Code/Media/FfmpegEncode.cs index 225e1da..d18151e 100644 --- a/Code/Media/FfmpegEncode.cs +++ b/Code/Media/FfmpegEncode.cs @@ -1,6 +1,7 @@ using Flowframes.Data; using Flowframes.IO; using Flowframes.MiscUtils; +using Flowframes.Properties; using System; using System.Collections.Generic; using System.IO; @@ -38,23 +39,28 @@ namespace Flowframes.Media { string pre = i == 0 ? "" : $" && ffmpeg {AvProcess.GetFfmpegDefaultArgs()}"; string post = (i == 0 && encArgs.Length > 1) ? $"-f null -" : outPath.Wrap(); - args += $"{pre} {GetFfmpegExportArgsIn(fps, itsScale)} {inArg} {encArgs[i]} {GetFfmpegExportArgsOut(resampleFps, extraData, settings, isChunk)} {post} "; + args += $"{pre} {await GetFfmpegExportArgsIn(fps, itsScale)} {inArg} {encArgs[i]} {GetFfmpegExportArgsOut(resampleFps, extraData, settings, isChunk)} {post} "; } await RunFfmpeg(args, framesFile.GetParentDir(), logMode, !isChunk); IoUtils.TryDeleteIfExists(linksDir); } - public static string GetFfmpegExportArgsIn(Fraction fps, float itsScale) + public static async Task GetFfmpegExportArgsIn(Fraction fps, float itsScale) { + var args = new List(); + fps = fps / new Fraction(itsScale); - return $"-r {fps}"; + args.Add($"-r {fps}"); + + return string.Join(" ", args); } - public static string GetFfmpegExportArgsOut(Fraction resampleFps, VidExtraData extraData, OutputSettings settings, bool isChunk = false) + public static async Task GetFfmpegExportArgsOut(Fraction resampleFps, VidExtraData extraData, OutputSettings settings, bool isChunk = false) { - List filters = new List(); - string extraArgs = Config.Get(Config.Key.ffEncArgs); + var beforeArgs = new List(); + var filters = new List(); + var extraArgs = new List { Config.Get(Config.Key.ffEncArgs) }; if (resampleFps.GetFloat() >= 0.1f) filters.Add($"fps=fps={resampleFps}"); @@ -63,25 +69,36 @@ namespace Flowframes.Media { Logger.Log($"Applying color transfer ({extraData.colorSpace}).", true, false, "ffmpeg"); filters.Add($"scale=out_color_matrix={extraData.colorSpace}"); - extraArgs += $" -colorspace {extraData.colorSpace} -color_primaries {extraData.colorPrimaries} -color_trc {extraData.colorTransfer} -color_range:v {extraData.colorRange.Wrap()}"; + extraArgs.Add($"-colorspace {extraData.colorSpace} -color_primaries {extraData.colorPrimaries} -color_trc {extraData.colorTransfer} -color_range:v {extraData.colorRange.Wrap()}"); } if (!string.IsNullOrWhiteSpace(extraData.displayRatio) && !extraData.displayRatio.MatchesWildcard("*N/A*")) - extraArgs += $" -aspect {extraData.displayRatio}"; + extraArgs.Add($"-aspect {extraData.displayRatio}"); - if (!isChunk && settings.Format == Enums.Output.Format.Mp4) - extraArgs += $" -movflags +faststart"; + if (!isChunk && settings.Format == Enums.Output.Format.Mp4 || settings.Format == Enums.Output.Format.Mov) + extraArgs.Add($"-movflags +faststart"); - if(settings.Format == Enums.Output.Format.Gif) + if (settings.Format == Enums.Output.Format.Gif) { string dither = Config.Get(Config.Key.gifDitherType).Split(' ').First(); + string palettePath = Path.Combine(Paths.GetSessionDataPath(), "palette.png"); + string paletteFilter = $"[1:v]paletteuse=dither={dither}"; + int colors = OutputUtils.GetGifColors(ParseUtils.GetEnum(settings.Quality, true, Strings.VideoQuality)); - string paletteFilter = $"\"split[s0][s1];[s0]palettegen={colors}[p];[s1][p]paletteuse=dither={dither}\""; - filters.Add(paletteFilter); + await FfmpegExtract.GeneratePalette(Interpolate.currentMediaFile.ImportPath, palettePath, colors); + + if (File.Exists(palettePath)) + { + beforeArgs.Add($"-i {palettePath.Wrap()}"); + filters.Add(paletteFilter); + } } filters.Add(GetPadFilter()); - return filters.Count > 0 ? $"-vf {string.Join(",", filters)}" : "" + $" {extraArgs}"; + + return filters.Count > 0 ? + $"{string.Join(" ", beforeArgs)} -filter_complex [0:v]{string.Join("[vf],[vf]", filters.Where(f => !string.IsNullOrWhiteSpace(f)))}[vf] -map [vf] {string.Join(" ", extraArgs)}" : + $"{string.Join(" ", beforeArgs)} {string.Join(" ", extraArgs)}"; } public static string GetConcatFileExt(string concatFilePath) diff --git a/Code/Media/FfmpegExtract.cs b/Code/Media/FfmpegExtract.cs index 4aa4643..b930aee 100644 --- a/Code/Media/FfmpegExtract.cs +++ b/Code/Media/FfmpegExtract.cs @@ -99,7 +99,6 @@ namespace Flowframes.Media if (!alpha && compatible) { await CopyImages(inPath, outPath, showLog); - } else { @@ -311,5 +310,11 @@ namespace Flowframes.Media string args = $"{sseof} -i {inputFile.Wrap()} -update 1 {pixFmt} {sizeStr} {trim} {outputPath.Wrap()}"; await RunFfmpeg(args, LogMode.Hidden); } + + public static async Task GeneratePalette (string inputFile, string outputPath, int colors = 256) + { + string args = $"-i {inputFile.Wrap()} -vf palettegen={colors} {outputPath.Wrap()}"; + await Task.Run(() => AvProcess.RunFfmpegSync(args)); + } } } diff --git a/Code/MiscUtils/OutputUtils.cs b/Code/MiscUtils/OutputUtils.cs index c2ea948..8cc9755 100644 --- a/Code/MiscUtils/OutputUtils.cs +++ b/Code/MiscUtils/OutputUtils.cs @@ -115,7 +115,7 @@ namespace Flowframes.MiscUtils PixelFormats = new List() { PixFmt.Yuv422P10Le, PixFmt.Yuv444P10Le, PixFmt.Yuva444P10Le }, PixelFormatDefault = PixFmt.Yuv422P10Le, QualityLevels = ParseUtils.GetEnumStrings(), - QualityDefault = (int)Quality.ProResProfile.Hq, + QualityDefault = (int)Quality.ProResProfile.Standard, }; } @@ -125,12 +125,13 @@ namespace Flowframes.MiscUtils { Codec = Codec.Gif, Name = "gif", - PixelFormats = new List() { PixFmt.Rgb8 }, - PixelFormatDefault = PixFmt.Rgb8, + PixelFormats = new List() { PixFmt.Pal8 }, + PixelFormatDefault = PixFmt.Pal8, QualityLevels = ParseUtils.GetEnumStrings(), QualityDefault = (int)Quality.GifColors.High128, OverideExtension = "gif", MaxFramerate = 50, + Modulo = 1, }; }