Aves_mio0.88/integration_test/fmp4_progressive_ts_test.dart
2026-10-03 12:51:15 +02:00

353 lines
11 KiB
Dart

// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:io';
import 'package:aves/services/cast/cast_fmp4_fragment_parser.dart';
import 'package:aves/services/cast/cast_fmp4_progressive_reader.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path_provider/path_provider.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('aves/cast_video_optimizer');
testWidgets(
'produce HLS TS mentre il fragmented MP4 cresce',
(tester) async {
final cache = await getApplicationCacheDirectory();
final sourceFile = File('${cache.path}/stream_test.fmp4');
final growingFile = File('${cache.path}/growing_stream.fmp4');
final outputDirectory = Directory(
'${cache.path}/fmp4_progressive_ts_test',
);
expect(await sourceFile.exists(), isTrue);
expect(await sourceFile.length(), greaterThanOrEqualTo(11645632));
if (await growingFile.exists()) {
await growingFile.delete();
}
if (await outputDirectory.exists()) {
await outputDirectory.delete(recursive: true);
}
await outputDirectory.create(recursive: true);
await growingFile.create(recursive: true);
const boundaries = <int>[
1684,
2344107,
4720042,
6990038,
9331556,
11645632,
];
var videoContinuityCounter = 0;
var audioContinuityCounter = 0;
var patContinuityCounter = 0;
var pmtContinuityCounter = 0;
var publishedSegments = 0;
var readyElapsedMs = -1;
var totalPublishedAudioSamples = 0;
var totalPublishedVideoSamples = 0;
final stopwatch = Stopwatch()..start();
final completed = Completer<void>();
final pendingAudioSamples = <Fmp4SampleInfo>[];
final pendingVideoSamples = <Fmp4SampleInfo>[];
late final CastFmp4ProgressiveReader reader;
reader = CastFmp4ProgressiveReader(
sourcePath: growingFile.path,
pollingInterval: const Duration(milliseconds: 20),
onFragment: (fragment) async {
final segmentIndex = publishedSegments;
expect(segmentIndex, lessThan(5));
final audioTrack = fragment.tracks.singleWhere(
(track) => track.trackId == 1,
);
final videoTrack = fragment.tracks.singleWhere(
(track) => track.trackId == 2,
);
pendingAudioSamples.addAll(
audioTrack.samples,
);
pendingVideoSamples.addAll(
videoTrack.samples,
);
final videoStartDts = segmentIndex * 270000;
final videoEndDts = (segmentIndex + 1) * 270000;
final audioStartDts = segmentIndex * 144000;
final audioEndDts = (segmentIndex + 1) * 144000;
final videoSamples = pendingVideoSamples
.where(
(sample) => sample.decodeTime >= videoStartDts && sample.decodeTime < videoEndDts,
)
.toList(growable: false);
final audioSamples = pendingAudioSamples
.where(
(sample) => sample.decodeTime >= audioStartDts && sample.decodeTime < audioEndDts,
)
.toList(growable: false);
expect(videoSamples.length, 90);
expect(videoSamples.first.isSync, isTrue);
expect(
audioSamples.length,
inInclusiveRange(140, 141),
reason: 'Numero sample audio del segmento $segmentIndex',
);
final outputFile = File(
'${outputDirectory.path}/'
'segment_${segmentIndex.toString().padLeft(5, '0')}.ts',
);
final report = await channel.invokeMapMethod<String, dynamic>(
'writeFmp4FirstTsSegmentTest',
<String, dynamic>{
'inputPath': growingFile.path,
'outputPath': outputFile.path,
'segmentIndex': segmentIndex,
'videoStartDts': videoStartDts,
'videoEndDts': videoEndDts,
'audioStartDts': audioStartDts,
'audioEndDts': audioEndDts,
'initialVideoContinuityCounter': videoContinuityCounter,
'initialAudioContinuityCounter': audioContinuityCounter,
'patContinuityCounter': patContinuityCounter,
'pmtContinuityCounter': pmtContinuityCounter,
'audioSamples': audioSamples.map(_sampleToMap).toList(),
'videoSamples': videoSamples.map(_sampleToMap).toList(),
},
);
expect(report, isNotNull);
expect(report!['success'], isTrue);
expect(report['segmentIndex'], segmentIndex);
expect(report['videoSamples'], 90);
expect(await outputFile.exists(), isTrue);
expect((await outputFile.length()) % 188, 0);
totalPublishedAudioSamples += (report['audioSamples'] as num).toInt();
totalPublishedVideoSamples += (report['videoSamples'] as num).toInt();
videoContinuityCounter = (report['nextVideoContinuityCounter'] as num).toInt();
audioContinuityCounter = (report['nextAudioContinuityCounter'] as num).toInt();
patContinuityCounter = (patContinuityCounter + 1) & 0x0f;
pmtContinuityCounter = (pmtContinuityCounter + 1) & 0x0f;
publishedSegments++;
pendingVideoSamples.removeWhere(
(sample) => sample.decodeTime < videoEndDts,
);
pendingAudioSamples.removeWhere(
(sample) => sample.decodeTime < audioEndDts,
);
await _writeEventPlaylist(
outputDirectory: outputDirectory,
publishedSegments: publishedSegments,
completed: false,
);
print(
'PROGRESSIVE TS PUBLISHED '
'segmentIndex=$segmentIndex '
'publishedSegments=$publishedSegments '
'sourceBytes=${await growingFile.length()} '
'outputBytes=${report['outputBytes']} '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
if (publishedSegments == 3) {
readyElapsedMs = stopwatch.elapsedMilliseconds;
print(
'PROGRESSIVE TS READY '
'publishedSegments=3 '
'contentDurationSeconds=9 '
'sourceBytes=${await growingFile.length()} '
'elapsedMs=$readyElapsedMs',
);
}
if (publishedSegments == 5) {
await _writeEventPlaylist(
outputDirectory: outputDirectory,
publishedSegments: publishedSegments,
completed: true,
);
reader.requestStop();
if (!completed.isCompleted) {
completed.complete();
}
}
},
);
final readerFuture = reader.start();
final writerFuture = _growFile(
sourceFile: sourceFile,
targetFile: growingFile,
boundaries: boundaries,
onCompleted: reader.notifyConversionCompleted,
);
try {
await completed.future.timeout(const Duration(seconds: 45));
await writerFuture;
} finally {
reader.requestStop();
await readerFuture;
stopwatch.stop();
}
expect(publishedSegments, 5);
expect(
totalPublishedVideoSamples,
450,
);
expect(
totalPublishedAudioSamples,
704,
);
expect(
readyElapsedMs,
greaterThanOrEqualTo(0),
);
expect(
await growingFile.length(),
boundaries.last,
);
final playlist = File('${outputDirectory.path}/master.m3u8');
expect(await playlist.exists(), isTrue);
final playlistText = await playlist.readAsString();
expect(playlistText, contains('#EXT-X-PLAYLIST-TYPE:EVENT'));
expect(playlistText, contains('#EXT-X-ENDLIST'));
for (var index = 0; index < 5; index++) {
final segment = File(
'${outputDirectory.path}/'
'segment_${index.toString().padLeft(5, '0')}.ts',
);
expect(await segment.exists(), isTrue);
expect(await segment.length(), greaterThan(1000000));
}
print(
'FMP4 PROGRESSIVE TS TEST PASS '
'segments=$publishedSegments '
'videoSamples=$totalPublishedVideoSamples '
'audioSamples=$totalPublishedAudioSamples '
'readyElapsedMs=$readyElapsedMs '
'sourceBytes=${await growingFile.length()} '
'pendingVideoSamples=${pendingVideoSamples.length} '
'pendingAudioSamples=${pendingAudioSamples.length} '
'outputDirectory=${outputDirectory.path}',
);
print('FILES READY FOR ADB COPY waitingSeconds=300');
await Future<void>.delayed(const Duration(minutes: 5));
},
);
}
Future<void> _growFile({
required File sourceFile,
required File targetFile,
required List<int> boundaries,
required void Function() onCompleted,
}) async {
final source = await sourceFile.open(mode: FileMode.read);
final target = await targetFile.open(mode: FileMode.write);
var previous = 0;
try {
for (var index = 0; index < boundaries.length; index++) {
final boundary = boundaries[index];
final length = boundary - previous;
await source.setPosition(previous);
final bytes = await source.read(length);
expect(bytes.length, length);
await target.writeFrom(bytes);
await target.flush();
print(
'FMP4 SOURCE GROW '
'chunk=$index '
'chunkBytes=$length '
'totalBytes=$boundary',
);
previous = boundary;
await Future<void>.delayed(const Duration(milliseconds: 700));
}
} finally {
await source.close();
await target.close();
onCompleted();
}
}
Future<void> _writeEventPlaylist({
required Directory outputDirectory,
required int publishedSegments,
required bool completed,
}) async {
final buffer = StringBuffer()
..writeln('#EXTM3U')
..writeln('#EXT-X-VERSION:3')
..writeln('#EXT-X-TARGETDURATION:3')
..writeln('#EXT-X-MEDIA-SEQUENCE:0')
..writeln('#EXT-X-PLAYLIST-TYPE:EVENT')
..writeln('#EXT-X-INDEPENDENT-SEGMENTS');
for (var index = 0; index < publishedSegments; index++) {
buffer
..writeln('#EXTINF:3.000,')
..writeln('segment_${index.toString().padLeft(5, '0')}.ts');
}
if (completed) {
buffer.writeln('#EXT-X-ENDLIST');
}
final finalFile = File('${outputDirectory.path}/master.m3u8');
final temporaryFile = File('${finalFile.path}.part');
await temporaryFile.writeAsString(buffer.toString(), flush: true);
if (await finalFile.exists()) {
await finalFile.delete();
}
await temporaryFile.rename(finalFile.path);
}
Map<String, dynamic> _sampleToMap(Fmp4SampleInfo sample) {
return <String, dynamic>{
'offset': sample.absoluteOffset,
'size': sample.size,
'dts': sample.decodeTime,
'pts': sample.presentationTime,
'duration': sample.duration,
'sync': sample.isSync,
};
}