From 43a93b950bdb16bd1881c93f7a404af6e70fddcb Mon Sep 17 00:00:00 2001 From: Jon Adkins Date: Mon, 15 Jun 2026 14:29:33 -0400 Subject: [PATCH] Fix macOS threading deadlock in _event_generator (issue #61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace blocking mpv_wait_event(handle, -1) with a pipe-based wakeup mechanism to prevent deadlocks on macOS where Cocoa requires the main thread for its event loop. - Create a self-pipe for wakeup signaling - Register mpv_set_wakeup_callback to write to the pipe when events arrive - Use select.select() on the pipe fd with 100ms timeout instead of blocking forever on mpv_wait_event - Drain events with mpv_wait_event(handle, 0) (non-blocking) after each wakeup or timeout - Clean up pipe and unregister callback in finally block This resolves the deadlock where wait_for_playback() would hang forever on macOS because: 1. Main thread blocks on result.result() waiting for end_file event 2. Event thread blocks on mpv_wait_event(handle, -1) 3. Cocoa needs the main thread to process events, but it's blocked 4. No events are ever delivered → deadlock Closes #61 --- mpv.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/mpv.py b/mpv.py index f9bf561..ded3c8e 100644 --- a/mpv.py +++ b/mpv.py @@ -685,11 +685,59 @@ def _make_node_str_map(d): def _event_generator(handle): - while True: - event = _mpv_wait_event(handle, -1).contents - if event.event_id.value == MpvEventID.NONE: - raise StopIteration() - yield event + """Yield events from the mpv event queue. + + Uses a pipe-based wakeup mechanism to avoid deadlocks on macOS where + ``mpv_wait_event`` with an infinite timeout can block forever when the + Cocoa main-loop is blocked (see issue #61). + """ + import select as _select + + # Create a self-pipe for wakeup signaling. + _wakeup_fd_r, _wakeup_fd_w = os.pipe() + + # Keep a reference to the callback so the GC doesn't reclaim it while + # libmpv still holds a pointer to it. + @WakeupCallback + def _wakeup_cb(_userdata): + try: + os.write(_wakeup_fd_w, b'\x00') + except OSError: + pass + + _mpv_set_wakeup_callback(handle, _wakeup_cb, None) + + try: + while True: + # Wait for either a wakeup byte or a 100 ms timeout. + try: + _ready, _, _ = _select.select([_wakeup_fd_r], [], [], 0.1) + except (OSError, ValueError): + break + + if _ready: + # Drain the pipe. + try: + os.read(_wakeup_fd_r, 4096) + except OSError: + break + + # Drain all pending mpv events without blocking. + while True: + event = _mpv_wait_event(handle, 0).contents + if event.event_id.value == MpvEventID.NONE: + break + yield event + finally: + # Clean up: close the pipe and unregister the wakeup callback. + # The handle may already be destroyed if we're cleaning up after a + # SHUTDOWN event, so guard the unregister call. + os.close(_wakeup_fd_r) + os.close(_wakeup_fd_w) + try: + _mpv_set_wakeup_callback(handle, WakeupCallback(), None) + except Exception: + pass def _create_null_term_cmd_arg_array(name, args):