Aves_mio0.88/tool/cast/test_fmp4_multiple_fragments.dart
2026-10-03 12:51:15 +02:00

335 lines
7.4 KiB
Dart

// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:typed_data';
import 'package:aves/services/cast/cast_fmp4_fragment_parser.dart';
Future<void> main(List<String> 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 = <int, int>{
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<int, int>.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<Uint8List> _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;
}