Skip to content

Commit

Permalink
fix: rework dispose (#1480)
Browse files Browse the repository at this point in the history
# Description

- Add `PlayerState.disposed` (closes #1450)
- Add `AudioPool.dispose()` (closes #1478)
- Dispose players in tests
- Properly handle dispose on all platforms (closes #1475)
- Fix running method calls on non UI threads causes PlatformException
(closes #1481)
  • Loading branch information
Gustl22 committed May 3, 2023
1 parent a342c06 commit c64ef6d
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 157 deletions.
34 changes: 21 additions & 13 deletions getting_started.md
Expand Up @@ -54,6 +54,22 @@ Or, if you want to set the url and start playing, using the `play` shortcut:

After the URL is set, you can use the following methods to control the player:

### resume

Starts playback from current position (by default, from the start).

```dart
await player.resume();
```

### seek

Changes the current position (note: this does not affect the "playing" status).

```dart
await player.seek(Duration(milliseconds: 1200));
```

### pause

Stops the playback but keeps the current position.
Expand All @@ -72,20 +88,20 @@ Stops the playback and also resets the current position.

### release

Equivalent to calling `stop` and then disposing of any resources associated with this player.
Equivalent to calling `stop` and then releasing of any resources associated with this player.

This means that any streams will be disposed, memory might be de-allocated, etc.
This means that memory might be de-allocated, etc.

Note that the player is also in a ready-to-use state; if you call `resume` again any necessary resources will be re-fetch.

Particularly on Android, the media player is quite resource-intensive, and this will let it go. Data will be buffered again when needed (if it's a remote file, it will be downloaded again.

### resume
### dispose

Starts playback from current position (by default, from the start).
Disposes the player. It is calling `release` and also closes all open streams. This player instance must not be used anymore!

```dart
await player.resume();
await player.dispose();
```

### play
Expand All @@ -99,14 +115,6 @@ Play is just a shortcut method that allows you to:

All in a single function call. For most simple use cases, it might be the only method you need.

### seek

Changes the current position (note: this does not affect the "playing" status).

```dart
await player.seek(Duration(milliseconds: 1200));
```

## Player Parameters

You can also change the following parameters:
Expand Down
145 changes: 89 additions & 56 deletions packages/audioplayers/example/integration_test/lib_test.dart
Expand Up @@ -89,6 +89,7 @@ void main() {
}
await players[i].stop();
}
await Future.wait(players.map((p) => p.dispose()));
},
// FIXME: Causes media error on Android (see #1333, #1353)
// Unexpected platform error: MediaPlayer error with
Expand All @@ -114,6 +115,7 @@ void main() {
}
await player.stop();
}
await player.dispose();
});
});

Expand Down Expand Up @@ -155,6 +157,7 @@ void main() {
await tester.pumpAndSettle();
await tester.pump(td.duration + const Duration(seconds: 8));
expect(player.state, PlayerState.completed);
await player.dispose();
},
skip: !features.hasForceSpeaker,
);
Expand All @@ -166,7 +169,8 @@ void main() {
testWidgets(
'test changing AudioContextConfigs in LOW_LATENCY mode',
(WidgetTester tester) async {
final player = AudioPlayer()..setReleaseMode(ReleaseMode.stop);
final player = AudioPlayer();
await player.setReleaseMode(ReleaseMode.stop);
player.setPlayerMode(PlayerMode.lowLatency);

final td = audioTestDataList[0];
Expand Down Expand Up @@ -201,43 +205,35 @@ void main() {
expect(player.state, PlayerState.playing);
await player.stop();
expect(player.state, PlayerState.stopped);
await player.dispose();
},
skip: !features.hasForceSpeaker || !features.hasLowLatency,
);
});

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

const playerId = 'somePlayerId';
await platform.create(playerId);
await tester.pumpAndSettle();
await platform.dispose(playerId);
});
});

group('Logging', () {
testWidgets('Emit platform log', (tester) async {
final completer = Completer<String>();
final logCompleter = Completer<String>();
const playerId = 'somePlayerId';
final player = AudioPlayer(playerId: playerId);
player.onLog.listen(
completer.complete,
onError: completer.completeError,
final onLogSub = player.onLog.listen(
logCompleter.complete,
onError: logCompleter.completeError,
);

await player.creatingCompleter.future;
final platform = AudioplayersPlatformInterface.instance;
await platform.emitLog(playerId, 'SomeLog');

final log = await completer.future;
final log = await logCompleter.future;
expect(log, 'SomeLog');
await onLogSub.cancel();
await player.dispose();
});

testWidgets('Emit global platform log', (tester) async {
final completer = Completer<String>();
AudioPlayer.global.onLog.listen(
final eventStreamSub = AudioPlayer.global.onLog.listen(
completer.complete,
onError: completer.completeError,
);
Expand All @@ -247,48 +243,11 @@ void main() {

final log = await completer.future;
expect(log, 'SomeGlobalLog');
await eventStreamSub.cancel();
});
});

group('Errors', () {
testWidgets('Emit platform error', (tester) async {
final completer = Completer<Object>();
const playerId = 'somePlayerId';
final player = AudioPlayer(playerId: playerId);
player.eventStream.listen((_) {}, onError: completer.complete);

await player.creatingCompleter.future;
final platform = AudioplayersPlatformInterface.instance;
await platform.emitError(
playerId,
'SomeErrorCode',
'SomeErrorMessage',
);

final exception = await completer.future;
expect(exception, isInstanceOf<PlatformException>());
final platformException = exception as PlatformException;
expect(platformException.code, 'SomeErrorCode');
expect(platformException.message, 'SomeErrorMessage');
});

testWidgets('Emit global platform error', (tester) async {
final completer = Completer<Object>();
AudioPlayer.global.eventStream
.listen((_) {}, onError: completer.complete);

final global = GlobalAudioplayersPlatformInterface.instance;
await global.emitGlobalError(
'SomeGlobalErrorCode',
'SomeGlobalErrorMessage',
);
final exception = await completer.future;
expect(exception, isInstanceOf<PlatformException>());
final platformException = exception as PlatformException;
expect(platformException.code, 'SomeGlobalErrorCode');
expect(platformException.message, 'SomeGlobalErrorMessage');
});

testWidgets(
'Throw PlatformException, when playing invalid file',
(tester) async {
Expand All @@ -307,6 +266,7 @@ void main() {
expect(e, isInstanceOf<PlatformException>());
}
}
await player.dispose();
},
// Linux provides errors only asynchronously.
skip: !kIsWeb && Platform.isLinux,
Expand All @@ -330,9 +290,82 @@ void main() {
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';
await platform.create(playerId);
await tester.pumpAndSettle();
await platform.dispose(playerId);

try {
// Call method after player has been released should throw a
// PlatformException
await platform.stop(playerId);
fail('PlatformException not thrown');
} on PlatformException catch (e) {
expect(
e.message,
'Player has not yet been created or has already been disposed.',
);
}
});
});

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

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

final eventStreamSub = platform
.getEventStream(playerId)
.listen((_) {}, onError: errorCompleter.complete);

await platform.emitError(
playerId,
'SomeErrorCode',
'SomeErrorMessage',
);

final exception = await errorCompleter.future;
expect(exception, isInstanceOf<PlatformException>());
final platformException = exception as PlatformException;
expect(platformException.code, 'SomeErrorCode');
expect(platformException.message, 'SomeErrorMessage');
await eventStreamSub.cancel();
await platform.dispose(playerId);
});

testWidgets('Emit global platform error', (tester) async {
final errorCompleter = Completer<Object>();
final global = GlobalAudioplayersPlatformInterface.instance;
global
.getGlobalEventStream()
.listen((_) {}, onError: errorCompleter.complete);

await global.emitGlobalError(
'SomeGlobalErrorCode',
'SomeGlobalErrorMessage',
);
final exception = await errorCompleter.future;
expect(exception, isInstanceOf<PlatformException>());
final platformException = exception as PlatformException;
expect(platformException.code, 'SomeGlobalErrorCode');
expect(platformException.message, 'SomeGlobalErrorMessage');
// FIXME: cancelling the global event stream leads to
// MissingPluginException on Android
// await eventStreamSub.cancel();
});
});
}
4 changes: 4 additions & 0 deletions packages/audioplayers/lib/src/audio_pool.dart
Expand Up @@ -123,4 +123,8 @@ class AudioPool {
await player.setReleaseMode(ReleaseMode.stop);
return player;
}

/// Disposes the audio pool. Then it cannot be used anymore.
Future<void> dispose() =>
Future.wait(availablePlayers.map((e) => e.dispose()));
}
8 changes: 7 additions & 1 deletion packages/audioplayers/lib/src/audioplayer.dart
Expand Up @@ -35,6 +35,9 @@ class AudioPlayer {
Source? get source => _source;

set state(PlayerState state) {
if (_playerState == PlayerState.disposed) {
throw Exception('AudioPlayer has been disposed');
}
if (!_playerStateController.isClosed) {
_playerStateController.add(state);
}
Expand Down Expand Up @@ -350,7 +353,7 @@ class AudioPlayer {
// First stop and release all native resources.
await release();

await _platform.dispose(playerId);
state = PlayerState.disposed;

final futures = <Future>[
if (!_playerStateController.isClosed) _playerStateController.close(),
Expand All @@ -363,5 +366,8 @@ class AudioPlayer {
_source = null;

await Future.wait<dynamic>(futures);

// Needs to be called after cancelling event stream subscription:
await _platform.dispose(playerId);
}
}
4 changes: 2 additions & 2 deletions packages/audioplayers/test/audioplayers_test.dart
Expand Up @@ -129,8 +129,8 @@ void main() {
emitsInOrder(audioEvents),
);

audioEvents.forEach(platform.eventStreamController.add);
await platform.eventStreamController.close();
audioEvents.forEach(platform.eventStreamControllers['p1']!.add);
await platform.eventStreamControllers['p1']!.close();
});
});
}
8 changes: 4 additions & 4 deletions packages/audioplayers/test/fake_audioplayers_platform.dart
Expand Up @@ -15,8 +15,7 @@ class FakeCall {
class FakeAudioplayersPlatform extends AudioplayersPlatformInterface {
List<FakeCall> calls = [];

StreamController<AudioEvent> eventStreamController =
StreamController<AudioEvent>.broadcast();
Map<String, StreamController<AudioEvent>> eventStreamControllers = {};

void clear() {
calls.clear();
Expand All @@ -34,12 +33,13 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface {
@override
Future<void> create(String playerId) async {
calls.add(FakeCall(id: playerId, method: 'create'));
eventStreamControllers[playerId] = StreamController<AudioEvent>.broadcast();
}

@override
Future<void> dispose(String playerId) async {
calls.add(FakeCall(id: playerId, method: 'dispose'));
eventStreamController.close();
eventStreamControllers[playerId]?.close();
}

@override
Expand Down Expand Up @@ -147,6 +147,6 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface {
@override
Stream<AudioEvent> getEventStream(String playerId) {
calls.add(FakeCall(id: playerId, method: 'getEventStream'));
return eventStreamController.stream;
return eventStreamControllers[playerId]!.stream;
}
}

0 comments on commit c64ef6d

Please sign in to comment.