Search code examples
pythonpython-3.xlibvlc

Passing file-like objects to ctypes callbacks


I'm attempting to use the LibVLC Python bindings to play an in-memory stream (Python 3.4, Windows 7, LibVLC 3.x). Eventually, my aim is to feed data into a BytesIO instance which VLC will then read from and play. But for the moment, I decided to hack up a quick script to try reading from a file stream. Here's the code and traceback - to say I'm pretty new to ctypes would be an understatement so does anyone know what I'm doing wrong?

import ctypes
import io
import sys
import time

import vlc

MediaOpenCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_uint64))
MediaReadCb = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t)
MediaSeekCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_uint64)
MediaCloseCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)


def media_open_cb(opaque, data_pointer, size_pointer):
    data_pointer.value = opaque
    size_pointer.contents.value = sys.maxsize
    return 0


def media_read_cb(opaque, buffer, length):
    stream = ctypes.cast(opaque, ctypes.py_object).value
    new_data = stream.read(length.contents)
    buffer.contents.value = new_data
    return len(new_data)


def media_seek_cb(opaque, offset):
    stream = ctypes.cast(opaque, ctypes.py_object).value
    stream.seek(offset)
    return 0


def media_close_cb(opaque):
    stream = ctypes.cast(opaque, ctypes.py_object).value
    stream.close()


callbacks = {
    'open': MediaOpenCb(media_open_cb),
    'read': MediaReadCb(media_read_cb),
    'seek': MediaSeekCb(media_seek_cb),
    'close': MediaCloseCb(media_close_cb)
}


def main(path):
    stream = open(path, 'rb')
    instance = vlc.Instance()
    player = instance.media_player_new()
    media = instance.media_new_callbacks(callbacks['open'], callbacks['read'], callbacks['seek'], callbacks['close'], ctypes.byref(ctypes.py_object(stream)))
    player.set_media(media)
    player.play()

    while True:
        time.sleep(1)


if __name__ == '__main__':
    try:
        path = sys.argv[1]
    except IndexError:
        print('Usage: {0} <path>'.format(__file__))
        sys.exit(1)

    main(path)

[02f87cb0] imem demux error: Invalid get/release function pointers
Traceback (most recent call last):
  File "_ctypes/callbacks.c", line 234, in 'calling callback function'
  File "memory_stream.py", line 21, in media_read_cb
    stream = ctypes.cast(opaque, ctypes.py_object).value
ValueError: PyObject is NULL

The above traceback is repeated until I kill the program.


Solution

  • A NoneType is passed to the media_read_cb as indicated by the traceback. The problem in the code seems to be the media_open_cb function. If you replace this callback with None in the media_new_callbacks function, it will not be called and media_read_cb will be called with the appropiate opaque pointer.

    The reason for this is a bit obscure to me. If the open_cb is set to None, vlc will call its default open_cb, which will then set size_pointer to maxsize and data_pointer to opaque by default (which is identical to your function). Apparently, something in your code goes wrong when setting the value of the pointer. I do not know how to fix that as I am new to ctypes too.

    When I run your code with:

    media = instance.media_new_callbacks(None, callbacks['read'], callbacks['seek'], callbacks['close'], ctypes.byref(ctypes.py_object(stream)))
    

    The media_read_cb is succesfully called. However, python then crashes at:

    stream = ctypes.cast(opaque, ctypes.py_object).value
    

    I do not know how to solve this either, but there is a workaround. You could set the stream variable as a global variable, so you keep the pointer yourself instead of relying on the ctypes stuff.

    Writing to the buffer also does not seem to work, as the buffer is passed as a string to the media_read_cb. Since strings are immutable in python, this fails. A workaround for this is to change the CFUNCTYPE to contain a ctypes.POINTER to c_char instead of plain c_char_p (string in python). You can then populate the memory area with the bytes from the stream through iteration.

    Applying these changes, your code looks like this:

    import ctypes
    import io
    import sys
    import time
    
    import vlc
    
    MediaOpenCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_uint64))
    MediaReadCb = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t)
    MediaSeekCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_uint64)
    MediaCloseCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
    
    stream=None
    
    def media_open_cb(opaque, data_pointer, size_pointer):
        data_pointer.value = opaque
        size_pointer.contents.value = sys.maxsize
        return 0
    
    
    def media_read_cb(opaque, buffer, length):
        new_data = stream.read(length)
        for i in range(len(new_data)):
            buffer[i]=new_data[i]
        return len(new_data)
    
    
    def media_seek_cb(opaque, offset):
        stream.seek(offset)
        return 0
    
    
    def media_close_cb(opaque):
        stream.close()
    
    
    callbacks = {
        'open': MediaOpenCb(media_open_cb),
        'read': MediaReadCb(media_read_cb),
        'seek': MediaSeekCb(media_seek_cb),
        'close': MediaCloseCb(media_close_cb)
    }
    
    def main(path):
        global stream
        stream = open(path, 'rb')
        instance = vlc.Instance('-vvv')
        player = instance.media_player_new()
        media = instance.media_new_callbacks(None, callbacks['read'], callbacks['seek'], callbacks['close'], ctypes.byref(ctypes.py_object(stream)))
        player.set_media(media)
        player.play()
    
        while True:
            time.sleep(1)
    
    if __name__ == '__main__':
        try:
            path = sys.argv[1]
        except IndexError:
            print('Usage: {0} <path>'.format(__file__))
            sys.exit(1)
    
        main(path)
    

    And it succesfully runs!

    Of course, instead of using a global variable, it would be better to wrap all this inside a python class.

    EDIT: I figured out how to set the data_pointer appropiately. Here is the code:

    import ctypes
    import io
    import sys
    import time
    
    import vlc
    
    MediaOpenCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_uint64))
    MediaReadCb = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t)
    MediaSeekCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_uint64)
    MediaCloseCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
    
    
    def media_open_cb(opaque, data_pointer, size_pointer):
        data_pointer.contents.value = opaque
        size_pointer.contents.value = sys.maxsize
        return 0
    
    
    def media_read_cb(opaque, buffer, length):
        stream=ctypes.cast(opaque,ctypes.POINTER(ctypes.py_object)).contents.value
        new_data = stream.read(length)
        for i in range(len(new_data)):
            buffer[i]=new_data[i]
        return len(new_data)
    
    
    def media_seek_cb(opaque, offset):
        stream=ctypes.cast(opaque,ctypes.POINTER(ctypes.py_object)).contents.value
        stream.seek(offset)
        return 0
    
    
    def media_close_cb(opaque):
        stream=ctypes.cast(opaque,ctypes.POINTER(ctypes.py_object)).contents.value
        stream.close()
    
    
    callbacks = {
        'open': MediaOpenCb(media_open_cb),
        'read': MediaReadCb(media_read_cb),
        'seek': MediaSeekCb(media_seek_cb),
        'close': MediaCloseCb(media_close_cb)
    }
    
    def main(path):
        stream = open(path, 'rb')
        instance = vlc.Instance()
        player = instance.media_player_new()
        media = instance.media_new_callbacks(callbacks['open'], callbacks['read'], callbacks['seek'], callbacks['close'], ctypes.cast(ctypes.pointer(ctypes.py_object(stream)), ctypes.c_void_p))
        player.set_media(media)
        player.play()
    
        while True:
            time.sleep(1)
    
    if __name__ == '__main__':
        try:
            path = sys.argv[1]
        except IndexError:
            print('Usage: {0} <path>'.format(__file__))
            sys.exit(1)
    
        main(path)