401 lines
7.7 KiB
Dart
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;
|
|
}
|