Compare commits

..

No commits in common. "main" and "v1.0.2" have entirely different histories.
main ... v1.0.2

7 changed files with 122 additions and 479 deletions

View file

@ -4,8 +4,6 @@ name: 'Tests'
on: on:
push: push:
branches: [ '**' ] branches: [ '**' ]
pull_request:
branches: [ '**' ]
defaults: defaults:
@ -15,20 +13,19 @@ defaults:
jobs: jobs:
test-linux: test-linux:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
name: 'Linux - Python' name: 'Linux - Python'
strategy: strategy:
matrix: matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] python-version: [ '3.8', '3.9', '3.10' ]
fail-fast: false
env: env:
DISPLAY: :0 DISPLAY: :0
PY_MPV_SKIP_TESTS: >- PY_MPV_SKIP_TESTS: >-
test_wait_for_property_event_overflow test_wait_for_property_event_overflow
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: 'Install Python' - name: 'Install Python'
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: 'Update Packages' - name: 'Update Packages'
@ -60,35 +57,30 @@ jobs:
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
execute source venv/bin/activate execute source venv/bin/activate
execute xvfb-run python -m unittest execute xvfb-run python -m pytest
test-windows: test-windows:
runs-on: windows-latest runs-on: windows-latest
name: 'Windows - Python' name: 'Windows - Python'
strategy: strategy:
matrix: matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] python-version: [ '3.8', '3.9', '3.10' ]
fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: 'Install Python' - name: 'Install Python'
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: 'Provide libmpv' - name: 'Provide libmpv'
run: | run: |
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
ARTIFACT="mpv-dev-x86_64-v3-20240121-git-a39f9b6.7z" ARTIFACT="mpv-dev-x86_64-20220619-git-c1a46ec.7z"
SHA1="0764a4b899a7ebb1476e5b491897c5e2eed8a07f"
URL="https://sourceforge.net/projects/mpv-player-windows/files/libmpv/$ARTIFACT" URL="https://sourceforge.net/projects/mpv-player-windows/files/libmpv/$ARTIFACT"
execute curl -L -O "$URL" execute curl -L -O "$URL"
echo -e "\033[0;34mecho -n $SHA1 $ARTIFACT > $ARTIFACT.sha1\033[0m"
echo -n "$SHA1 $ARTIFACT" > "$ARTIFACT.sha1"
execute sha1sum --check "$ARTIFACT.sha1"
execute 7z x "$ARTIFACT" execute 7z x "$ARTIFACT"
execute mv libmpv-2.dll tests execute mv mpv-2.dll tests
- name: 'Setup Test Environment' - name: 'Setup Test Environment'
run: | run: |
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
@ -103,4 +95,4 @@ jobs:
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
execute source venv/Scripts/activate execute source venv/Scripts/activate
execute python -m unittest execute python -m pytest

View file

@ -11,7 +11,7 @@ Installation
.. code:: bash .. code:: bash
pip install mpv pip install python-mpv
...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file.
@ -29,14 +29,15 @@ 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 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 ``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. You can modify `%PATH%` before importing python-mpv into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post
to modify where python-mpv looks for the DLL. Consult `this stackoverflow post <https://stackoverflow.com/a/23805306>`__ <https://stackoverflow.com/a/23805306>`__ for details.
for details.
Python >= 3.9 Python >= 3.7 (officially)
............. ..........................
We only support python stable releases from the last couple of years. We only test the current stable python release. If you find a compatibility issue with an older python version that still has upstream support (that is less than about four years old), feel free to open an issue_ and we'll have a look. 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 .. _`issue`: https://github.com/jaseg/python-mpv/issues
.. _`pull request`: https://github.com/jaseg/python-mpv/pulls .. _`pull request`: https://github.com/jaseg/python-mpv/pulls
@ -69,7 +70,7 @@ Threading
~~~~~~~~~ ~~~~~~~~~
The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The
event queue has a fixed maximum size and some operations can cause a large number of events to be sent. 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 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 manually call the ``MPV`` object's ``_loop`` function. If you have some strong need to not use threads and use some

View file

@ -3,12 +3,6 @@
[ $# -eq 1 ] || exit 2 [ $# -eq 1 ] || exit 2
VER="$1" VER="$1"
echo "$VER" | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' || {
echo "Call this script as ./do_release.sh [version] where version has format 1.2.3, without \"v\" prefix."
exit 2
}
echo "Creating version $VER" echo "Creating version $VER"
if [ -n "$(git diff --name-only --cached)" ]; then if [ -n "$(git diff --name-only --cached)" ]; then
@ -16,9 +10,8 @@ if [ -n "$(git diff --name-only --cached)" ]; then
exit 2 exit 2
fi fi
sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1v"$VER"\\2/" pyproject.toml sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*,\\s*\\)$/\\1"$VER"\\2/" setup.py
sed -i "s/^\\(\\s*__version__\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py git add setup.py
git add pyproject.toml mpv.py
git commit -m "Version $VER" --no-edit git commit -m "Version $VER" --no-edit
git -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" env QUBES_GPG_DOMAIN=gpg git -c gpg.program=qubes-gpg-client -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER"
git push --tags origin git push --tags origin

