// ignore_for_file: avoid_print import 'dart:io'; import 'dart:typed_data'; import 'package:aves/services/cast/cast_fmp4_fragment_parser.dart'; Future main(List arguments) async { final sourcePath = arguments.isNotEmpty ? arguments.first : 'stream_test.fmp4'; final sourceFile = File(sourcePath); if (!await sourceFile.exists()) { throw StateError( 'File fMP4 non trovato: ${sourceFile.absolute.path}', ); } const maximumFragments = 5; const parser = CastFmp4FragmentParser(); final source = await sourceFile.open( mode: FileMode.read, ); final fileLength = await source.length(); final decodeTimes = { 1: 0, 2: 0, }; var offset = 0; var fragmentIndex = 0; _BoxInfo? pendingMoof; try { while (offset + 8 <= fileLength && fragmentIndex < maximumFragments) { final box = await _readBox( source: source, offset: offset, fileLength: fileLength, ); if (box == null) { break; } if (box.type == 'moof') { if (pendingMoof != null) { throw StateError( 'Nuovo moof prima del relativo mdat: ' 'offset=${box.offset}', ); } pendingMoof = box; } else if (box.type == 'mdat') { final moof = pendingMoof; if (moof == null) { throw StateError( 'mdat senza moof: offset=${box.offset}', ); } if (moof.end != box.offset) { throw StateError( 'moof e mdat non adiacenti: ' 'moofEnd=${moof.end} ' 'mdatOffset=${box.offset}', ); } final moofBytes = await _readRange( source: source, offset: moof.offset, length: moof.size, ); final initialDecodeTimes = Map.from(decodeTimes); final fragment = parser.parse( moofBytes: moofBytes, moofOffset: moof.offset, mdatOffset: box.offset, mdatSize: box.size, mdatHeaderSize: box.headerSize, initialDecodeTimes: initialDecodeTimes, ); print(''); print( 'FRAGMENT ' 'index=$fragmentIndex ' 'moofOffset=${fragment.moofOffset} ' 'moofSize=${fragment.moofSize} ' 'mdatOffset=${fragment.mdatOffset} ' 'mdatSize=${fragment.mdatSize}', ); for (final track in fragment.tracks) { final expectedInitial = initialDecodeTimes[track.trackId] ?? 0; if (track.initialDecodeTime != expectedInitial) { throw StateError( 'DTS iniziale inatteso: ' 'fragment=$fragmentIndex ' 'track=${track.trackId} ' 'actual=${track.initialDecodeTime} ' 'expected=$expectedInitial', ); } if (track.samples.isEmpty) { throw StateError( 'Track senza sample: ' 'fragment=$fragmentIndex ' 'track=${track.trackId}', ); } if (track.samples.first.decodeTime != expectedInitial) { throw StateError( 'Primo sample con DTS inatteso: ' 'fragment=$fragmentIndex ' 'track=${track.trackId}', ); } decodeTimes[track.trackId] = track.nextDecodeTime; print( 'TRACK ' 'trackId=${track.trackId} ' 'samples=${track.sampleCount} ' 'initialDts=${track.initialDecodeTime} ' 'nextDts=${track.nextDecodeTime} ' 'duration=${track.totalDuration} ' 'bytes=${track.totalSampleBytes} ' 'syncSamples=${track.syncSampleCount} ' 'firstOffset=${track.firstSampleAbsoluteOffset}', ); } final audio = fragment.tracks.singleWhere( (track) => track.trackId == 1, ); final video = fragment.tracks.singleWhere( (track) => track.trackId == 2, ); if (video.sampleCount != 90) { throw StateError( 'Numero frame video inatteso: ' 'fragment=$fragmentIndex ' 'actual=${video.sampleCount} ' 'expected=90', ); } if (video.totalDuration != 270000) { throw StateError( 'Durata video inattesa: ' 'fragment=$fragmentIndex ' 'actual=${video.totalDuration} ' 'expected=270000', ); } if (video.syncSampleCount < 1) { throw StateError( 'Frammento video senza keyframe: ' 'fragment=$fragmentIndex', ); } if (!video.samples.first.isSync) { throw StateError( 'Il frammento non inizia con un keyframe: ' 'fragment=$fragmentIndex', ); } if (audio.totalDuration <= 0) { throw StateError( 'Durata audio non valida: ' 'fragment=$fragmentIndex', ); } pendingMoof = null; fragmentIndex++; } offset = box.end; } } finally { await source.close(); } if (fragmentIndex != maximumFragments) { throw StateError( 'Frammenti analizzati insufficienti: ' 'actual=$fragmentIndex ' 'expected=$maximumFragments', ); } print(''); print( 'FINAL DECODE TIMES ' 'audio=${decodeTimes[1]} ' 'video=${decodeTimes[2]}', ); print( 'FMP4 MULTIPLE FRAGMENTS TEST: PASS ' 'fragments=$fragmentIndex', ); } Future<_BoxInfo?> _readBox({ required RandomAccessFile source, required int offset, required int fileLength, }) async { if (fileLength - offset < 8) { return null; } await source.setPosition(offset); final header = await source.read(8); if (header.length != 8) { return null; } final data = ByteData.sublistView( Uint8List.fromList(header), ); final size32 = data.getUint32( 0, Endian.big, ); final type = String.fromCharCodes( header.sublist(4, 8), ); var headerSize = 8; int size; if (size32 == 1) { final extended = await source.read(8); if (extended.length != 8) { return null; } size = ByteData.sublistView( Uint8List.fromList(extended), ).getUint64( 0, Endian.big, ); headerSize = 16; } else if (size32 == 0) { size = fileLength - offset; } else { size = size32; } if (size < headerSize) { throw StateError( 'Dimensione box non valida: ' 'offset=$offset ' 'type=$type ' 'size=$size', ); } if (offset + size > fileLength) { return null; } return _BoxInfo( type: type, offset: offset, size: size, headerSize: headerSize, ); } Future _readRange({ required RandomAccessFile source, required int offset, required int length, }) async { await source.setPosition(offset); final bytes = await source.read(length); if (bytes.length != length) { throw StateError( 'Lettura incompleta: ' 'offset=$offset ' 'actual=${bytes.length} ' 'expected=$length', ); } return Uint8List.fromList(bytes); } class _BoxInfo { final String type; final int offset; final int size; final int headerSize; const _BoxInfo({ required this.type, required this.offset, required this.size, required this.headerSize, }); int get end => offset + size; }