import 'dart:typed_data'; class CastFmp4FragmentParserException implements Exception { final String message; const CastFmp4FragmentParserException(this.message); @override String toString() => 'CastFmp4FragmentParserException: $message'; } class Fmp4SampleInfo { final int index; final int absoluteOffset; final int size; final int duration; final int decodeTime; final int presentationTime; final int compositionTimeOffset; final int flags; final bool isSync; const Fmp4SampleInfo({ required this.index, required this.absoluteOffset, required this.size, required this.duration, required this.decodeTime, required this.presentationTime, required this.compositionTimeOffset, required this.flags, required this.isSync, }); Map toMap() { return { 'index': index, 'absoluteOffset': absoluteOffset, 'size': size, 'duration': duration, 'decodeTime': decodeTime, 'presentationTime': presentationTime, 'compositionTimeOffset': compositionTimeOffset, 'flags': flags, 'isSync': isSync, }; } } class Fmp4TrackFragment { final int trackId; final int baseDataOffset; final int dataOffset; final int firstSampleAbsoluteOffset; final int initialDecodeTime; final int nextDecodeTime; final List samples; const Fmp4TrackFragment({ required this.trackId, required this.baseDataOffset, required this.dataOffset, required this.firstSampleAbsoluteOffset, required this.initialDecodeTime, required this.nextDecodeTime, required this.samples, }); int get sampleCount => samples.length; int get totalDuration => nextDecodeTime - initialDecodeTime; int get totalSampleBytes => samples.fold( 0, (sum, sample) => sum + sample.size, ); int get syncSampleCount => samples.where((sample) => sample.isSync).length; Map toMap() { return { 'trackId': trackId, 'baseDataOffset': baseDataOffset, 'dataOffset': dataOffset, 'firstSampleAbsoluteOffset': firstSampleAbsoluteOffset, 'initialDecodeTime': initialDecodeTime, 'nextDecodeTime': nextDecodeTime, 'sampleCount': sampleCount, 'totalDuration': totalDuration, 'totalSampleBytes': totalSampleBytes, 'syncSampleCount': syncSampleCount, 'samples': samples.map((sample) => sample.toMap()).toList(), }; } } class Fmp4FragmentInfo { final int moofOffset; final int moofSize; final int mdatOffset; final int mdatSize; final int mdatHeaderSize; final List tracks; const Fmp4FragmentInfo({ required this.moofOffset, required this.moofSize, required this.mdatOffset, required this.mdatSize, required this.mdatHeaderSize, required this.tracks, }); int get mdatPayloadOffset => mdatOffset + mdatHeaderSize; int get mdatPayloadSize => mdatSize - mdatHeaderSize; Map toMap() { return { 'moofOffset': moofOffset, 'moofSize': moofSize, 'mdatOffset': mdatOffset, 'mdatSize': mdatSize, 'mdatHeaderSize': mdatHeaderSize, 'mdatPayloadOffset': mdatPayloadOffset, 'mdatPayloadSize': mdatPayloadSize, 'tracks': tracks.map((track) => track.toMap()).toList(), }; } } class CastFmp4FragmentParser { static const int _sampleIsNonSyncMask = 0x00010000; const CastFmp4FragmentParser(); Fmp4FragmentInfo parse({ required Uint8List moofBytes, required int moofOffset, required int mdatOffset, required int mdatSize, required int mdatHeaderSize, required Map initialDecodeTimes, }) { _requireNonNegative('moofOffset', moofOffset); _requireNonNegative('mdatOffset', mdatOffset); _requirePositive('mdatSize', mdatSize); _requirePositive('mdatHeaderSize', mdatHeaderSize); if (mdatHeaderSize >= mdatSize) { throw CastFmp4FragmentParserException( 'Header mdat non valido: header=$mdatHeaderSize size=$mdatSize', ); } final reader = _ByteReader(moofBytes); final moof = reader.readBox(0, moofBytes.length); if (moof.type != 'moof') { throw CastFmp4FragmentParserException( 'Box atteso moof, trovato ${moof.type}', ); } if (moof.size != moofBytes.length) { throw CastFmp4FragmentParserException( 'Dimensione moof incoerente: box=${moof.size} bytes=${moofBytes.length}', ); } final tracks = []; for (final child in reader.children(moof.payloadOffset, moof.end)) { if (child.type != 'traf') { continue; } tracks.add( _parseTrackFragment( reader: reader, traf: child, initialDecodeTimes: initialDecodeTimes, ), ); } if (tracks.isEmpty) { throw const CastFmp4FragmentParserException( 'Nessun traf trovato nel moof', ); } final seenTrackIds = {}; for (final track in tracks) { if (!seenTrackIds.add(track.trackId)) { throw CastFmp4FragmentParserException( 'Track duplicata nel moof: ${track.trackId}', ); } } final payloadStart = mdatOffset + mdatHeaderSize; final payloadEnd = mdatOffset + mdatSize; var totalSampleBytes = 0; for (final track in tracks) { totalSampleBytes += track.totalSampleBytes; for (final sample in track.samples) { final sampleEnd = sample.absoluteOffset + sample.size; if (sample.absoluteOffset < payloadStart || sampleEnd > payloadEnd) { throw CastFmp4FragmentParserException( 'Sample fuori dal payload mdat: ' 'track=${track.trackId} index=${sample.index} ' 'offset=${sample.absoluteOffset} size=${sample.size} ' 'payload=$payloadStart..$payloadEnd', ); } } } if (totalSampleBytes != mdatSize - mdatHeaderSize) { throw CastFmp4FragmentParserException( 'Dimensione sample diversa dal payload mdat: ' 'samples=$totalSampleBytes payload=${mdatSize - mdatHeaderSize}', ); } return Fmp4FragmentInfo( moofOffset: moofOffset, moofSize: moofBytes.length, mdatOffset: mdatOffset, mdatSize: mdatSize, mdatHeaderSize: mdatHeaderSize, tracks: List.unmodifiable(tracks), ); } Fmp4TrackFragment _parseTrackFragment({ required _ByteReader reader, required _Mp4Box traf, required Map initialDecodeTimes, }) { _Tfhd? tfhd; _Trun? trun; for (final child in reader.children(traf.payloadOffset, traf.end)) { switch (child.type) { case 'tfhd': if (tfhd != null) { throw const CastFmp4FragmentParserException( 'tfhd duplicato nel traf', ); } tfhd = _parseTfhd(reader, child); break; case 'trun': if (trun != null) { throw const CastFmp4FragmentParserException( 'Più trun nello stesso traf non ancora supportati', ); } trun = _parseTrun(reader, child); break; } } if (tfhd == null || trun == null) { throw const CastFmp4FragmentParserException( 'traf senza tfhd o trun', ); } final baseDataOffset = tfhd.baseDataOffset; final dataOffset = trun.dataOffset; if (baseDataOffset == null || dataOffset == null) { throw CastFmp4FragmentParserException( 'Offset sample non determinabile per track ${tfhd.trackId}', ); } final initialDecodeTime = initialDecodeTimes[tfhd.trackId] ?? 0; var decodeTime = initialDecodeTime; var absoluteOffset = baseDataOffset + dataOffset; final samples = []; for (var index = 0; index < trun.samples.length; index++) { final entry = trun.samples[index]; final duration = entry.duration ?? tfhd.defaultSampleDuration; final size = entry.size ?? tfhd.defaultSampleSize; final flags = entry.flags ?? (index == 0 ? trun.firstSampleFlags : null) ?? tfhd.defaultSampleFlags; if (duration == null || duration <= 0) { throw CastFmp4FragmentParserException( 'Durata sample assente o invalida: track=${tfhd.trackId} index=$index', ); } if (size == null || size <= 0) { throw CastFmp4FragmentParserException( 'Dimensione sample assente o invalida: track=${tfhd.trackId} index=$index', ); } if (flags == null) { throw CastFmp4FragmentParserException( 'Flag sample assenti: track=${tfhd.trackId} index=$index', ); } final compositionOffset = entry.compositionTimeOffset; final presentationTime = decodeTime + compositionOffset; final isSync = (flags & _sampleIsNonSyncMask) == 0; samples.add( Fmp4SampleInfo( index: index, absoluteOffset: absoluteOffset, size: size, duration: duration, decodeTime: decodeTime, presentationTime: presentationTime, compositionTimeOffset: compositionOffset, flags: flags, isSync: isSync, ), ); absoluteOffset += size; decodeTime += duration; } return Fmp4TrackFragment( trackId: tfhd.trackId, baseDataOffset: baseDataOffset, dataOffset: dataOffset, firstSampleAbsoluteOffset: baseDataOffset + dataOffset, initialDecodeTime: initialDecodeTime, nextDecodeTime: decodeTime, samples: List.unmodifiable(samples), ); } _Tfhd _parseTfhd(_ByteReader reader, _Mp4Box box) { final fullBox = reader.readFullBox(box); var cursor = fullBox.dataOffset; final trackId = reader.uint32(cursor); cursor += 4; int? baseDataOffset; int? defaultSampleDuration; int? defaultSampleSize; int? defaultSampleFlags; if ((fullBox.flags & 0x000001) != 0) { baseDataOffset = reader.uint64(cursor); cursor += 8; } if ((fullBox.flags & 0x000002) != 0) { cursor += 4; } if ((fullBox.flags & 0x000008) != 0) { defaultSampleDuration = reader.uint32(cursor); cursor += 4; } if ((fullBox.flags & 0x000010) != 0) { defaultSampleSize = reader.uint32(cursor); cursor += 4; } if ((fullBox.flags & 0x000020) != 0) { defaultSampleFlags = reader.uint32(cursor); cursor += 4; } if (cursor != box.end) { throw CastFmp4FragmentParserException( 'tfhd non interpretato completamente: cursor=$cursor end=${box.end}', ); } return _Tfhd( trackId: trackId, baseDataOffset: baseDataOffset, defaultSampleDuration: defaultSampleDuration, defaultSampleSize: defaultSampleSize, defaultSampleFlags: defaultSampleFlags, ); } _Trun _parseTrun(_ByteReader reader, _Mp4Box box) { final fullBox = reader.readFullBox(box); var cursor = fullBox.dataOffset; final sampleCount = reader.uint32(cursor); cursor += 4; int? dataOffset; int? firstSampleFlags; if ((fullBox.flags & 0x000001) != 0) { dataOffset = reader.int32(cursor); cursor += 4; } if ((fullBox.flags & 0x000004) != 0) { firstSampleFlags = reader.uint32(cursor); cursor += 4; } final samples = <_TrunSample>[]; for (var index = 0; index < sampleCount; index++) { int? duration; int? size; int? flags; var compositionTimeOffset = 0; if ((fullBox.flags & 0x000100) != 0) { duration = reader.uint32(cursor); cursor += 4; } if ((fullBox.flags & 0x000200) != 0) { size = reader.uint32(cursor); cursor += 4; } if ((fullBox.flags & 0x000400) != 0) { flags = reader.uint32(cursor); cursor += 4; } if ((fullBox.flags & 0x000800) != 0) { compositionTimeOffset = fullBox.version == 0 ? reader.uint32(cursor) : reader.int32(cursor); cursor += 4; } samples.add( _TrunSample( duration: duration, size: size, flags: flags, compositionTimeOffset: compositionTimeOffset, ), ); } if (cursor != box.end) { throw CastFmp4FragmentParserException( 'trun non interpretato completamente: cursor=$cursor end=${box.end}', ); } return _Trun( dataOffset: dataOffset, firstSampleFlags: firstSampleFlags, samples: samples, ); } static void _requireNonNegative(String name, int value) { if (value < 0) { throw CastFmp4FragmentParserException('$name non valido: $value'); } } static void _requirePositive(String name, int value) { if (value <= 0) { throw CastFmp4FragmentParserException('$name non valido: $value'); } } } class _Tfhd { final int trackId; final int? baseDataOffset; final int? defaultSampleDuration; final int? defaultSampleSize; final int? defaultSampleFlags; const _Tfhd({ required this.trackId, required this.baseDataOffset, required this.defaultSampleDuration, required this.defaultSampleSize, required this.defaultSampleFlags, }); } class _Trun { final int? dataOffset; final int? firstSampleFlags; final List<_TrunSample> samples; const _Trun({ required this.dataOffset, required this.firstSampleFlags, required this.samples, }); } class _TrunSample { final int? duration; final int? size; final int? flags; final int compositionTimeOffset; const _TrunSample({ required this.duration, required this.size, required this.flags, required this.compositionTimeOffset, }); } class _FullBox { final int version; final int flags; final int dataOffset; const _FullBox({ required this.version, required this.flags, required this.dataOffset, }); } class _Mp4Box { final String type; final int offset; final int size; final int headerSize; const _Mp4Box({ required this.type, required this.offset, required this.size, required this.headerSize, }); int get payloadOffset => offset + headerSize; int get end => offset + size; } class _ByteReader { final Uint8List bytes; late final ByteData data = ByteData.sublistView(bytes); _ByteReader(this.bytes); int uint32(int offset) { _checkRange(offset, 4); return data.getUint32(offset, Endian.big); } int int32(int offset) { _checkRange(offset, 4); return data.getInt32(offset, Endian.big); } int uint64(int offset) { _checkRange(offset, 8); return data.getUint64(offset, Endian.big); } _Mp4Box readBox(int offset, int limit) { if (offset + 8 > limit) { throw CastFmp4FragmentParserException( 'Header box incompleto: offset=$offset limit=$limit', ); } final size32 = uint32(offset); final type = String.fromCharCodes(bytes.sublist(offset + 4, offset + 8)); var headerSize = 8; late final int size; if (size32 == 1) { if (offset + 16 > limit) { throw CastFmp4FragmentParserException( 'Header box esteso incompleto: offset=$offset limit=$limit', ); } size = uint64(offset + 8); headerSize = 16; } else if (size32 == 0) { size = limit - offset; } else { size = size32; } if (size < headerSize || offset + size > limit) { throw CastFmp4FragmentParserException( 'Box non valido: type=$type offset=$offset size=$size limit=$limit', ); } return _Mp4Box( type: type, offset: offset, size: size, headerSize: headerSize, ); } Iterable<_Mp4Box> children(int start, int end) sync* { var offset = start; while (offset + 8 <= end) { final box = readBox(offset, end); yield box; offset = box.end; } if (offset != end) { throw CastFmp4FragmentParserException( 'Byte residui nel container: offset=$offset end=$end', ); } } _FullBox readFullBox(_Mp4Box box) { final offset = box.payloadOffset; _checkRange(offset, 4); return _FullBox( version: bytes[offset], flags: (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3], dataOffset: offset + 4, ); } void _checkRange(int offset, int length) { if (offset < 0 || length < 0 || offset + length > bytes.length) { throw CastFmp4FragmentParserException( 'Lettura fuori intervallo: offset=$offset length=$length bytes=${bytes.length}', ); } } }