Fix callback teardown deadlock and audio-device lifecycle races - #18
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes cases where the native audio device could remain stopped after playback resumed, and prevents teardown deadlocks caused by native callbacks racing with engine disposal.
The original issue was most visible on Android after background playback, notification removal, and resuming through system media controls. The lifecycle fixes are implemented in shared native code and also improve interruption recovery on iOS.
Root causes
Callback teardown deadlock
Native teardown could hold the Dart callback mutex while waiting for the lifecycle scheduler to stop. At the same time, the scheduler could emit a state callback and wait for that same mutex.
This created a lock cycle:
dispose()held the callback mutex.Player::dispose()waited for the scheduler thread.Lost lifecycle requests
The lifecycle scheduler previously used a last-writer-wins pending request. Lower-priority idle work could replace a required device start, and a recovery start could be discarded while an interruption stop was pending or in progress.
This could leave:
Changes
Callback lifecycle and teardown
NativeCallableobjects alive until native teardown completes successfully.Audio-device lifecycle scheduler
Prioritize lifecycle operations as:
interruptionStop > start > idleStopPrevent idle requests from invalidating pending or in-flight immediate operations.
Defer recovery starts until an interruption stop completes.
Preserve newer idle work that arrives after a deferred recovery start.
Perform the authoritative active-voice check on the scheduler thread immediately before stopping the device.
Serialize explicit and automatic device start/stop operations.
Cancel pending lifecycle work safely during teardown.
Keep blocking backend operations off the Flutter UI isolate.
Voice operation error handling
PlayerErrorsfrom nativepauseSwitch,setPause, andstop.stop()behavior when a handle ends between validation and the native stop call.Regression coverage
Added and expanded tests for: