222 lines
6.5 KiB
C#
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}";
|
|
}
|
|
}
|
|
} |