Compare commits

...

30 commits
v1.0.3 ... main

Author SHA1 Message Date
jaseg
b26b72e183 Fix typo in MPV.osd_overlay
Some checks failed
Tests / Linux - Python (push) Has been cancelled
Tests / Linux - Python-1 (push) Has been cancelled
Tests / Linux - Python-2 (push) Has been cancelled
Tests / Linux - Python-3 (push) Has been cancelled
Tests / Linux - Python-4 (push) Has been cancelled
Tests / Windows - Python (push) Has been cancelled
Tests / Windows - Python-1 (push) Has been cancelled
Tests / Windows - Python-2 (push) Has been cancelled
Tests / Windows - Python-3 (push) Has been cancelled
Tests / Windows - Python-4 (push) Has been cancelled
Closes #293
2025-04-25 11:54:22 +02:00
jaseg
0c33c933dd Version 1.0.8 2025-04-25 11:48:30 +02:00
jaseg
12850b34bd Add support for libmpv's new args to key binding handlers
This changes the API, check your code if you use key bindings.
2024-12-21 14:47:38 +01:00
jaseg
1bc7e2525e Use callback id() instead of frame hash() to identify anonymous python streams
Frame hashes are not unique since the frame isn't kept around for the
life time of the stream.

Fixes #292.
2024-12-21 14:32:39 +01:00
jaseg
e6e9313af3 Tests: Fix race condition in test_wait_for_property_concurrency 2024-08-25 13:45:13 +02:00
jaseg
16cd0b338e Windows: Look for mpv.dll next to mpv.py 2024-08-14 11:05:01 +02:00
jaseg
3d09f5199e Windows: Improve DLL loading error messages 2024-08-14 10:55:51 +02:00
jaseg
ef3f47c3ec Fix quit and quit_watch_later commands 2024-08-14 10:48:37 +02:00
jaseg
775fc4a868 Add test for dict-valued properties 2024-07-16 11:10:36 +02:00
jaseg
f4086d4bb4 Add API to set dict-valued properties 2024-07-15 14:52:59 +02:00
jaseg
e1ae4f7da6 Version 1.0.7 2024-06-21 16:23:54 +02:00
jaseg
f1621b629d Fix race condition in property observer code leading to futures.InvalidStateError
Previously, prepare_and_wait_for_property was slightly confused on the
lifetime of that future.

This closes #282
2024-06-21 16:14:37 +02:00
jaseg
5bb298ad11
pyproject.toml: Update python version classifiers, bump min to 3.9 2024-05-16 13:36:49 +02:00
jaseg
4dcfba9d40
README: Clarify Python version support 2024-05-16 13:35:47 +02:00
jaseg
16093073b0 Version 1.0.6 2024-04-20 12:51:15 +02:00
jaseg
d96eaf7e64 Fix loadfile for mpv v0.38.0
mpv v0.38.0 added an argument to the loadfile command. Unfortunately the
parsing logic isn't very smart, and now mis-interprets the old argument
format, and breaks literally everything written against older versions
that used the `options` kv dict.

This commit adds a kludge that uses the right variant depending on the
mpv version.
2024-04-20 12:48:58 +02:00
Elias Müller
d26f801cec
Update CI pipeline (#271)
* Trigger on pull request
* run on ubuntu-latest
* run on 3.11 + 3.12 also
* upgrade actions
* upgrade libmpv
* use libmpv-v3 on Windows
* disable fail-fast behavior
* replace pytest with unittest
  Using pytest, we run into access violation errors on Windows.
  They disappear when we use the unittest module.
2024-01-29 16:52:42 +01:00
jaseg
b6ab5a4ab0 play_bytes: Add docstring. 2024-01-22 11:55:03 +01:00
jaseg
4e76f01ecc mpv.py: Add play_bytes convenience function 2024-01-22 11:53:02 +01:00
jaseg
141ec7d372 mpv.py: Add play_context convenience function 2024-01-22 11:53:02 +01:00
jaseg
c52a07e862 Version 1.0.5 2023-11-18 11:57:40 +01:00
jaseg
b42f79a4f1 Make release script properly update version in mpv.py 2023-11-18 11:57:25 +01:00
jaseg
93cf60e109 Add libmpv-2.dll to windows library search list 2023-11-18 11:45:07 +01:00
naglis
0bfd64d1f1 Fix typo in README 2023-10-12 10:12:32 +02:00
jaseg
fa3b0fdad1 Version 1.0.4 2023-07-22 16:04:20 +02:00
jaseg
03947f272b Add missing fields to MpvEventEndFile 2023-07-22 14:37:52 +02:00
Carsen Yates
2d787a977d Add field "playlist_entry_id" to MpvEventEndFile 2023-07-22 14:32:53 +02:00
sdaqo
f6d1269531 Add 'self' to mouse function 2023-06-03 18:31:40 +02:00
Michał Górny
3036f64b68 Replace xvfbwrapper with PyVirtualDisplay
Fixes #249
2023-06-03 18:31:21 +02:00
jaseg
5a1a2734f2 Add __version__ field to module
Closes #248.
2023-03-30 13:57:01 +02:00
7 changed files with 272 additions and 90 deletions

View file

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

View file

@ -29,15 +29,14 @@ 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.
into ctypes, which is different to the one Windows uses internally. You can modify `%PATH%` before importing python-mpv
to modify where python-mpv looks for the DLL. 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.
Python >= 3.9
.............
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.
.. _`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
@ -70,7 +69,7 @@ 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.
event queue has a fixed maximum 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

View file

@ -3,6 +3,12 @@
[ $# -eq 1 ] || exit 2
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"
if [ -n "$(git diff --name-only --cached)" ]; then
@ -10,8 +16,9 @@ if [ -n "$(git diff --name-only --cached)" ]; then
exit 2
fi
sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" pyproject.toml
git add pyproject.toml
sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1v"$VER"\\2/" pyproject.toml
sed -i "s/^\\(\\s*__version__\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py
git add pyproject.toml mpv.py
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"
git push --tags origin

178
mpv.py
View file

@ -2,7 +2,7 @@
# vim: ts=4 sw=4 et
#
# Python MPV library module
# Copyright (C) 2017-2022 Sebastian Götte <code@jaseg.net>
# Copyright (C) 2017-2024 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
# later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright
@ -17,11 +17,14 @@
#
# 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 *
import ctypes.util
import threading
import queue
import os
import os.path
import sys
from warnings import warn
from functools import partial, wraps
@ -33,14 +36,30 @@ import traceback
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.
dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
if dll is None:
raise OSError('Cannot find mpv-1.dll or mpv-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"].')
backend = CDLL(dll)
names = ['mpv-2.dll', 'libmpv-2.dll', 'mpv-1.dll']
for name in names:
dll = ctypes.util.find_library(name)
if dll:
break
else:
for name in names:
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'
else:
import locale
lc, enc = locale.getlocale(locale.LC_NUMERIC)
@ -50,10 +69,7 @@ else:
sofile = ctypes.util.find_library('mpv')
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)
fs_enc = sys.getfilesystemencoding()
@ -441,8 +457,13 @@ class MpvEventLogMessage(Structure):
return lazy_decoder(self._text)
class MpvEventEndFile(Structure):
_fields_ = [('reason', c_int),
('error', c_int)]
_fields_ = [
('reason', c_int),
('error', c_int),
('playlist_entry_id', c_ulonglong),
('playlist_insert_id', c_ulonglong),
('playlist_insert_num_entries', c_int),
]
EOF = 0
RESTARTED = 1
@ -892,6 +913,8 @@ class MPV(object):
self._event_thread.start()
else:
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):
@ -1033,30 +1056,38 @@ class MPV(object):
rv = cond(val)
if rv:
result.set_result(rv)
except InvalidStateError:
pass
except Exception as e:
try:
result.set_exception(e)
except InvalidStateError:
except:
pass
except InvalidStateError:
pass
self.observe_property(name, observer)
err_unregister = self._set_error_handler(result)
try:
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
if level_sensitive:
rv = cond(getattr(self, name.replace('-', '_')))
if level_sensitive and rv:
if rv:
result.set_result(rv)
return
else:
self.check_core_alive()
result.result(timeout)
except InvalidStateError:
pass
finally:
err_unregister()
self.unobserve_property(name, observer)
@ -1323,8 +1354,15 @@ class MPV(object):
def _encode_options(options):
return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items())
def loadfile(self, filename, mode='replace', **options):
def loadfile(self, filename, mode='replace', index=None, **options):
"""Mapped mpv loadfile command, see man mpv(1)."""
if self.mpv_version_tuple >= (0, 38, 0):
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'):
@ -1357,11 +1395,17 @@ class MPV(object):
def quit(self, code=None):
"""Mapped mpv quit command, see man mpv(1)."""
if code is not None:
self.command('quit', code)
else:
self.command('quit')
def quit_watch_later(self, code=None):
"""Mapped mpv quit_watch_later command, see man mpv(1)."""
if code is not None:
self.command('quit_watch_later', code)
else:
self.command('quit_watch_later')
def stop(self, keep_playlist=False):
"""Mapped mpv stop command, see man mpv(1)."""
@ -1446,7 +1490,7 @@ class MPV(object):
"""Mapped mpv discnav command, see man mpv(1)."""
self.command('discnav', command)
def mouse(x, y, button=None, mode='single'):
def mouse(self, x, y, button=None, mode='single'):
"""Mapped mpv mouse command, see man mpv(1)."""
if button is None:
self.command('mouse', x, y, mode)
@ -1485,7 +1529,7 @@ class MPV(object):
self.command('overlay_remove', overlay_id)
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')
def osd_overlay_remove(self, overlay_id):
@ -1654,9 +1698,10 @@ class MPV(object):
def _binding_name(callback_or_cmd):
return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff)
def on_key_press(self, keydef, mode='force'):
def on_key_press(self, keydef, mode='force', repetition=False):
"""Function decorator to register a simplified key binding. The callback is called whenever the key given is
*pressed*.
*pressed*. When the ``repetition=True`` is passed, the callback is called again repeatedly while the key is held
down.
To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute::
@ -1676,8 +1721,8 @@ class MPV(object):
def register(fun):
@self.key_binding(keydef, mode)
@wraps(fun)
def wrapper(state='p-', name=None, char=None):
if state[0] in ('d', 'p'):
def wrapper(state='p-', name=None, char=None, *_):
if state[0] in ('d', 'p') or (repetition and state[0] == 'r'):
fun()
return wrapper
return register
@ -1685,8 +1730,11 @@ class MPV(object):
def key_binding(self, keydef, mode='force'):
"""Function decorator to register a low-level key binding.
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 callback function signature is ``fun(key_state, key_name, key_char, scale, arg)``.
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
key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``).
@ -1741,12 +1789,12 @@ class MPV(object):
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')
def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None):
def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None, scale=None, arg=None, *_):
binding_name = binding_name.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_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)
self._key_binding_handlers[binding_name](key_state, key_name, key_char, scale, arg)
def unregister_key_binding(self, keydef):
"""Unregister a key binding by keydef."""
@ -1811,9 +1859,6 @@ class MPV(object):
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
@ -1915,6 +1960,10 @@ class MPV(object):
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.
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
object.
@ -1931,18 +1980,72 @@ class MPV(object):
reader.unregister()
"""
def register(cb):
nonlocal name
if name is None:
name = f'__python_mpv_anonymous_python_stream_{id(cb)}__'
if name in self._python_streams:
raise KeyError('Python stream name "{}" is already registered'.format(name))
self._python_streams[name] = (cb, size)
def unregister():
if name not in self._python_streams or\
self._python_streams[name][0] is not cb: # This is just a basic sanity check
raise RuntimeError('Python stream has already been unregistered')
del self._python_streams[name]
cb.unregister = unregister
cb.stream_name = name
cb.stream_uri = f'python://{name}'
return cb
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):
""" 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).
@ -2000,7 +2103,10 @@ class MPV(object):
def _set_property(self, name, value):
self.check_core_alive()
ename = name.encode('utf-8')
if isinstance(value, (list, set, dict)):
if isinstance(value, 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)
_mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
else:

View file

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

View file

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

View file

@ -23,7 +23,7 @@ from contextlib import contextmanager
import os.path
import os
import time
from concurrent.futures import Future
from concurrent.futures import Future, InvalidStateError
os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]
@ -31,11 +31,11 @@ import mpv
if os.name == 'nt':
Xvfb = mock.Mock()
Display = mock.Mock()
testvo='gpu'
else:
from xvfbwrapper import Xvfb
from pyvirtualdisplay import Display
testvo='x11'
@ -49,12 +49,13 @@ def timed_print():
start_time = time.time()
def do_print(level, prefix, text):
td = time.time() - start_time
print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text), flush=True)
print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text.strip()), flush=True)
return do_print
class MpvTestCase(unittest.TestCase):
def setUp(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
self.m = mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print())
@ -210,6 +211,11 @@ class TestProperties(MpvTestCase):
# See comment in test_property_decoding_invalid_utf8
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):
self.m.loop = 'inf'
self.m.play(TESTVID)
@ -444,7 +450,7 @@ class TestStreams(unittest.TestCase):
def test_python_stream(self):
handler = mock.Mock()
disp = Xvfb()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo)
def cb(evt):
@ -502,7 +508,7 @@ class TestStreams(unittest.TestCase):
stream_mock.seek = mock.Mock(return_value=0)
stream_mock.read = mock.Mock(return_value=b'')
disp = Xvfb()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo, video=False)
def cb(evt):
@ -536,7 +542,7 @@ class TestStreams(unittest.TestCase):
disp.stop()
def test_stream_open_exception(self):
disp = Xvfb()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo, video=False)
@ -572,7 +578,7 @@ class TestStreams(unittest.TestCase):
disp.stop()
def test_python_stream_exception(self):
disp = Xvfb()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo)
@ -610,7 +616,7 @@ class TestStreams(unittest.TestCase):
disp.stop()
def test_stream_open_forward(self):
disp = Xvfb()
disp = Display()
disp.start()
m = mpv.MPV(vo=testvo, video=False)
@ -643,6 +649,44 @@ class TestStreams(unittest.TestCase):
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):
@ -692,7 +736,7 @@ class TestLifecycle(unittest.TestCase):
handler.assert_not_called()
def test_wait_for_property_negative(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -715,7 +759,7 @@ class TestLifecycle(unittest.TestCase):
assert result.result()
def test_wait_for_property_positive(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
handler = mock.Mock()
m = mpv.MPV(vo=testvo)
@ -735,7 +779,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop()
def test_wait_for_event(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -757,7 +801,7 @@ class TestLifecycle(unittest.TestCase):
assert result.result()
def test_wait_for_property_shutdown(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -771,7 +815,7 @@ class TestLifecycle(unittest.TestCase):
@unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first")
def test_wait_for_property_event_overflow(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -792,7 +836,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop()
def test_wait_for_event_shutdown(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -802,7 +846,7 @@ class TestLifecycle(unittest.TestCase):
self.disp.stop()
def test_wait_for_shutdown(self):
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo)
m.play(TESTVID)
@ -814,7 +858,7 @@ class TestLifecycle(unittest.TestCase):
def test_log_handler(self):
handler = mock.Mock()
self.disp = Xvfb()
self.disp = Display()
self.disp.start()
m = mpv.MPV(vo=testvo, log_handler=handler)
m.play(TESTVID)
@ -877,6 +921,24 @@ class CommandTests(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):
"""
Ensure a `RuntimeError` is not thrown within
@ -928,3 +990,4 @@ class RegressionTests(MpvTestCase):
m.slang = 'ru'
m.terminate() # needed for synchronization of event thread
handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])])