dispose method

Future<void> dispose()
inherited

Tears down the player.

The teardown order is load-bearing. The sequence is:

  1. Flip _disposed so any subsequent setter or public-API call fails fast via _checkNotDisposed(). Before any await, continue the pending hooks and end the live PCM sources (a reader blocked on one wakes up), then release the OS media session.
  2. Await bring-up, then drop the core from the Hot Restart cleanup (MpvSession.untrack) so a hot-restart that fires before the destroy completes doesn't try to clean up a handle we're already cleaning up, and dispose the DSP pipelines (each turns off what it armed).
  3. Send the quit command to mpv. mpv processes it asynchronously and fires MPV_EVENT_SHUTDOWN inside its own event queue.
  4. requestStop (stop flag + mpv_wakeup natively) so the event loop unwinds deterministically even if quit was dropped or MPV_EVENT_SHUTDOWN is delayed by a stalled hook. Then dispose the filter taps, whose streams the app listens to directly.
  5. Await MpvSession.stop, which returns true only once the event pump has ACTUALLY stopped (natively, the isolate exited), so it is provably off mpv_wait_event. Then wait (bounded) for any registerHook still running mpv_hook_add, and close the client, which settles every in-flight call.
  6. MpvSession.terminateDestroy ONLY when the exit and the hook registrations are confirmed, in a throwaway isolate and awaited (it waits for the core to shut down and the output to close), then release the held content:// descriptors. Destroying the handle while the isolate is still inside mpv_wait_event would race the event-loop thread and crash libmpv when the handle is freed mid-syscall; on an unconfirmed exit we skip destroy and let the OS reclaim the handle.
  7. Close all reactive properties + controllers. Order within this group does not matter for correctness (they tolerate close() while a listener is attached); grouping by ownership is for auditability. The closes are not awaited: an active listener gets done right after dispose completes, a paused one when it resumes.

Idempotent AND await-safe: every call, including concurrent ones, returns the same teardown future, so a second await dispose() completes only when the teardown has actually finished (a bare _disposed early-return would let the second caller proceed while native resources are still being released).

Implementation

Future<void> dispose() => _disposeFuture ??= _disposeImpl();