Open kpj opened 6 years ago
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.
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)?
Thanks for your suggestions, I agree that a bug in Python's threading
is highly unlikely.
mpv['vo'] = 'opengl'
to my previous example does not alter its behavior.
[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?
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).
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.
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.
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?
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.
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?
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.
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?
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)
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?
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.
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 BrokenPipeError
s and TypeError
s 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: https://github.com/hydrusnetwork/hydrus/issues/1132#issuecomment-1248864461
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.
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
Hey, this is intriguing and something I'd be happy to look into, but first I'd need to know if a: this is still an issue (it's been two years) and b: if the bounty is still active.
Hey, this is intriguing and something I'd be happy to look into, but first I'd need to know if a: this is still an issue (it's been two years) and b: if the bounty is still active.
Yes, it's still an issue. However, I don't know whether that boss bounty system still works. I logged into my dashboard and see my bounties are still active, but the claim link shows that they are having some error. So, you might want to contact the owner of boss bounty about it if the error persists.
The original poster's example from Dec 2017 still doesn't work so, yes, this is still an issue. A fix that gets the example to work (or an example that's simpler than the QT approach) would be greatly appreciated. Thanks for looking into it.
I could probably toss USD10 or something into a pot if you need a monetary motivation.
From my end I can promise credit and thanks in the readme to whoever manages to solve this. I still don't have a mac, so I still can't work on this myself.
Hello,
consider the following code:
On Linux, this will print
waiting
, then playtest.webm
, and finally printdone
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:Using
mpv.wait_for_property('filename', lambda x: x is None)
instead, makes it stuck in line 569, which is: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.