jaseg / python-mpv

Python interface to the awesome mpv media player

Home Page:https://git.jaseg.de/python-mpv.git

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Stuck in threading on macOS

kpj opened this issue · comments

commented

Hello,

consider the following code:

import mpv

_url = 'test.webm'
mpv = mpv.MPV()
mpv.loadfile(_url)
print('waiting')
mpv.wait_for_playback()
print('done')

On Linux, this will print waiting, then play test.webm, and finally print done as expected.

On macOS 10.12.6 using Python 3.6 and MPV 0.27 however, it only prints waiting and is then stuck forever while the Python-Rocket starts jumping in my Dock.

I looked into python-mpv's code, and for mpv.wait_for_playback() it is stuck in line 557, which is:

self._playback_cond.wait()  # _playback_cond is threading.Condition

Using mpv.wait_for_property('filename', lambda x: x is None) instead, makes it stuck in line 569, which is:

sema.acquire()  # sema is threading.Semaphore

The problem thus seems to somehow be related to the handling of the threading library.
Do you have any suggestion what might be causing this?

This is also possibly a duplicate of #59.

commented

The semaphore is most likely working alright. That is part of python and that is probably well tested even on osx. The problem is most likely in the event handling. I'm a bit challenged here since I've never used osx and I don't have any osx machines available. Could you try running your script in gdb to get a stack trace of the event handler thread? See this comment for instructions.

commented

The bouncing launcher you mentioned might be evidence that libmpv tried to create a window, but for some reason did not succeed. This sounds vaguely similar to what @Shu-Ji describes in this comment. Could you try the workaround described there (manually creating a PyQT window and passing the window ID to mpv)?

commented

Thanks for your suggestions, I agree that a bug in Python's threading is highly unlikely.

