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 OrphanHandleTracker entry 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) 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 _eventIsolate.stop(), which returns true only once the isolate has ACTUALLY 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. mpvTerminateDestroy(_handle) 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();