324
mpv.py
View file

@ -2,7 +2,7 @@
# vim: ts=4 sw=4 et # vim: ts=4 sw=4 et
# #
# Python MPV library module # Python MPV library module
# Copyright (C) 2017-2024 Sebastian Götte <code@jaseg.net> # Copyright (C) 2017-2022 Sebastian Götte <code@jaseg.net>
# #
# python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or # 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 here: https://github.com/mpv-player/mpv/blob/master/Copyright # later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright
@ -17,14 +17,10 @@
# #
# You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files.
__version__ = '1.0.8'
from ctypes import * from ctypes import *
import ctypes.util import ctypes.util
import threading import threading
import queue
import os import os
import os.path
import sys import sys
from warnings import warn from warnings import warn
from functools import partial, wraps from functools import partial, wraps
@ -36,30 +32,14 @@ import traceback
if os.name == 'nt': if os.name == 'nt':
# Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too. # Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too.
names = ['mpv-2.dll', 'libmpv-2.dll', 'mpv-1.dll'] dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
for name in names: if dll is None:
dll = ctypes.util.find_library(name) raise OSError('Cannot find mpv-1.dll or mpv-2.dll in your system %PATH%. One way to deal with this is to ship '
if dll: 'the dll with your script and put the directory your script is in into %PATH% before '
break '"import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
else: 'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
for name in names: backend = CDLL(dll)
dll = os.path.join(os.path.dirname(__file__), name)
if os.path.isfile(dll):
break
else:
raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
try:
# flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
# cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa
backend = CDLL(dll, 0x00001000 | 0x00000100)
except Exception as e:
if not os.path.isabs(dll): # can only be find_library, not the "look next to mpv.py" thing
raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it. It looks like find_library found mpv.dll under a relative path entry in %PATH%. Please make sure all paths in %PATH% are absolute. Instead of trying to load mpv.dll from the current working directory, put it somewhere next to your script and add that path to %PATH% using os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]') from e
else:
raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it.') from e
fs_enc = 'utf-8' fs_enc = 'utf-8'
else: else:
import locale import locale
lc, enc = locale.getlocale(locale.LC_NUMERIC) lc, enc = locale.getlocale(locale.LC_NUMERIC)
@ -69,7 +49,10 @@ else:
sofile = ctypes.util.find_library('mpv') sofile = ctypes.util.find_library('mpv')
if sofile is None: if sofile is None:
raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult the documentation for ctypes.util.find_library which this script uses to look up the library filename.") raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an "
"mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult "
"the documentation for ctypes.util.find_library which this script uses to look up the library "
"filename.")
backend = CDLL(sofile) backend = CDLL(sofile)
fs_enc = sys.getfilesystemencoding() fs_enc = sys.getfilesystemencoding()
@ -457,14 +440,9 @@ class MpvEventLogMessage(Structure):
return lazy_decoder(self._text) return lazy_decoder(self._text)
class MpvEventEndFile(Structure): class MpvEventEndFile(Structure):
_fields_ = [ _fields_ = [('reason', c_int),
('reason', c_int), ('error', c_int)]
('error', c_int),
('playlist_entry_id', c_ulonglong),
('playlist_insert_id', c_ulonglong),
('playlist_insert_num_entries', c_int),
]
EOF = 0 EOF = 0
RESTARTED = 1 RESTARTED = 1
ABORTED = 2 ABORTED = 2
@ -902,7 +880,6 @@ class MPV(object):
self.register_stream_protocol('python', self._python_stream_open) self.register_stream_protocol('python', self._python_stream_open)
self._python_streams = {} self._python_streams = {}
self._python_stream_catchall = None self._python_stream_catchall = None
self._exception_futures = set()
self.overlay_ids = set() self.overlay_ids = set()
self.overlays = {} self.overlays = {}
if loglevel is not None or log_handler is not None: if loglevel is not None or log_handler is not None:
@ -913,22 +890,6 @@ class MPV(object):
self._event_thread.start() self._event_thread.start()
else: else:
self._event_thread = None self._event_thread = None
if (m := re.search(r'(\d+)\.(\d+)\.(\d+)', self.mpv_version)):
self.mpv_version_tuple = tuple(map(int, m.groups()))
@contextmanager
def _enqueue_exceptions(self):
try:
yield
except Exception as e:
for fut in self._exception_futures:
try:
fut.set_exception(e)
break
except InvalidStateError:
pass
else:
warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning)
def _loop(self): def _loop(self):
for event in _event_generator(self._event_handle): for event in _event_generator(self._event_handle):
@ -940,51 +901,45 @@ class MPV(object):
self._core_shutdown = True self._core_shutdown = True
for callback in self._event_callbacks: for callback in self._event_callbacks:
with self._enqueue_exceptions(): callback(event)
callback(event)
if eid == MpvEventID.PROPERTY_CHANGE: if eid == MpvEventID.PROPERTY_CHANGE:
pc = event.data pc = event.data
name, value, _fmt = pc.name, pc.value, pc.format name, value, _fmt = pc.name, pc.value, pc.format
for handler in self._property_handlers[name]: for handler in self._property_handlers[name]:
with self._enqueue_exceptions(): handler(name, value)
handler(name, value)
if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None: if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None:
ev = event.data ev = event.data
with self._enqueue_exceptions(): self._log_handler(ev.level, ev.prefix, ev.text)
self._log_handler(ev.level, ev.prefix, ev.text)
if eid == MpvEventID.CLIENT_MESSAGE: if eid == MpvEventID.CLIENT_MESSAGE:
# {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16}
target, *args = event.data.args target, *args = event.data.args
target = target.decode("utf-8") target = target.decode("utf-8")
if target in self._message_handlers: if target in self._message_handlers:
with self._enqueue_exceptions(): self._message_handlers[target](*args)
self._message_handlers[target](*args)
if eid == MpvEventID.COMMAND_REPLY: if eid == MpvEventID.COMMAND_REPLY:
key = event.reply_userdata key = event.reply_userdata
callback = self._command_reply_callbacks.pop(key, None) callback = self._command_reply_callbacks.pop(key, None)
if callback: if callback:
with self._enqueue_exceptions(): callback(ErrorCode.exception_for_ec(event.error), event.data)
callback(ErrorCode.exception_for_ec(event.error), event.data)
if eid == MpvEventID.QUEUE_OVERFLOW: if eid == MpvEventID.QUEUE_OVERFLOW:
# cache list, since error handlers will unregister themselves # cache list, since error handlers will unregister themselves
for cb in list(self._command_reply_callbacks.values()): for cb in list(self._command_reply_callbacks.values()):
with self._enqueue_exceptions(): cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None)
cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None)
if eid == MpvEventID.SHUTDOWN: if eid == MpvEventID.SHUTDOWN:
_mpv_destroy(self._event_handle) _mpv_destroy(self._event_handle)
for cb in list(self._command_reply_callbacks.values()): for cb in list(self._command_reply_callbacks.values()):
with self._enqueue_exceptions(): cb(ShutdownError('libmpv core has been shutdown'), None)
cb(ShutdownError('libmpv core has been shutdown'), None)
return return
except Exception as e: except Exception as e:
warn(f'Unhandled {e} inside python-mpv event loop!\n{traceback.format_exc()}', RuntimeWarning) print('Exception inside python-mpv event loop:', file=sys.stderr)
traceback.print_exc()
@property @property
def core_shutdown(self): def core_shutdown(self):
@ -998,35 +953,35 @@ class MPV(object):
if self._core_shutdown: if self._core_shutdown:
raise ShutdownError('libmpv core has been shutdown') raise ShutdownError('libmpv core has been shutdown')
def wait_until_paused(self, timeout=None, catch_errors=True): def wait_until_paused(self, timeout=None):
"""Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while
waiting.""" waiting."""
self.wait_for_property('core-idle', timeout=timeout, catch_errors=catch_errors) self.wait_for_property('core-idle', timeout=timeout)
def wait_for_playback(self, timeout=None, catch_errors=True): def wait_for_playback(self, timeout=None):
"""Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while
waiting. waiting.
""" """
self.wait_for_event('end_file', timeout=timeout, catch_errors=catch_errors) self.wait_for_event('end_file', timeout=timeout)
def wait_until_playing(self, timeout=None, catch_errors=True): def wait_until_playing(self, timeout=None):
"""Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while
waiting.""" waiting."""
self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout, catch_errors=catch_errors) self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout)
def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
"""Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for
properties such as ``idle_active`` indicating the player is done with regular playback and just idling around. properties such as ``idle_active`` indicating the player is done with regular playback and just idling around.
Raises a ShutdownError when the core is shutdown while waiting. Raises a ShutdownError when the core is shutdown while waiting.
""" """
with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout, catch_errors=catch_errors) as result: with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout) as result:
pass pass
return result.result() return result.result()
def wait_for_shutdown(self, timeout=None, catch_errors=True): def wait_for_shutdown(self, timeout=None):
'''Wait for core to shutdown (e.g. through quit() or terminate()).''' '''Wait for core to shutdown (e.g. through quit() or terminate()).'''
try: try:
self.wait_for_event(None, timeout=timeout, catch_errors=catch_errors) self.wait_for_event(None, timeout=timeout)
except ShutdownError: except ShutdownError:
return return
@ -1044,7 +999,7 @@ class MPV(object):
return shutdown_handler.unregister_mpv_events return shutdown_handler.unregister_mpv_events
@contextmanager @contextmanager
def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
"""Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See
prepare_and_wait_for_event for usage. prepare_and_wait_for_event for usage.
Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``. Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``.
@ -1056,54 +1011,42 @@ class MPV(object):
rv = cond(val) rv = cond(val)
if rv: if rv:
result.set_result(rv) result.set_result(rv)
except InvalidStateError:
pass
except Exception as e: except Exception as e:
try: try:
result.set_exception(e) result.set_exception(e)
except: except InvalidStateError:
pass pass
except InvalidStateError:
pass
self.observe_property(name, observer)
err_unregister = self._set_error_handler(result)
try: try:
result.set_running_or_notify_cancel() result.set_running_or_notify_cancel()
self.observe_property(name, observer)
err_unregister = self._set_error_handler(result)
if catch_errors:
self._exception_futures.add(result)
yield result yield result
if level_sensitive: rv = cond(getattr(self, name.replace('-', '_')))
rv = cond(getattr(self, name.replace('-', '_'))) if level_sensitive and rv:
if rv: result.set_result(rv)
result.set_result(rv)
return
self.check_core_alive()
result.result(timeout)
except InvalidStateError:
pass
else:
self.check_core_alive()
result.result(timeout)
finally: finally:
err_unregister() err_unregister()
self.unobserve_property(name, observer) self.unobserve_property(name, observer)
self._exception_futures.discard(result)
def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
"""Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError
if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error
inside ``cond``. inside ``cond``.
""" """
with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout, catch_errors=catch_errors) as result: with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout) as result:
pass pass
return result.result() return result.result()
@contextmanager @contextmanager
def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
"""Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given, """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given,
waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens
when 'shutdown' is in event_types. Re-raises any error inside ``cond``. when 'shutdown' is in event_types. Re-raises any error inside ``cond``.
@ -1121,6 +1064,7 @@ class MPV(object):
@self.event_callback(*event_types) @self.event_callback(*event_types)
def target_handler(evt): def target_handler(evt):
try: try:
rv = cond(evt) rv = cond(evt)
if rv: if rv:
@ -1137,18 +1081,13 @@ class MPV(object):
try: try:
result.set_running_or_notify_cancel() result.set_running_or_notify_cancel()
if catch_errors:
self._exception_futures.add(result)
yield result yield result
self.check_core_alive() self.check_core_alive()
result.result(timeout) result.result(timeout)
finally: finally:
err_unregister() err_unregister()
target_handler.unregister_mpv_events() target_handler.unregister_mpv_events()
self._exception_futures.discard(result)
def __del__(self): def __del__(self):
if self.handle: if self.handle:
@ -1354,16 +1293,9 @@ class MPV(object):
def _encode_options(options): def _encode_options(options):
return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items()) return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items())
def loadfile(self, filename, mode='replace', index=None, **options): def loadfile(self, filename, mode='replace', **options):
"""Mapped mpv loadfile command, see man mpv(1).""" """Mapped mpv loadfile command, see man mpv(1)."""
if self.mpv_version_tuple >= (0, 38, 0): self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options))
if index is None:
index = -1
self.command('loadfile', filename.encode(fs_enc), mode, index, MPV._encode_options(options))
else:
if index is not None:
warn(f'The index argument to the loadfile command is only supported on mpv >= 0.38.0')
self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options))
def loadlist(self, playlist, mode='replace'): def loadlist(self, playlist, mode='replace'):
"""Mapped mpv loadlist command, see man mpv(1).""" """Mapped mpv loadlist command, see man mpv(1)."""
@ -1395,17 +1327,11 @@ class MPV(object):
def quit(self, code=None): def quit(self, code=None):
"""Mapped mpv quit command, see man mpv(1).""" """Mapped mpv quit command, see man mpv(1)."""
if code is not None: self.command('quit', code)
self.command('quit', code)
else:
self.command('quit')
def quit_watch_later(self, code=None): def quit_watch_later(self, code=None):
"""Mapped mpv quit_watch_later command, see man mpv(1).""" """Mapped mpv quit_watch_later command, see man mpv(1)."""
if code is not None: self.command('quit_watch_later', code)
self.command('quit_watch_later', code)
else:
self.command('quit_watch_later')
def stop(self, keep_playlist=False): def stop(self, keep_playlist=False):
"""Mapped mpv stop command, see man mpv(1).""" """Mapped mpv stop command, see man mpv(1)."""
@ -1490,7 +1416,7 @@ class MPV(object):
"""Mapped mpv discnav command, see man mpv(1).""" """Mapped mpv discnav command, see man mpv(1)."""
self.command('discnav', command) self.command('discnav', command)
def mouse(self, x, y, button=None, mode='single'): def mouse(x, y, button=None, mode='single'):
"""Mapped mpv mouse command, see man mpv(1).""" """Mapped mpv mouse command, see man mpv(1)."""
if button is None: if button is None:
self.command('mouse', x, y, mode) self.command('mouse', x, y, mode)
@ -1529,7 +1455,7 @@ class MPV(object):
self.command('overlay_remove', overlay_id) self.command('overlay_remove', overlay_id)
def osd_overlay(self, overlay_id, data, res_x=0, res_y=720, z=0, hidden=False): def osd_overlay(self, overlay_id, data, res_x=0, res_y=720, z=0, hidden=False):
self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_y, z=z, hidden=hidden, self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_Y, z=z, hidden=hidden,
format='ass-events') format='ass-events')
def osd_overlay_remove(self, overlay_id): def osd_overlay_remove(self, overlay_id):
@ -1698,10 +1624,9 @@ class MPV(object):
def _binding_name(callback_or_cmd): def _binding_name(callback_or_cmd):
return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff) return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff)
def on_key_press(self, keydef, mode='force', repetition=False): def on_key_press(self, keydef, mode='force'):
"""Function decorator to register a simplified key binding. The callback is called whenever the key given is """Function decorator to register a simplified key binding. The callback is called whenever the key given is
*pressed*. When the ``repetition=True`` is passed, the callback is called again repeatedly while the key is held *pressed*.
down.
To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute:: To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute::
@ -1721,8 +1646,8 @@ class MPV(object):
def register(fun): def register(fun):
@self.key_binding(keydef, mode) @self.key_binding(keydef, mode)
@wraps(fun) @wraps(fun)
def wrapper(state='p-', name=None, char=None, *_): def wrapper(state='p-', name=None, char=None):
if state[0] in ('d', 'p') or (repetition and state[0] == 'r'): if state[0] in ('d', 'p'):
fun() fun()
return wrapper return wrapper
return register return register
@ -1730,11 +1655,8 @@ class MPV(object):
def key_binding(self, keydef, mode='force'): def key_binding(self, keydef, mode='force'):
"""Function decorator to register a low-level key binding. """Function decorator to register a low-level key binding.
The callback function signature is ``fun(key_state, key_name, key_char, scale, arg)``. The callback function signature is ``fun(key_state, key_name)`` where ``key_state`` is either ``'U'`` for "key
up" or ``'D'`` for "key down".
The key_state contains up to three chars, corresponding to the regex ``[udr]([m-][c-]?)?``. ``[udr]`` means
"key up", "key down", or "repetition" for when the key is held down. "m" indicates mouse events, and "c"
indicates key up events resulting from a logical cancellation. For details check out the mpv man page.
The keydef format is: ``[Shift+][Ctrl+][Alt+][Meta+]<key>`` where ``<key>`` is either the literal character the The keydef format is: ``[Shift+][Ctrl+][Alt+][Meta+]<key>`` where ``<key>`` is either the literal character the
key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``). key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``).
@ -1789,12 +1711,12 @@ class MPV(object):
raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.') raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.')
self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging') self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging')
def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None, scale=None, arg=None, *_): def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None):
binding_name = binding_name.decode('utf-8') binding_name = binding_name.decode('utf-8')
key_state = key_state.decode('utf-8') key_state = key_state.decode('utf-8')
key_name = key_name.decode('utf-8') if key_name is not None else None key_name = key_name.decode('utf-8') if key_name is not None else None
key_char = key_char.decode('utf-8') if key_char is not None else None key_char = key_char.decode('utf-8') if key_char is not None else None
self._key_binding_handlers[binding_name](key_state, key_name, key_char, scale, arg) self._key_binding_handlers[binding_name](key_state, key_name, key_char)
def unregister_key_binding(self, keydef): def unregister_key_binding(self, keydef):
"""Unregister a key binding by keydef.""" """Unregister a key binding by keydef."""
@ -1850,65 +1772,32 @@ class MPV(object):
frontend = open_fn(uri.decode('utf-8')) frontend = open_fn(uri.decode('utf-8'))
except ValueError: except ValueError:
return ErrorCode.LOADING_FAILED return ErrorCode.LOADING_FAILED
except Exception as e:
for fut in self._exception_futures:
try:
fut.set_exception(e)
break
except InvalidStateError:
pass
else:
warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}')
return ErrorCode.LOADING_FAILED
cb_info.contents.cookie = None
def read_backend(_userdata, buf, bufsize): def read_backend(_userdata, buf, bufsize):
with self._enqueue_exceptions(): data = frontend.read(bufsize)
data = frontend.read(bufsize) for i in range(len(data)):
for i in range(len(data)): buf[i] = data[i]
buf[i] = data[i] return len(data)
return len(data)
return -1
read = cb_info.contents.read = StreamReadFn(read_backend)
def close_backend(_userdata): cb_info.contents.cookie = None
with self._enqueue_exceptions(): read = cb_info.contents.read = StreamReadFn(read_backend)
del self._stream_protocol_frontends[proto][uri] close = cb_info.contents.close = StreamCloseFn(lambda _userdata: frontend.close())
if hasattr(frontend, 'close'):
frontend.close()
close = cb_info.contents.close = StreamCloseFn(close_backend)
seek, size, cancel = None, None, None seek, size, cancel = None, None, None
if hasattr(frontend, 'seek'): if hasattr(frontend, 'seek'):
def seek_backend(_userdata, offx): seek = cb_info.contents.seek = StreamSeekFn(lambda _userdata, offx: frontend.seek(offx))
with self._enqueue_exceptions():
return frontend.seek(offx)
return ErrorCode.GENERIC
seek = cb_info.contents.seek = StreamSeekFn(seek_backend)
if hasattr(frontend, 'size') and frontend.size is not None: if hasattr(frontend, 'size') and frontend.size is not None:
def size_backend(_userdata): size = cb_info.contents.size = StreamSizeFn(lambda _userdata: frontend.size)
with self._enqueue_exceptions():
return frontend.size
return 0
size = cb_info.contents.size = StreamSizeFn(size_backend)
if hasattr(frontend, 'cancel'): if hasattr(frontend, 'cancel'):
def cancel_backend(_userdata): cancel = cb_info.contents.cancel = StreamCancelFn(lambda _userdata: frontend.cancel())
with self._enqueue_exceptions():
frontend.cancel()
cancel = cb_info.contents.cancel = StreamCancelFn(cancel_backend)
# keep frontend and callbacks in memory until closed # keep frontend and callbacks in memory forever (TODO)
frontend._registered_callbacks = [read, close, seek, size, cancel] frontend._registered_callbacks = [read, close, seek, size, cancel]
self._stream_protocol_frontends[proto][uri] = frontend self._stream_protocol_frontends[proto][uri] = frontend
return 0 return 0
if proto in self._stream_protocol_cbs: if proto in self._stream_protocol_cbs:
raise KeyError('Stream protocol already registered') raise KeyError('Stream protocol already registered')
# keep backend in memory forever
self._stream_protocol_cbs[proto] = [open_backend] self._stream_protocol_cbs[proto] = [open_backend]
_mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend) _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend)
@ -1960,10 +1849,6 @@ class MPV(object):
Any given name can only be registered once. The catch-all can also only be registered once. To unregister a Any given name can only be registered once. The catch-all can also only be registered once. To unregister a
stream, call the .unregister function set on the callback. stream, call the .unregister function set on the callback.
If name is None (the default), a name and corresponding python:// URI are automatically generated. You can
access the name through the .stream_name property set on the callback, and the stream URI for passing into
mpv.play(...) through the .stream_uri property.
The generator signals EOF by returning, manually raising StopIteration or by yielding b'', an empty bytes The generator signals EOF by returning, manually raising StopIteration or by yielding b'', an empty bytes
object. object.
@ -1980,72 +1865,18 @@ class MPV(object):
reader.unregister() reader.unregister()
""" """
def register(cb): def register(cb):
nonlocal name
if name is None:
name = f'__python_mpv_anonymous_python_stream_{id(cb)}__'
if name in self._python_streams: if name in self._python_streams:
raise KeyError('Python stream name "{}" is already registered'.format(name)) raise KeyError('Python stream name "{}" is already registered'.format(name))
self._python_streams[name] = (cb, size) self._python_streams[name] = (cb, size)
def unregister(): def unregister():
if name not in self._python_streams or\ if name not in self._python_streams or\
self._python_streams[name][0] is not cb: # This is just a basic sanity check self._python_streams[name][0] is not cb: # This is just a basic sanity check
raise RuntimeError('Python stream has already been unregistered') raise RuntimeError('Python stream has already been unregistered')
del self._python_streams[name] del self._python_streams[name]
cb.unregister = unregister cb.unregister = unregister
cb.stream_name = name
cb.stream_uri = f'python://{name}'
return cb return cb
return register return register
@contextmanager
def play_context(self):
""" Context manager for streaming bytes straight into libmpv.
This is a convenience wrapper around python_stream. play_context returns a write method, which you can use in
the body of the context manager to feed libmpv bytes. All bytes you feed in with write() in the body of a single
call of this context manager are treated as one single file. A queue is used internally, so this function is
thread-safe. The queue is unlimited, so it cannot block and is safe to call from async code. You can use this
function to stream chunked data, e.g. from the network.
Use it like this:
with m.play_context() as write:
with open(TESTVID, 'rb') as f:
while (chunk := f.read(65536)): # Get some chunks of bytes
write(chunk)
"""
q = queue.Queue()
EOF = object() # Get some unique object as EOF marker
@self.python_stream()
def reader():
while (chunk := q.get()) is not EOF:
if chunk:
yield chunk
reader.unregister()
def write(chunk):
q.put(chunk)
# Start playback before yielding, the first call to reader() will block until write is called at least once.
self.play(reader.stream_uri)
yield write
q.put(EOF)
def play_bytes(self, data):
""" Play the given bytes object as a single file. """
@self.python_stream()
def reader():
yield data
reader.unregister() # unregister itself
self.play(reader.stream_uri)
def python_stream_catchall(self, cb): def python_stream_catchall(self, cb):
""" Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a
function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown). function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown).
@ -2103,10 +1934,7 @@ class MPV(object):
def _set_property(self, name, value): def _set_property(self, name, value):
self.check_core_alive() self.check_core_alive()
ename = name.encode('utf-8') ename = name.encode('utf-8')
if isinstance(value, dict): if isinstance(value, (list, set, dict)):
_1, _2, _3, pointer = _make_node_str_map(value)
_mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
elif isinstance(value, (list, set)):
_1, _2, _3, pointer = _make_node_str_list(value) _1, _2, _3, pointer = _make_node_str_list(value)
_mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer) _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
else: else:

View file

@ -6,13 +6,13 @@ build-backend = "setuptools.build_meta"
py-modules = ['mpv'] py-modules = ['mpv']
[project] [project]
name = "mpv" name = "python-mpv"
version = "v1.0.8" version = "v1.0.2"
description = "A python interface to the mpv media player" description = "A python interface to the mpv media player"
readme = "README.rst" readme = "README.rst"
authors = [{name = "jaseg", email = "mpv@jaseg.de"}] authors = [{name = "jaseg", email = "mpv@jaseg.de"}]
license = {text = "GPLv2+ or LGPLv2.1+"} license = {text = "GPLv2+ or LGPLv2.1+"}
requires-python = ">=3.9" requires-python = ">=3.7"
keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia'] keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia']
classifiers = [ classifiers = [
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
@ -23,10 +23,10 @@ classifiers = [
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: C', 'Programming Language :: C',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Multimedia :: Sound/Audio :: Players', 'Topic :: Multimedia :: Sound/Audio :: Players',
'Topic :: Multimedia :: Video :: Display' 'Topic :: Multimedia :: Video :: Display'
] ]
@ -36,4 +36,4 @@ homepage = "https://github.com/jaseg/python-mpv"
[project.optional-dependencies] [project.optional-dependencies]
screenshot_raw = ["Pillow"] screenshot_raw = ["Pillow"]
test = ['PyVirtualDisplay'] test = ['xvfbwrapper']

View file

@ -1 +1,2 @@
PyVirtualDisplay>=3.0 xvfbwrapper>=0.2.9
pytest>=7.1.2

View file

@ -23,7 +23,7 @@ from contextlib import contextmanager
import os.path import os.path
import os import os
import time import time
from concurrent.futures import Future, InvalidStateError from concurrent.futures import Future
os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]
@ -31,11 +31,11 @@ import mpv
if os.name == 'nt': if os.name == 'nt':
Display = mock.Mock() Xvfb = mock.Mock()
testvo='gpu' testvo='gpu'
else: else:
from pyvirtualdisplay import Display from xvfbwrapper import Xvfb
testvo='x11' testvo='x11'
@ -49,13 +49,12 @@ def timed_print():
start_time = time.time() start_time = time.time()
def do_print(level, prefix, text): def do_print(level, prefix, text):
td = time.time() - start_time td = time.time() - start_time
print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text.strip()), flush=True) print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text), flush=True)
return do_print
class MpvTestCase(unittest.TestCase): class MpvTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
self.m = mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) self.m = mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print())
@ -211,11 +210,6 @@ class TestProperties(MpvTestCase):
# See comment in test_property_decoding_invalid_utf8 # See comment in test_property_decoding_invalid_utf8
self.m.osd.alang self.m.osd.alang
def test_dict_valued_property(self):
nasty_stuff = '\xe2\x80\x8e Mozilla/5.0 Foobar \xe2\x80\x8e \xe2\x80\x81'
self.m.ytdl_raw_options = {'user-agent': nasty_stuff}
self.assertEqual(self.m.ytdl_raw_options, {'user-agent': nasty_stuff})
def test_option_read(self): def test_option_read(self):
self.m.loop = 'inf' self.m.loop = 'inf'
self.m.play(TESTVID) self.m.play(TESTVID)
@ -450,7 +444,7 @@ class TestStreams(unittest.TestCase):
def test_python_stream(self): def test_python_stream(self):
handler = mock.Mock() handler = mock.Mock()
disp = Display() disp = Xvfb()
disp.start() disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
def cb(evt): def cb(evt):
@ -508,7 +502,7 @@ class TestStreams(unittest.TestCase):
stream_mock.seek = mock.Mock(return_value=0) stream_mock.seek = mock.Mock(return_value=0)
stream_mock.read = mock.Mock(return_value=b'') stream_mock.read = mock.Mock(return_value=b'')
disp = Display() disp = Xvfb()
disp.start() disp.start()
m = mpv.MPV(vo=testvo, video=False) m = mpv.MPV(vo=testvo, video=False)
def cb(evt): def cb(evt):
@ -541,153 +535,6 @@ class TestStreams(unittest.TestCase):
m.terminate() m.terminate()
disp.stop() disp.stop()
def test_stream_open_exception(self):
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo, video=False)
@m.register_stream_protocol('raiseerror')
def open_fn(uri):
raise SystemError()
waiting = threading.Semaphore()
result = Future()
def run():
result.set_running_or_notify_cancel()
try:
waiting.release()
m.wait_for_playback()
result.set_result(False)
except SystemError:
result.set_result(True)
except Exception:
result.set_result(False)
t = threading.Thread(target=run, daemon=True)
t.start()
with waiting:
time.sleep(0.2)
m.play('raiseerror://foo')
m.wait_for_playback(catch_errors=False)
try:
assert result.result()
finally:
m.terminate()
disp.stop()
def test_python_stream_exception(self):
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo)
@m.python_stream('foo')
def foo_gen():
with open(TESTVID, 'rb') as f:
yield f.read(100)
raise SystemError()
waiting = threading.Semaphore()
result = Future()
def run():
result.set_running_or_notify_cancel()
try:
waiting.release()
m.wait_for_playback()
result.set_result(False)
except SystemError:
result.set_result(True)
except Exception:
result.set_result(False)
t = threading.Thread(target=run, daemon=True)
t.start()
with waiting:
time.sleep(0.2)
m.play('python://foo')
m.wait_for_playback(catch_errors=False)
try:
assert result.result()
finally:
m.terminate()
disp.stop()
def test_stream_open_forward(self):
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo, video=False)
@m.register_stream_protocol('raiseerror')
def open_fn(uri):
raise ValueError()
waiting = threading.Semaphore()
result = Future()
def run():
result.set_running_or_notify_cancel()
try:
waiting.release()
m.wait_for_playback()
result.set_result(True)
except Exception:
result.set_result(False)
t = threading.Thread(target=run, daemon=True)
t.start()
with waiting:
time.sleep(0.2)
m.play('raiseerror://foo')
m.wait_for_playback(catch_errors=False)
try:
assert result.result()
finally:
m.terminate()
disp.stop()
def test_play_context(self):
handler = mock.Mock()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo)
def cb(evt):
handler(evt.as_dict(decoder=mpv.lazy_decoder))
m.register_event_callback(cb)
with m.play_context() as write:
with open(TESTVID, 'rb') as f:
write(f.read(100))
write(f.read(1000))
write(f.read())
m.wait_for_playback()
handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1})
m.terminate()
disp.stop()
def test_play_bytes(self):
handler = mock.Mock()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo)
def cb(evt):
handler(evt.as_dict(decoder=mpv.lazy_decoder))
m.register_event_callback(cb)
with open(TESTVID, 'rb') as f:
m.play_bytes(f.read())
m.wait_for_playback()
handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1})
m.terminate()
disp.stop()
class TestLifecycle(unittest.TestCase): class TestLifecycle(unittest.TestCase):
def test_create_destroy(self): def test_create_destroy(self):
@ -736,7 +583,7 @@ class TestLifecycle(unittest.TestCase):
handler.assert_not_called() handler.assert_not_called()
def test_wait_for_property_negative(self): def test_wait_for_property_negative(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -759,7 +606,7 @@ class TestLifecycle(unittest.TestCase):
assert result.result() assert result.result()
def test_wait_for_property_positive(self): def test_wait_for_property_positive(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
handler = mock.Mock() handler = mock.Mock()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
@ -779,7 +626,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop() self.disp.stop()
def test_wait_for_event(self): def test_wait_for_event(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -801,7 +648,7 @@ class TestLifecycle(unittest.TestCase):
assert result.result() assert result.result()
def test_wait_for_property_shutdown(self): def test_wait_for_property_shutdown(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -815,7 +662,7 @@ class TestLifecycle(unittest.TestCase):
@unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first") @unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first")
def test_wait_for_property_event_overflow(self): def test_wait_for_property_event_overflow(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -836,7 +683,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop() self.disp.stop()
def test_wait_for_event_shutdown(self): def test_wait_for_event_shutdown(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -846,7 +693,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop() self.disp.stop()
def test_wait_for_shutdown(self): def test_wait_for_shutdown(self):
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo) m = mpv.MPV(vo=testvo)
m.play(TESTVID) m.play(TESTVID)
@ -858,7 +705,7 @@ class TestLifecycle(unittest.TestCase):
def test_log_handler(self): def test_log_handler(self):
handler = mock.Mock() handler = mock.Mock()
self.disp = Display() self.disp = Xvfb()
self.disp.start() self.disp.start()
m = mpv.MPV(vo=testvo, log_handler=handler) m = mpv.MPV(vo=testvo, log_handler=handler)
m.play(TESTVID) m.play(TESTVID)
@ -921,24 +768,6 @@ class CommandTests(MpvTestCase):
class RegressionTests(MpvTestCase): class RegressionTests(MpvTestCase):
def test_wait_for_property_concurrency(self):
players = [mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) for i in range(2)]
try:
for _ in range(150):
for player in players:
player.loadfile('tests/test.webm', loop='inf')
for player in players:
player.wait_for_property('seekable')
for player in players:
player.seek(0, reference='absolute', precision='exact')
except InvalidStateError:
self.fail('InvalidStateError thrown from wait_for_property')
finally:
for player in players:
player.terminate()
def test_unobserve_property_runtime_error(self): def test_unobserve_property_runtime_error(self):
""" """
Ensure a `RuntimeError` is not thrown within Ensure a `RuntimeError` is not thrown within
@ -990,4 +819,3 @@ class RegressionTests(MpvTestCase):
m.slang = 'ru' m.slang = 'ru'
m.terminate() # needed for synchronization of event thread m.terminate() # needed for synchronization of event thread
handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])]) handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])])