Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
9 changed files with 181 additions and 638 deletions
106
.github/workflows/tests.yml
vendored
106
.github/workflows/tests.yml
vendored
|
|
@ -1,106 +0,0 @@
|
||||||
name: 'Tests'
|
|
||||||
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ '**' ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ '**' ]
|
|
||||||
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: 'Linux - Python'
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
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@v4
|
|
||||||
- name: 'Install Python'
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: 'Update Packages'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute sudo apt update -y
|
|
||||||
execute sudo apt upgrade -y
|
|
||||||
- name: 'Install Dependencies'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute sudo apt install -y libmpv1 xvfb
|
|
||||||
- name: 'Start Xvfb'
|
|
||||||
run: |
|
|
||||||
echo -e "\033[0;34msudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 &\033[0m";
|
|
||||||
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 &
|
|
||||||
- name: 'Setup Test Environment'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute python -m venv venv
|
|
||||||
execute source venv/bin/activate
|
|
||||||
execute python -m pip install --upgrade pip
|
|
||||||
execute python -m pip install wheel
|
|
||||||
execute python -m pip install -r tests/requirements.txt
|
|
||||||
- name: 'Run Python Tests'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute source venv/bin/activate
|
|
||||||
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', '3.11', '3.12' ]
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: 'Install Python'
|
|
||||||
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-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 libmpv-2.dll tests
|
|
||||||
- name: 'Setup Test Environment'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute python -m venv venv
|
|
||||||
execute source venv/Scripts/activate
|
|
||||||
execute python -m pip install --upgrade pip
|
|
||||||
execute python -m pip install wheel
|
|
||||||
execute python -m pip install -r tests/requirements.txt
|
|
||||||
- name: 'Run Python Tests'
|
|
||||||
run: |
|
|
||||||
function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; }
|
|
||||||
|
|
||||||
execute source venv/Scripts/activate
|
|
||||||
execute python -m unittest
|
|
||||||
24
README.rst
24
README.rst
|
|
@ -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 ``master`` 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
|
||||||
|
|
||||||
|
|
@ -63,13 +64,13 @@ Usage
|
||||||
player.play('https://youtu.be/DOmdB7D-pUU')
|
player.play('https://youtu.be/DOmdB7D-pUU')
|
||||||
player.wait_for_playback()
|
player.wait_for_playback()
|
||||||
|
|
||||||
python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands <https://mpv.io/manual/master/#list-of-input-commands>`_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source <https://github.com/jaseg/python-mpv/blob/main/mpv.py>`__ for things you can do. Additional controls and status information are exposed through `MPV properties <https://mpv.io/manual/master/#properties>`_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``.
|
python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands <https://mpv.io/manual/master/#list-of-input-commands>`_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source <https://github.com/jaseg/python-mpv/blob/master/mpv.py>`__ for things you can do. Additional controls and status information are exposed through `MPV properties <https://mpv.io/manual/master/#properties>`_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``.
|
||||||
|
|
||||||
Threading
|
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
|
||||||
|
|
@ -240,8 +241,7 @@ The easiest way to load custom subtitles from a file is to pass the ``--sub-file
|
||||||
import mpv
|
import mpv
|
||||||
|
|
||||||
player = mpv.MPV()
|
player = mpv.MPV()
|
||||||
player.loadfile('test.webm', sub_file='test.srt')
|
player.play('test.webm', sub_file='test.srt')
|
||||||
player.wait_for_playback()
|
|
||||||
|
|
||||||
Note that you can also pass many other options to ``loadfile``. See the mpv docs for details.
|
Note that you can also pass many other options to ``loadfile``. See the mpv docs for details.
|
||||||
|
|
||||||
|
|
@ -395,7 +395,7 @@ python-mpv inherits the underlying libmpv's license, which can be either GPLv2 o
|
||||||
For details, see `the mpv copyright page`_.
|
For details, see `the mpv copyright page`_.
|
||||||
|
|
||||||
.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
|
.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
|
||||||
.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/main/mpv.py
|
.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/master/mpv.py
|
||||||
.. _cosven: https://github.com/cosven
|
.. _cosven: https://github.com/cosven
|
||||||
.. _Robozman: https://gitlab.com/robozman
|
.. _Robozman: https://gitlab.com/robozman
|
||||||
.. _dfaker: https://github.com/dfaker
|
.. _dfaker: https://github.com/dfaker
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
357
mpv.py
357
mpv.py
|
|
@ -2,29 +2,23 @@
|
||||||
# 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
|
# This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||||
# later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright
|
# License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
#
|
#
|
||||||
# You may copy, modify, and redistribute this file under the terms of the GNU General Public License version 2 (or, at
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
# your option, any later version), or the GNU Lesser General Public License as published by the Free Software
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
# Foundation; either version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
#
|
||||||
# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
# You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU
|
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
#
|
||||||
# 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 +30,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 +47,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()
|
||||||
|
|
||||||
|
|
@ -311,7 +292,7 @@ class MpvEventID(c_int):
|
||||||
FILE_LOADED, CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE)
|
FILE_LOADED, CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<MpvEventID {self.value} {_mpv_event_name(self.value).decode("utf-8")}>'
|
return f'<MpvEventID {self.value} (_mpv_event_name(self.value).decode("utf-8"))>'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_str(kls, s):
|
def from_str(kls, s):
|
||||||
|
|
@ -457,13 +438,8 @@ 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
|
||||||
|
|
@ -561,11 +537,6 @@ def _mpv_client_api_version():
|
||||||
ver = backend.mpv_client_api_version()
|
ver = backend.mpv_client_api_version()
|
||||||
return ver>>16, ver&0xFFFF
|
return ver>>16, ver&0xFFFF
|
||||||
|
|
||||||
MPV_VERSION = _mpv_client_api_version()
|
|
||||||
if MPV_VERSION < (1, 108):
|
|
||||||
ver = '.'.join(str(num) for num in MPV_VERSION)
|
|
||||||
raise RuntimeError(f"python-mpv requires libmpv with an API version of 1.108 or higher (libmpv >= 0.33), but you have an older version ({ver}).")
|
|
||||||
|
|
||||||
backend.mpv_free.argtypes = [c_void_p]
|
backend.mpv_free.argtypes = [c_void_p]
|
||||||
_mpv_free = backend.mpv_free
|
_mpv_free = backend.mpv_free
|
||||||
|
|
||||||
|
|
@ -902,7 +873,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 +883,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 +894,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 +946,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 +992,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 +1004,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 +1057,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 +1074,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 +1286,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 +1320,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)."""
|
||||||
|
|
@ -1466,7 +1385,7 @@ class MPV(object):
|
||||||
"""Mapped mpv print-text command, see man mpv(1)."""
|
"""Mapped mpv print-text command, see man mpv(1)."""
|
||||||
self.command('print-text', text)
|
self.command('print-text', text)
|
||||||
|
|
||||||
def show_text(self, string, duration='-1', level=0):
|
def show_text(self, string, duration='-1', level=None):
|
||||||
"""Mapped mpv show_text command, see man mpv(1)."""
|
"""Mapped mpv show_text command, see man mpv(1)."""
|
||||||
self.command('show_text', string, duration, level)
|
self.command('show_text', string, duration, level)
|
||||||
|
|
||||||
|
|
@ -1490,7 +1409,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 +1448,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):
|
||||||
|
|
@ -1559,13 +1478,13 @@ class MPV(object):
|
||||||
function decorator if no handler is given.
|
function decorator if no handler is given.
|
||||||
|
|
||||||
To unregister the observer, call either of ``mpv.unobserve_property(name, handler)``,
|
To unregister the observer, call either of ``mpv.unobserve_property(name, handler)``,
|
||||||
``mpv.unobserve_all_properties(handler)`` or the handler's ``unobserve_mpv_properties`` attribute::
|
``mpv.unobserve_all_properties(handler)`` or the handler's ``unregister_mpv_properties`` attribute::
|
||||||
|
|
||||||
@player.property_observer('volume')
|
@player.observe_property('volume')
|
||||||
def my_handler(property_name, new_volume):
|
def my_handler(new_volume, *):
|
||||||
print("It's loud!", new_volume)
|
print("It's loud!", volume)
|
||||||
|
|
||||||
my_handler.unobserve_mpv_properties()
|
my_handler.unregister_mpv_properties()
|
||||||
|
|
||||||
exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g.
|
exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g.
|
||||||
from calling MPV.terminate() or issuing a "quit" input command).
|
from calling MPV.terminate() or issuing a "quit" input command).
|
||||||
|
|
@ -1698,10 +1617,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 +1639,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 +1648,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 +1704,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 +1765,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 +1842,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 +1858,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 +1927,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:
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
py-modules = ['mpv']
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "mpv"
|
|
||||||
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.9"
|
|
||||||
keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia']
|
|
||||||
classifiers = [
|
|
||||||
'Development Status :: 5 - Production/Stable',
|
|
||||||
'Environment :: X11 Applications',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
|
|
||||||
'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
|
|
||||||
'Natural Language :: English',
|
|
||||||
'Operating System :: POSIX',
|
|
||||||
'Programming Language :: C',
|
|
||||||
'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'
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
homepage = "https://github.com/jaseg/python-mpv"
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
screenshot_raw = ["Pillow"]
|
|
||||||
test = ['PyVirtualDisplay']
|
|
||||||
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[metadata]
|
||||||
|
description-file = README.rst
|
||||||
40
setup.py
Executable file
40
setup.py
Executable file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from setuptools import setup
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = 'python-mpv',
|
||||||
|
version = '1.0.1',
|
||||||
|
py_modules = ['mpv'],
|
||||||
|
description = 'A python interface to the mpv media player',
|
||||||
|
long_description = (Path(__file__).parent / 'README.rst').read_text(),
|
||||||
|
long_description_content_type = 'text/x-rst',
|
||||||
|
url = 'https://github.com/jaseg/python-mpv',
|
||||||
|
author = 'jaseg',
|
||||||
|
author_email = 'github@jaseg.net',
|
||||||
|
license = 'GPLv3+',
|
||||||
|
extras_require = {
|
||||||
|
'screenshot_raw': ['Pillow']
|
||||||
|
},
|
||||||
|
tests_require = ['xvfbwrapper'],
|
||||||
|
test_suite = 'tests',
|
||||||
|
keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display',
|
||||||
|
'multimedia'],
|
||||||
|
python_requires='>=3.7',
|
||||||
|
classifiers = [
|
||||||
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
'Environment :: X11 Applications',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
|
||||||
|
'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
|
||||||
|
'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',
|
||||||
|
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||||
|
'Topic :: Multimedia :: Video :: Display']
|
||||||
|
)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
PyVirtualDisplay>=3.0
|
|
||||||
|
|
@ -18,12 +18,19 @@
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import math
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from functools import wraps
|
||||||
|
import gc
|
||||||
import os.path
|
import os.path
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future, InvalidStateError
|
import io
|
||||||
|
import platform
|
||||||
|
import ctypes
|
||||||
|
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,31 +38,27 @@ 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'
|
||||||
|
|
||||||
|
|
||||||
TESTVID = os.path.join(os.path.dirname(__file__), 'test.webm')
|
TESTVID = os.path.join(os.path.dirname(__file__), 'test.webm')
|
||||||
TESTSRT = os.path.join(os.path.dirname(__file__), 'sub_test.srt')
|
TESTSRT = os.path.join(os.path.dirname(__file__), 'sub_test.srt')
|
||||||
MPV_ERRORS = [ l(ec) for ec, l in mpv.ErrorCode.EXCEPTION_DICT.items() if l ]
|
MPV_ERRORS = [ l(ec) for ec, l in mpv.ErrorCode.EXCEPTION_DICT.items() if l ]
|
||||||
SKIP_TESTS = os.environ.get('PY_MPV_SKIP_TESTS', '').split()
|
|
||||||
|
|
||||||
|
|
||||||
def timed_print():
|
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())
|
||||||
|
|
||||||
|
|
@ -63,7 +66,6 @@ class MpvTestCase(unittest.TestCase):
|
||||||
self.m.terminate()
|
self.m.terminate()
|
||||||
self.disp.stop()
|
self.disp.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestProperties(MpvTestCase):
|
class TestProperties(MpvTestCase):
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def swallow_mpv_errors(self, exception_exceptions=[]):
|
def swallow_mpv_errors(self, exception_exceptions=[]):
|
||||||
|
|
@ -211,11 +213,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)
|
||||||
|
|
@ -298,7 +295,6 @@ class ObservePropertyTest(MpvTestCase):
|
||||||
mock.call('slang', ['ru'])],
|
mock.call('slang', ['ru'])],
|
||||||
any_order=True)
|
any_order=True)
|
||||||
|
|
||||||
|
|
||||||
class KeyBindingTest(MpvTestCase):
|
class KeyBindingTest(MpvTestCase):
|
||||||
def test_register_direct_cmd(self):
|
def test_register_direct_cmd(self):
|
||||||
self.m.register_key_binding('a', 'playlist-clear')
|
self.m.register_key_binding('a', 'playlist-clear')
|
||||||
|
|
@ -445,12 +441,11 @@ class KeyBindingTest(MpvTestCase):
|
||||||
handler1.assert_has_calls([])
|
handler1.assert_has_calls([])
|
||||||
handler2.assert_has_calls([ mock.call() ])
|
handler2.assert_has_calls([ mock.call() ])
|
||||||
|
|
||||||
|
|
||||||
class TestStreams(unittest.TestCase):
|
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 +503,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,154 +536,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):
|
||||||
thread_names = lambda: [ t.name for t in threading.enumerate() ]
|
thread_names = lambda: [ t.name for t in threading.enumerate() ]
|
||||||
|
|
@ -730,13 +577,13 @@ class TestLifecycle(unittest.TestCase):
|
||||||
mock.call({'event': 'start-file', 'playlist_entry_id': 1}),
|
mock.call({'event': 'start-file', 'playlist_entry_id': 1}),
|
||||||
mock.call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 1, 'file_error': 'no audio or video data played'})
|
mock.call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 1, 'file_error': 'no audio or video data played'})
|
||||||
], any_order=True)
|
], any_order=True)
|
||||||
time.sleep(1)
|
|
||||||
handler.reset_mock()
|
handler.reset_mock()
|
||||||
|
|
||||||
m.terminate()
|
m.terminate()
|
||||||
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)
|
||||||
|
|
@ -753,13 +600,12 @@ class TestLifecycle(unittest.TestCase):
|
||||||
t.start()
|
t.start()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
m.terminate()
|
m.terminate()
|
||||||
time.sleep(1)
|
|
||||||
t.join()
|
t.join()
|
||||||
self.disp.stop()
|
self.disp.stop()
|
||||||
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)
|
||||||
|
|
@ -774,13 +620,13 @@ class TestLifecycle(unittest.TestCase):
|
||||||
m.mute = True
|
m.mute = True
|
||||||
t.join()
|
t.join()
|
||||||
m.terminate()
|
m.terminate()
|
||||||
time.sleep(1)
|
|
||||||
handler.assert_called()
|
handler.assert_called()
|
||||||
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()
|
||||||
|
handler = mock.Mock()
|
||||||
m = mpv.MPV(vo=testvo)
|
m = mpv.MPV(vo=testvo)
|
||||||
m.play(TESTVID)
|
m.play(TESTVID)
|
||||||
result = Future()
|
result = Future()
|
||||||
|
|
@ -801,8 +647,9 @@ 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()
|
||||||
|
handler = mock.Mock()
|
||||||
m = mpv.MPV(vo=testvo)
|
m = mpv.MPV(vo=testvo)
|
||||||
m.play(TESTVID)
|
m.play(TESTVID)
|
||||||
with self.assertRaises(mpv.ShutdownError):
|
with self.assertRaises(mpv.ShutdownError):
|
||||||
|
|
@ -810,13 +657,12 @@ class TestLifecycle(unittest.TestCase):
|
||||||
# handle
|
# handle
|
||||||
with m.prepare_and_wait_for_property('mute', level_sensitive=False):
|
with m.prepare_and_wait_for_property('mute', level_sensitive=False):
|
||||||
m.terminate()
|
m.terminate()
|
||||||
time.sleep(1)
|
|
||||||
self.disp.stop()
|
self.disp.stop()
|
||||||
|
|
||||||
@unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first")
|
def test_wait_for_prooperty_event_overflow(self):
|
||||||
def test_wait_for_property_event_overflow(self):
|
self.disp = Xvfb()
|
||||||
self.disp = Display()
|
|
||||||
self.disp.start()
|
self.disp.start()
|
||||||
|
handler = mock.Mock()
|
||||||
m = mpv.MPV(vo=testvo)
|
m = mpv.MPV(vo=testvo)
|
||||||
m.play(TESTVID)
|
m.play(TESTVID)
|
||||||
with self.assertRaises(mpv.EventOverflowError):
|
with self.assertRaises(mpv.EventOverflowError):
|
||||||
|
|
@ -831,12 +677,12 @@ class TestLifecycle(unittest.TestCase):
|
||||||
m.command_async('script-message', 'foo', 'bar')
|
m.command_async('script-message', 'foo', 'bar')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
m.terminate()
|
|
||||||
time.sleep(1)
|
|
||||||
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,8 +692,9 @@ 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()
|
||||||
|
handler = mock.Mock()
|
||||||
m = mpv.MPV(vo=testvo)
|
m = mpv.MPV(vo=testvo)
|
||||||
m.play(TESTVID)
|
m.play(TESTVID)
|
||||||
with self.assertRaises(mpv.ShutdownError):
|
with self.assertRaises(mpv.ShutdownError):
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -919,26 +766,9 @@ class CommandTests(MpvTestCase):
|
||||||
callback.assert_any_call(None, None)
|
callback.assert_any_call(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -991,3 +821,6 @@ class RegressionTests(MpvTestCase):
|
||||||
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'])])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue