jaseg / python-mpv

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

[Windows] Controls do not work in PyQT (?) #103

Open davFaithid opened 4 years ago

davFaithid commented 4 years ago

Take this code for instance.

    class MainWin(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.setWindowTitle(title) 
            self.resize(1000, 563)

            player = mpv.MPV(wid=str(int(self.container.winId())), ytdl=True, player_operation_mode='pseudo-gui',
                 script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3',
                 input_default_bindings=True,
                 input_vo_keyboard=True,
                 osc=True)
            @player.on_key_press('esc')
            def my_key_binding():
                print('')
                sys.exit()
            player.play(url)
        #   player.wait_for_playback()
            del player

It works pretty well, it plays video in the window and it read the video from the url variable and the window title from title. Yet despite me having set not only the pseudo-gui and keyboard bindings, neither of them initiate so inside my window is video playing with no keyboard or in-window controls. I am relatively new to PyQt, so maybe this is a simple to fix issue. Another oddity with the PyQt integration is that wait_for_playback() is automatic, and actually trying to call it prevents the video from playing.

I really like this module, and it is very handy for several of my video projects (ie this)

jaseg commented 4 years ago

I don't know Qt that well myself, but I guess the problem is that Qt catches all input events and they never get forwarded to mpv. My first idea on how to make this work without rewriting all of libmpv's input bindings would be to catch all key press events on the player QWidget and forward them into mpv using the keypress command. There is also a mouse command if you want mouse input as well.

The reason wait_for_playback doesn't work is that you were calling it in the constructor of your Qt window. This means it would wait before even displaying the window. Since Qt has its own main loop (the app.exec()), there shouldn't be any need for you to call wait_for_playback. Either have the user close the window through user interaction, or you register an event callback with mpv using MPV.register_event_callback that looks for event['event_id'] in (MpvEventID.SHUTDOWN, MpvEventID.END_FILE) and tells qt to exit when it sees one.

davFaithid commented 4 years ago

Thank you very much @jaseg for your helpful answer. I have tried using the keypress and keybind commands yet they do not seem to work

#First I tried this
player.command('keybind esc stop')
player.command('keybind right seek 1 relative')
#And then I tried
player.keybind('esc stop')
player.keybind('right seek 1 relative')

Example errors I get:

Traceback (most recent call last):
  File "main.py", line 131, in <module>
    play(url, title)
  File "main.py", line 127, in play
    win = MainWin()
  File "main.py", line 119, in __init__
    player.keybind('esc stop')
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 1335, in __getattr__
    return self._get_property(_py_to_mpv(name), lazy_decoder)
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 1313, in _get_property
    cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, out)
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 126, in raise_for_ec
    raise ex(ec, *args)
AttributeError: ('mpv property does not exist', -8, (<MpvHandle object at 0x03BD1E38>, b'keybind', 6, <ctypes.c_char_Array_16 object at 0x03BD1E80>))
Traceback (most recent call last):
  File "main.py", line 135, in <module>
    play(url, title)
  File "main.py", line 131, in play
    win = MainWin()
  File "main.py", line 119, in __init__
    @player.command('keybind esc stop')
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 719, in command
    _mpv_command(self.handle, (c_char_p*len(args))(*args))
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 126, in raise_for_ec
    raise ex(ec, *args)
ValueError: ('Invalid value for mpv parameter', -4, (<MpvHandle object at 0x03B21E80>, <mpv.c_char_p_Array_2 object at 0x03B21EC8>))

And when I tried using @player it errored

File "main.py", line 121
    player.play(url)
    ^
SyntaxError: invalid syntax

Do you have any further suggestions? Thank you.

davFaithid commented 4 years ago

@jaseg I have tried your recommendations and have written

...
from pynput import keyboard 

def play(url, title):
    #From https://stackoverflow.com/q/9377914/
    class TitleBar(QWidget):

      def __init__(self, parent):
        super(MyBar, self).__init__()
        self.parent = parent
        print(self.parent.width())
        self.layout = QHBoxLayout()
        self.layout.setContentsMargins(0,0,0,0)

        btn_size = 35

        self.btn_close = QPushButton("x")
        self.btn_close.clicked.connect(self.btn_close_clicked)
        self.btn_close.setFixedSize(btn_size,btn_size)
        self.btn_close.setStyleSheet("background-color: red;")

        self.btn_min = QPushButton("-")
        self.btn_min.clicked.connect(self.btn_min_clicked)
        self.btn_min.setFixedSize(btn_size, btn_size)
        self.btn_min.setStyleSheet("background-color: gray;")

        self.btn_max = QPushButton("+")
        self.btn_max.clicked.connect(self.btn_max_clicked)
        self.btn_max.setFixedSize(btn_size, btn_size)
        self.btn_max.setStyleSheet("background-color: gray;")

        self.title.setFixedHeight(35)
        self.title.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.btn_min)
        self.layout.addWidget(self.btn_max)
        self.layout.addWidget(self.btn_close)

        self.setLayout(self.layout)

        self.start = QPoint(0, 0)
        self.pressing = False

      def resizeEvent(self, QResizeEvent):
        super(MyBar, self).resizeEvent(QResizeEvent)
        self.title.setFixedWidth(self.parent.width())

      def mousePressEvent(self, event):
        self.start = self.mapToGlobal(event.pos())
        self.pressing = True

      def mouseMoveEvent(self, event):
        if self.pressing:
            self.end = self.mapToGlobal(event.pos())
            self.movement = self.end-self.start
            self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
                                self.mapToGlobal(self.movement).y(),
                                self.parent.width(),
                                self.parent.height())
            self.start = self.end

      def mouseReleaseEvent(self, QMouseEvent):
        self.pressing = False

      def btn_close_clicked(self):
        self.parent.close()

      def btn_max_clicked(self):
        self.parent.showMaximized()

      def btn_min_clicked(self):
        self.parent.showMinimized()

    class MainWin(QMainWindow):
        def on_press(self,key):
            print('{0} pressed'.format(
        key))
            if key == keyboard.Key.right:
                self.player.command('seek', '0.2', 'absolute+keyframes')

        def on_release(self, key):
            print('{0} released'.format(
        key))
            if key == keyboard.Key.esc:
                try:
                    print("Quitting")
                    sys.exit('quit')
                except KeyboardInterrupt:
                    sys.exit('quit')
        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.setWindowTitle(title) 
            self.resize(1000, 563)
            self.player = mpv.MPV(wid=str(int(self.container.winId())), ytdl=True, player_operation_mode='pseudo-gui',
            script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3',
            input_default_bindings=True,
            input_vo_keyboard=True,
            osc=True) #
            #player.play(url)
            # Collect events until released
            with keyboard.Listener(
            on_press=self.on_press,
            on_release=self.on_release) as listener:
                Thread(target = self.player.play, args=(url,)).start()
                listener.join()
                #Process(target=player.play, args=(url,)).start()

            #player.wait_for_playback()
            del self.player
            #@player.command('keybind esc stop')
            #def passthru():
            #    pass
            #@player.command('keybind right seek 1 relative')
            #def passthru():
            #    pass
    app = QApplication(sys.argv)
    import locale
    locale.setlocale(locale.LC_NUMERIC, 'C')
    win = MainWin()
    win.show()
    sys.exit(app.exec_())
play(url, title)

it registers commands, but does them all in a seperate process, which upon exiting plays the video. I couldn't seem to get keypress or keybind to give me anything but my prior error.

  File "main.py", line 137, in __init__
    self.player.command('keybind', 'right', 'seek', '0.2', 'absolute+keyframes')
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 719, in command
    _mpv_command(self.handle, (c_char_p*len(args))(*args))
  File "C:\Users\davfa\AppData\Local\Programs\Python\Python38-32\lib\site-packages\mpv.py", line 126, in raise_for_ec
    raise ex(ec, *args)
ValueError: ('Invalid value for mpv parameter', -4, (<MpvHandle object at 0x03256850>, <mpv.c_char_p_Array_6 object at 0x03256898>))

Do you have suggestions? Thank you.

Laeri commented 4 years ago

Are you on windows? For me the inbuilt libmpv controls work on Linux but not on Windows. This is the output on windows: Mpv Output when I run the mpv pyqt example from the readme (without the vo='x11' option of course) PyQt Mpv Code. It seems strange that qt would be catching the input on windows but not on linux but who knows. Do you have some ideas how this could be investigated further? I would like to have a more in-depth look at this.

davFaithid commented 4 years ago

I found a stackoverflow post on keypresses in PyQt, I will try to tweak it to input to mpv. https://stackoverflow.com/q/38507011/

player commands aren't working

    self.player.command('seek', '0.2', 'absolute+keyframes')
AttributeError: 'MainWin' object has no attribute 'player'
        def __init__(self, parent=None): #, parent=None
            super(MainWin, self).__init__(parent) #parent
            self.container = QWidget(self)
            self.setCentralWidget(self.container)
            self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
            self.container.setAttribute(Qt.WA_NativeWindow)
            self.setWindowTitle(title) 
            self.resize(1000, 563)
            self.player = mpv.MPV(wid=str(int(self.container.winId())), ytdl=True, player_operation_mode='pseudo-gui',
            script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3',
            input_default_bindings=True,
            input_vo_keyboard=True,
            osc=True) #
            self.player.play(url)
            #player.wait_for_playback()
            del self.player
        def keyPressEvent(self, event):
            if event.key() == Qt.Key_Q:
                print("Quitting")
                sys.exit('quit')
            elif event.key() == Qt.Key_Right:
                self.player.command('seek', '0.2', 'absolute+keyframes')
            event.accept()
Laeri commented 4 years ago

Sorry in my gist I forgot the options: player_operation_mode='pseudo-gui' , input_default_bindings=True, input_vo_keyboard=True,osc=True. But on Linux (Manjaro Linux x86_64, 5.3.18-1-MANJARO), mpv 0.32.0, ffmpeg version: n4.2.2 the inbuilt gui of libmpv is shown without problem and I can interact with it without forwarding any events to mpv. Therefore this might rather be a problem with the libmpv dll on Windows or what do you think?

davFaithid commented 4 years ago

That seems very likely, but I cannot run any VM because of credential guard (whatever that is).

I'll continue tweaking the code; and if I cannot find a solution then I can mark it up to libmpv on windows

reaganiwadha commented 4 months ago

Would love to have an update on this, I really like this module

jaseg commented 4 months ago

@reaganiwadha All I can say is that on my (Linux) machine, the example from the README works for me with OP's code, both for keyboard and for mouse input. I only needed to change their custom key binding handler a bit to mesh with both libmpv and PyQt's respective event loops.

If things don't work for you, please post an example script that I can run to reproduce the behavior, post your Python, PyQt and libmpv versions, and tell us what operating system you're running things on. At this point, I suspect that this is a Windows-only issue.

#!/usr/bin/env python3
import mpv
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', # You may not need this
                loglevel='debug',
                script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3',
                input_default_bindings=True,
                input_vo_keyboard=True,
                osc=True)
        @player.on_key_press('esc')
        def my_key_binding(*args):
            print('ESC pressed')
            # Note: We can't just call sys.exit() here because that would only terminate the python interpreter, but not libmpv or PyQt.
            player.quit() # Tell libmpv to terminate itself
            QApplication.instance().quit() # Tell PyQt to terminate. This closes the window, and makes PyQt return from app.exec below.
        player.loop = 'inf'
        player.play('tests/test.webm')

app = QApplication(sys.argv)

# This is necessary since PyQT stomps over the locale settings needed by libmpv.
# This needs to happen after importing PyQT before creating the first mpv.MPV instance.
import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
win = Test()
win.show()
sys.exit(app.exec_())