summaryrefslogtreecommitdiff
path: root/content/projects/python-mpv
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-03-19 00:53:31 +0100
committerjaseg <git@jaseg.de>2023-03-19 00:53:31 +0100
commit92e3b5f49f6f5336530988e7839ab3ed283b86e4 (patch)
tree23abd87cb15055b7f4cbb5c0e4f2d1518d3ac6cc /content/projects/python-mpv
parent072b2d38e254cfa662d4d9e994e624f612d1766e (diff)
downloadblog-92e3b5f49f6f5336530988e7839ab3ed283b86e4.tar.gz
blog-92e3b5f49f6f5336530988e7839ab3ed283b86e4.tar.bz2
blog-92e3b5f49f6f5336530988e7839ab3ed283b86e4.zip
Big site update
Diffstat (limited to 'content/projects/python-mpv')
-rw-r--r--content/projects/python-mpv/README.rst401
-rw-r--r--content/projects/python-mpv/index.rst18
2 files changed, 419 insertions, 0 deletions
diff --git a/content/projects/python-mpv/README.rst b/content/projects/python-mpv/README.rst
new file mode 100644
index 0000000..26815d1
--- /dev/null
+++ b/content/projects/python-mpv/README.rst
@@ -0,0 +1,401 @@
+.. vim: tw=120 sw=4 et
+
+python-mpv is a ctypes-based python interface to the mpv media player. It gives you more or less full control of all
+features of the player, just as the lua interface does.
+
+Installation
+------------
+
+.. code:: bash
+
+ pip install mpv
+
+
+...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file.
+
+Requirements
+~~~~~~~~~~~~
+
+libmpv
+......
+``libmpv.so`` either locally (in your current working directory) or somewhere in your system library search path. This
+module is somewhat lenient as far as ``libmpv`` versions are concerned but since ``libmpv`` is changing quite frequently
+you'll only get all the newest features when using an up-to-date version of this module. The unit tests for this module
+do some basic automatic version compatibility checks. If you discover anything missing here, please open an `issue`_ or
+submit a `pull request`_ on github.
+
+On Windows you can place libmpv anywhere in your ``%PATH%`` (e.g. next to ``python.exe``) or next to this module's
+``mpv.py``. Before falling back to looking in the mpv module's directory, python-mpv uses the DLL search order built
+into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post
+<https://stackoverflow.com/a/23805306>`__ for details.
+
+Python >= 3.7 (officially)
+..........................
+The ``main`` branch officially only supports recent python releases (3.5 onwards), but there is the somewhat outdated
+but functional `py2compat branch`_ providing Python 2 compatibility.
+
+.. _`py2compat branch`: https://github.com/jaseg/python-mpv/tree/py2compat
+.. _`issue`: https://github.com/jaseg/python-mpv/issues
+.. _`pull request`: https://github.com/jaseg/python-mpv/pulls
+
+Supported Platforms
+...................
+
+**Linux**, **Windows** and **OSX** all seem to work mostly fine. For some notes on the installation on Windows see
+`this comment`__. Shared library handling is quite bad on windows, so expect some pain there. On OSX there seems to be
+some bug int the event logic. See `issue 36`_ and `issue 61`_ for details. Creating a pyQT window and having mpv draw
+into it seems to be a workaround (about 10loc), but in case you want this fixed please weigh in on the issue tracker
+since right now there is not many OSX users.
+
+.. __: https://github.com/jaseg/python-mpv/issues/60#issuecomment-352719773
+.. _`issue 61`: https://github.com/jaseg/python-mpv/issues/61
+.. _`issue 36`: https://github.com/jaseg/python-mpv/issues/36
+
+Usage
+-----
+
+.. code:: python
+
+ import mpv
+ player = mpv.MPV(ytdl=True)
+ player.play('https://youtu.be/DOmdB7D-pUU')
+ player.wait_for_playback()
+
+python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands <https://mpv.io/manual/master/#list-of-input-commands>`_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source <https://github.com/jaseg/python-mpv/blob/main/mpv.py>`__ for things you can do. Additional controls and status information are exposed through `MPV properties <https://mpv.io/manual/master/#properties>`_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``.
+
+Threading
+~~~~~~~~~
+
+The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The
+event queue has a fixed maxmimum size and some operations can cause a large number of events to be sent.
+
+If you want to handle threading yourself, you can pass ``start_event_thread=False`` to the ``MPV`` constructor and
+manually call the ``MPV`` object's ``_loop`` function. If you have some strong need to not use threads and use some
+external event loop (such as asyncio) instead you can do that, too with some work. The API of the backend C ``libmpv``
+has a function for producing a sort of event file descriptor for a handle. You can use that to produce a file descriptor
+that can be passed to an event loop to tell it to wake up the python-mpv event handler on every incoming event.
+
+All API functions are thread-safe. If one is not, please file an issue on github.
+
+Advanced Usage
+~~~~~~~~~~~~~~
+
+Logging, Properties, Python Key Bindings, Screenshots and youtube-dl
+....................................................................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ def my_log(loglevel, component, message):
+ print('[{}] {}: {}'.format(loglevel, component, message))
+
+ player = mpv.MPV(log_handler=my_log, ytdl=True, input_default_bindings=True, input_vo_keyboard=True)
+
+ # Property access, these can be changed at runtime
+ @player.property_observer('time-pos')
+ def time_observer(_name, value):
+ # Here, _value is either None if nothing is playing or a float containing
+ # fractional seconds since the beginning of the file.
+ print('Now playing at {:.2f}s'.format(value))
+
+ player.fullscreen = True
+ player.loop_playlist = 'inf'
+ # Option access, in general these require the core to reinitialize
+ player['vo'] = 'gpu'
+
+ @player.on_key_press('q')
+ def my_q_binding():
+ print('THERE IS NO ESCAPE')
+
+ @player.on_key_press('s')
+ def my_s_binding():
+ pillow_img = player.screenshot_raw()
+ pillow_img.save('screenshot.png')
+
+ player.play('https://youtu.be/DLzxrzFCyOs')
+ player.wait_for_playback()
+
+ del player
+
+Skipping silence using libav filters
+....................................
+
+The following code uses the libav silencedetect filter to skip silence at the beginning of a file. It works by loading
+the filter, then parsing its output from mpv's log. Thanks to Sean DeNigris on github (#202) for the original code!
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import sys
+ import mpv
+
+ p = mpv.MPV()
+ p.play(sys.argv[1])
+
+ def skip_silence():
+ p.set_loglevel('debug')
+ p.af = 'lavfi=[silencedetect=n=-20dB:d=1]'
+ p.speed = 100
+ def check(evt):
+ toks = evt['event']['text'].split()
+ if 'silence_end:' in toks:
+ return float(toks[2])
+ p.time_pos = p.wait_for_event('log_message', cond=check)
+ p.speed = 1
+ p.af = ''
+
+ skip_silence()
+ p.wait_for_playback()
+
+Video overlays
+..............
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import time
+ from PIL import Image, ImageDraw, ImageFont
+ import mpv
+
+ player = mpv.MPV()
+
+ player.loop = True
+ player.play('test.webm')
+ player.wait_until_playing()
+
+ font = ImageFont.truetype('DejaVuSans.ttf', 40)
+
+ while not player.core_idle:
+
+ time.sleep(0.5)
+ overlay = player.create_image_overlay()
+
+ for pos in range(0, 500, 5):
+ ts = player.time_pos
+ if ts is None:
+ break
+
+ img = Image.new('RGBA', (400, 150), (255, 255, 255, 0))
+ d = ImageDraw.Draw(img)
+ d.text((10, 10), 'Hello World', font=font, fill=(0, 255, 255, 128))
+ d.text((10, 60), f't={ts:.3f}', font=font, fill=(255, 0, 255, 255))
+
+ overlay.update(img, pos=(2*pos, pos))
+ time.sleep(0.05)
+
+ overlay.remove()
+
+
+Playlist handling
+.................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True)
+
+ player.playlist_append('https://youtu.be/PHIGke6Yzh8')
+ player.playlist_append('https://youtu.be/Ji9qSuQapFY')
+ player.playlist_append('https://youtu.be/6f78_Tf4Tdk')
+
+ player.playlist_pos = 0
+
+ while True:
+ # To modify the playlist, use player.playlist_{append,clear,move,remove}. player.playlist is read-only
+ print(player.playlist)
+ player.wait_for_playback()
+
+Directly feeding mpv data from python
+.....................................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ @player.python_stream('foo')
+ def reader():
+ with open('test.webm', 'rb') as f:
+ while True:
+ yield f.read(1024*1024)
+
+ player.play('python://foo')
+ player.wait_for_playback()
+
+Using external subtitles
+........................
+
+The easiest way to load custom subtitles from a file is to pass the ``--sub-file`` option to the ``loadfile`` call:
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ player.loadfile('test.webm', sub_file='test.srt')
+ player.wait_for_playback()
+
+Note that you can also pass many other options to ``loadfile``. See the mpv docs for details.
+
+If you want to add subtitle files or streams at runtime, you can use the ``sub-add`` command. ``sub-add`` can only be
+called once the player is done loading the file and starts playing. An easy way to wait for this is to wait for the
+``core-idle`` property.
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ player.play('test.webm')
+ player.wait_until_playing()
+ player.sub_add('test.srt')
+ player.wait_for_playback()
+
+Using MPV's built-in GUI
+........................
+
+python-mpv is using mpv via libmpv. libmpv is meant for embedding into other applications and by default disables most
+GUI features such as the OSD or keyboard input. To enable the built-in GUI, use the following options when initializing
+the MPV instance. See `Issue 102`_ for more details
+
+.. _`issue 102`: https://github.com/jaseg/python-mpv/issues/61
+
+.. code:: python
+
+ # Enable the on-screen controller and keyboard shortcuts
+ player = mpv.MPV(input_default_bindings=True, input_vo_keyboard=True, osc=True)
+
+ # Alternative version using the old "floating box" style on-screen controller
+ player = mpv.MPV(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)
+
+PyQT embedding
+..............
+
+.. code:: python
+
+ #!/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
+ log_handler=print,
+ loglevel='debug')
+ player.play('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_())
+
+PyGObject embedding
+...................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import gi
+
+ import mpv
+
+ gi.require_version('Gtk', '3.0')
+ from gi.repository import Gtk
+
+
+ class MainClass(Gtk.Window):
+
+ def __init__(self):
+ super(MainClass, self).__init__()
+ self.set_default_size(600, 400)
+ self.connect("destroy", self.on_destroy)
+
+ widget = Gtk.Frame()
+ self.add(widget)
+ self.show_all()
+
+ # Must be created >after< the widget is shown, else property 'window' will be None
+ self.mpv = mpv.MPV(wid=str(widget.get_property("window").get_xid()))
+ self.mpv.play("test.webm")
+
+ def on_destroy(self, widget, data=None):
+ self.mpv.terminate()
+ Gtk.main_quit()
+
+
+ if __name__ == '__main__':
+ # This is necessary since like Qt, Gtk stomps over the locale settings needed by libmpv.
+ # Like with Qt, this needs to happen after importing Gtk but before creating the first mpv.MPV instance.
+ import locale
+ locale.setlocale(locale.LC_NUMERIC, 'C')
+
+ application = MainClass()
+ Gtk.main()
+
+Using OpenGL from PyGObject
+...........................
+
+Just like it is possible to render into a GTK widget through X11 windows, it `also is possible to render into a GTK
+widget using OpenGL <https://gist.github.com/jaseg/657e8ecca3267c0d82ec85d40f423caa>`__ through this python API.
+
+Using OpenGL from PyQt5/QML
+...........................
+
+Robozman_ has mangaed to `make mpv render into a PyQt5/QML widget using OpenGL
+<https://gitlab.com/robozman/python-mpv-qml-example>`__ through this python API.
+
+Using mpv inside imgui inside OpenGL via GLFW
+.............................................
+
+dfaker_ has written a demo (`link <https://github.com/dfaker/imgui_glfw_pythonmpv_demo/blob/main/main.py>`__) that uses mpv to render video into an `imgui <https://github.com/ocornut/imgui>`__ UI running on an OpenGL context inside `GLFW <https://www.glfw.org/>`__. Check out their demo to see how to integrate with imgui/OpenGL and how to access properties and manage the lifecycle of an MPV instance.
+
+Running tests
+-------------
+
+Use pytest to run tests.
+
+Coding Conventions
+------------------
+
+The general aim is `PEP 8`_, with liberal application of the "consistency" section. 120 cells line width. Four spaces.
+No tabs. Probably don't bother making pure-formatting PRs except if you think it *really* helps readability or it
+*really* irks you if you don't.
+
+License
+-------
+
+python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or later.
+For details, see `the mpv copyright page`_.
+
+.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
+.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/main/mpv.py
+.. _cosven: https://github.com/cosven
+.. _Robozman: https://gitlab.com/robozman
+.. _dfaker: https://github.com/dfaker
+.. _`the mpv copyright page`: https://github.com/mpv-player/mpv/blob/master/Copyright
+
diff --git a/content/projects/python-mpv/index.rst b/content/projects/python-mpv/index.rst
new file mode 100644
index 0000000..a1bdcd5
--- /dev/null
+++ b/content/projects/python-mpv/index.rst
@@ -0,0 +1,18 @@
+---
+title: "python-mpv"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/python-mpv.git"
+ - name: Issues
+ url: "https://github.com/jaseg/python-mpv/issues"
+ - name: Docs
+ url: "https://neinseg.gitlab.io/python-mpv"
+ - name: PyPI
+ url: "https://pypi.org/projects/mpv"
+summary: >
+ python-mpv is a small, ctypes-based Python library wrapping the libmpv media player library. Despite its small size
+ and simple API, python-mpv allows advanced control over libmpv and beyond simple remote control of mpv can be used
+ to embed mpv in OpenGL, Qt, and GTK-based Python applications.
+---
+
+.. include:: content/projects/python-mpv/README.rst