335 lines
7.4 KiB
Dart
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;
|
|
}
|