Files
video-v1/vav1/Vav1Player/Video/VideoFileReader.cs

522 lines
20 KiB
C#
Raw Normal View History

2025-09-17 04:16:34 +09:00
using System;
using System.IO;
2025-09-18 01:00:04 +09:00
using System.Linq;
using System.Collections.Generic;
2025-09-17 04:16:34 +09:00
using System.Threading;
using System.Threading.Tasks;
using Vav1Player.Container;
namespace Vav1Player.Video
{
/// <summary>
/// Streaming video file reader that loads data on-demand
/// </summary>
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");
}
}
/// <summary>
/// Read the next chunk of video data
/// </summary>
public async Task<VideoDataChunk?> 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;
}
}
/// <summary>
/// Seek to a specific time position
/// </summary>
public async Task<bool> 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;
}
}
/// <summary>
/// Reset to beginning of file
/// </summary>
public void Reset()
{
_currentSampleIndex = 0;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_parser?.Dispose();
_fileStream?.Dispose();
}
}
/// <summary>
/// Represents a chunk of video data with timing information
/// </summary>
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}";
}
}
/// <summary>
/// Video track information
/// </summary>
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<SampleInfo>? Samples { get; init; }
public byte[]? Av1ConfigurationRecord { get; init; }
}
/// <summary>
/// Sample information for navigation
/// </summary>
public class SampleInfo
{
public long Offset { get; init; }
public int Size { get; init; }
public long PresentationTimeMs { get; init; }
public bool IsKeyFrame { get; init; }
2025-09-19 04:42:07 +09:00
public byte[]? Data { get; init; } // Pre-extracted pure AV1 data
2025-09-17 04:16:34 +09:00
}
/// <summary>
/// Interface for container parsers
/// </summary>
public interface IVideoContainerParser : IDisposable
{
VideoTrackInfo? ParseHeader();
Task<VideoDataChunk?> ReadNextChunkAsync(long chunkIndex, CancellationToken cancellationToken = default);
Task<long> SeekToTimeAsync(TimeSpan time, CancellationToken cancellationToken = default);
}
/// <summary>
/// Streaming MP4 parser
/// </summary>
public class StreamingMp4Parser : IVideoContainerParser
{
private readonly FileStream _stream;
private VideoTrackInfo? _trackInfo;
private List<SampleInfo>? _samples;
public StreamingMp4Parser(FileStream stream)
{
_stream = stream;
}
public VideoTrackInfo? ParseHeader()
{
try
{
2025-09-18 01:00:04 +09:00
// Parse with optimized memory usage
var parser = CreateOptimizedMp4Parser();
2025-09-17 04:16:34 +09:00
var tracks = parser.Parse();
var av1Track = tracks.FirstOrDefault(t => t.CodecType == "av01");
if (av1Track != null)
{
2025-09-18 01:00:04 +09:00
var estimatedFrameRate = av1Track.Duration > 0 ? av1Track.Samples?.Count / av1Track.Duration ?? 30.0 : 30.0;
2025-09-17 04:16:34 +09:00
2025-09-18 01:00:04 +09:00
_samples = av1Track.Samples?.Select((s, index) => new SampleInfo
2025-09-17 04:16:34 +09:00
{
Offset = s.Offset,
Size = (int)s.Size,
PresentationTimeMs = (long)(index * 1000.0 / estimatedFrameRate),
IsKeyFrame = s.IsKeyFrame
2025-09-18 01:00:04 +09:00
}).ToList() ?? new List<SampleInfo>();
2025-09-17 04:16:34 +09:00
_trackInfo = new VideoTrackInfo
{
Width = (int)av1Track.Width,
Height = (int)av1Track.Height,
Duration = av1Track.Duration,
EstimatedFrameRate = estimatedFrameRate,
2025-09-18 01:00:04 +09:00
CodecType = av1Track.CodecType ?? string.Empty,
2025-09-17 04:16:34 +09:00
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<VideoDataChunk?> 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<long> 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
}
2025-09-18 01:00:04 +09:00
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");
}
2025-09-17 04:16:34 +09:00
public void Dispose()
{
// Stream is owned by VideoFileReader, don't dispose here
}
}
/// <summary>
/// Streaming Matroska parser
/// </summary>
public class StreamingMatroskaParser : IVideoContainerParser
{
private readonly FileStream _stream;
private VideoTrackInfo? _trackInfo;
private List<SampleInfo>? _blocks;
public StreamingMatroskaParser(FileStream stream)
{
_stream = stream;
}
public VideoTrackInfo? ParseHeader()
{
try
{
2025-09-18 01:00:04 +09:00
// Parse with limited memory usage
2025-09-17 04:16:34 +09:00
_stream.Position = 0;
2025-09-18 01:00:04 +09:00
var parser = CreateOptimizedMatroskaParser();
2025-09-17 04:16:34 +09:00
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,
2025-09-19 04:42:07 +09:00
IsKeyFrame = block.IsKeyFrame,
Data = block.Data // Include pre-extracted pure AV1 data
2025-09-17 04:16:34 +09:00
}).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,
2025-09-18 01:00:04 +09:00
CodecType = av1Track.CodecId ?? string.Empty,
2025-09-17 04:16:34 +09:00
Samples = _blocks,
2025-09-18 01:00:04 +09:00
Av1ConfigurationRecord = av1Track.CodecPrivate
2025-09-17 04:16:34 +09:00
};
}
return _trackInfo;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Error parsing header: {ex.Message}");
return null;
}
}
public async Task<VideoDataChunk?> ReadNextChunkAsync(long chunkIndex, CancellationToken cancellationToken = default)
{
if (_blocks == null || chunkIndex >= _blocks.Count)
return null;
try
{
var block = _blocks[(int)chunkIndex];
2025-09-19 04:42:07 +09:00
// 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)
2025-09-17 04:16:34 +09:00
{
2025-09-19 04:42:07 +09:00
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);
2025-09-17 04:16:34 +09:00
}
2025-09-19 04:42:07 +09:00
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;
2025-09-17 04:16:34 +09:00
2025-09-19 04:42:07 +09:00
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);
}
2025-09-17 04:16:34 +09:00
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Error reading chunk {chunkIndex}: {ex.Message}");
return null;
}
}
public Task<long> 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);
}
2025-09-18 01:00:04 +09:00
private MatroskaParser CreateOptimizedMatroskaParser()
{
2025-09-19 04:42:07 +09:00
// 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)");
2025-09-18 01:00:04 +09:00
2025-09-19 04:42:07 +09:00
var buffer = new byte[_stream.Length];
2025-09-18 01:00:04 +09:00
_stream.Position = 0;
var totalRead = 0;
2025-09-19 04:42:07 +09:00
while (totalRead < _stream.Length)
2025-09-18 01:00:04 +09:00
{
2025-09-19 04:42:07 +09:00
var bytesRead = _stream.Read(buffer, totalRead, (int)_stream.Length - totalRead);
2025-09-18 01:00:04 +09:00
if (bytesRead == 0) break;
totalRead += bytesRead;
}
2025-09-19 04:42:07 +09:00
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Successfully read {totalRead} bytes for parsing");
2025-09-18 01:00:04 +09:00
return new MatroskaParser(buffer);
}
2025-09-17 04:16:34 +09:00
public void Dispose()
{
// Stream is owned by VideoFileReader, don't dispose here
}
}
}