Files
video-v1/vav1/Vav1Player/Video/FrameBuffer.cs
2025-09-17 04:16:34 +09:00

222 lines
6.5 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace Vav1Player.Video
{
/// <summary>
/// Thread-safe frame buffer for video playback
/// Maintains a rolling buffer of decoded frames for smooth playback
/// </summary>
public class FrameBuffer : IDisposable
{
private readonly ConcurrentQueue<VideoFrame> _frames = new();
private readonly SemaphoreSlim _bufferSemaphore;
private readonly int _maxBufferSizeMs;
private readonly int _maxFrameCount;
private volatile bool _disposed = false;
private volatile bool _endOfStream = false;
private long _totalBufferedMs = 0;
private readonly object _statsLock = new();
public int Count => _frames.Count;
public bool IsEndOfStream => _endOfStream;
public long TotalBufferedMs
{
get
{
lock (_statsLock)
{
return _totalBufferedMs;
}
}
}
public FrameBuffer(int maxBufferSizeMs = 500, int maxFrameCount = 30)
{
_maxBufferSizeMs = maxBufferSizeMs;
_maxFrameCount = maxFrameCount;
_bufferSemaphore = new SemaphoreSlim(maxFrameCount, maxFrameCount);
}
/// <summary>
/// Add a frame to the buffer (blocks if buffer is full)
/// </summary>
public async Task<bool> TryEnqueueAsync(VideoFrame frame, CancellationToken cancellationToken = default)
{
if (_disposed)
return false;
try
{
await _bufferSemaphore.WaitAsync(cancellationToken);
// Check buffer size limits
if (Count >= _maxFrameCount)
{
_bufferSemaphore.Release();
return false;
}
lock (_statsLock)
{
// Remove old frames if buffer is too long
while (_totalBufferedMs > _maxBufferSizeMs && _frames.TryDequeue(out var oldFrame))
{
var oldFrameDuration = GetFrameDuration(oldFrame);
_totalBufferedMs -= oldFrameDuration;
oldFrame.Dispose();
_bufferSemaphore.Release();
}
}
_frames.Enqueue(frame);
lock (_statsLock)
{
var frameDuration = GetFrameDuration(frame);
_totalBufferedMs += frameDuration;
}
return true;
}
catch (OperationCanceledException)
{
return false;
}
}
/// <summary>
/// Try to get the next frame to display (non-blocking)
/// </summary>
public bool TryDequeue(out VideoFrame? frame)
{
frame = null;
if (_disposed)
return false;
if (_frames.TryDequeue(out frame))
{
lock (_statsLock)
{
var frameDuration = GetFrameDuration(frame);
_totalBufferedMs -= frameDuration;
}
_bufferSemaphore.Release();
return true;
}
return false;
}
/// <summary>
/// Peek at the next frame without removing it
/// </summary>
public bool TryPeek(out VideoFrame? frame)
{
frame = null;
if (_disposed)
return false;
return _frames.TryPeek(out frame);
}
/// <summary>
/// Wait for a frame to be available or timeout
/// </summary>
public async Task<VideoFrame?> WaitForFrameAsync(int timeoutMs = 100, CancellationToken cancellationToken = default)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
{
if (TryDequeue(out var frame))
return frame;
if (_endOfStream && Count == 0)
return null;
await Task.Delay(1, cancellationToken);
}
return null;
}
/// <summary>
/// Clear all frames from buffer
/// </summary>
public void Clear()
{
lock (_statsLock)
{
while (_frames.TryDequeue(out var frame))
{
frame.Dispose();
_bufferSemaphore.Release();
}
_totalBufferedMs = 0;
}
}
/// <summary>
/// Mark end of stream (no more frames will be added)
/// </summary>
public void MarkEndOfStream()
{
_endOfStream = true;
}
/// <summary>
/// Get buffer statistics
/// </summary>
public BufferStats GetStats()
{
lock (_statsLock)
{
return new BufferStats
{
FrameCount = Count,
BufferedMs = _totalBufferedMs,
MaxBufferMs = _maxBufferSizeMs,
MaxFrameCount = _maxFrameCount,
IsEndOfStream = _endOfStream,
BufferUtilization = (double)Count / _maxFrameCount
};
}
}
private long GetFrameDuration(VideoFrame frame)
{
// Estimate frame duration based on common frame rates
// This could be improved with actual timing information
return 33; // ~30 FPS default
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Clear();
_bufferSemaphore.Dispose();
}
}
public struct BufferStats
{
public int FrameCount { get; init; }
public long BufferedMs { get; init; }
public long MaxBufferMs { get; init; }
public int MaxFrameCount { get; init; }
public bool IsEndOfStream { get; init; }
public double BufferUtilization { get; init; }
public override string ToString()
{
return $"Buffer: {FrameCount} frames, {BufferedMs}ms/{MaxBufferMs}ms ({BufferUtilization:P1}), EOS: {IsEndOfStream}";
}
}
}