using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Vav1Player.Container; namespace Vav1Player.Video { /// /// Streaming video file reader that loads data on-demand /// public class VideoFileReader : IDisposable { private readonly string _filePath; private readonly FileStream _fileStream; private readonly VideoTrackInfo? _trackInfo; private readonly IVideoContainerParser _parser; private volatile bool _disposed = false; private long _currentSampleIndex = 0; public string FilePath => _filePath; public VideoTrackInfo? TrackInfo => _trackInfo; public long TotalSamples => _trackInfo?.Samples?.Count ?? 0; public long CurrentSampleIndex => _currentSampleIndex; public bool HasMoreData => _currentSampleIndex < TotalSamples; public double EstimatedFrameRate => _trackInfo?.EstimatedFrameRate ?? 30.0; public VideoFileReader(string filePath) { _filePath = filePath; _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 64 * 1024, useAsync: true); // Determine parser based on file extension var extension = Path.GetExtension(filePath).ToLowerInvariant(); _parser = extension switch { ".mp4" => new StreamingMp4Parser(_fileStream), ".webm" or ".mkv" => new StreamingMatroskaParser(_fileStream), _ => throw new NotSupportedException($"Unsupported video format: {extension}") }; // Parse header to get track information _trackInfo = _parser.ParseHeader(); if (_trackInfo == null) { throw new InvalidDataException("No AV1 video track found in file"); } } /// /// Read the next chunk of video data /// public async Task ReadNextChunkAsync(CancellationToken cancellationToken = default) { if (_disposed) { System.Diagnostics.Debug.WriteLine("[VideoFileReader] Disposed, returning null"); return null; } if (!HasMoreData) { System.Diagnostics.Debug.WriteLine($"[VideoFileReader] No more data: {_currentSampleIndex}/{TotalSamples}"); return null; } try { System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Reading chunk {_currentSampleIndex}/{TotalSamples}"); var chunk = await _parser.ReadNextChunkAsync(_currentSampleIndex, cancellationToken); if (chunk != null) { _currentSampleIndex++; System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Successfully read chunk: {chunk}"); } else { System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Parser returned null chunk for index {_currentSampleIndex}"); } return chunk; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Error reading chunk: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Stack trace: {ex.StackTrace}"); return null; } } /// /// Seek to a specific time position /// public async Task SeekToTimeAsync(TimeSpan time, CancellationToken cancellationToken = default) { if (_disposed || _trackInfo == null) return false; try { var targetSampleIndex = await _parser.SeekToTimeAsync(time, cancellationToken); if (targetSampleIndex >= 0) { _currentSampleIndex = targetSampleIndex; return true; } return false; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[VideoFileReader] Error seeking: {ex.Message}"); return false; } } /// /// Reset to beginning of file /// public void Reset() { _currentSampleIndex = 0; } public void Dispose() { if (_disposed) return; _disposed = true; _parser?.Dispose(); _fileStream?.Dispose(); } } /// /// Represents a chunk of video data with timing information /// public class VideoDataChunk { public byte[] Data { get; } public long PresentationTimeMs { get; } public bool IsKeyFrame { get; } public long SampleIndex { get; } public long FileOffset { get; } public VideoDataChunk(byte[] data, long presentationTimeMs, bool isKeyFrame, long sampleIndex, long fileOffset = 0) { Data = data; PresentationTimeMs = presentationTimeMs; IsKeyFrame = isKeyFrame; SampleIndex = sampleIndex; FileOffset = fileOffset; } public override string ToString() { return $"Chunk #{SampleIndex}: {Data.Length} bytes, PTS: {PresentationTimeMs}ms, Key: {IsKeyFrame}"; } } /// /// Video track information /// public class VideoTrackInfo { public int Width { get; init; } public int Height { get; init; } public double Duration { get; init; } public double EstimatedFrameRate { get; init; } public string CodecType { get; init; } = ""; public List? Samples { get; init; } public byte[]? Av1ConfigurationRecord { get; init; } } /// /// Sample information for navigation /// public class SampleInfo { public long Offset { get; init; } public int Size { get; init; } public long PresentationTimeMs { get; init; } public bool IsKeyFrame { get; init; } public byte[]? Data { get; init; } // Pre-extracted pure AV1 data } /// /// Interface for container parsers /// public interface IVideoContainerParser : IDisposable { VideoTrackInfo? ParseHeader(); Task ReadNextChunkAsync(long chunkIndex, CancellationToken cancellationToken = default); Task SeekToTimeAsync(TimeSpan time, CancellationToken cancellationToken = default); } /// /// Streaming MP4 parser /// public class StreamingMp4Parser : IVideoContainerParser { private readonly FileStream _stream; private VideoTrackInfo? _trackInfo; private List? _samples; public StreamingMp4Parser(FileStream stream) { _stream = stream; } public VideoTrackInfo? ParseHeader() { try { // Parse with optimized memory usage var parser = CreateOptimizedMp4Parser(); var tracks = parser.Parse(); var av1Track = tracks.FirstOrDefault(t => t.CodecType == "av01"); if (av1Track != null) { var estimatedFrameRate = av1Track.Duration > 0 ? av1Track.Samples?.Count / av1Track.Duration ?? 30.0 : 30.0; _samples = av1Track.Samples?.Select((s, index) => new SampleInfo { Offset = s.Offset, Size = (int)s.Size, PresentationTimeMs = (long)(index * 1000.0 / estimatedFrameRate), IsKeyFrame = s.IsKeyFrame }).ToList() ?? new List(); _trackInfo = new VideoTrackInfo { Width = (int)av1Track.Width, Height = (int)av1Track.Height, Duration = av1Track.Duration, EstimatedFrameRate = estimatedFrameRate, CodecType = av1Track.CodecType ?? string.Empty, Samples = _samples, Av1ConfigurationRecord = av1Track.Av1ConfigurationRecord }; } return _trackInfo; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[StreamingMp4Parser] Error parsing header: {ex.Message}"); return null; } } public async Task ReadNextChunkAsync(long chunkIndex, CancellationToken cancellationToken = default) { if (_samples == null || chunkIndex >= _samples.Count) return null; try { var sample = _samples[(int)chunkIndex]; _stream.Position = sample.Offset; var buffer = new byte[sample.Size]; var bytesRead = await _stream.ReadAsync(buffer, 0, sample.Size, cancellationToken); if (bytesRead != sample.Size) { System.Diagnostics.Debug.WriteLine($"[StreamingMp4Parser] Expected {sample.Size} bytes, got {bytesRead}"); return null; } return new VideoDataChunk(buffer, sample.PresentationTimeMs, sample.IsKeyFrame, chunkIndex, sample.Offset); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[StreamingMp4Parser] Error reading chunk {chunkIndex}: {ex.Message}"); return null; } } public Task SeekToTimeAsync(TimeSpan time, CancellationToken cancellationToken = default) { if (_samples == null) return Task.FromResult(-1L); var targetTimeMs = (long)time.TotalMilliseconds; // Find the closest keyframe before or at the target time var keyFrames = _samples .Select((sample, index) => new { sample, index }) .Where(x => x.sample.IsKeyFrame && x.sample.PresentationTimeMs <= targetTimeMs) .OrderBy(x => x.sample.PresentationTimeMs) .ToList(); if (keyFrames.Any()) { return Task.FromResult((long)keyFrames.Last().index); } return Task.FromResult(0L); // Return to beginning if no suitable keyframe found } private Mp4Parser CreateOptimizedMp4Parser() { // Find and read only the moov box instead of entire file var buffer = new byte[8192]; // 8KB buffer for reading boxes _stream.Position = 0; while (_stream.Position < _stream.Length) { // Read box header (8 bytes minimum) var headerRead = _stream.Read(buffer, 0, 8); if (headerRead < 8) break; var boxSize = Mp4Reader.ReadUInt32BigEndian(buffer, 0); var boxType = Mp4Reader.ReadFourCC(buffer, 4); if (boxSize == 0) // Box extends to end of file boxSize = (uint)(_stream.Length - _stream.Position + 8); if (boxType == "moov") { // Found metadata box - read only this box var moovSize = boxSize - 8; var moovData = new byte[moovSize]; var totalRead = 0; while (totalRead < moovSize) { var bytesRead = _stream.Read(moovData, totalRead, (int)moovSize - totalRead); if (bytesRead == 0) break; totalRead += bytesRead; } // Create full moov box data for parser var fullMoovData = new byte[boxSize]; Array.Copy(buffer, 0, fullMoovData, 0, 8); Array.Copy(moovData, 0, fullMoovData, 8, (int)moovSize); return new Mp4Parser(fullMoovData); } else { // Skip this box _stream.Position += boxSize - 8; } } throw new InvalidDataException("No moov box found in MP4 file"); } public void Dispose() { // Stream is owned by VideoFileReader, don't dispose here } } /// /// Streaming Matroska parser /// public class StreamingMatroskaParser : IVideoContainerParser { private readonly FileStream _stream; private VideoTrackInfo? _trackInfo; private List? _blocks; public StreamingMatroskaParser(FileStream stream) { _stream = stream; } public VideoTrackInfo? ParseHeader() { try { // Parse with limited memory usage _stream.Position = 0; var parser = CreateOptimizedMatroskaParser(); var tracks = parser.Parse(); var av1Track = tracks.FirstOrDefault(t => t.CodecId == "V_AV1"); if (av1Track != null) { var sortedBlocks = av1Track.Blocks.OrderBy(b => b.Timestamp).ToList(); _blocks = sortedBlocks.Select((block, index) => new SampleInfo { Offset = block.Offset, Size = block.Size, PresentationTimeMs = (long)block.Timestamp, IsKeyFrame = block.IsKeyFrame, Data = block.Data // Include pre-extracted pure AV1 data }).ToList(); // Estimate frame rate from timestamps double frameRate = 30.0; // Default if (sortedBlocks.Count > 1) { var avgTimeDiff = sortedBlocks.Skip(1) .Select((block, i) => (double)(block.Timestamp - sortedBlocks[i].Timestamp)) .Where(diff => diff > 0) .DefaultIfEmpty(33) .Average(); frameRate = 1000.0 / avgTimeDiff; } _trackInfo = new VideoTrackInfo { Width = (int)av1Track.PixelWidth, Height = (int)av1Track.PixelHeight, Duration = sortedBlocks.Count / frameRate, EstimatedFrameRate = frameRate, CodecType = av1Track.CodecId ?? string.Empty, Samples = _blocks, Av1ConfigurationRecord = av1Track.CodecPrivate }; } return _trackInfo; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Error parsing header: {ex.Message}"); return null; } } public async Task ReadNextChunkAsync(long chunkIndex, CancellationToken cancellationToken = default) { if (_blocks == null || chunkIndex >= _blocks.Count) return null; try { var block = _blocks[(int)chunkIndex]; // Use pre-extracted pure AV1 data from MatroskaParser instead of re-reading from file // This ensures we only pass pure AV1 bitstream to the decoder, not WebM container data if (block.Data != null && block.Data.Length > 0) { System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] ✅ Using pre-extracted pure AV1 data: {block.Data.Length} bytes (was {block.Size} in container)"); Console.WriteLine($"[AV1_EXTRACT] Using pure AV1 data: {block.Data.Length} bytes (container size: {block.Size})"); var hexData = string.Join(" ", block.Data.Take(16).Select(b => b.ToString("X2"))); Console.WriteLine($"[AV1_EXTRACT] Pure AV1 data starts with: {hexData}"); return new VideoDataChunk(block.Data, block.PresentationTimeMs, block.IsKeyFrame, chunkIndex, block.Offset); } else { // Fallback to file reading if Data is not available System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Fallback: reading from file at offset {block.Offset}"); _stream.Position = block.Offset; var buffer = new byte[block.Size]; var bytesRead = await _stream.ReadAsync(buffer, 0, block.Size, cancellationToken); if (bytesRead != block.Size) { System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Expected {block.Size} bytes, got {bytesRead}"); return null; } return new VideoDataChunk(buffer, block.PresentationTimeMs, block.IsKeyFrame, chunkIndex, block.Offset); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Error reading chunk {chunkIndex}: {ex.Message}"); return null; } } public Task SeekToTimeAsync(TimeSpan time, CancellationToken cancellationToken = default) { if (_blocks == null) return Task.FromResult(-1L); var targetTimeMs = (long)time.TotalMilliseconds; // Find the closest keyframe before or at the target time var keyFrames = _blocks .Select((block, index) => new { block, index }) .Where(x => x.block.IsKeyFrame && x.block.PresentationTimeMs <= targetTimeMs) .OrderBy(x => x.block.PresentationTimeMs) .ToList(); if (keyFrames.Any()) { return Task.FromResult((long)keyFrames.Last().index); } return Task.FromResult(0L); } private MatroskaParser CreateOptimizedMatroskaParser() { // For Matroska, we need to read the entire file to get correct offsets // The previous approach of reading only 10MB caused offset misalignment System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Reading entire WebM file for accurate parsing ({_stream.Length} bytes)"); var buffer = new byte[_stream.Length]; _stream.Position = 0; var totalRead = 0; while (totalRead < _stream.Length) { var bytesRead = _stream.Read(buffer, totalRead, (int)_stream.Length - totalRead); if (bytesRead == 0) break; totalRead += bytesRead; } System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Successfully read {totalRead} bytes for parsing"); return new MatroskaParser(buffer); } public void Dispose() { // Stream is owned by VideoFileReader, don't dispose here } } }