Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b26b72e183 | ||
|
|
0c33c933dd | ||
|
|
12850b34bd | ||
|
|
1bc7e2525e | ||
|
|
e6e9313af3 | ||
|
|
16cd0b338e | ||
|
|
3d09f5199e | ||
|
|
ef3f47c3ec | ||
|
|
775fc4a868 | ||
|
|
f4086d4bb4 | ||
|
|
e1ae4f7da6 | ||
|
|
f1621b629d | ||
|
|
5bb298ad11 | ||
|
|
4dcfba9d40 | ||
|
|
16093073b0 | ||
|
|
d96eaf7e64 | ||
|
|
d26f801cec | ||
|
|
b6ab5a4ab0 | ||
|
|
4e76f01ecc | ||
|
|
141ec7d372 |
6 changed files with 233 additions and 59 deletions
30
.github/workflows/tests.yml
vendored
30
.github/workflows/tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
13
README.rst
13
README.rst
|
|
@ -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
|
||||
|
||||
|
|
|
|||
157
mpv.py
157
mpv.py
|
|
@ -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,13 +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.5'
|
||||
__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
|
||||
|
|
@ -35,11 +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('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
|
||||
if dll is None:
|
||||
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"].')
|
||||
backend = CDLL(dll)
|
||||
|
||||
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)
|
||||
|
|
@ -893,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):
|
||||
|
|
@ -1034,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)
|
||||
|
|
@ -1324,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'):
|
||||
|
|
@ -1358,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)."""
|
||||
|
|
@ -1486,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):
|
||||
|
|
@ -1655,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::
|
||||
|
||||
|
|
@ -1677,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
|
||||
|
|
@ -1686,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``).
|
||||
|
|
@ -1742,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."""
|
||||
|
|
@ -1812,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
|
||||
|
|
@ -1916,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.
|
||||
|
||||
|
|
@ -1932,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).
|
||||
|
|
@ -2001,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:
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ py-modules = ['mpv']
|
|||
|
||||
[project]
|
||||
name = "mpv"
|
||||
version = "v1.0.5"
|
||||
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'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
PyVirtualDisplay>=3.0
|
||||
pytest>=7.1.2
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -49,7 +49,8 @@ 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):
|
||||
|
|
@ -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)
|
||||
|
|
@ -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):
|
||||
|
|
@ -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'])])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue