Compare commits

..

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

4 changed files with 33 additions and 81 deletions

View file

@ -29,9 +29,8 @@ 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. 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.
into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post
<https://stackoverflow.com/a/23805306>`__ for details.
Python >= 3.9
.............

96
mpv.py
View file

@ -2,7 +2,7 @@
# vim: ts=4 sw=4 et
#
# 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
# later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright
@ -17,14 +17,13 @@
#
# 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'
__version__ = '1.0.7'
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
@ -36,30 +35,11 @@ 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.
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
dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
if dll is None:
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"].')
backend = CDLL(dll)
fs_enc = 'utf-8'
else:
import locale
lc, enc = locale.getlocale(locale.LC_NUMERIC)
@ -1395,17 +1375,11 @@ 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')
self.command('quit', code)
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')
self.command('quit_watch_later', code)
def stop(self, keep_playlist=False):
"""Mapped mpv stop command, see man mpv(1)."""
@ -1529,7 +1503,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):
@ -1698,10 +1672,9 @@ 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', 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
*pressed*. When the ``repetition=True`` is passed, the callback is called again repeatedly while the key is held
down.
*pressed*.
To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute::
@ -1721,8 +1694,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') or (repetition and state[0] == 'r'):
def wrapper(state='p-', name=None, char=None):
if state[0] in ('d', 'p'):
fun()
return wrapper
return register
@ -1730,11 +1703,8 @@ 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, 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 callback function signature is ``fun(key_state, key_name)`` where ``key_state`` is either ``'U'`` for "key
up" or ``'D'`` for "key down".
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``).
@ -1789,12 +1759,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, 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')
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, scale, arg)
self._key_binding_handlers[binding_name](key_state, key_name, key_char)
def unregister_key_binding(self, keydef):
"""Unregister a key binding by keydef."""
@ -1960,10 +1930,6 @@ 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.
@ -1980,25 +1946,16 @@ 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
@ -2020,8 +1977,10 @@ class MPV(object):
"""
q = queue.Queue()
EOF = object() # Get some unique object as EOF marker
@self.python_stream()
frame = sys._getframe()
stream_name = f'__python_mpv_play_generator_{hash(frame)}'
EOF = frame # Get some unique object as EOF marker
@self.python_stream(stream_name)
def reader():
while (chunk := q.get()) is not EOF:
if chunk:
@ -2032,19 +1991,21 @@ class MPV(object):
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)
self.play(f'python://{stream_name}')
yield write
q.put(EOF)
def play_bytes(self, data):
""" Play the given bytes object as a single file. """
frame = sys._getframe()
stream_name = f'__python_mpv_play_generator_{hash(frame)}'
@self.python_stream()
@self.python_stream(stream_name)
def reader():
yield data
reader.unregister() # unregister itself
self.play(reader.stream_uri)
self.play(f'python://{stream_name}')
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
@ -2103,10 +2064,7 @@ class MPV(object):
def _set_property(self, name, value):
self.check_core_alive()
ename = name.encode('utf-8')
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)):
if isinstance(value, (list, set, dict)):
_1, _2, _3, pointer = _make_node_str_list(value)
_mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer)
else:

View file

@ -7,7 +7,7 @@ py-modules = ['mpv']
[project]
name = "mpv"
version = "v1.0.8"
version = "v1.0.7"
description = "A python interface to the mpv media player"
readme = "README.rst"
authors = [{name = "jaseg", email = "mpv@jaseg.de"}]

View file

@ -49,8 +49,7 @@ 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.strip()), flush=True)
return do_print
print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text), flush=True)
class MpvTestCase(unittest.TestCase):
@ -211,11 +210,6 @@ 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)
@ -923,10 +917,11 @@ 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')
player.play('tests/test.webm')
for player in players:
player.wait_for_property('seekable')
for player in players: