jaseg / python-mpv

Python interface to the awesome mpv media player
https://git.jaseg.de/python-mpv.git
Other
547 stars 68 forks source link

tkinter embedding: mpv.terminate() hangs when observe_property has been added to time-pos #114

Open dfaker opened 4 years ago

dfaker commented 4 years ago

Reproducible on windows 10 with python-mpv 0.4.6 and mpv-dev-x86_64-20200426-git-640db1e, the last print statement in this snippet is never reached:

import tkinter as tk
import mpv

root=tk.Tk()

player = mpv.MPV(wid=str(int(root.winfo_id())))
player.play('out.mp4')

def handlePropertyChange(name,value):
  print('property change',name,value)
  root.title(str(value))

player.observe_property('time-pos', handlePropertyChange)
tk.mainloop()

print('calling player.terminate.')
player.terminate()
print('terminate player.returned.')

The same behaviour isn't noted if osd-width or duration is observed, additionally placing a player.unobserve_property('time-pos', handlePropertyChange) directly before player.terminate() has no effect, however removing it ahead of time,presumably giving enough time for the event queue to empty does seem to work:

import tkinter as tk
import mpv

root=tk.Tk()

player = mpv.MPV(wid=str(int(root.winfo_id())))

player.play('out.mp4')

def handlePropertyChange(name,value):
  print('property change',name,value)
  root.title(str(value))
  if value is not None and value > 5:
    player.unobserve_property('time-pos', handlePropertyChange)

player.observe_property('time-pos', handlePropertyChange)

tk.mainloop()

print('calling player.terminate.')
player.terminate()
print('terminate player.returned.')

Does the event loop stop working when the frame with the wid passed to mpv is destroyed?

dfaker commented 4 years ago

Inspecting the output of _event_generator in this scenario shows that that final event generated is a 22 property change, event 1 for shutdown never makes it through the pipe and no NONE 0 event is sent either, the grim_reaper will therefore I think wait forever blocking exit.

dfaker commented 4 years ago

If it is seemingly an issue that the shutdown command is never sent at least giving an option to pass daemonic_reaper to terminate as terminate(daemonic_reaper=True) would seem to make sense so that the reaper thread doesn't block exit when we know we don't care about it.

jaseg commented 4 years ago

There were some issues around multithreading and terminate(). I re-structured the code somewhat and now things should generally work much more reliably than before. I tried your first example and it seemed to work as expected after replacint terminate() with quit(). Here's my test code:

import tkinter as tk
import mpv

root=tk.Tk()

player = mpv.MPV(wid=str(int(root.winfo_id())), vo='x11')
player.play('test.webm')

def handlePropertyChange(name,value):
  print('property change',name,value)
  root.title(str(value))

player.observe_property('time-pos', handlePropertyChange)

tk.mainloop()

print('calling player.terminate.')
player.quit()
print('terminate player.returned.')

When using terminate() and closing the TK window while mpv is still playing, it seems either mpv or the tk mainloop try to do some teardown work on the X11 window that was already done by the other and some x11 lib barfs with an error message:

calling player.terminate.
X Error of failed request:  BadWindow (invalid Window parameter)
  Major opcode of failed request:  2 (X_ChangeWindowAttributes)
  Resource id in failed request:  0x1200002
  Serial number of failed request:  144
  Current serial number in output stream:  149

I'm not sure where that is coming from. If you can, just calling quit() and exiting the program should already shut down everything cleanly enough and should be a decent workaround.

dfaker commented 4 years ago

Thanks! I'll pull these and report back.

m44soroush commented 2 years ago

I had the same issue in linux (Ubuntu 20.04) and pyqt5. Terminating mpv player from another thread solved my problem.

class TerminateMPV(QRunnable): 
    def __init__(self,player): 
        super().__init__()
        self.player: mpv.MPV = player 

     def run(self) -> None: 
          self.player.terminate() 

Main thread (which mpv player is defined and started in):

...
def on_stop():
    self.player.stop()
    self.threadpool = QThreadPool()
    terminate_mpv_thread =  TerminateMPV(self.player)
    self.threadpool.start(terminate_mpv_thread)
...
jaseg commented 2 years ago

@m44soroush I'm glad that your workaround functions, but it would be interesting to see why this happened. Do you think you could get a stack trace of the hanging code? Python should spit out a stack trace if you just Ctrl+C, and you can get a stack trace of mpv's threads by running python inside gdb via gdb --args python3 the_script.py, then Ctrl+C'ing the hanging program, then running thread apply all bt in the gdb shell that opens. Alternatively, if you could post a hanging testcase I could reproduce the issue myself.