Skip to content

Commit

Permalink
fix: Race condition when playing/pausing audio (#1705)
Browse files Browse the repository at this point in the history
# Description

Fixes #1687
  • Loading branch information
Gustl22 committed Nov 20, 2023
1 parent 02e4c63 commit 463b2a1
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 13 deletions.
24 changes: 24 additions & 0 deletions packages/audioplayers/example/integration_test/lib_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,30 @@ void main() async {
);
});

testWidgets('Race condition on play and pause (#1687)',
(WidgetTester tester) async {
final player = AudioPlayer();

await tester.pumpLinux();
final futurePlay = player.play(mp3Url1TestData.source);

// Player is still in `stopped` state as it isn't playing yet.
expect(player.state, PlayerState.stopped);
expect(player.desiredState, PlayerState.playing);

// Execute `pause` before `play` has finished.
final futurePause = player.pause();
expect(player.desiredState, PlayerState.paused);

await futurePlay;
await futurePause;

expect(player.state, PlayerState.paused);

await tester.pumpLinux();
await player.dispose();
});

group(
'Android only:',
() {
Expand Down
53 changes: 40 additions & 13 deletions packages/audioplayers/lib/src/audioplayer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,25 @@ class AudioPlayer {

ReleaseMode get releaseMode => _releaseMode;

/// Auxiliary variable to re-check the volatile player state during async
/// operations.
@visibleForTesting
PlayerState desiredState = PlayerState.stopped;

PlayerState _playerState = PlayerState.stopped;

PlayerState get state => _playerState;

/// The current playback state.
/// It is only set, when the corresponding action succeeds.
set state(PlayerState state) {
if (_playerState == PlayerState.disposed) {
throw Exception('AudioPlayer has been disposed');
}
if (!_playerStateController.isClosed) {
_playerStateController.add(state);
}
_playerState = state;
_playerState = desiredState = state;
}

PositionUpdater? _positionUpdater;
Expand Down Expand Up @@ -179,6 +186,10 @@ class AudioPlayer {
}
}

/// Play an audio [source].
///
/// To reduce preparation latency, instead consider calling [setSource]
/// beforehand and then [resume] separately.
Future<void> play(
Source source, {
double? volume,
Expand All @@ -187,6 +198,8 @@ class AudioPlayer {
Duration? position,
PlayerMode? mode,
}) async {
desiredState = PlayerState.playing;

if (mode != null) {
await setPlayerMode(mode);
}
Expand All @@ -205,7 +218,7 @@ class AudioPlayer {
await seek(position);
}

await resume();
await _resume();
}

Future<void> setAudioContext(AudioContext ctx) async {
Expand All @@ -224,29 +237,43 @@ class AudioPlayer {
/// If you call [resume] later, the audio will resume from the point that it
/// has been paused.
Future<void> pause() async {
desiredState = PlayerState.paused;
await creatingCompleter.future;
await _platform.pause(playerId);
state = PlayerState.paused;
await _positionUpdater?.stopAndUpdate();
if (desiredState == PlayerState.paused) {
await _platform.pause(playerId);
state = PlayerState.paused;
await _positionUpdater?.stopAndUpdate();
}
}

/// Stops the audio that is currently playing.
///
/// The position is going to be reset and you will no longer be able to resume
/// from the last point.
Future<void> stop() async {
desiredState = PlayerState.stopped;
await creatingCompleter.future;
await _platform.stop(playerId);
state = PlayerState.stopped;
await _positionUpdater?.stopAndUpdate();
if (desiredState == PlayerState.stopped) {
await _platform.stop(playerId);
state = PlayerState.stopped;
await _positionUpdater?.stopAndUpdate();
}
}

/// Resumes the audio that has been paused or stopped.
Future<void> resume() async {
desiredState = PlayerState.playing;
await _resume();
}

/// Resume without setting the desired state.
Future<void> _resume() async {
await creatingCompleter.future;
await _platform.resume(playerId);
state = PlayerState.playing;
_positionUpdater?.start();
if (desiredState == PlayerState.playing) {
await _platform.resume(playerId);
state = PlayerState.playing;
_positionUpdater?.start();
}
}

/// Releases the resources associated with this media player.
Expand All @@ -256,7 +283,7 @@ class AudioPlayer {
Future<void> release() async {
await stop();
await _platform.release(playerId);
state = PlayerState.stopped;
// Stop state already set in stop()
_source = null;
}

Expand Down Expand Up @@ -428,7 +455,7 @@ class AudioPlayer {
// First stop and release all native resources.
await release();

state = PlayerState.disposed;
state = desiredState = PlayerState.disposed;

final futures = <Future>[
if (_positionUpdater != null) _positionUpdater!.dispose(),
Expand Down

0 comments on commit 463b2a1

Please sign in to comment.