353 lines
11 KiB
Dart
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,
|
|
};
|
|
}
|