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
}
}
}