649 lines
16 KiB
Dart
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}',
|
|
);
|
|
}
|
|
}
|
|
}
|