Skip to content

Fix callback teardown deadlock and audio-device lifecycle races - #18

Merged
Colton127 merged 13 commits into
mainfrom
detach-callback
Jul 24, 2026
Merged

Fix callback teardown deadlock and audio-device lifecycle races#18
Colton127 merged 13 commits into
mainfrom
detach-callback

Conversation

@Colton127

@Colton127 Colton127 commented Jul 24, 2026

Copy link
Copy Markdown
Owner

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:

  1. dispose() held the callback mutex.
  2. Player::dispose() waited for the scheduler thread.
  3. The scheduler attempted to invoke a Dart callback.
  4. The callback waited for the callback mutex.
  5. Teardown and subsequent native calls became permanently blocked.

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:

  • valid and unpaused voices;
  • Dart playback state reporting active playback;
  • the native audio device permanently stopped.

Changes

Callback lifecycle and teardown

  • Serialize retained Dart callback registration, invocation, and clearing.
  • Clear callback registrations before native teardown.
  • Release the callback mutex before joining scheduler threads, stopping the device, or destroying native audio sources.
  • Keep Dart NativeCallable objects alive until native teardown completes successfully.
  • Keep post-deinit cleanup Dart-local so stale cleanup cannot erase callbacks registered by another Flutter engine.
  • Track callback ownership using the Flutter engine ID.
  • Clear Android callback registrations only when the detaching engine still owns them.
  • Add a self-contained Java Android plugin lifecycle bridge.

Audio-device lifecycle scheduler

  • Prioritize lifecycle operations as:

    interruptionStop > start > idleStop

  • Prevent 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

  • Return PlayerErrors from native pauseSwitch, setPause, and stop.
  • Propagate real native errors through Dart.
  • Preserve idempotent stop() behavior when a handle ends between validation and the native stop call.
  • Update the Web bindings to match the revised native return types.

Regression coverage

Added and expanded tests for:

  • start followed immediately by an idle-policy update;
  • stale delayed idle stops;
  • concurrent explicit start/stop operations;
  • interruption begin/end recovery;
  • rapid interruption end while the stop is pending or in flight;
  • active voice state preservation across interruptions;
  • finite idle and indefinite keep-alive interruption policies;
  • teardown during a pending idle deadline;
  • teardown during an active lifecycle operation;
  • repeated start/deinit races;
  • post-deinit cleanup not invoking native callback clearing.

@Colton127 Colton127 changed the title fix dead player upon resuming from background Fix callback teardown deadlock and audio-device lifecycle races Jul 24, 2026
@Colton127
Colton127 merged commit b15ab25 into main Jul 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant