Aves_mio0.88/lib/services/cast/cast_fmp4_fragment_parser.dart
2026-10-03 12:51:15 +02:00

649 lines
16 KiB
Dart

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<String, Object> toMap() {
return <String, Object>{
'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<Fmp4SampleInfo> 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<int>(
0,
(sum, sample) => sum + sample.size,
);
int get syncSampleCount => samples.where((sample) => sample.isSync).length;
Map<String, Object> toMap() {
return <String, Object>{
'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<Fmp4TrackFragment> 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<String, Object> toMap() {
return <String, Object>{
'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<int, int> 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 = <Fmp4TrackFragment>[];
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 = <int>{};
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<Fmp4TrackFragment>.unmodifiable(tracks),
);
}
Fmp4TrackFragment _parseTrackFragment({
required _ByteReader reader,
required _Mp4Box traf,
required Map<int, int> 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 = <Fmp4SampleInfo>[];
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<Fmp4SampleInfo>.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}',
);
}
}
}