Skip to content

Instantly share code, notes, and snippets.

@tqk2811
Last active May 17, 2025 08:40
Show Gist options
  • Save tqk2811/3dee9dfbea3bf660cdb458637bf8bfb8 to your computer and use it in GitHub Desktop.
Save tqk2811/3dee9dfbea3bf660cdb458637bf8bfb8 to your computer and use it in GitHub Desktop.
using FFmpegArgs;
using FFmpegArgs.Cores;
using FFmpegArgs.Cores.Enums;
using FFmpegArgs.Cores.Maps;
using FFmpegArgs.Cores.Utils;
using FFmpegArgs.Executes;
using FFmpegArgs.Filters.AudioFilters;
using FFmpegArgs.Filters.MultimediaFilters;
using FFmpegArgs.Filters.VideoFilters;
using FFmpegArgs.Filters.VideoSources;
using FFmpegArgs.Inputs;
using FFmpegArgs.Outputs;
using FFMpegCore;
using FFmpegImageTransition.DataClass;
using FFmpegImageTransition.Enums;
using FFmpegImageTransition.UI.ViewModels;
using FFmpegTransition.Enums;
using FFmpegTransition.Interfaces;
using FFmpegTransition.Transitions;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TqkLibrary.Linq;
using TqkLibrary.Queues.TaskQueues;
namespace FFmpegImageTransition.Works
{
internal class RenderWork : BaseWork
{
static RenderSetting RenderSetting => Singleton.Setting.Data.RenderSetting;
readonly ILogger _logger;
readonly string _outputPath;
public const PixFmt DefaultPixFmt = PixFmt.rgba;
//pre calc
Size SizeVideoOutput { get; }
Size SizeImageNeed { get; }
TimeSpan DurationOfTransition { get; }
TimeSpan ZoomDuration { get; }
double ZoomSpeed => RenderSetting.ZoomSetting.ZoomSpeed;
double MaxZoom { get; }
double Fps => RenderSetting.Fps;
int Bitrate => RenderSetting.Bitrate * 1000;
public RenderWork(string outputPath, int threadInex)
{
this._outputPath = outputPath;
this._logger = Singleton.ILoggerFactory.CreateLogger($"RenderWork [Thread {threadInex}]");
this.SizeVideoOutput = new Size(RenderSetting.VideoOutputWitdh, RenderSetting.VideoOutputHeight);
this.DurationOfTransition = TimeSpan.FromMilliseconds(RenderSetting.ImageTransitionSetting.Duration);
this.ZoomDuration = TimeSpan.FromMilliseconds(RenderSetting.ZoomSetting.Duration);
this.MaxZoom = 1 + ZoomSpeed * ZoomDuration.TotalSeconds;
this.SizeImageNeed = new(
(int)(Math.Ceiling(SizeVideoOutput.Width * MaxZoom / 2) * 2),
(int)(Math.Ceiling(SizeVideoOutput.Height * MaxZoom / 2) * 2)
);
}
public override async Task DoWorkAsync()
{
string rootDir = Path.Combine(Singleton.RenderTmpDir, Guid.NewGuid().ToString());
try
{
Directory.CreateDirectory(rootDir);
if (!Directory.Exists(RenderSetting.InputImagesDir))
throw new DirectoryNotFoundException(RenderSetting.InputImagesDir);
if (!Directory.Exists(RenderSetting.OutputVideosDir))
throw new DirectoryNotFoundException(RenderSetting.OutputVideosDir);
_logger.LogInformation($"Start");
var allfiles = RenderSetting.InputImagesDir.DirectoryGetFilesWithExtension("jpg;jpeg;png").ToList();
List<ITransition> transitions = GetTransitions().ToList();
List<RenderItem> renderItems = new List<RenderItem>();
//phân tích file ảnh
Dictionary<IMediaAnalysis, string> dict_MediaAnalysis_File = new();
Dictionary<string, IMediaAnalysis> dict_File_MediaAnalysis = new();
List<string> files = new List<string>();
while (files.Count < RenderSetting.NumberOfImagePerVideo)
{
string? file = allfiles.GetRandomItem(Random.Shared);
if (string.IsNullOrWhiteSpace(file))
{
throw new Exception($"Không đủ tệp đủ điều kiện trong {RenderSetting.InputImagesDir}");
}
allfiles.Remove(file);
IMediaAnalysis mediaAnalysis = await FFProbe.AnalyseAsync(file);
if (mediaAnalysis.PrimaryVideoStream is null) continue;
if (RenderSetting.IsCheckImageSize == false || mediaAnalysis.PrimaryVideoStream.Height > SizeImageNeed.Height)
{
dict_MediaAnalysis_File[mediaAnalysis] = file;
dict_File_MediaAnalysis[file] = mediaAnalysis;
files.Add(file);
}
}
//tạo các hiệu ứng chuyển ảnh
for (int i = 0; i < files.Count - 1; i++)//[0,1,2,3,4] =>
{
string fileBegin = files.Skip(i % files.Count).First();
string fileEnd = files.Skip((i + 1) % files.Count).First();
string fileOutput = $"Transition_{i % files.Count}_{(i + 1) % files.Count}.mp4";
ITransition transition = transitions.GetRandomItem(Random.Shared)!;
renderItems.Add(MakePhotoTransitionEffect(transition, fileBegin, fileEnd, fileOutput));
}
//ghép <ảnh><Transition><ảnh><Transition>......<ảnh><Transition><ảnh>
renderItems.Add(RenderFinal(files));
using StreamWriter sw = new(Path.Combine(Singleton.LogDir, $"Render_{new FileInfo(_outputPath).Name}.log"), true, Encoding.UTF8);
for (int i = 0; i < renderItems.Count; i++)
{
var render = FFmpegRender.FromArguments(
renderItems[i].Arguments,
x => x.WithWorkingDirectory(rootDir).WithFFmpegBinaryPath(Singleton.FFmpeg)
);
render.OnEncodingProgress += (RenderProgress obj) =>
{
_logger.LogInformation($"Render: {i + 1}/{renderItems.Count}: {obj.Time / renderItems[i].Duration * 100:00.00}% ");
};
var result = await render.ExecuteAsync(CancellationToken);
sw.WriteLine(renderItems[i].Arguments);
sw.WriteLine();
foreach (var item in result.ErrorDatas)
sw.WriteLine(item);
sw.WriteLine("_________________________________________________________________________________");
sw.WriteLine();
sw.WriteLine();
result.EnsureSuccess();
}
}
catch (Exception ex)
{
_logger.LogErrorFunction(ex);
FileInfo fileInfo = new FileInfo(_outputPath);
if (fileInfo.Exists)
{
try { fileInfo.Delete(); } catch { }
}
}
finally
{
try { Directory.Delete(rootDir, true); } catch { }
}
}
class RenderItem
{
public required string Arguments { get; set; }
public required TimeSpan Duration { get; set; }
}
IEnumerable<ITransition> GetTransitions()
{
foreach (var item in Enum.GetValues<TransitionType>()
.Except(TransitionType.None)
.Where(x => RenderSetting.ImageTransitionSetting.TransitionType.HasFlag(x)))
{
switch (item)
{
case TransitionType.Clock:
yield return new ClockTransition() { Contrary = true };
yield return new ClockTransition() { Contrary = false };
break;
case TransitionType.Collapse:
yield return new CollapseTransition(CollapseExpandMode.Circular);
yield return new CollapseTransition(CollapseExpandMode.Both);
yield return new CollapseTransition(CollapseExpandMode.Vertical);
yield return new CollapseTransition(CollapseExpandMode.Horizontal);
break;
case TransitionType.CrossFade:
yield return new CrossFadeTransition();
break;
case TransitionType.Expand:
yield return new ExpandTransition(CollapseExpandMode.Circular);
yield return new ExpandTransition(CollapseExpandMode.Both);
yield return new ExpandTransition(CollapseExpandMode.Vertical);
yield return new ExpandTransition(CollapseExpandMode.Horizontal);
break;
case TransitionType.FadeInTwo:
yield return new FadeInTwoTransition();
break;
}
}
}
RenderItem MakePhotoTransitionEffect(ITransition transition, string fileBegin, string fileEnd, string fileOutput)
{
FFmpegArg ffmpegArg = new FFmpegArg()
.OverWriteOutput()
.VSync(VSyncMethod.cfr);
ImageFileInput beginImageFileInput = new ImageFileInput(fileBegin)
.SetOption("-loop", 1)
.StreamLoop(-1)
.Duration(DurationOfTransition);
ImageMap beginImageMap = ffmpegArg.AddImagesInput(beginImageFileInput)
.First();
beginImageMap = GetImageAtEndOfZoom(beginImageMap, ScreenMode.Blur);
ImageFileInput endImageFileInput = new ImageFileInput(fileEnd)
.SetOption("-loop", 1)
.StreamLoop(-1)
.Duration(DurationOfTransition);
ImageMap endImageMap = ffmpegArg.AddImagesInput(endImageFileInput)
.First();
endImageMap = GetImageAtStartOfZoom(endImageMap, ScreenMode.Blur);
ImageMap imageMap = transition.MakeTransition(
beginImageMap,
endImageMap,
DurationOfTransition,
Fps
);
imageMap = imageMap.FormatFilter(PixFmt.yuv420p).MapOut;
ImageFileOutput imageFileOutput = new ImageFileOutput(fileOutput, imageMap)
.Duration(DurationOfTransition);
imageFileOutput.ImageOutputAVStream
.Codec("libx264")
.Fps(Fps)
.B(Bitrate)
.SetOption("-g", "0")
.SetOption("-rc-lookahead", "0");
ffmpegArg.AddOutput(imageFileOutput);
return new RenderItem()
{
Arguments = ffmpegArg.GetFullCommandline(),
Duration = DurationOfTransition
};
ImageMap GetImageAtEndOfZoom(ImageMap imageMap, ScreenMode screenMode)
{
return RenderSetting.ZoomSetting.ZoomMode switch
{
ZoomMode.In => imageMap.SizeSynchronizationImage(SizeImageNeed, screenMode)
.CropFilter()
.W(SizeVideoOutput.Width)
.H(SizeVideoOutput.Height)
.MapOut,
ZoomMode.Out => imageMap.SizeSynchronizationImage(SizeVideoOutput, screenMode),
_ => throw new NotSupportedException()
};
}
ImageMap GetImageAtStartOfZoom(ImageMap imageMap, ScreenMode screenMode)
{
return RenderSetting.ZoomSetting.ZoomMode switch
{
ZoomMode.Out => imageMap.SizeSynchronizationImage(SizeImageNeed, screenMode)
.CropFilter()
.W(SizeVideoOutput.Width)
.H(SizeVideoOutput.Height)
.MapOut,
ZoomMode.In => imageMap.SizeSynchronizationImage(SizeVideoOutput, screenMode),
_ => throw new NotSupportedException()
};
}
}
RenderItem RenderFinal(IReadOnlyList<string> files)
{
FFmpegArg ffmpegArg = new FFmpegArg()
.OverWriteOutput()
.VSync(VSyncMethod.cfr);
List<ImageMap> imageMaps = files
.Select(x =>
{
ImageFileInput imageFileInput = new ImageFileInput(x)
.SetOption("-loop", 1)
.StreamLoop(-1)
.Duration(ZoomDuration);
ImageMap imageMap = ffmpegArg.AddImagesInput(imageFileInput).First();
switch (RenderSetting.ZoomSetting.ZoomMethod)
{
case ZoomMethod.ScaleAndCrop:
imageMap = ScaleAndCrop(imageMap);
break;
case ZoomMethod.Geq:
imageMap = Geq(imageMap);
break;
case ZoomMethod.OpenCv:
imageMap = OpenCv(imageMap);
break;
default:
throw new NotSupportedException(RenderSetting.ZoomSetting.ZoomMethod.ToString());
}
imageMap = imageMap.SetSarFilter().Ratio(1).MapOut;
return imageMap;
})
.ToList();
List<ImageMap> Transitions = files
.SkipLast(1)
.Select((x, i) =>
{
ImageFileInput imageFileInput = new ImageFileInput($"Transition_{i % files.Count}_{(i + 1) % files.Count}.mp4");
ImageMap imageMap = ffmpegArg.AddImagesInput(imageFileInput).First()
.FormatFilter(DefaultPixFmt).MapOut
.SetSarFilter().Ratio(1).MapOut
;
return imageMap;
})
.ToList();
List<ConcatGroup> concatGroups = new();
for (int i = 0; i < imageMaps.Count; i++)
{
concatGroups.Add(new ConcatGroup(imageMaps[i]));
if (i < Transitions.Count)
concatGroups.Add(new ConcatGroup(Transitions[i]));
}
ImageMap imageMap = concatGroups.ConcatFilter().ImageMapsOut.First()
.FpsFilter().Fps(Fps).MapOut
.FormatFilter(DefaultPixFmt).MapOut
;
ImageFileOutput imageFileOutput = new ImageFileOutput(_outputPath, imageMap);
imageFileOutput.ImageOutputAVStream
.Codec("libx264")
.Fps(Fps)
.B(Bitrate)
.SetOption("-g", "0")
.SetOption("-rc-lookahead", "0");
ffmpegArg.AddOutput(imageFileOutput);
return new RenderItem()
{
Arguments = ffmpegArg.GetFullCommandline(),
Duration = DurationOfTransition * Transitions.Count + ZoomDuration * files.Count,
};
}
ImageMap ScaleAndCrop(ImageMap imageMap)
{
string zoomExpr = RenderSetting.ZoomSetting.ZoomMode switch
{
ZoomMode.In => $"(1+t*{ZoomSpeed})",
ZoomMode.Out => $"(1+{ZoomSpeed * ZoomDuration.TotalSeconds}-t*{ZoomSpeed})",
_ => throw new NotSupportedException(RenderSetting.ZoomSetting.ZoomMode.ToString())
};
string scale_w = $"trunc(iw*{zoomExpr}/2)*2";
string scale_h = $"trunc(ih*{zoomExpr}/2)*2";
(string crop_x, string crop_y) = RenderSetting.ZoomSetting.ZoomMode switch
{
//iw ih là từ frame đầu tiên, không thay đổi trong các frame tiếp theo
ZoomMode.In => (//nhỏ -> to
$"({scale_w}-ow)/2",
$"({scale_h}-oh)/2"
),
ZoomMode.Out => (//to -> nhỏ => iw đã nhân sẵn 1 + ZoomSpeed * ZoomDuration.TotalSeconds và LÀM TRÒN => chia ra để có số gốc rồi nhân tỉ lệ
$"(trunc({SizeImageNeed.Width}*{zoomExpr}/2)*2-ow)/2",
$"(trunc({SizeImageNeed.Height}*{zoomExpr}/2)*2-oh)/2"
),
_ => throw new NotSupportedException(RenderSetting.ZoomSetting.ZoomMode.ToString())
};
imageMap = imageMap
.SizeSynchronizationImage(SizeImageNeed, ScreenMode.Blur)
.SetPtsFilter($"PTS-STARTPTS").MapOut//reset t
.FormatFilter(DefaultPixFmt).MapOut
.ScaleFilter()
.W(scale_w)
.H(scale_h)
.CustomOption()
.MapOut
.CropFilter()
.W(SizeImageNeed.Width)
.H(SizeImageNeed.Height)
.X(crop_x)
.Y(crop_y)
.Exact(true)
.MapOut
.ScaleFilter()
.W(SizeVideoOutput.Width)
.H(SizeVideoOutput.Height)
.CustomOption()
.MapOut;
return imageMap;
}
ImageMap Geq(ImageMap imageMap)
{
throw new NotImplementedException();
}
ImageMap OpenCv(ImageMap imageMap)
{
throw new NotImplementedException();
}
}
static class RenderExtension
{
public static ScaleFilter CustomOption(this ScaleFilter scaleFilter)
=> scaleFilter
.Flags(SwsFlags.bilinear)
.Interl(ScaleInterl.Optional)
.Eval(ScaleEval.Frame)
;
static ImageMap FixImageInput(this ImageMap imageMap)
=> imageMap
.FormatFilter(RenderWork.DefaultPixFmt).MapOut
.SetPtsFilter($"PTS-STARTPTS").MapOut
.SetSarFilter()
.Ratio(1).MapOut
.ScaleFilter()
.W("trunc(iw/2)*2")
.H("trunc(ih/2)*2")
.CustomOption()
.MapOut
;
internal static ImageMap SizeSynchronizationImage(
this ImageMap imageMap,
Size size,
ScreenMode screenMode
)
{
imageMap = imageMap.FixImageInput();
switch (screenMode)
{
case ScreenMode.Center:
imageMap = imageMap
.ScaleFilter()
.W($"if(gte(iw/ih,{size.Width}/{size.Height}),min(iw,{size.Width}),-1)")
.H($"if(gte(iw/ih,{size.Width}/{size.Height}),-1,min(ih,{size.Height}))")
.CustomOption()
.MapOut
.ScaleFilter()
.W("trunc(iw/2)*2")
.H("trunc(ih/2)*2")
.CustomOption()
.MapOut
.SetSarFilter().Ratio("1/1").MapOut
//.FpsFilter().Fps(fps).MapOut
.FormatFilter(RenderWork.DefaultPixFmt).MapOut;
break;
case ScreenMode.Crop:
imageMap = imageMap
.ScaleFilter()
.W($"if(gte(iw/ih,{size.Width}/{size.Height}),-1,{size.Width})")
.H($"if(gte(iw/ih,{size.Width}/{size.Height}),{size.Height},-1)")
.CustomOption()
.MapOut
.CropFilter()
.W($"{size.Width}")
.H($"{size.Height}").MapOut
.SetSarFilter().Ratio("1/1").MapOut
//.FpsFilter().Fps(fps).MapOut
.FormatFilter(RenderWork.DefaultPixFmt).MapOut;
break;
case ScreenMode.Scale:
imageMap = imageMap
.ScaleFilter()
.W($"{size.Width}")
.H($"{size.Height}")
.CustomOption()
.MapOut
.SetSarFilter().Ratio("1/1").MapOut
//.FpsFilter().Fps(fps).MapOut
.FormatFilter(RenderWork.DefaultPixFmt).MapOut;
break;
case ScreenMode.Blur:
imageMap = imageMap.MakeBlurredBackground(size);
break;
}
return imageMap;
}
static ImageMap MakeBlurredBackground(
this ImageMap image,
Size size,
//int fps = 24,
int lumaRadius = 90
)
{
List<ImageMap> inputs = new List<ImageMap>();
if (image.IsInput)
{
inputs.Add(image);
inputs.Add(image);
}
else
{
inputs.AddRange(image.SplitFilter(2).MapsOut);
}
var blurred = inputs.First()
.ScaleFilter()
.W(size.Width)
.H(size.Height)
.CustomOption()
.MapOut
.SetSarFilter().Ratio("1/1").MapOut
//.FpsFilter().Fps(fps).MapOut
.FormatFilter(RenderWork.DefaultPixFmt).MapOut
.BoxBlurFilter().LumaRadius($"{lumaRadius}").MapOut
.SetSarFilter().Ratio("1/1").MapOut;
var raw = inputs.Last()
.ScaleFilter()
//.W($"if(gte(iw/ih,{size.Width}/{size.Height}),min(iw,{size.Width}),-1)")
//.H($"if(gte(iw/ih,{size.Width}/{size.Height}),-1,min(ih,{size.Height}))")
.W(-1)//tự động theo H
.H(size.Height)
.CustomOption()
.MapOut
.CropFilter()
.W($"min({size.Width},iw)")
.H($"min({size.Width},ih)")
.MapOut
.SetSarFilter()
.Ratio("1/1")
.MapOut
//.FpsFilter().Fps(fps).MapOut
.FormatFilter(RenderWork.DefaultPixFmt)
.MapOut;
return raw
.OverlayFilterOn(blurred)
.X("(main_w-overlay_w)/2")
.Y("(main_h-overlay_h)/2").MapOut//center
.SetPtsFilter("PTS-STARTPTS").MapOut;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment