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;
//