Files
video-v1/vav1/Vav1Player/Video/VideoPlayer.cs
2025-09-18 01:00:04 +09:00

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})";
}
}
}