Skip to content

Commit

Permalink
fix: onPrepared event to wait until player is ready / finished loadin…
Browse files Browse the repository at this point in the history
…g the source (#1469)

# Description

Add a callback when the player has prepared its source, which is tangled
to the asynchronous `setSource` method, to improve experience, when
player should actually play and not wait for the player to prepare /
load the source. 

## Related Issues

Closes #1118 
Closes #1384 
Closes #1359

#1191
flutter/flutter#126209
  • Loading branch information
Gustl22 committed May 8, 2023
1 parent ab5bdf6 commit 50f5636
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 94 deletions.
127 changes: 94 additions & 33 deletions packages/audioplayers/example/integration_test/lib_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'mock_html.dart' if (dart.library.html) 'dart:html' show DomException;
import 'platform_features.dart';
import 'source_test_data.dart';
import 'test_utils.dart';
Expand All @@ -19,12 +18,15 @@ void main() {

IntegrationTestWidgetsFlutterBinding.ensureInitialized();

final isAndroid = !kIsWeb && Platform.isAndroid;
final isLinux = !kIsWeb && Platform.isLinux;

final wavUrl1TestData = LibSourceTestData(
source: UrlSource(wavUrl1),
duration: const Duration(milliseconds: 451),
);
final audioTestDataList = [
if (features.hasUrlSource)
LibSourceTestData(
source: UrlSource(wavUrl1),
duration: const Duration(milliseconds: 451),
),
if (features.hasUrlSource) wavUrl1TestData,
if (features.hasUrlSource)
LibSourceTestData(
source: UrlSource(wavUrl2),
Expand Down Expand Up @@ -73,6 +75,7 @@ void main() {

// Start all players simultaneously
final iterator = List<int>.generate(audioTestDataList.length, (i) => i);
await tester.pump();
await Future.wait<void>(
iterator.map((i) => players[i].play(audioTestDataList[i].source)),
);
Expand All @@ -94,7 +97,7 @@ void main() {
// FIXME: Causes media error on Android (see #1333, #1353)
// Unexpected platform error: MediaPlayer error with
// what:MEDIA_ERROR_UNKNOWN {what:1} extra:MEDIA_ERROR_SYSTEM
skip: !kIsWeb && Platform.isAndroid,
skip: isAndroid,
);

testWidgets('play multiple sources consecutively',
Expand All @@ -103,6 +106,7 @@ void main() {

for (var i = 0; i < audioTestDataList.length; i++) {
final td = audioTestDataList[i];
await tester.pump();
await player.play(td.source);
await tester.pumpAndSettle();
// Sources take some time to get initialized
Expand Down Expand Up @@ -130,7 +134,7 @@ void main() {
final player = AudioPlayer();
await player.setReleaseMode(ReleaseMode.stop);

final td = audioTestDataList[0];
final td = wavUrl1TestData;

var audioContext = AudioContextConfig(
//ignore: avoid_redundant_argument_values
Expand All @@ -141,6 +145,7 @@ void main() {
await AudioPlayer.global.setAudioContext(audioContext);
await player.setAudioContext(audioContext);

await tester.pump();
await player.play(td.source);
await tester.pumpAndSettle();
await tester.pump(td.duration + const Duration(seconds: 8));
Expand Down Expand Up @@ -173,7 +178,7 @@ void main() {
await player.setReleaseMode(ReleaseMode.stop);
player.setPlayerMode(PlayerMode.lowLatency);

final td = audioTestDataList[0];
final td = wavUrl1TestData;

var audioContext = AudioContextConfig(
//ignore: avoid_redundant_argument_values
Expand All @@ -184,6 +189,7 @@ void main() {
await AudioPlayer.global.setAudioContext(audioContext);
await player.setAudioContext(audioContext);

await tester.pump();
await player.setSource(td.source);
await player.resume();
await tester.pumpAndSettle();
Expand Down Expand Up @@ -214,7 +220,9 @@ void main() {
group('Logging', () {
testWidgets('Emit platform log', (tester) async {
final logCompleter = Completer<String>();
const playerId = 'somePlayerId';

// FIXME: Cannot reuse event channel with same id on Linux
final playerId = isLinux ? 'somePlayerId0' : 'somePlayerId';
final player = AudioPlayer(playerId: playerId);
final onLogSub = player.onLog.listen(
logCompleter.complete,
Expand Down Expand Up @@ -249,59 +257,46 @@ void main() {

group('Errors', () {
testWidgets(
'Throw PlatformException, when playing invalid file',
'Throw PlatformException, when loading invalid file',
(tester) async {
final player = AudioPlayer();
try {
// Throws PlatformException via MethodChannel:
await tester.pump();
await player.setSource(AssetSource(invalidAsset));
await player.resume();
fail('PlatformException not thrown');
// ignore: avoid_catches_without_on_clauses
} catch (e) {
if (kIsWeb) {
expect(e, isInstanceOf<DomException>());
expect((e as DomException).name, 'NotSupportedError');
} else {
expect(e, isInstanceOf<PlatformException>());
}
expect(e, isInstanceOf<PlatformException>());
}
await player.dispose();
},
// Linux provides errors only asynchronously.
skip: !kIsWeb && Platform.isLinux,
);

testWidgets(
'Throw PlatformException, when playing non existent file',
'Throw PlatformException, when loading non existent file',
(tester) async {
final player = AudioPlayer();
try {
// Throws PlatformException via MethodChannel:
await tester.pump();
await player.setSource(UrlSource('non_existent.txt'));
await player.resume();
fail('PlatformException not thrown');
// ignore: avoid_catches_without_on_clauses
} catch (e) {
if (kIsWeb) {
expect(e, isInstanceOf<DomException>());
expect((e as DomException).name, 'NotSupportedError');
} else {
expect(e, isInstanceOf<PlatformException>());
}
expect(e, isInstanceOf<PlatformException>());
}
await player.dispose();
},
// Linux provides errors only asynchronously.
skip: !kIsWeb && Platform.isLinux,
);
});

group('Platform method channel', () {
testWidgets('#create and #dispose', (tester) async {
final platform = AudioplayersPlatformInterface.instance;

const playerId = 'somePlayerId';
// FIXME: Cannot reuse event channel with same id on Linux
final playerId = isLinux ? 'somePlayerId1' : 'somePlayerId';
await platform.create(playerId);
await tester.pumpAndSettle();
await platform.dispose(playerId);
Expand All @@ -318,14 +313,78 @@ void main() {
);
}
});

testWidgets('#setSource #getPosition and #getDuration', (tester) async {
final platform = AudioplayersPlatformInterface.instance;

// FIXME: Cannot reuse event channel with same id on Linux
final playerId = isLinux ? 'somePlayerId2' : 'somePlayerId';
await platform.create(playerId);

final preparedCompleter = Completer<void>();
final eventStream = platform.getEventStream(playerId);
final onPreparedSub = eventStream
.where((event) => event.eventType == AudioEventType.prepared)
.map((event) => event.isPrepared!)
.listen(
(isPrepared) {
if (isPrepared) {
preparedCompleter.complete();
}
},
onError: preparedCompleter.completeError,
);
await tester.pump();
await platform.setSourceUrl(
playerId,
(wavUrl1TestData.source as UrlSource).url,
);
await preparedCompleter.future.timeout(const Duration(seconds: 30));

expect(await platform.getCurrentPosition(playerId), 0);
expect(
await platform.getDuration(playerId),
wavUrl1TestData.duration.inMilliseconds,
);

await onPreparedSub.cancel();
await platform.dispose(playerId);
});
});

group('Platform event channel', () {
// TODO(gustl22): remove once https://github.com/flutter/flutter/issues/126209 is fixed
testWidgets(
'Reuse same platform event channel id',
(tester) async {
final platform = AudioplayersPlatformInterface.instance;

const playerId = 'somePlayerId';
await platform.create(playerId);

final eventStreamSub = platform.getEventStream(playerId).listen((_) {});

await eventStreamSub.cancel();
await platform.dispose(playerId);

// Recreate player with same player Id
await platform.create(playerId);

final eventStreamSub2 =
platform.getEventStream(playerId).listen((_) {});

await eventStreamSub2.cancel();
await platform.dispose(playerId);
},
skip: isLinux,
);

testWidgets('Emit platform error', (tester) async {
final errorCompleter = Completer<Object>();
final platform = AudioplayersPlatformInterface.instance;

const playerId = 'somePlayerId';
// FIXME: Cannot reuse event channel with same id on Linux
final playerId = isLinux ? 'somePlayerId3' : 'somePlayerId';
await platform.create(playerId);

final eventStreamSub = platform
Expand All @@ -350,6 +409,8 @@ void main() {
testWidgets('Emit global platform error', (tester) async {
final errorCompleter = Completer<Object>();
final global = GlobalAudioplayersPlatformInterface.instance;

/* final eventStreamSub = */
global
.getGlobalEventStream()
.listen((_) {}, onError: errorCompleter.complete);
Expand All @@ -364,7 +425,7 @@ void main() {
expect(platformException.code, 'SomeGlobalErrorCode');
expect(platformException.message, 'SomeGlobalErrorMessage');
// FIXME: cancelling the global event stream leads to
// MissingPluginException on Android
// MissingPluginException on Android, if dispose app afterwards
// await eventStreamSub.cancel();
});
});
Expand Down
6 changes: 0 additions & 6 deletions packages/audioplayers/example/integration_test/mock_html.dart

This file was deleted.

47 changes: 39 additions & 8 deletions packages/audioplayers/lib/src/audioplayer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class AudioPlayer {
Stream<void> get onSeekComplete => eventStream
.where((event) => event.eventType == AudioEventType.seekComplete);

Stream<bool> get _onPrepared => eventStream
.where((event) => event.eventType == AudioEventType.prepared)
.map((event) => event.isPrepared!);

/// Stream of log events.
Stream<String> get onLog => eventStream
.where((event) => event.eventType == AudioEventType.log)
Expand Down Expand Up @@ -150,8 +154,8 @@ class AudioPlayer {
onError: _eventStreamController.addError,
);
creatingCompleter.complete();
} on Exception catch (e, st) {
creatingCompleter.completeError(e, st);
} on Exception catch (e, stackTrace) {
creatingCompleter.completeError(e, stackTrace);
}
}

Expand Down Expand Up @@ -279,8 +283,27 @@ class AudioPlayer {
/// This will delegate to one of the specific methods below depending on
/// the source type.
Future<void> setSource(Source source) async {
await creatingCompleter.future;
return source.setOnPlayer(this);
// Implementations of setOnPlayer also call `creatingCompleter.future`
await source.setOnPlayer(this);
}

Future<void> _completePrepared(Future<void> Function() fun) async {
final preparedCompleter = Completer<void>();
final onPreparedSubscription = _onPrepared.listen(
(isPrepared) {
if (isPrepared) {
preparedCompleter.complete();
}
},
onError: (Object e, [StackTrace? stackTrace]) {
if (preparedCompleter.isCompleted == false) {
preparedCompleter.completeError(e, stackTrace);
}
},
);
await fun();
await preparedCompleter.future.timeout(const Duration(seconds: 30));
onPreparedSubscription.cancel();
}

/// Sets the URL to a remote link.
Expand All @@ -290,7 +313,9 @@ class AudioPlayer {
Future<void> setSourceUrl(String url) async {
_source = UrlSource(url);
await creatingCompleter.future;
return _platform.setSourceUrl(playerId, url, isLocal: false);
await _completePrepared(
() => _platform.setSourceUrl(playerId, url, isLocal: false),
);
}

/// Sets the URL to a file in the users device.
Expand All @@ -300,7 +325,9 @@ class AudioPlayer {
Future<void> setSourceDeviceFile(String path) async {
_source = DeviceFileSource(path);
await creatingCompleter.future;
return _platform.setSourceUrl(playerId, path, isLocal: true);
await _completePrepared(
() => _platform.setSourceUrl(playerId, path, isLocal: true),
);
}

/// Sets the URL to an asset in your Flutter application.
Expand All @@ -312,13 +339,17 @@ class AudioPlayer {
_source = AssetSource(path);
final url = await audioCache.load(path);
await creatingCompleter.future;
return _platform.setSourceUrl(playerId, url.path, isLocal: true);
await _completePrepared(
() => _platform.setSourceUrl(playerId, url.path, isLocal: true),
);
}

Future<void> setSourceBytes(Uint8List bytes) async {
_source = BytesSource(bytes);
await creatingCompleter.future;
return _platform.setSourceBytes(playerId, bytes);
await _completePrepared(
() => _platform.setSourceBytes(playerId, bytes),
);
}

/// Get audio duration after setting url.
Expand Down
6 changes: 6 additions & 0 deletions packages/audioplayers/test/fake_audioplayers_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface {
@override
Future<void> setSourceBytes(String playerId, Uint8List bytes) async {
calls.add(FakeCall(id: playerId, method: 'setSourceBytes', value: bytes));
eventStreamControllers[playerId]?.add(
const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true),
);
}

@override
Expand All @@ -132,6 +135,9 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface {
bool? isLocal,
}) async {
calls.add(FakeCall(id: playerId, method: 'setSourceUrl', value: url));
eventStreamControllers[playerId]?.add(
const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true),
);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class MediaPlayerPlayer(
}

override fun prepare() {
mediaPlayer.prepare()
mediaPlayer.prepareAsync()
}

override fun reset() {
Expand Down
Loading

0 comments on commit 50f5636

Please sign in to comment.