diff --git a/packages/audioplayers/example/integration_test/app/app_source_test_data.dart b/packages/audioplayers/example/integration_test/app/app_source_test_data.dart index f5b5236f8..1723ce151 100644 --- a/packages/audioplayers/example/integration_test/app/app_source_test_data.dart +++ b/packages/audioplayers/example/integration_test/app/app_source_test_data.dart @@ -8,68 +8,73 @@ class AppSourceTestData extends SourceTestData { AppSourceTestData({ required this.sourceKey, required super.duration, + super.isVBR, }); @override String toString() { return 'UiSourceTestData(' 'sourceKey: $sourceKey, ' - 'duration: $duration' + 'duration: $duration, ' + 'isVBR: $isVBR' ')'; } } final _features = PlatformFeatures.instance(); +// All sources are tested again in lib or platform tests, +// therefore comment most of them to save testing time final audioTestDataList = [ if (_features.hasUrlSource) AppSourceTestData( sourceKey: 'url-remote-wav-1', duration: const Duration(milliseconds: 451), ), - if (_features.hasUrlSource) + /*if (_features.hasUrlSource) AppSourceTestData( sourceKey: 'url-remote-wav-2', duration: const Duration(seconds: 1, milliseconds: 068), - ), - if (_features.hasUrlSource) + ),*/ + /*if (_features.hasUrlSource) AppSourceTestData( sourceKey: 'url-remote-mp3-1', + isVBR: true, duration: const Duration(minutes: 3, seconds: 30, milliseconds: 77), - ), - if (_features.hasUrlSource) + ),*/ + /*if (_features.hasUrlSource) AppSourceTestData( sourceKey: 'url-remote-mp3-2', duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ), + ),*/ if (_features.hasUrlSource && _features.hasPlaylistSourceType) AppSourceTestData( sourceKey: 'url-remote-m3u8', duration: null, ), - if (_features.hasUrlSource) + /*if (_features.hasUrlSource) AppSourceTestData( sourceKey: 'url-remote-mpga', duration: null, - ), - if (_features.hasAssetSource) + ),*/ + /*if (_features.hasAssetSource) AppSourceTestData( sourceKey: 'asset-wav', duration: const Duration(seconds: 1, milliseconds: 068), - ), - if (_features.hasAssetSource) + ),*/ + /*if (_features.hasAssetSource) AppSourceTestData( sourceKey: 'asset-mp3', duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ), - if (_features.hasBytesSource) + ),*/ + /*if (_features.hasBytesSource) AppSourceTestData( sourceKey: 'bytes-local', duration: const Duration(seconds: 1, milliseconds: 068), - ), - if (_features.hasBytesSource) + ),*/ + /*if (_features.hasBytesSource) AppSourceTestData( sourceKey: 'bytes-remote', duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76), - ), + ),*/ ]; diff --git a/packages/audioplayers/example/integration_test/app/app_test_utils.dart b/packages/audioplayers/example/integration_test/app/app_test_utils.dart index 3cd8f6306..7c35c61b2 100644 --- a/packages/audioplayers/example/integration_test/app/app_test_utils.dart +++ b/packages/audioplayers/example/integration_test/app/app_test_utils.dart @@ -125,20 +125,6 @@ $lastFailureMsg''', } await pumpAndSettle(); } - - bool durationRangeMatcher( - Duration? actual, - Duration? expected, { - Duration deviation = const Duration(seconds: 1), - }) { - if (actual == null && expected == null) { - return true; - } - if (actual == null || expected == null) { - return false; - } - return actual >= (expected - deviation) && actual <= (expected + deviation); - } } void expectWidgetHasText( diff --git a/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart index 35dd2991c..0c0dec17e 100644 --- a/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart +++ b/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart @@ -66,6 +66,7 @@ Future testControlsTab( await tester.stop(); } + // Test all features in low latency mode: final isBytesSource = audioSourceTestData.sourceKey.contains('bytes'); if (features.hasLowLatency && !audioSourceTestData.isLiveStream && @@ -150,7 +151,6 @@ extension ControlsWidgetTester on WidgetTester { printWithTimeOnFailure('Test Volume: $volume'); await scrollToAndTap(Key('control-volume-$volume')); await resume(); - // TODO(Gustl22): get volume from native implementation await pump(timeout); await stop(); } @@ -162,7 +162,6 @@ extension ControlsWidgetTester on WidgetTester { printWithTimeOnFailure('Test Balance: $balance'); await scrollToAndTap(Key('control-balance-$balance')); await resume(); - // TODO(novikov): get balance from native implementation await pump(timeout); await stop(); } @@ -174,7 +173,6 @@ extension ControlsWidgetTester on WidgetTester { printWithTimeOnFailure('Test Rate: $rate'); await scrollToAndTap(Key('control-rate-$rate')); await resume(); - // TODO(Gustl22): get rate from native implementation await pump(timeout); await stop(); } @@ -224,6 +222,5 @@ extension ControlsWidgetTester on WidgetTester { if (isResume) { await resume(); } - // TODO(Gustl22): get release mode from native implementation } } diff --git a/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart index 57fda2452..a5ee641ef 100644 --- a/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart +++ b/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart @@ -23,10 +23,7 @@ Future testStreamsTab( await tester.testPosition(Duration.zero); } - final isImmediateDurationSupported = - features.hasMp3Duration || !audioSourceTestData.sourceKey.contains('mp3'); - - if (features.hasDurationEvent && isImmediateDurationSupported) { + if (features.hasDurationEvent && !audioSourceTestData.isVBR) { // Display duration before playing await tester.testDuration(audioSourceTestData.duration); } diff --git a/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart b/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart index 2a0754b8b..03333760a 100644 --- a/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart +++ b/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart @@ -11,13 +11,15 @@ class LibSourceTestData extends SourceTestData { LibSourceTestData({ required this.source, required super.duration, + super.isVBR, }); @override String toString() { - return 'RawSourceTestData(' + return 'LibSourceTestData(' 'source: $source, ' - 'duration: $duration' + 'duration: $duration, ' + 'isVBR: $isVBR' ')'; } } @@ -32,39 +34,67 @@ final wavUrl1TestData = LibSourceTestData( final mp3Url1TestData = LibSourceTestData( source: UrlSource(mp3Url1), duration: const Duration(minutes: 3, seconds: 30, milliseconds: 77), + isVBR: true, ); -final audioTestDataList = [ - if (_features.hasUrlSource) wavUrl1TestData, - if (_features.hasUrlSource) - LibSourceTestData( - source: UrlSource(wavUrl2), - duration: const Duration(seconds: 1, milliseconds: 068), - ), - if (_features.hasUrlSource) mp3Url1TestData, - if (_features.hasUrlSource) - LibSourceTestData( - source: UrlSource(mp3Url2), - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ), - if (_features.hasUrlSource && _features.hasPlaylistSourceType) - LibSourceTestData( - source: UrlSource(m3u8StreamUrl), - duration: null, - ), - if (_features.hasUrlSource) - LibSourceTestData( - source: UrlSource(mpgaStreamUrl), - duration: null, - ), - if (_features.hasAssetSource) - LibSourceTestData( - source: AssetSource(wavAsset), - duration: const Duration(seconds: 1, milliseconds: 068), - ), - if (_features.hasAssetSource) - LibSourceTestData( - source: AssetSource(mp3Asset), - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ), -]; +final m3u8UrlTestData = LibSourceTestData( + source: UrlSource(m3u8StreamUrl), + duration: null, +); + +final mpgaUrlTestData = LibSourceTestData( + source: UrlSource(mpgaStreamUrl), + duration: null, +); + +final wavAssetTestData = LibSourceTestData( + source: AssetSource(wavAsset), + duration: const Duration(seconds: 1, milliseconds: 068), +); + +final invalidAssetTestData = LibSourceTestData( + source: AssetSource(invalidAsset), + duration: null, +); + +final nonExistentUrlTestData = LibSourceTestData( + source: UrlSource('non_existent.txt'), + duration: null, +); + +// Some sources are commented which are considered redundant +Future> getAudioTestDataList() async { + return [ + if (_features.hasUrlSource) wavUrl1TestData, + /*if (_features.hasUrlSource) + LibSourceTestData( + source: UrlSource(wavUrl2), + duration: const Duration(seconds: 1, milliseconds: 068), + ),*/ + if (_features.hasUrlSource) mp3Url1TestData, + /*if (_features.hasUrlSource) + LibSourceTestData( + source: UrlSource(mp3Url2), + duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), + ),*/ + if (_features.hasUrlSource && _features.hasPlaylistSourceType) + m3u8UrlTestData, + if (_features.hasUrlSource) mpgaUrlTestData, + if (_features.hasAssetSource) wavAssetTestData, + /*if (_features.hasAssetSource) + LibSourceTestData( + source: AssetSource(mp3Asset), + duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), + ),*/ + if (_features.hasBytesSource) + LibSourceTestData( + source: BytesSource(await AudioCache.instance.loadAsBytes(wavAsset)), + duration: const Duration(seconds: 1, milliseconds: 068), + ), + /*if (_features.hasBytesSource) + LibSourceTestData( + source: BytesSource(await readBytes(Uri.parse(mp3Url1))), + duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76), + ),*/ + ]; +} diff --git a/packages/audioplayers/example/integration_test/lib_test.dart b/packages/audioplayers/example/integration_test/lib_test.dart index df82262d2..6dfb1eb07 100644 --- a/packages/audioplayers/example/integration_test/lib_test.dart +++ b/packages/audioplayers/example/integration_test/lib_test.dart @@ -10,12 +10,11 @@ import 'lib/lib_test_utils.dart'; import 'platform_features.dart'; import 'test_utils.dart'; -void main() { - final features = PlatformFeatures.instance(); - +void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - + final features = PlatformFeatures.instance(); final isAndroid = !kIsWeb && Platform.isAndroid; + final audioTestDataList = await getAudioTestDataList(); group('play multiple sources', () { testWidgets( @@ -56,8 +55,7 @@ void main() { (WidgetTester tester) async { final player = AudioPlayer(); - for (var i = 0; i < audioTestDataList.length; i++) { - final td = audioTestDataList[i]; + for (final td in audioTestDataList) { await tester.pumpLinux(); await player.play(td.source); await tester.pumpAndSettle(); diff --git a/packages/audioplayers/example/integration_test/platform_test.dart b/packages/audioplayers/example/integration_test/platform_test.dart index 7c50dc758..1d41d8c76 100644 --- a/packages/audioplayers/example/integration_test/platform_test.dart +++ b/packages/audioplayers/example/integration_test/platform_test.dart @@ -1,107 +1,75 @@ import 'dart:async'; +import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_example/tabs/sources.dart'; import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'lib/lib_source_test_data.dart'; import 'lib/lib_test_utils.dart'; +import 'platform_features.dart'; +import 'test_utils.dart'; -void main() { +void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final features = PlatformFeatures.instance(); + final isLinux = !kIsWeb && Platform.isLinux; + final audioTestDataList = await getAudioTestDataList(); - group('Logging', () { - testWidgets('Emit platform log', (tester) async { - final logCompleter = Completer(); - - const playerId = 'somePlayerId'; - final player = AudioPlayer(playerId: playerId); - final onLogSub = player.onLog.listen( - logCompleter.complete, - onError: logCompleter.completeError, - ); - - await player.creatingCompleter.future; - final platform = AudioplayersPlatformInterface.instance; - await platform.emitLog(playerId, 'SomeLog'); + group('Platform method channel', () { + late AudioplayersPlatformInterface platform; + late String playerId; - final log = await logCompleter.future; - expect(log, 'SomeLog'); - await onLogSub.cancel(); - await tester.pumpLinux(); - await player.dispose(); + setUp(() async { + platform = AudioplayersPlatformInterface.instance; + playerId = 'somePlayerId'; + await platform.create(playerId); }); - testWidgets('Emit global platform log', (tester) async { - final completer = Completer(); - final eventStreamSub = AudioPlayer.global.onLog.listen( - completer.complete, - onError: completer.completeError, - ); - - final global = GlobalAudioplayersPlatformInterface.instance; - await global.emitGlobalLog('SomeGlobalLog'); - - final log = await completer.future; - expect(log, 'SomeGlobalLog'); - await eventStreamSub.cancel(); + tearDown(() async { + await platform.dispose(playerId); }); - }); - group('Errors', () { testWidgets( 'Throw PlatformException, when loading invalid file', (tester) async { - final player = AudioPlayer(); try { - await tester.pumpLinux(); // Throws PlatformException via MethodChannel: - await player.setSource(AssetSource(invalidAsset)); + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: invalidAssetTestData, + ); fail('PlatformException not thrown'); // ignore: avoid_catches_without_on_clauses } catch (e) { expect(e, isInstanceOf()); } await tester.pumpLinux(); - await player.dispose(); }, ); testWidgets( 'Throw PlatformException, when loading non existent file', (tester) async { - final player = AudioPlayer(); try { - await tester.pumpLinux(); // Throws PlatformException via MethodChannel: - await player.setSource(UrlSource('non_existent.txt')); + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: nonExistentUrlTestData, + ); fail('PlatformException not thrown'); // ignore: avoid_catches_without_on_clauses } catch (e) { expect(e, isInstanceOf()); } await tester.pumpLinux(); - await player.dispose(); }, ); - }); - - group('Platform method channel', () { - late AudioplayersPlatformInterface platform; - late String playerId; - - setUp(() async { - platform = AudioplayersPlatformInterface.instance; - playerId = 'somePlayerId'; - await platform.create(playerId); - }); - - tearDown(() async { - await platform.dispose(playerId); - }); testWidgets('#create and #dispose', (tester) async { await tester.pumpAndSettle(); @@ -124,43 +92,193 @@ void main() { await tester.pumpLinux(); }); - testWidgets('#setSource #getPosition and #getDuration', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: wavUrl1TestData, - ); - expect(await platform.getCurrentPosition(playerId), 0); - expect( - await platform.getDuration(playerId), - wavUrl1TestData.duration!.inMilliseconds, - ); - await tester.pumpLinux(); - }); - - testWidgets('#seek with millisecond precision', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: mp3Url1TestData, - ); - - final eventStream = platform.getEventStream(playerId); - final seekCompleter = Completer(); - final onSeekSub = eventStream - .where((event) => event.eventType == AudioEventType.seekComplete) - .listen( - (_) { - seekCompleter.complete(); + for (final td in audioTestDataList) { + testWidgets( + '#setSource #getPosition and #getDuration ${td.source}', + (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + expect(await platform.getCurrentPosition(playerId), 0); + final durationMs = await platform.getDuration(playerId); + expect( + durationMs != null ? Duration(milliseconds: durationMs) : null, + // TODO(gustl22): once duration is always null for streams, + // then can remove fallback for Duration.zero + (Duration? actual) => durationRangeMatcher( + actual ?? Duration.zero, + td.duration ?? Duration.zero, + deviation: Duration(milliseconds: td.isVBR ? 100 : 1), + ), + ); + await tester.pumpLinux(); }, - onError: seekCompleter.completeError, + // FIXME(gustl22): cannot determine initial duration for VBR on Linux + // FIXME(gustl22): determines wrong initial position for m3u8 on Linux + skip: isLinux && td.isVBR || + isLinux && td.source == m3u8UrlTestData.source, ); - await platform.seek(playerId, const Duration(milliseconds: 21)); - await seekCompleter.future.timeout(const Duration(seconds: 30)); - await onSeekSub.cancel(); - expect(await platform.getCurrentPosition(playerId), 21); - await tester.pumpLinux(); - }); + } + + if (features.hasVolume) { + for (final td in audioTestDataList) { + testWidgets('#volume ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + for (final volume in [0.0, 0.5, 1.0]) { + await platform.setVolume(playerId, volume); + await platform.resume(playerId); + await tester.pump(const Duration(seconds: 1)); + await platform.stop(playerId); + } + // May check native volume here + await tester.pumpLinux(); + }); + } + } + + if (features.hasBalance) { + for (final td in audioTestDataList) { + testWidgets('#balance ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + for (final balance in [-1.0, 0.0, 1.0]) { + await platform.setBalance(playerId, balance); + await platform.resume(playerId); + await tester.pump(const Duration(seconds: 1)); + await platform.stop(playerId); + } + // May check native balance here + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + if (features.hasPlaybackRate && !td.isLiveStream) { + testWidgets('#playbackRate ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + for (final playbackRate in [0.5, 1.0, 2.0]) { + await platform.setPlaybackRate(playerId, playbackRate); + await platform.resume(playerId); + await tester.pump(const Duration(seconds: 1)); + await platform.stop(playerId); + } + // May check native playback rate here + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + if (features.hasSeek && !td.isLiveStream) { + testWidgets('#seek with millisecond precision ${td.source}', + (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + + final eventStream = platform.getEventStream(playerId); + final seekCompleter = Completer(); + final onSeekSub = eventStream + .where((event) => event.eventType == AudioEventType.seekComplete) + .listen( + (_) => seekCompleter.complete(), + onError: seekCompleter.completeError, + ); + await platform.seek(playerId, const Duration(milliseconds: 22)); + await seekCompleter.future.timeout(const Duration(seconds: 30)); + await onSeekSub.cancel(); + final positionMs = await platform.getCurrentPosition(playerId); + expect( + positionMs != null ? Duration(milliseconds: positionMs) : null, + (Duration? actual) => durationRangeMatcher( + actual, + const Duration(milliseconds: 22), + deviation: const Duration(milliseconds: 1), + ), + ); + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + if (features.hasReleaseModeLoop && + !td.isLiveStream && + td.duration! < const Duration(seconds: 2)) { + testWidgets('#ReleaseMode.loop ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + await platform.setReleaseMode(playerId, ReleaseMode.loop); + await platform.resume(playerId); + await tester.pump(const Duration(seconds: 3)); + await platform.stop(playerId); + + // May check number of loops here + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + if (features.hasReleaseModeRelease && !td.isLiveStream) { + testWidgets('#ReleaseMode.release ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + await platform.setReleaseMode(playerId, ReleaseMode.release); + await platform.resume(playerId); + if (td.duration! < const Duration(seconds: 2)) { + await tester.pumpAndSettle(const Duration(seconds: 3)); + // No need to call stop, as it should be released by now + } else { + await tester.pumpAndSettle(const Duration(seconds: 1)); + await platform.stop(playerId); + } + // TODO(Gustl22): test if source was released + expect(await platform.getDuration(playerId), null); + expect(await platform.getCurrentPosition(playerId), null); + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + testWidgets('#release ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + await tester.pump(const Duration(seconds: 1)); + await platform.release(playerId); + // TODO(Gustl22): test if source was released + // Check if position & duration is zero after play & release + expect(await platform.getDuration(playerId), null); + expect(await platform.getCurrentPosition(playerId), null); + await tester.pumpLinux(); + }); + } testWidgets('Set same source twice (#1520)', (tester) async { for (var i = 0; i < 2; i++) { @@ -188,12 +306,149 @@ void main() { await platform.dispose(playerId); }); + for (final td in audioTestDataList) { + if (features.hasDurationEvent && !td.isLiveStream) { + testWidgets( + '#durationEvent ${td.source}', + (tester) async { + final eventStream = platform.getEventStream(playerId); + final durationCompleter = Completer(); + final onDurationSub = eventStream + .where((event) => event.eventType == AudioEventType.duration) + .listen( + (event) => durationCompleter.complete(event.duration), + onError: durationCompleter.completeError, + ); + + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + + if (td.source == wavAssetTestData.source) { + await tester.pumpLinux(); + } + + expect( + await durationCompleter.future + .timeout(const Duration(seconds: 30)), + (Duration? actual) => durationRangeMatcher( + actual, + td.duration, + deviation: Duration(milliseconds: td.isVBR ? 100 : 1), + ), + ); + await onDurationSub.cancel(); + await tester.pumpLinux(); + }, + // TODO(gustl22): cannot determine duration for VBR on Linux + // FIXME(gustl22): duration event is not emitted for short duration + // WAV on Linux (only platform tests, may be a race condition). + skip: isLinux && td.isVBR || + isLinux && td.duration! < const Duration(seconds: 5), + ); + } + } + + for (final td in audioTestDataList) { + if (features.hasPositionEvent && + (td.isLiveStream || td.duration! > const Duration(seconds: 2))) { + testWidgets('#positionEvent ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + + final eventStream = platform.getEventStream(playerId); + Duration? position; + final onPositionSub = eventStream + .where((event) => event.eventType == AudioEventType.position) + .listen( + (event) => position = event.position, + ); + + await platform.resume(playerId); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(position, greaterThan(Duration.zero)); + await platform.stop(playerId); + await onPositionSub.cancel(); + await tester.pumpLinux(); + }); + } + } + + for (final td in audioTestDataList) { + if (!td.isLiveStream && td.duration! < const Duration(seconds: 2)) { + testWidgets('#completeEvent ${td.source}', (tester) async { + await tester.prepareSource( + playerId: playerId, + platform: platform, + testData: td, + ); + + final eventStream = platform.getEventStream(playerId); + final completeCompleter = Completer(); + final onCompleteSub = eventStream + .where((event) => event.eventType == AudioEventType.complete) + .listen( + completeCompleter.complete, + onError: completeCompleter.completeError, + ); + + await platform.resume(playerId); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await completeCompleter.future.timeout(const Duration(seconds: 30)); + onCompleteSub.cancel(); + await tester.pumpLinux(); + }); + } + } + testWidgets('Listen and cancel twice', (tester) async { final eventStream = platform.getEventStream(playerId); for (var i = 0; i < 2; i++) { final eventSub = eventStream.listen(null); await eventSub.cancel(); + await tester.pumpLinux(); } + }); + + testWidgets('Emit platform log', (tester) async { + final logCompleter = Completer(); + final logSub = platform + .getEventStream(playerId) + .where((event) => event.eventType == AudioEventType.log) + .map((event) => event.logMessage) + .listen(logCompleter.complete, onError: logCompleter.completeError); + + await platform.emitLog(playerId, 'SomeLog'); + + final log = await logCompleter.future; + expect(log, 'SomeLog'); + await logSub.cancel(); + await tester.pumpLinux(); + }); + + testWidgets('Emit global platform log', (tester) async { + final global = GlobalAudioplayersPlatformInterface.instance; + final logCompleter = Completer(); + + /* final eventStreamSub = */ + global + .getGlobalEventStream() + .where((event) => event.eventType == GlobalAudioEventType.log) + .map((event) => event.logMessage) + .listen(logCompleter.complete, onError: logCompleter.completeError); + + await global.emitGlobalLog('SomeGlobalLog'); + + final log = await logCompleter.future; + expect(log, 'SomeGlobalLog'); + // FIXME: cancelling the global event stream leads to + // MissingPluginException on Android, if dispose app afterwards + // await eventStreamSub.cancel(); await tester.pumpLinux(); }); @@ -239,6 +494,7 @@ void main() { // FIXME: cancelling the global event stream leads to // MissingPluginException on Android, if dispose app afterwards // await eventStreamSub.cancel(); + await tester.pumpLinux(); }); }); } @@ -260,13 +516,22 @@ extension on WidgetTester { preparedCompleter.complete(); } }, - onError: preparedCompleter.completeError, + onError: (Object e, [StackTrace? st]) { + if (!preparedCompleter.isCompleted) { + preparedCompleter.completeError(e, st); + } + }, ); await pumpLinux(); - await platform.setSourceUrl( - playerId, - (testData.source as UrlSource).url, - ); + final source = testData.source; + if (source is UrlSource) { + await platform.setSourceUrl(playerId, source.url); + } else if (source is AssetSource) { + final url = await AudioCache.instance.load(source.path); + await platform.setSourceUrl(playerId, url.path, isLocal: true); + } else if (source is BytesSource) { + await platform.setSourceBytes(playerId, source.bytes); + } await preparedCompleter.future.timeout(const Duration(seconds: 30)); await onPreparedSub.cancel(); } diff --git a/packages/audioplayers/example/integration_test/source_test_data.dart b/packages/audioplayers/example/integration_test/source_test_data.dart index c1c90902e..eb49a8606 100644 --- a/packages/audioplayers/example/integration_test/source_test_data.dart +++ b/packages/audioplayers/example/integration_test/source_test_data.dart @@ -4,14 +4,19 @@ abstract class SourceTestData { bool get isLiveStream => duration == null; + /// Whether this source has variable bitrate + bool isVBR; + SourceTestData({ required this.duration, + this.isVBR = false, }); @override String toString() { return 'SourceTestData(' - 'duration: $duration' + 'duration: $duration, ' + 'isVBR: $isVBR' ')'; } } diff --git a/packages/audioplayers/example/integration_test/test_utils.dart b/packages/audioplayers/example/integration_test/test_utils.dart index 24a09943e..2e05c5ea2 100644 --- a/packages/audioplayers/example/integration_test/test_utils.dart +++ b/packages/audioplayers/example/integration_test/test_utils.dart @@ -3,3 +3,17 @@ import 'package:flutter_test/flutter_test.dart'; void printWithTimeOnFailure(String message) { printOnFailure('${DateTime.now()}: $message'); } + +bool durationRangeMatcher( + Duration? actual, + Duration? expected, { + Duration deviation = const Duration(seconds: 1), +}) { + if (actual == null && expected == null) { + return true; + } + if (actual == null || expected == null) { + return false; + } + return actual >= (expected - deviation) && actual <= (expected + deviation); +} diff --git a/packages/audioplayers/example/lib/tabs/sources.dart b/packages/audioplayers/example/lib/tabs/sources.dart index 6643707ba..129d0093e 100644 --- a/packages/audioplayers/example/lib/tabs/sources.dart +++ b/packages/audioplayers/example/lib/tabs/sources.dart @@ -122,7 +122,7 @@ class _SourcesTabState extends State ), _createSourceTile( setSourceKey: const Key('setSource-url-remote-mp3-1'), - title: 'Remote URL MP3 1', + title: 'Remote URL MP3 1 (VBR)', subtitle: 'ambient_c_motion.mp3', source: UrlSource(mp3Url1), ), diff --git a/packages/audioplayers/lib/src/audioplayer.dart b/packages/audioplayers/lib/src/audioplayer.dart index 73ab50f0a..c676e2a1d 100644 --- a/packages/audioplayers/lib/src/audioplayer.dart +++ b/packages/audioplayers/lib/src/audioplayer.dart @@ -308,7 +308,7 @@ class AudioPlayer { } }, onError: (Object e, [StackTrace? stackTrace]) { - if (preparedCompleter.isCompleted == false) { + if (!preparedCompleter.isCompleted) { preparedCompleter.completeError(e, stackTrace); } }, diff --git a/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift b/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift index 47c741381..cebd2999d 100644 --- a/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift +++ b/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift @@ -146,6 +146,7 @@ class WrappedMediaPlayer { func release(completer: Completer? = nil) { stop { self.reset() + self.url = nil completer?() } }