diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ba0178a..5f4b0269 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# CHANGELOG
+## v0.16.14
+
+- 优化 FFmpeg 导出速度
+- 修复可能的内存泄漏
+
## v0.16.13
- 增加 FFmpeg 下载页面跳转菜单项
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3f382ae3..2865d835 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,9 +2,10 @@
## 仓库分支
-仓库目前包含 4 个分支:
+仓库目前包含以下分支:
-- `main`: 默认分支, 也是项目最新版的发布用分支
+- `main`: 默认分支, 也是项目最新版的代码分支
+- `release/wpf`: WPF 版本发布分支
- `dev/wpf`: WPF 版本开发分支
- `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复)
- `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复)
diff --git a/Spine/Exporters/BaseExporter.cs b/Spine/Exporters/BaseExporter.cs
index 0679eb1d..f8b537a9 100644
--- a/Spine/Exporters/BaseExporter.cs
+++ b/Spine/Exporters/BaseExporter.cs
@@ -160,14 +160,15 @@ public float Rotation
///
/// 获取的一帧, 结果是预乘的
///
- protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines)
+ protected SFMLImageVideoFrame GetFrame(SpineObject[] spines)
{
_renderTexture.SetActive(true);
_renderTexture.Clear(_backgroundColorPma);
foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp);
_renderTexture.Display();
+ var frame = new SFMLImageVideoFrame(_renderTexture.Texture.CopyToImage());
_renderTexture.SetActive(false);
- return new(_renderTexture.Texture.CopyToImage());
+ return frame;
}
///
diff --git a/Spine/Exporters/CustomFFmpegExporter.cs b/Spine/Exporters/CustomFFmpegExporter.cs
index 016ae493..bc0af230 100644
--- a/Spine/Exporters/CustomFFmpegExporter.cs
+++ b/Spine/Exporters/CustomFFmpegExporter.cs
@@ -65,22 +65,6 @@ private void SetOutputOptions(FFMpegArgumentOptions options)
if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}");
}
- ///
- /// 获取的一帧, 结果是预乘的
- ///
- protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
- {
- // BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
- // 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
- using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
- using var view = _renderTexture.GetView();
- tex.SetView(view);
- tex.Clear(_backgroundColorPma);
- foreach (var sp in spines.Reverse()) tex.Draw(sp);
- tex.Display();
- return new(tex.Texture.CopyToImage());
- }
-
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
{
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
diff --git a/Spine/Exporters/FFmpegVideoExporter.cs b/Spine/Exporters/FFmpegVideoExporter.cs
index 860e397a..10dbfd2a 100644
--- a/Spine/Exporters/FFmpegVideoExporter.cs
+++ b/Spine/Exporters/FFmpegVideoExporter.cs
@@ -104,22 +104,6 @@ public enum MovProfile
public MovProfile Profile { get => _profile; set => _profile = value; }
private MovProfile _profile = MovProfile.Yuv4444Extreme;
- ///
- /// 获取的一帧, 结果是预乘的
- ///
- protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
- {
- // BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
- // 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
- using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
- using var view = _renderTexture.GetView();
- tex.SetView(view);
- tex.Clear(_backgroundColorPma);
- foreach (var sp in spines.Reverse()) tex.Draw(sp);
- tex.Display();
- return new(tex.Texture.CopyToImage());
- }
-
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
{
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
diff --git a/Spine/Exporters/VideoExporter.cs b/Spine/Exporters/VideoExporter.cs
index 9cf2a97d..a3211a69 100644
--- a/Spine/Exporters/VideoExporter.cs
+++ b/Spine/Exporters/VideoExporter.cs
@@ -16,6 +16,9 @@ namespace Spine.Exporters
///
public abstract class VideoExporter : BaseExporter
{
+ private readonly object _frameOutputLock = new();
+ private SFMLImageVideoFrame? _frameOutput;
+
public VideoExporter(uint width, uint height) : base(width, height) { }
public VideoExporter(Vector2u resolution) : base(resolution) { }
@@ -83,11 +86,10 @@ public int GetFrameCount()
{
var delta = 1f / _fps;
var total = (int)(_duration * _fps); // 完整帧的数量
-
var deltaFinal = _duration - delta * total; // 最后一帧时长
- var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0;
+ bool hasFinal = _keepLast && deltaFinal > 1e-3;
- var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
+ var frameCount = 1 + total + (hasFinal ? 1 : 0); // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
return frameCount;
}
@@ -98,7 +100,8 @@ protected IEnumerable GetFrames(SpineObject[] spines)
{
float delta = 1f / _fps;
int total = (int)(_duration * _fps); // 完整帧的数量
- bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3;
+ var deltaFinal = _duration - delta * total; // 最后一帧时长
+ bool hasFinal = _keepLast && deltaFinal > 1e-3;
// 导出首帧
var firstFrame = GetFrame(spines);
@@ -114,12 +117,39 @@ protected IEnumerable GetFrames(SpineObject[] spines)
// 导出最后一帧
if (hasFinal)
{
- // XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
- foreach (var spine in spines) spine.Update(delta * _speed);
+ foreach (var spine in spines) spine.Update(deltaFinal * _speed);
yield return GetFrame(spines);
}
}
+ ///
+ /// 帧渲染任务, 用于保证每一帧的渲染都在同一个线程里完成
+ ///
+ private void GetFramesTask(SpineObject[] spines, CancellationToken ct)
+ {
+ // XXX: 也许和 SFML 多线程或者 FFmpeg 调用有关, GetFrame 无法在异步调用中连续使用, 会导致画面帧丢失或者卡死等异常现象
+ // 因此把帧生成包在一个子线程中连续调用, 通过成员变量和锁来输出帧数据
+ foreach (var frame in GetFrames(spines))
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ // 等待之前的数据被取走
+ lock (_frameOutputLock)
+ {
+ if (_frameOutput is null)
+ break;
+ }
+ Thread.Sleep(10);
+ }
+ if (ct.IsCancellationRequested)
+ {
+ frame.Dispose();
+ break;
+ }
+ _frameOutput = frame;
+ }
+ }
+
///
/// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出
///
@@ -127,21 +157,43 @@ protected IEnumerable GetFrames(SpineObject[] spines, strin
{
int frameCount = GetFrameCount();
int frameIdx = 0;
+ SFMLImageVideoFrame frame = null;
+
+ using var getFramesTask = Task.Run(() => GetFramesTask(spines, ct), ct);
_progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}");
- foreach (var frame in GetFrames(spines))
+ while (frameIdx < frameCount)
{
+ while (!ct.IsCancellationRequested)
+ {
+ // 等待新帧的生成
+ lock (_frameOutputLock)
+ {
+ if (_frameOutput is not null)
+ {
+ frame = _frameOutput;
+ _frameOutput = null;
+ break;
+ }
+ }
+
+ Thread.Sleep(10);
+ }
+
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
- frame.Dispose();
+ frame?.Dispose();
break;
}
_progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {output}");
yield return frame;
+ frame = null;
frameIdx++;
}
+
+ getFramesTask.Wait(CancellationToken.None); // 等待结束 (正常结束或者被取消)
}
public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines);
diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj
index 054c2638..34cf0d5e 100644
--- a/Spine/Spine.csproj
+++ b/Spine/Spine.csproj
@@ -11,7 +11,7 @@
$(SolutionDir)out
$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)
false
- 0.16.13
+ 0.16.14
diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj
index 113468b2..114cfdcb 100644
--- a/SpineViewer/SpineViewer.csproj
+++ b/SpineViewer/SpineViewer.csproj
@@ -11,7 +11,7 @@
$(SolutionDir)out
$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)
false
- 0.16.14-beta
+ 0.16.14
WinExe
true
diff --git a/SpineViewerCLI/SpineViewerCLI.csproj b/SpineViewerCLI/SpineViewerCLI.csproj
index 6b6b54c5..e787d461 100644
--- a/SpineViewerCLI/SpineViewerCLI.csproj
+++ b/SpineViewerCLI/SpineViewerCLI.csproj
@@ -11,7 +11,7 @@
$(SolutionDir)out
$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)
false
- 0.16.13
+ 0.16.14
Exe