// 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( 'genera cinque segmenti TS e la playlist HLS', (tester) async { final cacheDirectory = await getApplicationCacheDirectory(); final inputFile = File('${cacheDirectory.path}/stream_test.fmp4'); final outputDirectory = Directory( '${cacheDirectory.path}/fmp4_five_ts_test', ); expect( await inputFile.exists(), isTrue, reason: 'Copia stream_test.fmp4 nella cache Android prima del test', ); if (await outputDirectory.exists()) { await outputDirectory.delete(recursive: true); } await outputDirectory.create(recursive: true); const fragmentLimit = 5; final fragmentCompleter = Completer>(); final fragments = []; late final CastFmp4ProgressiveReader reader; reader = CastFmp4ProgressiveReader( sourcePath: inputFile.path, pollingInterval: const Duration(milliseconds: 20), onFragment: (fragment) async { fragments.add(fragment); print( 'FMP4 FRAGMENT READY ' 'index=${fragments.length - 1} ' 'moofOffset=${fragment.moofOffset} ' 'mdatOffset=${fragment.mdatOffset}', ); if (fragments.length == fragmentLimit && !fragmentCompleter.isCompleted) { fragmentCompleter.complete(List.unmodifiable(fragments)); reader.requestStop(); } }, ); final readerFuture = reader.start(); late final List parsedFragments; try { parsedFragments = await fragmentCompleter.future.timeout( const Duration(seconds: 30), ); } finally { reader.requestStop(); await readerFuture; } final allAudioSamples = []; final allVideoSamples = []; for (final fragment in parsedFragments) { allAudioSamples.addAll( fragment.tracks.singleWhere((track) => track.trackId == 1).samples, ); allVideoSamples.addAll( fragment.tracks.singleWhere((track) => track.trackId == 2).samples, ); } expect(allVideoSamples.length, 450); expect(allAudioSamples.length, greaterThanOrEqualTo(704)); var videoContinuityCounter = 0; var audioContinuityCounter = 0; var patContinuityCounter = 0; var pmtContinuityCounter = 0; final reports = >[]; for (var segmentIndex = 0; segmentIndex < 5; segmentIndex++) { final videoStartDts = segmentIndex * 270000; final videoEndDts = (segmentIndex + 1) * 270000; final audioStartDts = segmentIndex * 144000; final audioEndDts = (segmentIndex + 1) * 144000; final selectedVideo = allVideoSamples .where( (sample) => sample.decodeTime >= videoStartDts && sample.decodeTime < videoEndDts, ) .toList(growable: false); final selectedAudio = allAudioSamples .where( (sample) => sample.decodeTime >= audioStartDts && sample.decodeTime < audioEndDts, ) .toList(growable: false); expect( selectedVideo.length, 90, reason: 'Segmento video $segmentIndex', ); expect(selectedVideo.first.isSync, isTrue); expect(selectedAudio.length, inInclusiveRange(140, 141)); final outputFile = File( '${outputDirectory.path}/' 'segment_${segmentIndex.toString().padLeft(5, '0')}.ts', ); final report = await channel.invokeMapMethod( 'writeFmp4FirstTsSegmentTest', { 'inputPath': inputFile.path, 'outputPath': outputFile.path, 'segmentIndex': segmentIndex, 'videoStartDts': videoStartDts, 'videoEndDts': videoEndDts, 'audioStartDts': audioStartDts, 'audioEndDts': audioEndDts, 'initialVideoContinuityCounter': videoContinuityCounter, 'initialAudioContinuityCounter': audioContinuityCounter, 'patContinuityCounter': patContinuityCounter, 'pmtContinuityCounter': pmtContinuityCounter, 'audioSamples': selectedAudio.map(_sampleToMap).toList(), 'videoSamples': selectedVideo.map(_sampleToMap).toList(), }, ); expect(report, isNotNull); expect(report!['success'], isTrue); expect(report['segmentIndex'], segmentIndex); expect(report['videoSamples'], 90); expect(report['audioSamples'], inInclusiveRange(140, 141)); expect(report['accessUnitDelimiterCount'], 90); expect(await outputFile.exists(), isTrue); expect(await outputFile.length(), greaterThan(0)); expect((await outputFile.length()) % 188, 0); videoContinuityCounter = (report['nextVideoContinuityCounter'] as num).toInt(); audioContinuityCounter = (report['nextAudioContinuityCounter'] as num).toInt(); patContinuityCounter = (patContinuityCounter + 1) & 0x0f; pmtContinuityCounter = (pmtContinuityCounter + 1) & 0x0f; reports.add(Map.from(report)); print( 'TS SEGMENT PUBLISHED ' 'index=$segmentIndex ' 'videoSamples=${report['videoSamples']} ' 'audioSamples=${report['audioSamples']} ' 'bytes=${report['outputBytes']} ' 'nextVideoCc=$videoContinuityCounter ' 'nextAudioCc=$audioContinuityCounter', ); if (segmentIndex == 2) { print( 'TS HLS READY ' 'publishedSegments=3 ' 'contentDurationSeconds=9', ); } } final playlist = File('${outputDirectory.path}/master.m3u8'); await playlist.writeAsString( _buildVodPlaylist(segmentCount: reports.length), flush: true, ); expect(await playlist.exists(), isTrue); expect(await playlist.length(), greaterThan(0)); print( 'FMP4 FIVE TS TEST PASS ' 'segments=${reports.length} ' 'playlist=${playlist.path} ' 'outputDirectory=${outputDirectory.path}', ); print('FILES READY FOR ADB COPY waitingSeconds=30'); await Future.delayed(const Duration(seconds: 30)); }, ); } Map _sampleToMap(Fmp4SampleInfo sample) { return { 'offset': sample.absoluteOffset, 'size': sample.size, 'dts': sample.decodeTime, 'pts': sample.presentationTime, 'duration': sample.duration, 'sync': sample.isSync, }; } String _buildVodPlaylist({required int segmentCount}) { 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:VOD') ..writeln('#EXT-X-INDEPENDENT-SEGMENTS'); for (var index = 0; index < segmentCount; index++) { buffer ..writeln('#EXTINF:3.000,') ..writeln('segment_${index.toString().padLeft(5, '0')}.ts'); } buffer.writeln('#EXT-X-ENDLIST'); return buffer.toString(); }