From 14d7f4af0eeea4c6e0bd152b7f54d7531b97d90d Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 30 Mar 2025 11:56:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0MP4=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExportArgs/Mp4ExportArgs.cs | 35 ++++++++ .../Implementations/Exporter/Mp4Exporter.cs | 85 +++++++++++++++++++ SpineViewer/MainForm.Designer.cs | 51 +++++------ 3 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs new file mode 100644 index 0000000..9aa5f26 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// MP4 导出参数 + /// + [ExportImplementation(ExportType.MP4)] + public class Mp4ExportArgs : VideoExportArgs + { + public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + { + // MP4 默认用绿幕 + BackgroundColor = new(0, 255, 0, 0); + } + + /// + /// CRF + /// + [Category("[2] MP4 参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] + public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } + private int crf = 23; + + /// + /// 编码器 TODO: 增加其他编码器 + /// + [Category("[2] MP4 参数"), DisplayName("编码器"), Description("要使用的编码器")] + public string Codec { get => "libx264"; } + } +} diff --git a/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs b/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs new file mode 100644 index 0000000..0c77458 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs @@ -0,0 +1,85 @@ +using FFMpegCore.Pipes; +using FFMpegCore; +using SpineViewer.Exporter.Implementations.ExportArgs; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FFMpegCore.Arguments; +using System.Diagnostics; + +namespace SpineViewer.Exporter.Implementations.Exporter +{ + /// + /// MP4 导出器 + /// + [ExportImplementation(ExportType.MP4)] + public class Mp4Exporter : VideoExporter + { + public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { } + + protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) + { + var args = (Mp4ExportArgs)ExportArgs; + + // 导出单个时必定提供输出文件夹 + var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4"; + var savePath = Path.Combine(args.OutputDir, filename); + + var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS }; + try + { + var ffmpegArgs = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(savePath, true, options => options + .ForceFormat("mp4") + .WithVideoCodec(args.Codec) + .WithConstantRateFactor(args.CRF) + .WithFastStart()); + + logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); + ffmpegArgs.ProcessSynchronously(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + logger.Error("Failed to export mp4 {}", savePath); + } + } + + protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) + { + var args = (Mp4ExportArgs)ExportArgs; + foreach (var spine in spinesToRender) + { + if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 + + // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下 + var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4"; + var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename); + + var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS }; + try + { + var ffmpegArgs = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(savePath, true, options => options + .ForceFormat("mp4") + .WithVideoCodec(args.Codec) + .WithConstantRateFactor(args.CRF) + .WithFastStart()); + + logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); + ffmpegArgs.ProcessSynchronously(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath); + } + } + } + } +} diff --git a/SpineViewer/MainForm.Designer.cs b/SpineViewer/MainForm.Designer.cs index 618be09..b50cd51 100644 --- a/SpineViewer/MainForm.Designer.cs +++ b/SpineViewer/MainForm.Designer.cs @@ -114,88 +114,91 @@ // toolStripMenuItem_Open.Name = "toolStripMenuItem_Open"; toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O; - toolStripMenuItem_Open.Size = new Size(254, 34); + toolStripMenuItem_Open.Size = new Size(270, 34); toolStripMenuItem_Open.Text = "打开(&O)..."; toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click; // // toolStripMenuItem_BatchOpen // toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen"; - toolStripMenuItem_BatchOpen.Size = new Size(254, 34); + toolStripMenuItem_BatchOpen.Size = new Size(270, 34); toolStripMenuItem_BatchOpen.Text = "批量打开(&B)..."; toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click; // // toolStripSeparator1 // toolStripSeparator1.Name = "toolStripSeparator1"; - toolStripSeparator1.Size = new Size(251, 6); + toolStripSeparator1.Size = new Size(267, 6); // // toolStripMenuItem_Export // toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm }); toolStripMenuItem_Export.Name = "toolStripMenuItem_Export"; - toolStripMenuItem_Export.Size = new Size(254, 34); + toolStripMenuItem_Export.Size = new Size(270, 34); toolStripMenuItem_Export.Text = "导出(&E)"; // // toolStripMenuItem_ExportFrame // toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame"; - toolStripMenuItem_ExportFrame.Size = new Size(194, 34); + toolStripMenuItem_ExportFrame.Size = new Size(270, 34); toolStripMenuItem_ExportFrame.Text = "单帧画面..."; toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportFrameSequence // toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence"; - toolStripMenuItem_ExportFrameSequence.Size = new Size(194, 34); + toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34); toolStripMenuItem_ExportFrameSequence.Text = "帧序列..."; toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportGif // toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif"; - toolStripMenuItem_ExportGif.Size = new Size(194, 34); + toolStripMenuItem_ExportGif.Size = new Size(270, 34); toolStripMenuItem_ExportGif.Text = "GIF..."; toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMkv // toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; - toolStripMenuItem_ExportMkv.Size = new Size(194, 34); + toolStripMenuItem_ExportMkv.Size = new Size(270, 34); toolStripMenuItem_ExportMkv.Text = "MKV"; + toolStripMenuItem_ExportMkv.Visible = false; toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMp4 // toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4"; - toolStripMenuItem_ExportMp4.Size = new Size(194, 34); + toolStripMenuItem_ExportMp4.Size = new Size(270, 34); toolStripMenuItem_ExportMp4.Text = "MP4..."; toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMov // toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov"; - toolStripMenuItem_ExportMov.Size = new Size(194, 34); + toolStripMenuItem_ExportMov.Size = new Size(270, 34); toolStripMenuItem_ExportMov.Text = "MOV..."; + toolStripMenuItem_ExportMov.Visible = false; toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportWebm // toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm"; - toolStripMenuItem_ExportWebm.Size = new Size(194, 34); + toolStripMenuItem_ExportWebm.Size = new Size(270, 34); toolStripMenuItem_ExportWebm.Text = "WebM..."; + toolStripMenuItem_ExportWebm.Visible = false; toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click; // // toolStripSeparator2 // toolStripSeparator2.Name = "toolStripSeparator2"; - toolStripSeparator2.Size = new Size(251, 6); + toolStripSeparator2.Size = new Size(267, 6); // // toolStripMenuItem_Exit // toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit"; toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4; - toolStripMenuItem_Exit.Size = new Size(254, 34); + toolStripMenuItem_Exit.Size = new Size(270, 34); toolStripMenuItem_Exit.Text = "退出(&X)"; toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click; // @@ -263,7 +266,7 @@ rtbLog.Margin = new Padding(3, 2, 3, 2); rtbLog.Name = "rtbLog"; rtbLog.ReadOnly = true; - rtbLog.Size = new Size(1758, 130); + rtbLog.Size = new Size(1758, 134); rtbLog.TabIndex = 0; rtbLog.Text = ""; rtbLog.WordWrap = false; @@ -287,7 +290,7 @@ splitContainer_MainForm.Panel2.Controls.Add(rtbLog); splitContainer_MainForm.Panel2.Cursor = Cursors.Default; splitContainer_MainForm.Size = new Size(1758, 1097); - splitContainer_MainForm.SplitterDistance = 959; + splitContainer_MainForm.SplitterDistance = 955; splitContainer_MainForm.SplitterWidth = 8; splitContainer_MainForm.TabIndex = 3; splitContainer_MainForm.TabStop = false; @@ -311,7 +314,7 @@ // splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview); splitContainer_Functional.Panel2.Cursor = Cursors.Default; - splitContainer_Functional.Size = new Size(1758, 959); + splitContainer_Functional.Size = new Size(1758, 955); splitContainer_Functional.SplitterDistance = 759; splitContainer_Functional.SplitterWidth = 8; splitContainer_Functional.TabIndex = 2; @@ -335,7 +338,7 @@ // splitContainer_Information.Panel2.Controls.Add(splitContainer_Config); splitContainer_Information.Panel2.Cursor = Cursors.Default; - splitContainer_Information.Size = new Size(759, 959); + splitContainer_Information.Size = new Size(759, 955); splitContainer_Information.SplitterDistance = 354; splitContainer_Information.SplitterWidth = 8; splitContainer_Information.TabIndex = 1; @@ -349,7 +352,7 @@ groupBox_SkelList.Dock = DockStyle.Fill; groupBox_SkelList.Location = new Point(0, 0); groupBox_SkelList.Name = "groupBox_SkelList"; - groupBox_SkelList.Size = new Size(354, 959); + groupBox_SkelList.Size = new Size(354, 955); groupBox_SkelList.TabIndex = 0; groupBox_SkelList.TabStop = false; groupBox_SkelList.Text = "模型列表"; @@ -360,7 +363,7 @@ spineListView.Location = new Point(3, 26); spineListView.Name = "spineListView"; spineListView.PropertyGrid = propertyGrid_Spine; - spineListView.Size = new Size(348, 930); + spineListView.Size = new Size(348, 926); spineListView.TabIndex = 0; // // propertyGrid_Spine @@ -369,7 +372,7 @@ propertyGrid_Spine.HelpVisible = false; propertyGrid_Spine.Location = new Point(3, 26); propertyGrid_Spine.Name = "propertyGrid_Spine"; - propertyGrid_Spine.Size = new Size(391, 596); + propertyGrid_Spine.Size = new Size(391, 592); propertyGrid_Spine.TabIndex = 0; propertyGrid_Spine.ToolbarVisible = false; propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged; @@ -392,7 +395,7 @@ // splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig); splitContainer_Config.Panel2.Cursor = Cursors.Default; - splitContainer_Config.Size = new Size(397, 959); + splitContainer_Config.Size = new Size(397, 955); splitContainer_Config.SplitterDistance = 326; splitContainer_Config.SplitterWidth = 8; splitContainer_Config.TabIndex = 0; @@ -428,7 +431,7 @@ groupBox_SkelConfig.Dock = DockStyle.Fill; groupBox_SkelConfig.Location = new Point(0, 0); groupBox_SkelConfig.Name = "groupBox_SkelConfig"; - groupBox_SkelConfig.Size = new Size(397, 625); + groupBox_SkelConfig.Size = new Size(397, 621); groupBox_SkelConfig.TabIndex = 0; groupBox_SkelConfig.TabStop = false; groupBox_SkelConfig.Text = "模型参数"; @@ -439,7 +442,7 @@ groupBox_Preview.Dock = DockStyle.Fill; groupBox_Preview.Location = new Point(0, 0); groupBox_Preview.Name = "groupBox_Preview"; - groupBox_Preview.Size = new Size(991, 959); + groupBox_Preview.Size = new Size(991, 955); groupBox_Preview.TabIndex = 1; groupBox_Preview.TabStop = false; groupBox_Preview.Text = "预览画面"; @@ -450,7 +453,7 @@ spinePreviewer.Location = new Point(3, 26); spinePreviewer.Name = "spinePreviewer"; spinePreviewer.PropertyGrid = propertyGrid_Previewer; - spinePreviewer.Size = new Size(985, 930); + spinePreviewer.Size = new Size(985, 926); spinePreviewer.SpineListView = spineListView; spinePreviewer.TabIndex = 0; //