Hello community,

here is the log from the commit of package python-python-mpv for 
openSUSE:Factory checked in at 2019-12-02 11:36:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-python-mpv (Old)
 and      /work/SRC/openSUSE:Factory/.python-python-mpv.new.4691 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-python-mpv"

Mon Dec  2 11:36:10 2019 rev:8 rq:752821 version:0.4.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-python-mpv/python-python-mpv.changes      
2019-11-27 13:54:20.484324915 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-python-mpv.new.4691/python-python-mpv.changes
    2019-12-02 11:38:34.534463639 +0100
@@ -1,0 +2,6 @@
+Mon Dec  2 08:22:36 UTC 2019 - Luigi Baldoni <aloi...@gmx.com>
+
+- Update to version 0.4.1
+  * Add stream protocol handling
+
+-------------------------------------------------------------------

Old:
----
  python-mpv-0.4.0.tar.gz

New:
----
  python-mpv-0.4.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-python-mpv.spec ++++++
--- /var/tmp/diff_new_pack.0cJyvf/_old  2019-12-02 11:38:34.842463704 +0100
+++ /var/tmp/diff_new_pack.0cJyvf/_new  2019-12-02 11:38:34.842463704 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python-python-mpv
-Version:        0.4.0
+Version:        0.4.1
 Release:        0
 Summary:        Python interface to the mpv media player
 License:        AGPL-3.0-or-later

++++++ python-mpv-0.4.0.tar.gz -> python-mpv-0.4.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-mpv-0.4.0/PKG-INFO 
new/python-mpv-0.4.1/PKG-INFO
--- old/python-mpv-0.4.0/PKG-INFO       2019-11-26 12:28:58.000000000 +0100
+++ new/python-mpv-0.4.1/PKG-INFO       2019-12-01 21:28:56.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: python-mpv
-Version: 0.4.0
+Version: 0.4.1
 Summary: A python interface to the mpv media player
 Home-page: https://github.com/jaseg/python-mpv
 Author: jaseg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-mpv-0.4.0/README.rst 
new/python-mpv-0.4.1/README.rst
--- old/python-mpv-0.4.0/README.rst     2019-11-26 12:26:37.000000000 +0100
+++ new/python-mpv-0.4.1/README.rst     2019-12-01 21:21:57.000000000 +0100
@@ -135,6 +135,24 @@
         print(player.playlist)
         player.wait_for_playback()
 
+Directly feeding mpv data from python
+.....................................
+
+.. code:: python
+
+    #!/usr/bin/env python3
+    import mpv
+
+    player = mpv.MPV()
+    @player.python_stream('foo')
+    def reader():
+        with open('test.webm', 'rb') as f:
+            while True:
+                yield f.read(1024*1024)
+
+    player.play('python://foo')
+    player.wait_for_playback()
+
 PyQT embedding
 ..............
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-mpv-0.4.0/mpv.py new/python-mpv-0.4.1/mpv.py
--- old/python-mpv-0.4.0/mpv.py 2019-11-26 12:26:37.000000000 +0100
+++ new/python-mpv-0.4.1/mpv.py 2019-12-01 21:24:34.000000000 +0100
@@ -72,6 +72,14 @@
     PROPERTY_UNAVAILABLE    = -10
     PROPERTY_ERROR          = -11
     COMMAND                 = -12
