import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:aves/services/cast/cast_fmp4_fragment_parser.dart'; class CastFmp4ProgressiveReaderException implements Exception { final String message; const CastFmp4ProgressiveReaderException( this.message, ); @override String toString() { return 'CastFmp4ProgressiveReaderException: $message'; } } class CastFmp4ProgressiveReader { CastFmp4ProgressiveReader({ required this.sourcePath, required this.pollingInterval, required this.onFragment, }); final String sourcePath; final Duration pollingInterval; final Future Function( Fmp4FragmentInfo fragment, ) onFragment; final CastFmp4FragmentParser _parser = const CastFmp4FragmentParser(); final Map _decodeTimes = {}; RandomAccessFile? _source; bool _running = false; bool _stopRequested = false; bool _conversionCompleted = false; int _nextOffset = 0; int _fragmentCount = 0; _PendingMoof? _pendingMoof; bool get isRunning => _running; int get fragmentCount => _fragmentCount; Map get decodeTimes => Map.unmodifiable(_decodeTimes); Future start() async { if (_running) { throw const CastFmp4ProgressiveReaderException( 'Lettore fMP4 giĆ  in esecuzione', ); } _running = true; _stopRequested = false; try { await _run(); } finally { _running = false; await _closeSource(); } } void notifyConversionCompleted() { _conversionCompleted = true; } void requestStop() { _stopRequested = true; } Future _run() async { final file = File(sourcePath); while (!_stopRequested) { if (!await file.exists()) { if (_conversionCompleted) { throw CastFmp4ProgressiveReaderException( 'File fMP4 non trovato: $sourcePath', ); } await Future.delayed( pollingInterval, ); continue; } _source ??= await file.open( mode: FileMode.read, ); final progressed = await _consumeAvailableBoxes( file, ); if (progressed) { continue; } if (_conversionCompleted) { final fileLength = await file.length(); if (_nextOffset != fileLength) { throw CastFmp4ProgressiveReaderException( 'Conversione terminata con box incompleto: ' 'nextOffset=$_nextOffset ' 'fileLength=$fileLength', ); } if (_pendingMoof != null) { throw const CastFmp4ProgressiveReaderException( 'Conversione terminata con moof senza mdat', ); } return; } await Future.delayed( pollingInterval, ); } } Future _consumeAvailableBoxes( File file, ) async { var progressed = false; while (!_stopRequested) { final fileLength = await file.length(); final header = await _readBoxHeader( fileLength, ); if (header == null) { break; } if (header.end > fileLength) { break; } await _consumeBox(header); _nextOffset = header.end; progressed = true; } return progressed; } Future<_BoxHeader?> _readBoxHeader( int fileLength, ) async { const normalHeaderSize = 8; if (fileLength - _nextOffset < normalHeaderSize) { return null; } final source = _source; if (source == null) { return null; } await source.setPosition(_nextOffset); final normalHeader = await source.read( normalHeaderSize, ); if (normalHeader.length != normalHeaderSize) { return null; } final normalData = ByteData.sublistView( Uint8List.fromList(normalHeader), ); final size32 = normalData.getUint32( 0, Endian.big, ); final type = String.fromCharCodes( normalHeader.sublist(4, 8), ); var headerSize = normalHeaderSize; late final int size; if (size32 == 1) { if (fileLength - _nextOffset < 16) { return null; } final extendedBytes = await source.read( 8, ); if (extendedBytes.length != 8) { return null; } size = ByteData.sublistView( Uint8List.fromList(extendedBytes), ).getUint64( 0, Endian.big, ); headerSize = 16; } else if (size32 == 0) { if (!_conversionCompleted) { return null; } size = fileLength - _nextOffset; } else { size = size32; } if (size < headerSize) { throw CastFmp4ProgressiveReaderException( 'Dimensione box non valida: ' 'offset=$_nextOffset ' 'type=$type ' 'size=$size', ); } return _BoxHeader( offset: _nextOffset, size: size, headerSize: headerSize, type: type, ); } Future _consumeBox( _BoxHeader header, ) async { switch (header.type) { case 'ftyp': case 'moov': return; case 'moof': _consumeMoof(header); return; case 'mdat': await _consumeMdat(header); return; default: return; } } void _consumeMoof( _BoxHeader header, ) { if (_pendingMoof != null) { throw CastFmp4ProgressiveReaderException( 'Nuovo moof prima del relativo mdat: ' 'offset=${header.offset}', ); } _pendingMoof = _PendingMoof( offset: header.offset, size: header.size, ); } Future _consumeMdat( _BoxHeader header, ) async { final pending = _pendingMoof; if (pending == null) { throw CastFmp4ProgressiveReaderException( 'mdat senza moof: ' 'offset=${header.offset}', ); } if (pending.end != header.offset) { throw CastFmp4ProgressiveReaderException( 'moof e mdat non adiacenti: ' 'moofEnd=${pending.end} ' 'mdatOffset=${header.offset}', ); } final moofBytes = await _readRange( offset: pending.offset, length: pending.size, ); final fragment = _parser.parse( moofBytes: moofBytes, moofOffset: pending.offset, mdatOffset: header.offset, mdatSize: header.size, mdatHeaderSize: header.headerSize, initialDecodeTimes: Map.from(_decodeTimes), ); for (final track in fragment.tracks) { _decodeTimes[track.trackId] = track.nextDecodeTime; } await onFragment(fragment); _fragmentCount++; _pendingMoof = null; } Future _readRange({ required int offset, required int length, }) async { final source = _source; if (source == null) { throw const CastFmp4ProgressiveReaderException( 'File fMP4 non aperto', ); } await source.setPosition(offset); final bytes = await source.read(length); if (bytes.length != length) { throw CastFmp4ProgressiveReaderException( 'Lettura incompleta: ' 'offset=$offset ' 'actual=${bytes.length} ' 'expected=$length', ); } return Uint8List.fromList(bytes); } Future _closeSource() async { final source = _source; _source = null; if (source != null) { await source.close(); } } } class _PendingMoof { final int offset; final int size; const _PendingMoof({ required this.offset, required this.size, }); int get end => offset + size; } class _BoxHeader { final int offset; final int size; final int headerSize; final String type; const _BoxHeader({ required this.offset, required this.size, required this.headerSize, required this.type, }); int get end => offset + size; }