// 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 = [ 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(); final pendingAudioSamples = []; final pendingVideoSamples = []; 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( 'writeFmp4FirstTsSegmentTest', { '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.delayed(const Duration(minutes: 5)); }, ); } Future _growFile({ required File sourceFile, required File targetFile, required List 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.delayed(const Duration(milliseconds: 700)); } } finally { await source.close(); await target.close(); onCompleted(); } } Future _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 _sampleToMap(Fmp4SampleInfo sample) { return { 'offset': sample.absoluteOffset, 'size': sample.size, 'dts': sample.decodeTime, 'pts': sample.presentationTime, 'duration': sample.duration, 'sync': sample.isSync, }; }