Last active
May 17, 2025 08:40
-
-
Save tqk2811/3dee9dfbea3bf660cdb458637bf8bfb8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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