+    LOADING_FAILED          = -13
+    AO_INIT_FAILED          = -14
+    VO_INIT_FAILED          = -15
+    NOTHING_TO_PLAY         = -16
+    UNKNOWN_FORMAT          = -17
+    UNSUPPORTED             = -18
+    NOT_IMPLEMENTED         = -19
+    GENERIC                 = -20
 
     EXCEPTION_DICT = {
              0:     None,
@@ -88,7 +96,17 @@
             -9:     lambda *a: TypeError('Tried to get/set mpv property using 
wrong format, or passed invalid value', *a),
             -10:    lambda *a: PropertyUnavailableError('mpv property is not 
available', *a),
             -11:    lambda *a: RuntimeError('Generic error getting or setting 
mpv property', *a),
-            -12:    lambda *a: SystemError('Error running mpv command', *a) }
+            -12:    lambda *a: SystemError('Error running mpv command', *a),
+            -14:    lambda *a: RuntimeError('Initializing the audio output 
failed', *a),
+            -15:    lambda *a: RuntimeError('Initializing the video output 
failed'),
+            -16:    lambda *a: RuntimeError('There was no audio or video data 
to play. This also happens if the file '
+                                            'was recognized, but did not 
contain any audio or video streams, or no '
+                                            'streams were selected.'),
+            -17:    lambda *a: RuntimeError('When trying to load the file, the 
file format could not be determined, '
+                                            'or the file was too broken to 
open it'),
+            -18:    lambda *a: ValueError('Generic error for signaling that 
certain system requirements are not fulfilled'),
+            -19:    lambda *a: NotImplementedError('The API function which was 
called is a stub only'),
+            -20:    lambda *a: RuntimeError('Unspecified error') }
 
     @staticmethod
     def default_error_handler(ec, *args):
@@ -282,14 +300,24 @@
                  'level':  self.level.decode('utf-8'),
                  'text':   decoder(self.text).rstrip() }
 
-class MpvEventEndFile(c_int):
-    EOF_OR_INIT_FAILURE = 0
+class MpvEventEndFile(Structure):
+    _fields_ = [('reason', c_int),
+                ('error', c_int)]
+
+    EOF                 = 0
     RESTARTED           = 1
     ABORTED             = 2
     QUIT                = 3
+    ERROR               = 4
+    REDIRECT            = 5
+
+    # For backwards-compatibility
+    @property
+    def value(self):
+        return self.reason
 
     def as_dict(self, decoder=identity_decoder):
-        return {'reason': self.value}
+        return {'reason': self.reason, 'error': self.error}
 
 class MpvEventScriptInputDispatch(Structure):
     _fields_ = [('arg0', c_int),
@@ -305,6 +333,22 @@
     def as_dict(self, decoder=identity_decoder):
         return { 'args': [ self.args[i].decode('utf-8') for i in 
range(self.num_args) ] }
 
+StreamReadFn = CFUNCTYPE(c_int64, c_void_p, POINTER(c_char), c_uint64)
+StreamSeekFn = CFUNCTYPE(c_int64, c_void_p, c_int64)
+StreamSizeFn = CFUNCTYPE(c_int64, c_void_p)
+StreamCloseFn = CFUNCTYPE(None, c_void_p)
+StreamCancelFn = CFUNCTYPE(None, c_void_p)
+
+class StreamCallbackInfo(Structure):
+    _fields_ = [('cookie', c_void_p),
+                ('read', StreamReadFn),
+                ('seek', StreamSeekFn),
+                ('size', StreamSizeFn),
+                ('close', StreamCloseFn), ]
+#                ('cancel', StreamCancelFn)]
+
+StreamOpenFn = CFUNCTYPE(c_int, c_void_p, c_char_p, 
POINTER(StreamCallbackInfo))
+
 WakeupCallback = CFUNCTYPE(None, c_void_p)
 
 OpenGlCbUpdateFn = CFUNCTYPE(None, c_void_p)
@@ -387,6 +431,8 @@
 _handle_func('mpv_set_wakeup_callback',     [WakeupCallback, c_void_p],        
         None, errcheck=None)
 _handle_func('mpv_get_wakeup_pipe',         [],                                
         c_int, errcheck=None)
 
+_handle_func('mpv_stream_cb_add_ro',        [c_char_p, c_void_p, 
StreamOpenFn],         c_int, ec_errcheck)
+
 _handle_func('mpv_get_sub_api',             [MpvSubApi],                       
         c_void_p, notnull_errcheck)
 
 _handle_gl_func('mpv_opengl_cb_set_update_callback',    [OpenGlCbUpdateFn, 
c_void_p])
@@ -517,6 +563,40 @@
     def __setattr__(self, name, value):
         setattr(self.mpv, _py_to_mpv(name), value)
 
+class GeneratorStream:
+    """Transform a python generator into an mpv-compatible stream object. This 
only supports size() and read(), and
+    does not support seek(), close() or cancel().
+    """
+
+    def __init__(self, generator_fun, size=None):
+        self._generator_fun = generator_fun
+        self.size = size
+
+    def seek(self, offset):
+        self._read_iter = iter(self._generator_fun())
+        self._read_chunk = b''
+        return 0 # We only support seeking to the first byte atm
+        # implementation in case seeking to arbitrary offsets would be 
necessary
+        # while offset > 0:
+        #     offset -= len(self.read(offset))
+        # return offset
+
+    def read(self, size):
+        if not self._read_chunk:
+            try:
+                self._read_chunk += next(self._read_iter)
+            except StopIteration:
+                return b''
+        rv, self._read_chunk = self._read_chunk[:size], self._read_chunk[size:]
+        return rv
+
+    def close(self):
+        self._read_iter = iter([]) # make next read() call return EOF
+
+    def cancel(self):
+        self._read_iter = iter([]) # make next read() call return EOF
+        # TODO?
+
 class MPV(object):
     """See man mpv(1) for the details of the implemented commands. All mpv 
properties can be accessed as
     ``my_mpv.some_property`` and all mpv options can be accessed as 
``my_mpv['some-option']``.
@@ -565,6 +645,11 @@
         self._event_handle = _mpv_create_client(self.handle, 
b'py_event_handler')
         self._loop = partial(_event_loop, self._event_handle, 
self._playback_cond, self._event_callbacks,
                 self._message_handlers, self._property_handlers, log_handler)
+        self._stream_protocol_cbs = {}
+        self._stream_protocol_frontends = collections.defaultdict(lambda: {})
+        self.register_stream_protocol('python', self._python_stream_open)
+        self._python_streams = {}
+        self._python_stream_catchall = None
         if loglevel is not None or log_handler is not None:
             self.set_loglevel(loglevel or 'terminal-default')
         if start_event_thread:
@@ -1028,6 +1113,87 @@
             if not self._key_binding_handlers:
                 self.unregister_message_handler('key-binding')
 
+    def register_stream_protocol(self, proto, open_fn=None):
+        """ Register a custom stream protocol as documented in 
libmpv/stream_cb.h:
+            https://github.com/mpv-player/mpv/blob/master/libmpv/stream_cb.h
+
+            proto is the protocol scheme, e.g. "foo" for "foo://" urls.
+
+            This function can either be used with two parameters or it can be 
used as a decorator on the target
+            function.
+
+            open_fn is a function taking an URI string and returning an mpv 
stream object.
+            open_fn may raise a ValueError to signal libmpv the URI could not 
be opened.
+
+            The mpv stream protocol is as follows:
+            class Stream:
+                @property
+                def size(self):
+                    return None # unknown size
+                    return size # int with size in bytes
+
+                def read(self, size):
+                    ...
+                    return read # non-empty bytes object with input
+                    return b'' # empty byte object signals permanent EOF
+
+                def seek(self, pos):
+                    return new_offset # integer with new byte offset. The new 
offset may be before the requested offset
+                    in case an exact seek is inconvenient.
+
+                def close(self):
+                    ...
+
+                # def cancel(self): (future API versions only)
+                #     Abort a running read() or seek() operation
+                #     ...
+
+        """
+
+        def decorator(open_fn):
+            @StreamOpenFn
+            def open_backend(_userdata, uri, cb_info):
+                try:
+                    frontend = open_fn(uri.decode('utf-8'))
+                except ValueError:
+                    return ErrorCode.LOADING_FAILED
+
+                def read_backend(_userdata, buf, bufsize):
+                    data = frontend.read(bufsize)
+                    for i in range(len(data)):
+                        buf[i] = data[i]
+                    return len(data)
+
+                cb_info.contents.cookie = None
+                read = cb_info.contents.read = StreamReadFn(read_backend)
+                close = cb_info.contents.close = StreamCloseFn(lambda 
_userdata: frontend.close())
+
+                seek, size, cancel = None, None, None
+                if hasattr(frontend, 'seek'):
+                    seek = cb_info.contents.seek = StreamSeekFn(lambda 
_userdata, offx: frontend.seek(offx))
+                if hasattr(frontend, 'size') and frontend.size is not None:
+                    size = cb_info.contents.size = StreamSizeFn(lambda 
_userdata: frontend.size)
+
+                # Future API versions only
+                # if hasattr(frontend, 'cancel'):
+                #     cb_info.contents.cancel = StreamCancelFn(lambda 
_userdata: frontend.cancel())
+
+                # keep frontend and callbacks in memory forever (TODO)
+                frontend._registered_callbacks = [read, close, seek, size, 
cancel]
+                self._stream_protocol_frontends[proto][uri] = frontend
+                return 0
+
+            if proto in self._stream_protocol_cbs:
+                raise KeyError('Stream protocol already registered')
+            self._stream_protocol_cbs[proto] = [open_backend]
+            _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), 
c_void_p(), open_backend)
+
+            return open_fn
+
+        if open_fn is not None:
+            decorator(open_fn)
+        return decorator
+
     # Convenience functions
     def play(self, filename):
         """Play a path or URL (requires ``ytdl`` option to be set)."""
@@ -1043,6 +1209,97 @@
         ``MPV.loadfile(filename, 'append-play')``."""
         self.loadfile(filename, 'append', **options)
 
+    # "Python stream" logic. This is some porcelain for directly playing data 
from python generators.
+
+    def _python_stream_open(self, uri):
+        """Internal handler for python:// protocol streams registered through 
@python_stream(...) and
+        @python_stream_catchall
+        """
+        name, = re.fullmatch('python://(.*)', uri).groups()
+
+        if name in self._python_streams:
+            generator_fun, size = self._python_streams[name]
+        else:
+            if self._python_stream_catchall is not None:
+                generator_fun, size = self._python_stream_catchall(name)
+            else:
+                raise ValueError('Python stream name not found and no 
catch-all defined')
+
+        return GeneratorStream(generator_fun, size)
+
+    def python_stream(self, name=None, size=None):
+        """Register a generator for the python stream with the given name.
+
+        name is the name, i.e. the part after the "python://" in the URI, that 
this generator is registered as.
+        size is the total number of bytes in the stream (if known).
+
+        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.
+
+        The generator signals EOF by returning, manually raising StopIteration 
or by yielding b'', an empty bytes
+        object.
+
+        The generator may be called multiple times if libmpv seeks or loops.
+
+        See also: @mpv.python_stream_catchall
+
+        @mpv.python_stream('foobar')
+        def reader():
+            for chunk in chunks:
+                yield chunk
+        mpv.play('python://foobar')
+        mpv.wait_for_playback()
+        reader.unregister()
+        """
+        def register(cb):
+            if name in self._python_streams:
+                raise KeyError(f'Python stream name "{name}" is already 
registered')
+            self._python_streams[name] = (cb, size)
+            def unregister():
+                if name not in self._python_streams or\
+                        self._python_streams[name][0] is not cb: # This is 
just a basic sanity check
+                    raise RuntimeError('Python stream has already been 
unregistered')
+                del self._python_streams[name]
+            cb.unregister = unregister
+            return cb
+        return register
+
+    def python_stream_catchall(self, cb):
+        """ Register a catch-all python stream to be called when no name 
matches can be found. Use this decorator on a
+        function that takes a name argument and returns a (generator, size) 
tuple (with size being None if unknown).
+
+        An invalid URI can be signalled to libmpv by raising a ValueError 
inside the callback.
+
+        See also: @mpv.python_stream(name, size)
+
+        @mpv.python_stream_catchall
+        def catchall(name):
+            if not name.startswith('foo'):
+                raise ValueError('Unknown Name')
+
+            def foo_reader():
+                with open(name, 'rb') as f:
+                    while True:
+                        chunk = f.read(1024)
+                        if not chunk:
+                            break
+                        yield chunk
+            return foo_reader, None
+        mpv.play('python://foo23')
+        mpv.wait_for_playback()
+        catchall.unregister()
+        """
+        if self._python_stream_catchall is not None:
+            raise KeyError('A catch-all python stream is already registered')
+
+        self._python_stream_catchall = cb
+        def unregister():
+            if self._python_stream_catchall is not cb:
+                    raise RuntimeError('This catch-all python stream has 
already been unregistered')
+            self._python_stream_catchall = None
+        cb.unregister = unregister
+        return cb
+
     # Property accessors
     def _get_property(self, name, decoder=strict_decoder, fmt=MpvFormat.NODE):
         out = create_string_buffer(sizeof(MpvNode))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-mpv-0.4.0/python_mpv.egg-info/PKG-INFO 
new/python-mpv-0.4.1/python_mpv.egg-info/PKG-INFO
--- old/python-mpv-0.4.0/python_mpv.egg-info/PKG-INFO   2019-11-26 
12:28:58.000000000 +0100
+++ new/python-mpv-0.4.1/python_mpv.egg-info/PKG-INFO   2019-12-01 
21:28:56.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: python-mpv
-Version: 0.4.0
+Version: 0.4.1
 Summary: A python interface to the mpv media player
 Home-page: https://github.com/jaseg/python-mpv
 Author: jaseg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-mpv-0.4.0/setup.py 
new/python-mpv-0.4.1/setup.py
--- old/python-mpv-0.4.0/setup.py       2019-11-26 12:26:56.000000000 +0100
+++ new/python-mpv-0.4.1/setup.py       2019-12-01 21:27:50.000000000 +0100
@@ -3,7 +3,7 @@
 from setuptools import setup
 setup(
     name = 'python-mpv',
-    version = '0.4.0',
+    version = '0.4.1',
     py_modules = ['mpv'],
     description = 'A python interface to the mpv media player',
     url = 'https://github.com/jaseg/python-mpv',


Reply via email to