diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3d13aca --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,108 @@ +name: deploy + +on: + push: + tags: + - 'v*.*.*' + branches: + - 'publish-test' + +jobs: + + create-windows-wheel: + + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Download libmpv + run: | + $VERSION = "libmpv/mpv-dev-x86_64-20200524-git-685bd6a.7z" + $URL = "https://sourceforge.net/projects/mpv-player-windows/files" + curl -L -o libmpv.7z "$URL/$VERSION" + 7z x libmpv.7z + + - name: Set VS environment variables + run: | + $VsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer" + echo "$VsPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Set MSVC x86_64 linker path + run: | + $LinkGlob = "VC\Tools\MSVC\*\bin\Hostx64\x64" + $LinkPath = vswhere -latest -products * -find "$LinkGlob" | + Select-Object -Last 1 + echo "$LinkPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Create the libmpv zip + shell: cmd + run: | + lib /def:mpv.def /machine:x64 /out:mpv.lib + ren mpv-1.dll mpv.dll + ren include mpv + 7z a libmpv.zip mpv/ mpv.dll mpv.lib + + - name: Install Python 3.9 version + uses: actions/setup-python@v1 + with: + python-version: '3.9' + architecture: 'x64' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + pip install --upgrade cython + + - name: Create Python wheel + run: | + python setup.py bdist_wheel --plat-name win_amd64 --python-tag cp37 + + - name: Upload Windows wheel + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + deploy-binaries: + + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + needs: create-windows-wheel + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Download Windows wheel + uses: actions/download-artifact@v4 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools twine + pip install --upgrade cython + pip install --upgrade packaging + + - name: Release source code + run: | + python setup.py sdist + + - name: Twine check + run: | + python -m twine check dist/* + + - name: Publish distribution to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Publish distribution to TestPyPI + if: github.event_name == 'push' && github.ref == 'refs/heads/publish-test' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository_url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4d1dd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +pympv.egg-info +dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 27f433a..33cb710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ pympv ===== +## 0.8.0 +- Update to libmpv API 2.0, removing previously deprecated APIs +- Optimise string conversion functions with Cython +- Give str its Python 3 meaning +- Simplify string conversions, since python2 is no more + +## 0.7.1 +- Extract examples into its own directory + +## 0.6.0 +- Add support for the render API +- Deprecate the legacy opengl-cb API + +## 0.5.1 +- Rename async -> asynchronous for Python 3.7 compatibility +- Force Cython rebuilds if Cython is installed + +## 0.4.1 + +- Prepare packaging for PyPI + +## 0.4.0 +- Fix string support in Python2 +- Add opengl-cb support +- Improve packaging/setup.py + ## 0.3.0 - Fix various failures of data and callbacks diff --git a/README.md b/README.md index d3c1f80..df100d2 100644 --- a/README.md +++ b/README.md @@ -2,44 +2,51 @@ pympv ===== A python wrapper for libmpv. -To use - - import sys - import mpv - - def main(args): - if len(args) != 1: - print('pass a single media file as argument') - return 1 - - try: - m = mpv.Context() - except mpv.MPVError: - print('failed creating context') - return 1 - - m.set_option('input-default-bindings') - m.set_option('osc') - m.set_option('input-vo-keyboard') - m.initialize() - - m.command('loadfile', args[0]) - - while True: - event = m.wait_event(.01) - if event.id == mpv.Events.none: - continue - print(event.name) - if event.id in [mpv.Events.end_file, mpv.Events.shutdown]: - break - - if __name__ == '__main__': - try: - exit(main(sys.argv[1:]) or 0) - except mpv.MPVError as e: - print(str(e)) - exit(1) +#### Basic usage +```python +import sys +import mpv + +def main(args): + if len(args) != 1: + print('pass a single media file as argument') + return 1 + + try: + m = mpv.Context() + except mpv.MPVError: + print('failed creating context') + return 1 + + m.set_option('input-default-bindings') + m.set_option('osc') + m.set_option('input-vo-keyboard') + m.initialize() + + m.command('loadfile', args[0]) + + while True: + event = m.wait_event(.01) + if event.id == mpv.Events.none: + continue + print(event.name) + if event.id in [mpv.Events.end_file, mpv.Events.shutdown]: + break + + +if __name__ == '__main__': + try: + exit(main(sys.argv[1:]) or 0) + except mpv.MPVError as e: + print(str(e)) + exit(1) +``` + +More examples can be found in the [samples](samples) directory. libmpv is a client library for the media player mpv For more info see: https://github.com/mpv-player/mpv/blob/master/libmpv/client.h + +pympv was originally written by Andre D, and the PyPI package is maintained +by Hector Martin. diff --git a/client.pxd b/client.pxd index a6509a1..08fb4d0 100644 --- a/client.pxd +++ b/client.pxd @@ -75,26 +75,26 @@ cdef extern from "mpv/client.h": pass cdef enum mpv_error: - MPV_ERROR_SUCCESS = 0 - MPV_ERROR_EVENT_QUEUE_FULL = -1 - MPV_ERROR_NOMEM = -2 - MPV_ERROR_UNINITIALIZED = -3 - MPV_ERROR_INVALID_PARAMETER = -4 - MPV_ERROR_OPTION_NOT_FOUND = -5 - MPV_ERROR_OPTION_FORMAT = -6 - MPV_ERROR_OPTION_ERROR = -7 - MPV_ERROR_PROPERTY_NOT_FOUND = -8 - MPV_ERROR_PROPERTY_FORMAT = -9 - MPV_ERROR_PROPERTY_UNAVAILABLE = -10 - MPV_ERROR_PROPERTY_ERROR = -11 - MPV_ERROR_COMMAND = -12 - MPV_ERROR_LOADING_FAILED = -13 - MPV_ERROR_AO_INIT_FAILED = -14 - MPV_ERROR_VO_INIT_FAILED = -15 - MPV_ERROR_NOTHING_TO_PLAY = -16 - MPV_ERROR_UNKNOWN_FORMAT = -17 - MPV_ERROR_UNSUPPORTED = -18 - MPV_ERROR_NOT_IMPLEMENTED = -19 + MPV_ERROR_SUCCESS + MPV_ERROR_EVENT_QUEUE_FULL + MPV_ERROR_NOMEM + MPV_ERROR_UNINITIALIZED + MPV_ERROR_INVALID_PARAMETER + MPV_ERROR_OPTION_NOT_FOUND + MPV_ERROR_OPTION_FORMAT + MPV_ERROR_OPTION_ERROR + MPV_ERROR_PROPERTY_NOT_FOUND + MPV_ERROR_PROPERTY_FORMAT + MPV_ERROR_PROPERTY_UNAVAILABLE + MPV_ERROR_PROPERTY_ERROR + MPV_ERROR_COMMAND + MPV_ERROR_LOADING_FAILED + MPV_ERROR_AO_INIT_FAILED + MPV_ERROR_VO_INIT_FAILED + MPV_ERROR_NOTHING_TO_PLAY + MPV_ERROR_UNKNOWN_FORMAT + MPV_ERROR_UNSUPPORTED + MPV_ERROR_NOT_IMPLEMENTED const char *mpv_error_string(int error) nogil @@ -106,16 +106,10 @@ cdef extern from "mpv/client.h": int mpv_initialize(mpv_handle *ctx) nogil - void mpv_detach_destroy(mpv_handle *ctx) nogil - void mpv_terminate_destroy(mpv_handle *ctx) nogil int mpv_load_config_file(mpv_handle *ctx, const char *filename) nogil - void mpv_suspend(mpv_handle *ctx) nogil - - void mpv_resume(mpv_handle *ctx) nogil - int64_t mpv_get_time_us(mpv_handle *ctx) nogil cdef enum mpv_format: @@ -152,6 +146,10 @@ cdef extern from "mpv/client.h": mpv_node *values char **keys + cdef struct mpv_byte_array: + void *data + size_t size + void mpv_free_node_contents(mpv_node *node) nogil int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format, void *data) nogil @@ -186,7 +184,7 @@ cdef extern from "mpv/client.h": int mpv_unobserve_property(mpv_handle *mpv, uint64_t registered_reply_userdata) nogil - cdef enum mpv_event_id: + enum mpv_event_id: MPV_EVENT_NONE MPV_EVENT_SHUTDOWN MPV_EVENT_LOG_MESSAGE @@ -196,21 +194,14 @@ cdef extern from "mpv/client.h": MPV_EVENT_START_FILE MPV_EVENT_END_FILE MPV_EVENT_FILE_LOADED - MPV_EVENT_TRACKS_CHANGED - MPV_EVENT_TRACK_SWITCHED MPV_EVENT_IDLE - MPV_EVENT_PAUSE - MPV_EVENT_UNPAUSE MPV_EVENT_TICK - MPV_EVENT_SCRIPT_INPUT_DISPATCH MPV_EVENT_CLIENT_MESSAGE MPV_EVENT_VIDEO_RECONFIG MPV_EVENT_AUDIO_RECONFIG - MPV_EVENT_METADATA_UPDATE MPV_EVENT_SEEK MPV_EVENT_PLAYBACK_RESTART MPV_EVENT_PROPERTY_CHANGE - MPV_EVENT_CHAPTER_CHANGE const char *mpv_event_name(mpv_event_id event) nogil @@ -219,15 +210,15 @@ cdef extern from "mpv/client.h": mpv_format format void *data - cdef enum mpv_log_level: - MPV_LOG_LEVEL_NONE = 0 - MPV_LOG_LEVEL_FATAL = 10 - MPV_LOG_LEVEL_ERROR = 20 - MPV_LOG_LEVEL_WARN = 30 - MPV_LOG_LEVEL_INFO = 40 - MPV_LOG_LEVEL_V = 50 - MPV_LOG_LEVEL_DEBUG = 60 - MPV_LOG_LEVEL_TRACE = 70 + enum mpv_log_level: + MPV_LOG_LEVEL_NONE + MPV_LOG_LEVEL_FATAL + MPV_LOG_LEVEL_ERROR + MPV_LOG_LEVEL_WARN + MPV_LOG_LEVEL_INFO + MPV_LOG_LEVEL_V + MPV_LOG_LEVEL_DEBUG + MPV_LOG_LEVEL_TRACE cdef struct mpv_event_log_message: const char *prefix @@ -235,11 +226,11 @@ cdef extern from "mpv/client.h": const char *text int log_level - cdef enum mpv_end_file_reason: - MPV_END_FILE_REASON_EOF = 0 - MPV_END_FILE_REASON_STOP = 2 - MPV_END_FILE_REASON_QUIT = 3 - MPV_END_FILE_REASON_ERROR = 4 + enum mpv_end_file_reason: + MPV_END_FILE_REASON_EOF + MPV_END_FILE_REASON_STOP + MPV_END_FILE_REASON_QUIT + MPV_END_FILE_REASON_ERROR cdef struct mpv_event_end_file: int reason @@ -271,4 +262,104 @@ cdef extern from "mpv/client.h": int mpv_get_wakeup_pipe(mpv_handle *ctx) nogil + void mpv_wait_async_requests(mpv_handle *ctx) nogil + + enum mpv_sub_api: + MPV_SUB_API_OPENGL_CB + + void *mpv_get_sub_api(mpv_handle *ctx, mpv_sub_api sub_api) nogil + +cdef extern from "mpv/render.h": + struct mpv_render_context: + pass + + enum mpv_render_param_type: + MPV_RENDER_PARAM_INVALID + MPV_RENDER_PARAM_API_TYPE + MPV_RENDER_PARAM_OPENGL_INIT_PARAMS + MPV_RENDER_PARAM_OPENGL_FBO + MPV_RENDER_PARAM_FLIP_Y + MPV_RENDER_PARAM_DEPTH + MPV_RENDER_PARAM_ICC_PROFILE + MPV_RENDER_PARAM_AMBIENT_LIGHT + MPV_RENDER_PARAM_X11_DISPLAY + MPV_RENDER_PARAM_WL_DISPLAY + MPV_RENDER_PARAM_ADVANCED_CONTROL + MPV_RENDER_PARAM_NEXT_FRAME_INFO + MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME + MPV_RENDER_PARAM_SKIP_RENDERING + MPV_RENDER_PARAM_DRM_DISPLAY + MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE + MPV_RENDER_PARAM_DRM_DISPLAY_V2 + MPV_RENDER_PARAM_SW_SIZE + MPV_RENDER_PARAM_SW_FORMAT + MPV_RENDER_PARAM_SW_STRIDE + MPV_RENDER_PARAM_SW_POINTER + + char *MPV_RENDER_API_TYPE_OPENGL + char *MPV_RENDER_API_TYPE_SW + + enum mpv_render_frame_info_flag: + MPV_RENDER_FRAME_INFO_PRESENT + MPV_RENDER_FRAME_INFO_REDRAW + MPV_RENDER_FRAME_INFO_REPEAT + MPV_RENDER_FRAME_INFO_BLOCK_VSYNC + + struct mpv_render_param: + mpv_render_param_type type + void *data + + struct mpv_render_frame_info: + uint64_t flags + int64_t target_time + + int mpv_render_context_create(mpv_render_context **res, mpv_handle *mpv, + mpv_render_param *params) nogil + + int mpv_render_context_set_parameter(mpv_render_context *ctx, + mpv_render_param param) nogil + + int mpv_render_context_get_info(mpv_render_context *ctx, + mpv_render_param param) nogil + + ctypedef void (*mpv_render_update_fn)(void *cb_ctx) nogil + + void mpv_render_context_set_update_callback(mpv_render_context *ctx, + mpv_render_update_fn callback, + void *callback_ctx) nogil + + uint64_t mpv_render_context_update(mpv_render_context *ctx) nogil + + enum mpv_render_update_flag: + MPV_RENDER_UPDATE_FRAME + + int mpv_render_context_render(mpv_render_context *ctx, mpv_render_param *params) nogil + + void mpv_render_context_report_swap(mpv_render_context *ctx) nogil + + void mpv_render_context_free(mpv_render_context *ctx) nogil + +cdef extern from "mpv/render_gl.h": + struct mpv_opengl_init_params: + void *(*get_proc_address)(void *ctx, const char *name) + void *get_proc_address_ctx + + struct mpv_opengl_fbo: + int fbo + int w + int h + int internal_format + + struct _drmModeAtomicReq: + pass + + struct mpv_opengl_drm_params: + int fd + int crtc_id + int connector_id + _drmModeAtomicReq **atomic_request_ptr + int render_fd + struct mpv_opengl_drm_draw_surface_size: + int width + int height diff --git a/mpv.pyx b/mpv.pyx index 14c4596..6f428a9 100644 --- a/mpv.pyx +++ b/mpv.pyx @@ -1,3 +1,5 @@ +# cython: language_level=3 + # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -18,18 +20,20 @@ libmpv is a client library for the media player mpv For more info see: https://github.com/mpv-player/mpv/blob/master/libmpv/client.h """ -import sys +import cython +import sys, warnings from threading import Thread, Semaphore from libc.stdlib cimport malloc, free -from libc.string cimport strcpy +from libc.string cimport strcpy, strlen, memset +from cpython.pycapsule cimport PyCapsule_IsValid, PyCapsule_GetPointer from client cimport * __version__ = "0.3.0" __author__ = "Andre D" -_REQUIRED_CAPI_MAJOR = 1 -_MIN_CAPI_MINOR = 9 +_REQUIRED_CAPI_MAJOR = 2 +_MIN_CAPI_MINOR = 0 cdef unsigned long _CAPI_VERSION with nogil: @@ -44,27 +48,18 @@ if _CAPI_MAJOR != _REQUIRED_CAPI_MAJOR or _CAPI_MINOR < _MIN_CAPI_MINOR: (_REQUIRED_CAPI_MAJOR, _MIN_CAPI_MINOR, _CAPI_MAJOR, _CAPI_MINOR) ) -cdef extern from "Python.h": - void PyEval_InitThreads() - -_is_py3 = sys.version_info >= (3,) -_strdec_err = "surrogateescape" if _is_py3 else "strict" # mpv -> Python -def _strdec(s): - try: - return s.decode("utf-8", _strdec_err) - except UnicodeDecodeError: - # In python2, bail to bytes on failure - return bytes(s) +cdef str _strdec(bytes s): + return s.decode("utf-8", "surrogateescape") # Python -> mpv -def _strenc(s): - try: - return s.encode("utf-8", _strdec_err) - except UnicodeEncodeError: - # In python2, assume bytes and walk right through - return s +cdef bytes _strenc(str s): + return s.encode("utf-8", "surrogateescape") +# TODO: Remove this call once Python 3.6 is EOL: it is automatically called by +# Py_Initialize() since Python 3.7 and will be removed in Python 3.11. +cdef extern from "Python.h": + void PyEval_InitThreads() PyEval_InitThreads() class Errors: @@ -112,21 +107,14 @@ class Events: start_file = MPV_EVENT_START_FILE end_file = MPV_EVENT_END_FILE file_loaded = MPV_EVENT_FILE_LOADED - tracks_changed = MPV_EVENT_TRACKS_CHANGED - tracks_switched = MPV_EVENT_TRACK_SWITCHED idle = MPV_EVENT_IDLE - pause = MPV_EVENT_PAUSE - unpause = MPV_EVENT_UNPAUSE tick = MPV_EVENT_TICK - script_input_dispatch = MPV_EVENT_SCRIPT_INPUT_DISPATCH client_message = MPV_EVENT_CLIENT_MESSAGE video_reconfig = MPV_EVENT_VIDEO_RECONFIG audio_reconfig = MPV_EVENT_AUDIO_RECONFIG - metadata_update = MPV_EVENT_METADATA_UPDATE seek = MPV_EVENT_SEEK playback_restart = MPV_EVENT_PLAYBACK_RESTART property_change = MPV_EVENT_PROPERTY_CHANGE - chapter_change = MPV_EVENT_CHAPTER_CHANGE class LogLevels: @@ -164,19 +152,6 @@ cdef class EndOfFileReached(object): return self -cdef class InputDispatch(object): - """Data field for MPV_EVENT_SCRIPT_INPUT_DISPATCH events. - - Wraps: mpv_event_script_input_dispatch - """ - cdef public object arg0, type - - cdef _init(self, mpv_event_script_input_dispatch* input): - self.arg0 = input.arg0 - self.type = _strdec(input.type) - return self - - cdef class LogMessage(object): """Data field for MPV_EVENT_LOG_MESSAGE events. @@ -274,8 +249,6 @@ cdef class Event(object): return Property()._init(data) elif self.id == MPV_EVENT_LOG_MESSAGE: return LogMessage()._init(data) - elif self.id == MPV_EVENT_SCRIPT_INPUT_DISPATCH: - return InputDispatch()._init(data) elif self.id == MPV_EVENT_CLIENT_MESSAGE: climsg = data args = [] @@ -298,7 +271,7 @@ cdef class Event(object): return _strdec(name_c) cdef _init(self, mpv_event* event, ctx): - cdef uint64_t ctxid = id(ctx) + ctxid = id(ctx) self.id = event.event_id self.data = self._data(event) userdata = _reply_userdatas[ctxid].get(event.reply_userdata, None) @@ -327,15 +300,19 @@ class MPVError(Exception): def __init__(self, e): self.code = e cdef const char* err_c - cdef int e_i = e - if not isinstance(e, str): + cdef int e_i + if not isinstance(e, basestring): + e_i = e with nogil: err_c = mpv_error_string(e_i) e = _strdec(err_c) Exception.__init__(self, e) -cdef _callbacks = dict() -cdef _reply_userdatas = dict() +class PyMPVError(Exception): + pass + +cdef dict _callbacks = {} +cdef dict _reply_userdatas = {} class _ReplyUserData(object): def __init__(self, data): @@ -386,18 +363,6 @@ cdef class Context(object): time = mpv_get_time_us(self._ctx) return time - def suspend(self): - """Wraps: mpv_suspend""" - assert self._ctx - with nogil: - mpv_suspend(self._ctx) - - def resume(self): - """Wraps: mpv_resume""" - assert self._ctx - with nogil: - mpv_resume(self._ctx) - @_errors def request_event(self, event, enable): """Enable or disable a given event. @@ -439,7 +404,7 @@ cdef class Context(object): return err def _format_for(self, value): - if isinstance(value, str): + if isinstance(value, basestring): return MPV_FORMAT_STRING elif isinstance(value, bool): return MPV_FORMAT_FLAG @@ -517,7 +482,7 @@ cdef class Context(object): elif node.format == MPV_FORMAT_STRING: free(node.u.string) - def command(self, *cmdlist, async=False, data=None): + def command(self, *cmdlist, asynchronous=False, data=None): """Send a command to mpv. Non-async success returns the command's response data, otherwise None @@ -526,7 +491,7 @@ cdef class Context(object): Accepts parameters as args Keyword Arguments: - async: True will return right away, status comes in as MPV_EVENT_COMMAND_REPLY + asynchronous: True will return right away, status comes in as MPV_EVENT_COMMAND_REPLY data: Only valid if async, gets sent back as reply_userdata in the Event Wraps: mpv_command_node and mpv_command_node_async @@ -539,7 +504,7 @@ cdef class Context(object): result = None try: data_id = id(data) - if not async: + if not asynchronous: with nogil: err = mpv_command_node(self._ctx, &node, &noderesult) try: @@ -623,7 +588,7 @@ cdef class Context(object): return v @_errors - def set_property(self, prop, value=True, async=False, data=None): + def set_property(self, prop, value=True, asynchronous=False, data=None): """Wraps: mpv_set_property and mpv_set_property_async""" assert self._ctx prop = _strenc(prop) @@ -634,7 +599,7 @@ cdef class Context(object): cdef const char* prop_c try: prop_c = prop - if not async: + if not asynchronous: with nogil: err = mpv_set_property( self._ctx, @@ -724,7 +689,7 @@ cdef class Context(object): return pipe def __cinit__(self): - cdef uint64_t ctxid = id(self) + ctxid = id(self) with nogil: self._ctx = mpv_create() if not self._ctx: @@ -776,7 +741,9 @@ cdef class Context(object): return err def shutdown(self): - cdef uint64_t ctxid = id(self) + if self._ctx == NULL: + return + ctxid = id(self) with nogil: mpv_terminate_destroy(self._ctx) self.callbackthread.shutdown() @@ -789,6 +756,258 @@ cdef class Context(object): def __dealloc__(self): self.shutdown() +cdef void *_c_getprocaddress(void *ctx, const char *name) noexcept with gil: + return (ctx)(name) + +cdef void _c_updatecb(void *ctx) noexcept with gil: + (ctx)() + +DEF MAX_RENDER_PARAMS = 32 + +cdef class _RenderParams(object): + cdef: + mpv_render_param params[MAX_RENDER_PARAMS + 1] + object owned + + def __init__(self): + self.owned = [] + self.params[0].type = MPV_RENDER_PARAM_INVALID + + cdef add_voidp(self, mpv_render_param_type t, void *p, bint owned=False): + count = len(self.owned) + if count >= MAX_RENDER_PARAMS: + if owned: + free(p) + raise PyMPVError("RenderParams overflow") + + self.params[count].type = t + self.params[count].data = p + self.params[count + 1].type = MPV_RENDER_PARAM_INVALID + self.owned.append(owned) + + cdef add_int(self, mpv_render_param_type t, int val): + cdef int *p = malloc(sizeof(int)) + p[0] = val + self.add_voidp(t, p) + + cdef add_string(self, mpv_render_param_type t, char *s): + cdef char *p = malloc(strlen(s) + 1) + strcpy(p, s) + self.add_voidp(t, p) + + def __dealloc__(self): + for i, j in enumerate(self.owned): + if j: + free(self.params[i].data) + +cdef void *get_pointer(const char *name, object obj): + cdef void *p + if PyCapsule_IsValid(obj, name): + p = PyCapsule_GetPointer(obj, name) + elif isinstance(obj, int) and obj: + p = obj + else: + raise PyMPVError("Unknown or invalid pointer object: %r" % obj) + return p + +@cython.internal +cdef class RenderFrameInfo(object): + cdef _from_struct(self, mpv_render_frame_info *info): + self.present = bool(info[0].flags & MPV_RENDER_FRAME_INFO_PRESENT) + self.redraw = bool(info[0].flags & MPV_RENDER_FRAME_INFO_REDRAW) + self.repeat = bool(info[0].flags & MPV_RENDER_FRAME_INFO_REPEAT) + self.block_vsync = bool(info[0].flags & MPV_RENDER_FRAME_INFO_BLOCK_VSYNC) + self.target_time = info[0].target_time + return self + +cdef class RenderContext(object): + API_OPENGL = "opengl" + UPDATE_FRAME = MPV_RENDER_UPDATE_FRAME + + cdef: + Context _mpv + mpv_render_context *_ctx + object update_cb + object _x11_display + object _wl_display + object _get_proc_address + bint inited + + def __init__(self, mpv, + api_type, + opengl_init_params=None, + advanced_control=False, + x11_display=None, + wl_display=None, + drm_display=None, + drm_draw_surface_size=None + ): + + cdef: + mpv_opengl_init_params gl_params + mpv_opengl_drm_params drm_params + mpv_opengl_drm_draw_surface_size _drm_draw_surface_size + + self._mpv = mpv + + memset(&gl_params, 0, sizeof(gl_params)) + memset(&drm_params, 0, sizeof(drm_params)) + memset(&_drm_draw_surface_size, 0, sizeof(_drm_draw_surface_size)) + + params = _RenderParams() + + if api_type == self.API_OPENGL: + params.add_string(MPV_RENDER_PARAM_API_TYPE, MPV_RENDER_API_TYPE_OPENGL) + else: + raise PyMPVError("Unknown api_type %r" % api_type) + + if opengl_init_params is not None: + self._get_proc_address = opengl_init_params["get_proc_address"] + gl_params.get_proc_address = &_c_getprocaddress + gl_params.get_proc_address_ctx = self._get_proc_address + params.add_voidp(MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_params) + if advanced_control: + params.add_int(MPV_RENDER_PARAM_ADVANCED_CONTROL, 1) + if x11_display: + self._x11_display = x11_display + params.add_voidp(MPV_RENDER_PARAM_X11_DISPLAY, get_pointer("Display", x11_display)) + if wl_display: + self._wl_display = wl_display + params.add_voidp(MPV_RENDER_PARAM_WL_DISPLAY, get_pointer("wl_display", wl_display)) + if drm_display: + drm_params.fd = drm_display.get("fd", -1) + drm_params.crtc_id = drm_display.get("crtc_id", -1) + drm_params.connector_id = drm_display.get("connector_id", -1) + arp = drm_display.get("atomic_request_ptr", None) + if arp is not None: + drm_params.atomic_request_ptr = <_drmModeAtomicReq **>get_pointer(arp, "drmModeAtomicReq*") + drm_params.render_fd = drm_display.get("render_fd", -1) + params.add_voidp(MPV_RENDER_PARAM_DRM_DISPLAY, &drm_params) + if drm_draw_surface_size: + _drm_draw_surface_size.width, _drm_draw_surface_size.height = drm_draw_surface_size + params.add_voidp(MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE, &_drm_draw_surface_size) + + err = mpv_render_context_create(&self._ctx, self._mpv._ctx, params.params) + if err < 0: + raise MPVError(err) + self.inited = True + + @_errors + def set_icc_profile(self, icc_blob): + cdef: + mpv_render_param param + mpv_byte_array val + int err + + if not isinstance(icc_blob, bytes): + raise PyMPVError("icc_blob should be a bytes instance") + val.data = icc_blob + val.size = len(icc_blob) + + param.type = MPV_RENDER_PARAM_ICC_PROFILE + param.data = &val + + with nogil: + err = mpv_render_context_set_parameter(self._ctx, param) + return err + + @_errors + def set_ambient_light(self, lux): + cdef: + mpv_render_param param + int val + int err + + val = lux + param.type = MPV_RENDER_PARAM_AMBIENT_LIGHT + param.data = &val + + with nogil: + err = mpv_render_context_set_parameter(self._ctx, param) + return err + + def get_next_frame_info(self): + cdef: + mpv_render_frame_info info + mpv_render_param param + + param.type = MPV_RENDER_PARAM_NEXT_FRAME_INFO + param.data = &info + with nogil: + err = mpv_render_context_get_info(self._ctx, param) + if err < 0: + raise MPVError(err) + + return RenderFrameInfo()._from_struct(&info) + + def set_update_callback(self, cb): + with nogil: + mpv_render_context_set_update_callback(self._ctx, &_c_updatecb, cb) + self.update_cb = cb + + def update(self): + cdef uint64_t ret + with nogil: + ret = mpv_render_context_update(self._ctx) + return ret + + @_errors + def render(self, + opengl_fbo=None, + flip_y=False, + depth=None, + block_for_target_time=True, + skip_rendering=False): + + cdef: + mpv_opengl_fbo fbo + + params = _RenderParams() + + if opengl_fbo: + memset(&fbo, 0, sizeof(fbo)) + fbo.fbo = opengl_fbo.get("fbo", 0) or 0 + fbo.w = opengl_fbo["w"] + fbo.h = opengl_fbo["h"] + fbo.internal_format = opengl_fbo.get("internal_format", 0) or 0 + params.add_voidp(MPV_RENDER_PARAM_OPENGL_FBO, &fbo) + if flip_y: + params.add_int(MPV_RENDER_PARAM_FLIP_Y, 1) + if depth is not None: + params.add_int(MPV_RENDER_PARAM_DEPTH, depth) + if not block_for_target_time: + params.add_int(MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, 0) + if skip_rendering: + params.add_int(MPV_RENDER_PARAM_SKIP_RENDERING, 1) + + with nogil: + ret = mpv_render_context_render(self._ctx, params.params) + return ret + + def report_swap(self): + with nogil: + mpv_render_context_report_swap(self._ctx) + + def close(self): + if not self.inited: + return + with nogil: + mpv_render_context_free(self._ctx) + self.inited = False + + def __dealloc__(self): + self.close() + +cdef class OpenGLRenderContext(RenderContext): + def __init__(self, mpv, + get_proc_address, + *args, **kwargs): + init_params = { + "get_proc_address": get_proc_address + } + RenderContext.__init__(self, mpv, RenderContext.API_OPENGL, + init_params, *args, **kwargs) + class CallbackThread(Thread): def __init__(self): Thread.__init__(self) @@ -820,7 +1039,7 @@ class CallbackThread(Thread): except Exception as e: sys.stderr.write("pympv error during callback: %s\n" % e) -cdef void _c_callback(void* d) with gil: +cdef void _c_callback(void* d) noexcept with gil: cdef uint64_t name = d callback = _callbacks.get(name) callback.call() diff --git a/samples/basic.py b/samples/basic.py new file mode 100644 index 0000000..3008b4a --- /dev/null +++ b/samples/basic.py @@ -0,0 +1,51 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys + +import mpv + + +def main(args): + if len(args) != 1: + print('pass a single media file as argument') + return 1 + + try: + m = mpv.Context() + except mpv.MPVError: + print('failed creating context') + return 1 + + m.set_option('input-default-bindings') + m.set_option('osc') + m.set_option('input-vo-keyboard') + m.initialize() + + m.command('loadfile', args[0]) + + while True: + event = m.wait_event(.01) + if event.id == mpv.Events.none: + continue + print(event.name) + if event.id in [mpv.Events.end_file, mpv.Events.shutdown]: + break + + +if __name__ == '__main__': + try: + exit(main(sys.argv[1:]) or 0) + except mpv.MPVError as e: + print(str(e)) + exit(1) diff --git a/samples/pygobject.py b/samples/pygobject.py new file mode 100755 index 0000000..7b697f0 --- /dev/null +++ b/samples/pygobject.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ctypes +import sys +from ctypes import CFUNCTYPE, c_char_p, c_void_p + +import gi +import mpv + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + +from OpenGL import GL, GLX + + +class MainClass(Gtk.Window): + + def __init__(self, media): + super(MainClass, self).__init__() + self.media = media + self.set_default_size(600, 400) + self.connect("destroy", self.on_destroy) + + frame = Gtk.Frame() + self.area = OpenGlArea() + self.area.connect("realize", self.play) + frame.add(self.area) + self.add(frame) + self.show_all() + + def on_destroy(self, widget, data=None): + Gtk.main_quit() + + def play(self, arg1): + self.area.play(self.media) + + +class OpenGlArea(Gtk.GLArea): + + def __init__(self, **properties): + super().__init__(**properties) + + self.gl_context: mpv.OpenGLRenderContext = None + + try: + self.mpv = mpv.Context() + self.mpv.initialize() + except mpv.MPVError: + raise RuntimeError('failed creating context') + + # self.mpv.set_property("gpu-context", "wayland") + self.mpv.set_property("terminal", True) + + self.connect("realize", self.on_realize) + self.connect("render", self.on_render) + self.connect("unrealize", self.on_unrealize) + + # noinspection PyUnusedLocal + def on_realize(self, area: Gtk.GLArea): + self.make_current() + self.gl_context = mpv.OpenGLRenderContext(self.mpv, get_process_address) + self.gl_context.set_update_callback(self.queue_render) + + def on_unrealize(self, arg): + if self.gl_context: + self.gl_context.close() + self.mpv.shutdown() + + def on_render(self, arg1, arg2): + factor = self.get_scale_factor() + rect: Gdk.Rectangle = self.get_allocated_size()[0] + + if self.gl_context: + fbo = { + "fbo": GL.glGetIntegerv(GL.GL_DRAW_FRAMEBUFFER_BINDING), + "w": rect.width * factor, + "h": rect.height * factor, + } + self.gl_context.render(opengl_fbo=fbo, flip_y=True) + + return True + + def play(self, media): + self.mpv.command('loadfile', media) + + +@CFUNCTYPE(c_void_p, c_char_p) +def get_process_address(name): + address = GLX.glXGetProcAddress(name.decode("utf-8")) + return ctypes.cast(address, ctypes.c_void_p).value + + +if __name__ == '__main__': + args = sys.argv[1:] + + if len(args) == 1: + import locale + + locale.setlocale(locale.LC_NUMERIC, 'C') + + application = MainClass(args[0]) + Gtk.main() + else: + print('pass a single media file as argument and try again') diff --git a/setup.py b/setup.py index 2e5f62f..8401508 100644 --- a/setup.py +++ b/setup.py @@ -14,35 +14,74 @@ # along with this program. If not, see . import os -from subprocess import call -from distutils.core import setup -from distutils.extension import Extension -from distutils.command.clean import clean -from Cython.Distutils import build_ext -from Cython.Build import cythonize - -def tryremove(filename): - if not os.path.isfile(filename): - return +import sys +from glob import glob +from os.path import join + +from setuptools import Extension, find_packages, setup + +USE_CYTHON = False +extension_src = "mpv.c" + +if os.path.exists("mpv.pyx"): try: - os.remove(filename) - except OSError as e: - print(e) + from Cython.Build import cythonize + USE_CYTHON = True + extension_src = "mpv.pyx" + except ImportError: + pass + +extra_data = {} +extensions = [Extension("mpv", [extension_src], libraries=["mpv"])] + +if set(["setup.py", "--version", "-V"]) >= set(sys.argv): + extensions = [] -class Clean(clean): - side_effects = [ - "mpv.c", +if set(["bdist_wheel", "--plat-name", "win_amd64"]) <= set(sys.argv): + extra_data["data_files"] = [ + ("Scripts", ["mpv.dll"]), + ("libs", ["mpv.lib"]), + ("include", glob("mpv/*")), ] + extensions = [ + Extension( + "mpv", + [extension_src], + libraries=["mpv"], + library_dirs=[os.curdir], + include_dirs=[join(os.curdir, "mpv")], + ) + ] + +if USE_CYTHON: + extensions = cythonize(extensions, force=True) + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() - def run(self): - for f in self.side_effects: - tryremove(f) - clean.run(self) setup( - cmdclass = { - "build_ext": build_ext, - "clean": Clean, - }, - ext_modules = cythonize([Extension("mpv", ["mpv.pyx"], libraries=['mpv'])]) + name="pympv", + version="0.9.0", + description="Python bindings for the libmpv library", + # This is supposed to be reST. Cheating by using a common subset of + # reST and Markdown... + long_description=read("README.md"), + long_description_content_type="text/markdown", + author="Andre D", + author_email="andre@andred.ca", + maintainer="Hector Martin", + maintainer_email="marcan@marcan.st", + url="https://github.com/marcan/pympv", + classifiers=[ + "Development Status :: 3 - Alpha", + "Programming Language :: Cython", + "Topic :: Multimedia :: Sound/Audio :: Players", + "Topic :: Multimedia :: Video", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + ], + ext_modules=extensions, + **extra_data )