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

401 lines
7.7 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:aves/services/cast/cast_fmp4_fragment_parser.dart';
class CastFmp4ProgressiveReaderException implements Exception {
final String message;
const CastFmp4ProgressiveReaderException(
this.message,
);
@override
String toString() {
return 'CastFmp4ProgressiveReaderException: $message';
}
}
class CastFmp4ProgressiveReader {
CastFmp4ProgressiveReader({
required this.sourcePath,
required this.pollingInterval,
required this.onFragment,
});
final String sourcePath;
final Duration pollingInterval;
final Future<void> Function(
Fmp4FragmentInfo fragment,
)
onFragment;
final CastFmp4FragmentParser _parser = const CastFmp4FragmentParser();
final Map<int, int> _decodeTimes = <int, int>{};
RandomAccessFile? _source;
bool _running = false;
bool _stopRequested = false;
bool _conversionCompleted = false;
int _nextOffset = 0;
int _fragmentCount = 0;
_PendingMoof? _pendingMoof;
bool get isRunning => _running;
int get fragmentCount => _fragmentCount;
Map<int, int> get decodeTimes => Map<int, int>.unmodifiable(_decodeTimes);
Future<void> start() async {
if (_running) {
throw const CastFmp4ProgressiveReaderException(
'Lettore fMP4 già in esecuzione',
);
}
_running = true;
_stopRequested = false;
try {
await _run();
} finally {
_running = false;
await _closeSource();
}
}
void notifyConversionCompleted() {
_conversionCompleted = true;
}
void requestStop() {
_stopRequested = true;
}
Future<void> _run() async {
final file = File(sourcePath);
while (!_stopRequested) {
if (!await file.exists()) {
if (_conversionCompleted) {
throw CastFmp4ProgressiveReaderException(
'File fMP4 non trovato: $sourcePath',
);
}
await Future<void>.delayed(
pollingInterval,
);
continue;
}
_source ??= await file.open(
mode: FileMode.read,
);
final progressed = await _consumeAvailableBoxes(
file,
);
if (progressed) {
continue;
}
if (_conversionCompleted) {
final fileLength = await file.length();
if (_nextOffset != fileLength) {
throw CastFmp4ProgressiveReaderException(
'Conversione terminata con box incompleto: '
'nextOffset=$_nextOffset '
'fileLength=$fileLength',
);
}
if (_pendingMoof != null) {
throw const CastFmp4ProgressiveReaderException(
'Conversione terminata con moof senza mdat',
);
}
return;
}
await Future<void>.delayed(
pollingInterval,
);
}
}
Future<bool> _consumeAvailableBoxes(
File file,
) async {
var progressed = false;
while (!_stopRequested) {
final fileLength = await file.length();
final header = await _readBoxHeader(
fileLength,
);
if (header == null) {
break;
}
if (header.end > fileLength) {
break;
}
await _consumeBox(header);
_nextOffset = header.end;
progressed = true;
}
return progressed;
}
Future<_BoxHeader?> _readBoxHeader(
int fileLength,
) async {
const normalHeaderSize = 8;
if (fileLength - _nextOffset < normalHeaderSize) {
return null;
}
final source = _source;
if (source == null) {
return null;
}
await source.setPosition(_nextOffset);
final normalHeader = await source.read(
normalHeaderSize,
);
if (normalHeader.length != normalHeaderSize) {
return null;
}
final normalData = ByteData.sublistView(
Uint8List.fromList(normalHeader),
);
final size32 = normalData.getUint32(
0,
Endian.big,
);
final type = String.fromCharCodes(
normalHeader.sublist(4, 8),
);
var headerSize = normalHeaderSize;
late final int size;
if (size32 == 1) {
if (fileLength - _nextOffset < 16) {
return null;
}
final extendedBytes = await source.read(
8,
);
if (extendedBytes.length != 8) {
return null;
}
size =
ByteData.sublistView(
Uint8List.fromList(extendedBytes),
).getUint64(
0,
Endian.big,
);
headerSize = 16;
} else if (size32 == 0) {
if (!_conversionCompleted) {
return null;
}
size = fileLength - _nextOffset;
} else {
size = size32;
}
if (size < headerSize) {
throw CastFmp4ProgressiveReaderException(
'Dimensione box non valida: '
'offset=$_nextOffset '
'type=$type '
'size=$size',
);
}
return _BoxHeader(
offset: _nextOffset,
size: size,
headerSize: headerSize,
type: type,
);
}
Future<void> _consumeBox(
_BoxHeader header,
) async {
switch (header.type) {
case 'ftyp':
case 'moov':
return;
case 'moof':
_consumeMoof(header);
return;
case 'mdat':
await _consumeMdat(header);
return;
default:
return;
}
}
void _consumeMoof(
_BoxHeader header,
) {
if (_pendingMoof != null) {
throw CastFmp4ProgressiveReaderException(
'Nuovo moof prima del relativo mdat: '
'offset=${header.offset}',
);
}
_pendingMoof = _PendingMoof(
offset: header.offset,
size: header.size,
);
}
Future<void> _consumeMdat(
_BoxHeader header,
) async {
final pending = _pendingMoof;
if (pending == null) {
throw CastFmp4ProgressiveReaderException(
'mdat senza moof: '
'offset=${header.offset}',
);
}
if (pending.end != header.offset) {
throw CastFmp4ProgressiveReaderException(
'moof e mdat non adiacenti: '
'moofEnd=${pending.end} '
'mdatOffset=${header.offset}',
);
}
final moofBytes = await _readRange(
offset: pending.offset,
length: pending.size,
);
final fragment = _parser.parse(
moofBytes: moofBytes,
moofOffset: pending.offset,
mdatOffset: header.offset,
mdatSize: header.size,
mdatHeaderSize: header.headerSize,
initialDecodeTimes: Map<int, int>.from(_decodeTimes),
);
for (final track in fragment.tracks) {
_decodeTimes[track.trackId] = track.nextDecodeTime;
}
await onFragment(fragment);
_fragmentCount++;
_pendingMoof = null;
}
Future<Uint8List> _readRange({
required int offset,
required int length,
}) async {
final source = _source;
if (source == null) {
throw const CastFmp4ProgressiveReaderException(
'File fMP4 non aperto',
);
}
await source.setPosition(offset);
final bytes = await source.read(length);
if (bytes.length != length) {
throw CastFmp4ProgressiveReaderException(
'Lettura incompleta: '
'offset=$offset '
'actual=${bytes.length} '
'expected=$length',
);
}
return Uint8List.fromList(bytes);
}
Future<void> _closeSource() async {
final source = _source;
_source = null;
if (source != null) {
await source.close();
}
}
}
class _PendingMoof {
final int offset;
final int size;
const _PendingMoof({
required this.offset,
required this.size,
});
int get end => offset + size;
}
class _BoxHeader {
final int offset;
final int size;
final int headerSize;
final String type;
const _BoxHeader({
required this.offset,
required this.size,
required this.headerSize,
required this.type,
});
int get end => offset + size;
}