302 lines
11 KiB
C#
302 lines
11 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Vav1Player.Decoder;
|
|
using Vav1Player.Rendering;
|
|
|
|
namespace Vav1Player.Video
|
|
{
|
|
public enum PlayerState
|
|
{
|
|
Stopped,
|
|
Loading,
|
|
Playing,
|
|
Paused
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete video player with buffering, decoding, and rendering pipelines
|
|
/// </summary>
|
|
public class VideoPlayer : IDisposable
|
|
{
|
|
private VideoFileReader? _fileReader;
|
|
private VideoDecoderPipeline? _decoderPipeline;
|
|
private VideoRenderingPipeline? _renderingPipeline;
|
|
private readonly FrameBuffer _frameBuffer;
|
|
private readonly Dav1dDecoder _decoder;
|
|
private readonly WpfVideoRenderer _renderer;
|
|
private volatile bool _disposed = false;
|
|
private string? _currentFilePath;
|
|
private PlayerState _currentState = PlayerState.Stopped;
|
|
private readonly object _stateLock = new object();
|
|
|
|
public PlayerState CurrentState
|
|
{
|
|
get
|
|
{
|
|
lock (_stateLock)
|
|
{
|
|
return _currentState;
|
|
}
|
|
}
|
|
private set
|
|
{
|
|
lock (_stateLock)
|
|
{
|
|
if (_currentState != value)
|
|
{
|
|
_currentState = value;
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] State changed to: {value}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsPlaying => CurrentState == PlayerState.Playing;
|
|
public string? CurrentFilePath => _currentFilePath;
|
|
public VideoTrackInfo? TrackInfo => _fileReader?.TrackInfo;
|
|
public TimeSpan CurrentTime => _renderingPipeline?.CurrentPlaybackTime ?? TimeSpan.Zero;
|
|
public double PlaybackSpeed
|
|
{
|
|
get => _renderingPipeline?.PlaybackSpeed ?? 1.0;
|
|
set { if (_renderingPipeline != null) _renderingPipeline.PlaybackSpeed = value; }
|
|
}
|
|
|
|
public VideoPlayer(Dav1dDecoder decoder, WpfVideoRenderer renderer)
|
|
{
|
|
_decoder = decoder ?? throw new ArgumentNullException(nameof(decoder));
|
|
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
|
_frameBuffer = new FrameBuffer(maxBufferSizeMs: 500, maxFrameCount: 30);
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Created with 500ms buffer (30 frames max)");
|
|
}
|
|
|
|
public Task<bool> LoadVideoAsync(string filePath)
|
|
{
|
|
if (_disposed)
|
|
return Task.FromResult(false);
|
|
|
|
CurrentState = PlayerState.Loading;
|
|
Stop(); // Stop current playback if any
|
|
|
|
try
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Loading video: {filePath}");
|
|
|
|
_fileReader = new VideoFileReader(filePath);
|
|
var trackInfo = _fileReader.TrackInfo;
|
|
|
|
if (trackInfo == null)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] No AV1 track found in video file");
|
|
_fileReader.Dispose();
|
|
_fileReader = null;
|
|
CurrentState = PlayerState.Stopped;
|
|
return Task.FromResult(false);
|
|
}
|
|
|
|
_currentFilePath = filePath;
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Video loaded: {trackInfo.Width}x{trackInfo.Height}, " +
|
|
$"{trackInfo.Duration:F2}s, {trackInfo.EstimatedFrameRate:F2} FPS, {_fileReader.TotalSamples} samples");
|
|
|
|
CurrentState = PlayerState.Stopped;
|
|
return Task.FromResult(true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Error loading video: {ex.Message}");
|
|
Stop();
|
|
CurrentState = PlayerState.Stopped;
|
|
return Task.FromResult(false);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> PlayAsync()
|
|
{
|
|
if (_disposed || _fileReader == null || CurrentState == PlayerState.Playing || CurrentState == PlayerState.Loading)
|
|
return false;
|
|
|
|
if (CurrentState == PlayerState.Paused)
|
|
{
|
|
Resume();
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Starting playback");
|
|
CurrentState = PlayerState.Playing;
|
|
|
|
_frameBuffer.Clear();
|
|
|
|
_decoderPipeline = new VideoDecoderPipeline(_fileReader, _decoder, _frameBuffer);
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Waiting for initial buffer fill...");
|
|
var bufferFillStart = DateTime.UtcNow;
|
|
while (_frameBuffer.Count < 10 && !_frameBuffer.IsEndOfStream &&
|
|
DateTime.UtcNow - bufferFillStart < TimeSpan.FromSeconds(5))
|
|
{
|
|
await Task.Delay(50);
|
|
}
|
|
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Initial buffer filled: {_frameBuffer.GetStats()}");
|
|
|
|
if (_frameBuffer.Count == 0 && _frameBuffer.IsEndOfStream)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] No frames decoded, playback failed");
|
|
Stop();
|
|
return false;
|
|
}
|
|
|
|
_renderingPipeline = new VideoRenderingPipeline(_frameBuffer, _renderer);
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Playback started successfully");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Error starting playback: {ex.Message}");
|
|
Stop();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
if (CurrentState != PlayerState.Playing)
|
|
return;
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Pausing playback");
|
|
_decoderPipeline?.Pause();
|
|
_renderingPipeline?.Pause();
|
|
CurrentState = PlayerState.Paused;
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Playback paused");
|
|
}
|
|
|
|
public void Resume()
|
|
{
|
|
if (CurrentState != PlayerState.Paused)
|
|
return;
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Resuming playback");
|
|
_decoderPipeline?.Resume();
|
|
_renderingPipeline?.Resume();
|
|
CurrentState = PlayerState.Playing;
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Playback resumed");
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (CurrentState == PlayerState.Stopped)
|
|
return;
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Stopping playback");
|
|
|
|
_renderingPipeline?.Dispose();
|
|
_renderingPipeline = null;
|
|
|
|
_decoderPipeline?.Dispose();
|
|
_decoderPipeline = null;
|
|
|
|
_fileReader?.Dispose(); // Ensure file reader is disposed
|
|
_fileReader = null;
|
|
|
|
_frameBuffer.Clear();
|
|
_currentFilePath = null;
|
|
|
|
CurrentState = PlayerState.Stopped;
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Playback stopped");
|
|
}
|
|
|
|
public async Task<bool> SeekAsync(TimeSpan time)
|
|
{
|
|
var currentState = CurrentState;
|
|
if (currentState != PlayerState.Playing && currentState != PlayerState.Paused)
|
|
return false;
|
|
|
|
if (_decoderPipeline == null || _renderingPipeline == null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Seeking to {time}");
|
|
|
|
var wasPaused = currentState == PlayerState.Paused;
|
|
if (!wasPaused) _renderingPipeline.Pause(); // Pause rendering during seek
|
|
|
|
var success = await _decoderPipeline.SeekAsync(time);
|
|
if (success)
|
|
{
|
|
_renderingPipeline.Seek(time);
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Seek to {time} completed");
|
|
}
|
|
else
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Seek to {time} failed");
|
|
}
|
|
|
|
if (!wasPaused) _renderingPipeline.Resume(); // Resume rendering after seek
|
|
|
|
return success;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Error seeking: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public PlaybackStats GetStats()
|
|
{
|
|
var bufferStats = _frameBuffer.GetStats();
|
|
var renderingStats = _renderingPipeline?.GetStats() ?? new RenderingStats();
|
|
|
|
return new PlaybackStats
|
|
{
|
|
State = CurrentState,
|
|
CurrentTime = CurrentTime,
|
|
PlaybackSpeed = PlaybackSpeed,
|
|
DecodedFrames = _decoderPipeline?.DecodedFrameCount ?? 0,
|
|
RenderedFrames = renderingStats.RenderedFrameCount,
|
|
DroppedFrames = renderingStats.DroppedFrameCount,
|
|
BufferStats = bufferStats,
|
|
TrackInfo = TrackInfo
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Disposing");
|
|
_disposed = true;
|
|
Stop();
|
|
_frameBuffer?.Dispose();
|
|
System.Diagnostics.Debug.WriteLine("[VideoPlayer] Disposed");
|
|
}
|
|
}
|
|
|
|
public struct PlaybackStats
|
|
{
|
|
public PlayerState State { get; init; }
|
|
public TimeSpan CurrentTime { get; init; }
|
|
public double PlaybackSpeed { get; init; }
|
|
public int DecodedFrames { get; init; }
|
|
public int RenderedFrames { get; init; }
|
|
public int DroppedFrames { get; init; }
|
|
public BufferStats BufferStats { get; init; }
|
|
public VideoTrackInfo? TrackInfo { get; init; }
|
|
|
|
public double DropRate => RenderedFrames + DroppedFrames > 0
|
|
? (double)DroppedFrames / (RenderedFrames + DroppedFrames)
|
|
: 0.0;
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"State: {State}, Time: {CurrentTime.ToString("mm\\:ss\\.fff")} @ {PlaybackSpeed:F1}x, " +
|
|
$"Decoded: {DecodedFrames}, Rendered: {RenderedFrames}, Dropped: {DroppedFrames} ({DropRate:P1}), " +
|
|
$"Buffer: {BufferStats.FrameCount}/{BufferStats.MaxFrameCount} frames ({BufferStats.BufferUtilization:P1})";
|
|
}
|
|
}
|
|
} |