Event Handler Stack Trace

  • I am using mpv 0.27 and libmpv (1, 25).
  • Adding mpv['vo'] = 'opengl' to my previous example does not alter its behavior.
  • I can currently not run GDB due to weird macOS-related problems after gdb-codesigning (Homebrew/homebrew-core#20047), and simply get
[New Thread 0x2703 of process 1513]
warning: unhandled dyld version (15)
[New Thread 0x1a03 of process 1513]

Thread 3 received signal SIGTRAP, Trace/breakpoint trap.
[Switching to Thread 0x1a03 of process 1513]
0x000000010000519c in ?? ()

I will try again when I have more time.

Manual PyQT window

Is there a way of doing this with your python-mpv version?

commented

You can use the linked pyqt example almost unmodified:

#!/usr/bin/env python3
import mpv
import time
import os
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class Test(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.container = QWidget(self)
        self.setCentralWidget(self.container)
        self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
        self.container.setAttribute(Qt.WA_NativeWindow)
        player = mpv.MPV(wid=str(int(self.container.winId())),
                vo='x11',
                log_handler=print,
                loglevel='debug')
        player.play('test.webm')

app = QApplication(sys.argv)

import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
win = Test()
win.show()

sys.exit(app.exec_())

You may not need the vo='x11'. The setlocale is necessary since pyqt apparently stomps all over that on load. This could also be solved by just import pyqt before mpv (which already includes that line).

commented

Neat, your example works nicely and plays the video on macOS for me (after removing vo='x11',).

How is the wid argument handled in python-mpv? It seems to be part of extra_mpv_opts, which is then somehow processed in _mpv_set_option_string.

Do you think that python-mpv should take care of this QWidget-creation itself, in order to provide a fix for macOS?
Or one could try to debug this even further, using gdb.

commented

Yep, wid is effectively passed in as a command line option. See also this upstream example.

According to the README of the upstream examples a caveat seems to be that this way of embedding is not very stable, so you might also try embedding via OpenGL instead. The callbacks are supported by python-mpv. Upstream issue #556 gives some detail on mpv GUI initialization on OSX, though it still doesn't explain the hang observed here.

I would like to avoid putting PyQT-specific code into python-mpv. Since getting that up and running is very simple (3loc) I'd keep it in the README examples for now. For now, I think two things to add there are a) an OpenGL callback example and b) a hint to OSX users to create their own window.

As for proper debugging, yes, that would be great. However, I don't have an Apple machine so you or one of the other OSX users would have to do most of that. And given what upstream issue #556 says we might well find out that maybe on OSX embedding is only supported if you bring your own window.

commented

I agree, adding PyQT-specific code to handle macOS-specific problems does not seem sensible in this scenario.
For now I'll try to use this work-around instead of diving into more in-depth debugging craziness.

Assuming that I want to use python-mpv's MPV object in another application (for me that would be here), i.e. I don't want the PyQT event loop blocking my main thread.
What would be the best way of handling this case? At first, I thought about simply starting it in another thread (and then somehow try to access MPV's loadfile, etc. methods):

[..]

def foo():
    app = QApplication(sys.argv)

    import locale
    locale.setlocale(locale.LC_NUMERIC, 'C')
    win = Test()
    win.show()

    sys.exit(app.exec_())

threading.Thread(target=foo).start()

This however crashes:

2017-12-29 12:29:13.231 Python[21946:737607] *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /BuildRoot/Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1450.16/Foundation/Misc.subproj/NSUndoManager.m:361
2017-12-29 12:29:13.246 Python[21946:737607] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.'

with the main point probably being: is only safe to invoke on the main thread.

Do you have a suggestion for this, or is it again some macOS-specific difficulty?

commented

According to the Qt doc, Qt must run on the application's main thread. So this is not a limitation of python-mpv, mpv or even PyQt. I know this is inconvenient, but the best solution to this is probably to move other python work to auxiliary threads and possibly use PyQts Qt threading bindings.

A more low-effort alternative would be to farm out Qt code to a separate process using the multiprocessing module. It sounds like that would be quite easy to implement in your use case.

commented

I can indeed display the video using an extra multiprocessing process.
The problem then is however, that I still need to access the MPV object in the main process.

The simplest code (which abuses concurrency quite heavily) I could come up with to achieve this (in theory), is this:

import sys
import multiprocessing

from PyQt5.QtWidgets import QWidget, QMainWindow, QApplication
from PyQt5.QtCore import Qt

import mpv


class Test(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.container = QWidget(self)
        self.setCentralWidget(self.container)
        self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
        self.container.setAttribute(Qt.WA_NativeWindow)
        self.window_id = str(int(self.container.winId()))

def foo(player):
    app = QApplication(sys.argv)
    win = Test()
    win.show()

    mpv._mpv_set_option_string(player, 'wid', win.window_id)

    sys.exit(app.exec_())

player = mpv.MPV()
multiprocessing.Process(target=foo, args=(player,)).start()
player.play('test.webm')

Unfortunately, it crashes:

objc[61728]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called.
objc[61728]: +[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.

Playing around with arbitrary time.sleep calls or using multiprocessing.Manager has not helped me so far.
Do you have a suggestion?

commented

Yes. You cannot create the mpv object in the parent process, since it initializes the mpv handle. When using multiprocessing, import mpv and create the MPV object in the child process, then pass commands to the child process either low-level using multiprocessing's pipes and queues or high-level using multiprocessing's managers and proxies.

commented

Uuuff. This special treatment of macOS starts to become annoying :-P

Consider this thread-inside-a-process solution:

import sys
import time
import threading
import multiprocessing

from PyQt5.QtWidgets import QWidget, QMainWindow, QApplication
from PyQt5.QtCore import Qt


class MPVProxy:
    def __init__(self):
        # setup MPV
        self.pipe = multiprocessing.Pipe()
        multiprocessing.Process(
            target=self._run, args=(self.pipe,)).start()

    def __getattr__(self, cmd):
        def wrapper(*args, **kwargs):
            output_p, input_p = self.pipe
            input_p.send((cmd, args, kwargs))
        return wrapper

    def _run(self, pipe):
        class Test(QMainWindow):
            def __init__(self, parent=None):
                super().__init__(parent)
                self.container = QWidget(self)
                self.setCentralWidget(self.container)
                self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
                self.container.setAttribute(Qt.WA_NativeWindow)
                self.window_id = str(int(self.container.winId()))

        # setup QT window
        app = QApplication(sys.argv)
        win = Test()
        win.show()

        # initialize MPV
        import mpv
        player = mpv.MPV()
        mpv._mpv_set_option_string(
            player.handle,
            'wid'.encode('utf-8'), win.window_id.encode('utf-8'))

        # poll pipe
        def handle_pipe():
            output_p, input_p = pipe
            while True:
                try:
                    msg = output_p.recv()
                    cmd, args, kwargs = msg

                    try:
                        func = getattr(player, cmd)
                    except AttributeError:
                        print(f'Invalid command "{cmd}"')
                        continue

                    func(*args, **kwargs)
                except EOFError:
                    break
        threading.Thread(target=handle_pipe).start()

        # run QT main-loop
        sys.exit(app.exec_())


mp = MPVProxy()
time.sleep(2)
mp.non_existing_function()
time.sleep(2)
mp.play('test.webm')

It waits 2 seconds, prints 'Invalid command "non_existing_function"', waits another 2 seconds and then plays the movie.

I had to use the thread inside of the process, in order to poll the pipe and run the QT main-loop at the same time.
Although this (I think) perfectly mirrors the interface of MPV, this solutions seems rather non-optimal to me. Would you have a suggestion how to improve it?

commented

Sorry for not actually improving your code, but I just played around a bit and came up with the solution below. So far I tested it on Linux and it works fine there.

The main advantage of that is that it's rather simple and it provides access to most methods on an MPV instance by just adding them to the exposed array. The main disadvantages are that all the magic functions taking callables as well as direct property access don't work. Property access must be emulated with player._set_property('loop', 'inf'). You could build your own multiprocessing.BaseProxy descendant improving that interface, but I don't think that'd be worth it.

#!/usr/bin/env python3
import mpv
import time
import os
import sys

import multiprocessing
from multiprocessing.managers import BaseManager, BaseProxy

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class MpvManager(BaseManager):
    pass

MpvManager.register('MPV', mpv.MPV,
    exposed=[
        'play',
        '_get_property',
        '_set_property'
    ])

class MpvWindowThing(QMainWindow):
    def __init__(self, manager, parent=None):
        super().__init__(parent)
        self.container = QWidget(self)
        self.setCentralWidget(self.container)
        self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
        self.container.setAttribute(Qt.WA_NativeWindow)
        print('Running as pid', os.getpid())
        player = manager.MPV(wid=str(int(self.container.winId())),
                vo='x11', # You may not need this
                log_handler=print,
                loglevel='debug')
        player._set_property('loop', 'inf')
        player.play('test.webm')

with MpvManager() as manager:
    app = QApplication(sys.argv)
    win = MpvWindowThing(manager)
    win.show()
    sys.exit(app.exec_())

sys.exit(0)
commented

On macOS there seems to be a problem with manager.MPV.
It gets stuck on v vo/opengl Initializing OpenGL backend 'cocoa' and never plays the video.
If I replace it with mpv.MPV everything works as expected.

Furthermore, everything in your code happens in the main-process, right?

commented

Oh my. No, what happens is multiprocessing.manager.BaseManager creates a proxy thing for MPV such that when you do manager.MPV you get a proxy for a MPV object living inside a subprocess. This way you have the PyQT process running in the parent process and mpv running in the subprocess. I have no idea why that doesn't work on mac.

I'm sorry I don't have any better suggestions. All ways I could think of to replace your custom multiprocessing.Pipe logic with the autogenerated proxies from multirprocessing.managers.BaseManager are ugly as sin as multiprocessing really does not have a very good or flexible API. I'd definitely go for your way now that I've had a read through multiprocessing's source.

I guess your MPVProxy really is fine. I'd maybe factor out the receive loop into another class. 50loc for that sort of workaround is ok I guess.

commented

I see. I now ended up with this implementation.

It's not as flexible as I want it to be (e.g. attribute setting doesn't work properly yet, setting time-observers fails as lambdas cannot be pickled, ...), but it's a start.

Looks like you guys are having some issues with multiprocessing.

May I suggest using my library, zproc?

Will try to cook something up myself.

P.S. Awesome library!

If anyone is looking for a workaround to this, my external MPV library works on OSX. It implements a decent amount of the API of python-mpv, but it probably isn't a drop-in replacement. (It was originally implemented to allow MPV support on platforms where getting a build of libmpv1 is a pain.)

Hi, is there any new on this topic or an workaround? The PyQt solution is not really an option for me.

@iwalton3 I actually gave your library a shot, and got some success. You may or may not find this interesting. To make a long story short, I had to shove the MpvEventID class from python-mpv into your imported library, and then it worked, for the most part. There were some BrokenPipeErrors and TypeErrors but it mostly worked (though, the intended experience of the Hydrus client is for the player to be embedded in Hydrus' own media viewer window, rather than have the player open as its own separate program. Still, it's progress.) Full writeup here: hydrusnetwork/hydrus#1132 (comment)

This issue has been bugging me so much that I have opened a $250 bounty for anyone who solves this (in a way that ends up also resolving the issue for hydrusnetwork/hydrus#1132, i.e., gets an embedded player working in a QWidget.)

FWIW, SMPlayer is a Qt based app that embeds MPV, apparently. I don't know what they do differently from python-mpv to make it work, but it's probably worth referencing their code: https://github.com/smplayer-dev/smplayer/blob/master/src/mplayerwindow.cpp

@roachcord3 I'm very glad you opened this bounty because I've been stuck with this issue for a while now.