Skip to content

Commit d2dcf01

Browse files
authored
Merge pull request #1439 from b-editor/feat/buffered-output
2 parents a7c507d + 4e871f4 commit d2dcf01

File tree

4 files changed

+279
-58
lines changed

4 files changed

+279
-58
lines changed

src/Beutl.Extensions.FFmpeg/Encoding/FFmpegEncodingController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public override async ValueTask Encode(IFrameProvider frameProvider, ISampleProv
253253
private static async ValueTask<MediaFrame?> GetAudioFrame(MediaFrame frame, SampleConverter swr, EncodeState state,
254254
ISampleProvider sampleProvider)
255255
{
256-
if (state.NextPts > sampleProvider.SampleCount)
256+
if (state.NextPts >= sampleProvider.SampleCount)
257257
return null;
258258

259259
using var pcm = await sampleProvider.Sample(state.NextPts, frame.NbSamples);
@@ -283,7 +283,7 @@ private static async ValueTask<bool> WriteAudioFrame(
283283
MediaFrame srcFrame, PixelConverter sws, EncodeState state,
284284
IFrameProvider frameProvider)
285285
{
286-
if (state.NextPts > frameProvider.FrameCount)
286+
if (state.NextPts >= frameProvider.FrameCount)
287287
return null;
288288

289289
using var bitmap = await frameProvider.RenderFrame(state.NextPts);
Lines changed: 117 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,57 @@
11
using System.Reactive.Subjects;
2+
using System.Threading.Channels;
3+
using Beutl.Configuration;
24
using Beutl.Graphics.Rendering;
5+
using Beutl.Logging;
36
using Beutl.Media;
47
using Beutl.Media.Pixel;
58
using Beutl.ProjectSystem;
9+
using Microsoft.Extensions.Logging;
610

711
namespace Beutl.Models;
812

9-
public class FrameProviderImpl(Scene scene, Rational rate, SceneRenderer renderer, Subject<TimeSpan> progress)
10-
: IFrameProvider
13+
public sealed class FrameProviderImpl : IFrameProvider, IDisposable
1114
{
12-
public long FrameCount => (long)(scene.Duration.TotalSeconds * rate.ToDouble());
15+
private readonly ILogger _logger = Log.CreateLogger<FrameProviderImpl>();
16+
private readonly Scene _scene;
17+
private readonly Rational _rate;
18+
private readonly SceneRenderer _renderer;
19+
private readonly Subject<TimeSpan> _progress;
20+
private readonly Channel<(long Frame, Bitmap<Bgra8888> Bitmap)> _channel;
21+
private readonly CancellationTokenSource _cts = new();
22+
private readonly Task _producerTask;
23+
private bool _disposed;
1324

14-
public Rational FrameRate => rate;
25+
public FrameProviderImpl(Scene scene, Rational rate, SceneRenderer renderer, Subject<TimeSpan> progress)
26+
{
27+
_scene = scene;
28+
_rate = rate;
29+
_renderer = renderer;
30+
_progress = progress;
31+
32+
int bufferSize = Preferences.Default.Get("Output.FrameBufferSize", 100);
33+
_channel = Channel.CreateBounded<(long Frame, Bitmap<Bgra8888> Bitmap)>(
34+
new BoundedChannelOptions(bufferSize)
35+
{
36+
FullMode = BoundedChannelFullMode.Wait,
37+
SingleReader = true,
38+
SingleWriter = true,
39+
});
40+
41+
_producerTask = Task.Run(RenderFramesAsync, _cts.Token);
42+
}
43+
44+
public long FrameCount => (long)(_scene.Duration.TotalSeconds * _rate.ToDouble());
45+
46+
public Rational FrameRate => _rate;
1547

1648
private Bitmap<Bgra8888> RenderCore(TimeSpan time)
1749
{
1850
int retry = 0;
1951
Retry:
20-
if (renderer.Render(time + scene.Start))
52+
if (_renderer.Render(time + _scene.Start))
2153
{
22-
return renderer.Snapshot();
54+
return _renderer.Snapshot();
2355
}
2456

2557
if (retry > 3)
@@ -29,25 +61,96 @@ private Bitmap<Bgra8888> RenderCore(TimeSpan time)
2961
goto Retry;
3062
}
3163

32-
public async ValueTask<Bitmap<Bgra8888>> RenderFrame(long frame)
64+
private async ValueTask<Bitmap<Bgra8888>> RenderFrameCore(long frame, CancellationToken cancellationToken)
3365
{
3466
// rate.Numerator, rate.Denominatorを使ってできるだけ正確に
3567
// (frame / (rate.Numerator / rate.Denominator)) * TimeSpan.TicksPerSecond
36-
var time = TimeSpan.FromTicks(frame * rate.Denominator * TimeSpan.TicksPerSecond / rate.Numerator);
68+
var time = TimeSpan.FromTicks(frame * _rate.Denominator * TimeSpan.TicksPerSecond / _rate.Numerator);
69+
70+
if (RenderThread.Dispatcher.CheckAccess())
71+
{
72+
return RenderCore(time);
73+
}
74+
else
75+
{
76+
return await RenderThread.Dispatcher.InvokeAsync(() => RenderCore(time), ct: cancellationToken);
77+
}
78+
}
79+
80+
private async Task RenderFramesAsync()
81+
{
3782
try
3883
{
39-
if (RenderThread.Dispatcher.CheckAccess())
84+
for (long frame = 0; frame < FrameCount && !_cts.Token.IsCancellationRequested; frame++)
4085
{
41-
return RenderCore(time);
86+
var bitmap = await RenderFrameCore(frame, _cts.Token);
87+
await _channel.Writer.WriteAsync((frame, bitmap), _cts.Token);
4288
}
43-
else
89+
}
90+
catch (OperationCanceledException)
91+
{
92+
// Ignore cancellation
93+
}
94+
catch (Exception ex)
95+
{
96+
_logger.LogError(ex, "An error occurred while rendering frames.");
97+
_channel.Writer.TryComplete(ex);
98+
return;
99+
}
100+
101+
_logger.LogDebug("Frame rendering completed.");
102+
_channel.Writer.TryComplete();
103+
}
104+
105+
public async ValueTask<Bitmap<Bgra8888>> RenderFrame(long frame)
106+
{
107+
ObjectDisposedException.ThrowIf(_disposed, this);
108+
109+
var time = TimeSpan.FromTicks(frame * _rate.Denominator * TimeSpan.TicksPerSecond / _rate.Numerator);
110+
_progress.OnNext(time);
111+
112+
while (await _channel.Reader.WaitToReadAsync(_cts.Token))
113+
{
114+
if (_channel.Reader.TryRead(out var item))
44115
{
45-
return await RenderThread.Dispatcher.InvokeAsync(() => RenderCore(time));
116+
if (item.Frame == frame)
117+
{
118+
return item.Bitmap;
119+
}
120+
121+
item.Bitmap.Dispose();
122+
_logger.LogWarning("The frame is misaligned. Requested frame: {RequestedFrame}, Received frame: {ReceivedFrame}", frame, item.Frame);
123+
return await RenderFrameCore(frame, _cts.Token);
46124
}
47125
}
48-
finally
126+
127+
_logger.LogWarning("The frame could not be read from the channel. Frame: {Frame}", frame);
128+
return await RenderFrameCore(frame, _cts.Token);
129+
}
130+
131+
public void Dispose()
132+
{
133+
if (_disposed) return;
134+
_disposed = true;
135+
_cts.Cancel();
136+
137+
if (!_producerTask.IsCompleted)
138+
{
139+
try
140+
{
141+
_producerTask.Wait();
142+
}
143+
catch
144+
{
145+
// ignore
146+
}
147+
}
148+
149+
while (_channel.Reader.TryRead(out var item))
49150
{
50-
progress.OnNext(time);
151+
item.Bitmap.Dispose();
51152
}
153+
154+
_cts.Dispose();
52155
}
53156
}

0 commit comments

Comments
 (0)