Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public override async ValueTask Encode(IFrameProvider frameProvider, ISampleProv
private static async ValueTask<MediaFrame?> GetAudioFrame(MediaFrame frame, SampleConverter swr, EncodeState state,
ISampleProvider sampleProvider)
{
if (state.NextPts > sampleProvider.SampleCount)
if (state.NextPts >= sampleProvider.SampleCount)
return null;

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

using var bitmap = await frameProvider.RenderFrame(state.NextPts);
Expand Down
131 changes: 117 additions & 14 deletions src/Beutl/Models/FrameProviderImpl.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
using System.Reactive.Subjects;
using System.Threading.Channels;
using Beutl.Configuration;
using Beutl.Graphics.Rendering;
using Beutl.Logging;
using Beutl.Media;
using Beutl.Media.Pixel;
using Beutl.ProjectSystem;
using Microsoft.Extensions.Logging;

namespace Beutl.Models;

public class FrameProviderImpl(Scene scene, Rational rate, SceneRenderer renderer, Subject<TimeSpan> progress)
: IFrameProvider
public sealed class FrameProviderImpl : IFrameProvider, IDisposable
{
public long FrameCount => (long)(scene.Duration.TotalSeconds * rate.ToDouble());
private readonly ILogger _logger = Log.CreateLogger<FrameProviderImpl>();
private readonly Scene _scene;
private readonly Rational _rate;
private readonly SceneRenderer _renderer;
private readonly Subject<TimeSpan> _progress;
private readonly Channel<(long Frame, Bitmap<Bgra8888> Bitmap)> _channel;
private readonly CancellationTokenSource _cts = new();
private readonly Task _producerTask;
private bool _disposed;

public Rational FrameRate => rate;
public FrameProviderImpl(Scene scene, Rational rate, SceneRenderer renderer, Subject<TimeSpan> progress)
{
_scene = scene;
_rate = rate;
_renderer = renderer;
_progress = progress;

int bufferSize = Preferences.Default.Get("Output.FrameBufferSize", 100);
_channel = Channel.CreateBounded<(long Frame, Bitmap<Bgra8888> Bitmap)>(
new BoundedChannelOptions(bufferSize)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true,
});

_producerTask = Task.Run(RenderFramesAsync, _cts.Token);
}

public long FrameCount => (long)(_scene.Duration.TotalSeconds * _rate.ToDouble());

public Rational FrameRate => _rate;

private Bitmap<Bgra8888> RenderCore(TimeSpan time)
{
int retry = 0;
Retry:
if (renderer.Render(time + scene.Start))
if (_renderer.Render(time + _scene.Start))
{
return renderer.Snapshot();
return _renderer.Snapshot();
}

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

public async ValueTask<Bitmap<Bgra8888>> RenderFrame(long frame)
private async ValueTask<Bitmap<Bgra8888>> RenderFrameCore(long frame, CancellationToken cancellationToken)
{
// rate.Numerator, rate.Denominatorを使ってできるだけ正確に
// (frame / (rate.Numerator / rate.Denominator)) * TimeSpan.TicksPerSecond
var time = TimeSpan.FromTicks(frame * rate.Denominator * TimeSpan.TicksPerSecond / rate.Numerator);
var time = TimeSpan.FromTicks(frame * _rate.Denominator * TimeSpan.TicksPerSecond / _rate.Numerator);

if (RenderThread.Dispatcher.CheckAccess())
{
return RenderCore(time);
}
else
{
return await RenderThread.Dispatcher.InvokeAsync(() => RenderCore(time), ct: cancellationToken);
}
}

private async Task RenderFramesAsync()
{
try
{
if (RenderThread.Dispatcher.CheckAccess())
for (long frame = 0; frame < FrameCount && !_cts.Token.IsCancellationRequested; frame++)
{
return RenderCore(time);
var bitmap = await RenderFrameCore(frame, _cts.Token);
await _channel.Writer.WriteAsync((frame, bitmap), _cts.Token);
}
else
}
catch (OperationCanceledException)
{
// Ignore cancellation
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while rendering frames.");
_channel.Writer.TryComplete(ex);
return;
}

_logger.LogDebug("Frame rendering completed.");
_channel.Writer.TryComplete();
}

public async ValueTask<Bitmap<Bgra8888>> RenderFrame(long frame)
{
ObjectDisposedException.ThrowIf(_disposed, this);

var time = TimeSpan.FromTicks(frame * _rate.Denominator * TimeSpan.TicksPerSecond / _rate.Numerator);
_progress.OnNext(time);

while (await _channel.Reader.WaitToReadAsync(_cts.Token))
{
if (_channel.Reader.TryRead(out var item))
{
return await RenderThread.Dispatcher.InvokeAsync(() => RenderCore(time));
if (item.Frame == frame)
{
return item.Bitmap;
}

item.Bitmap.Dispose();
_logger.LogWarning("The frame is misaligned. Requested frame: {RequestedFrame}, Received frame: {ReceivedFrame}", frame, item.Frame);
return await RenderFrameCore(frame, _cts.Token);
}
}
finally

_logger.LogWarning("The frame could not be read from the channel. Frame: {Frame}", frame);
return await RenderFrameCore(frame, _cts.Token);
}

public void Dispose()
{
if (_disposed) return;
_disposed = true;
_cts.Cancel();

if (!_producerTask.IsCompleted)
{
try
{
_producerTask.Wait();
}
catch
{
// ignore
}
}

while (_channel.Reader.TryRead(out var item))
{
progress.OnNext(time);
item.Bitmap.Dispose();
}

_cts.Dispose();
}
}
Loading
Loading