Last active
October 21, 2024 17:07
-
-
Save dcoles/ae4e601d23a6d81e1b215b9fed2d5a98 to your computer and use it in GitHub Desktop.
SPA Plugin Loading in Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SPA bindings | |
import os | |
import functools | |
from spa._cffi import ffi | |
from spa.pod import PodObject | |
from spa.constants import SPA_TYPE_COMMAND__NODE | |
CURRENT_DIR = os.path.dirname(__file__) | |
SPA_HANDLE_FACTORY_ENUM_FUNC_NAME = 'spa_handle_factory_enum' | |
SPA_PLUGIN_DIR = os.getenv('SPA_PLUGIN_DIR', 'plugins') | |
class Plugin: | |
@classmethod | |
def load_plugin(cls, path: str) -> 'Plugin': | |
path = os.path.join(SPA_PLUGIN_DIR, path) | |
lib = ffi.dlopen(path) | |
return cls(lib, path) | |
def __init__(self, lib, path: str) -> None: | |
self.lib = lib | |
self.path = path | |
def __del__(self) -> None: | |
ffi.dlclose(self.lib) | |
self.lib = None | |
def __repr__(self) -> str: | |
return f'Plugin<{self.path}>' | |
def get_factory(self, name) -> 'Factory': | |
name_ = name.encode('utf-8') if isinstance(name, str) else name | |
factory_p = ffi.new('struct spa_handle_factory **', ffi.NULL) | |
index_p = ffi.new('uint32_t *', 0) | |
while True: | |
res = self.lib.spa_handle_factory_enum(factory_p, index_p) | |
if res < 0: | |
raise RuntimeError('failed to enumerate plugin factory') | |
if res == 0: | |
raise ModuleNotFoundError(f'plugin factory {name!r} not found') | |
factory_name = ffi.string(factory_p[0].name) | |
if factory_name == name_: | |
break | |
return Factory(self, factory_p[0]) | |
class Factory: | |
def __init__(self, plugin: Plugin, ptr) -> None: | |
# Hold on to Plugin so shared library isn't closed | |
self._plugin = plugin | |
self._ptr = ptr | |
@property | |
def name(self) -> str: | |
return ffi.string(self._ptr.name).decode('utf-8') | |
def __repr__(self) -> str: | |
return f'Factory<{self.name}>' | |
def build(self, info, support, n_support) -> 'Handle': | |
size = self._ptr.get_size(self._ptr, info) | |
buffer = ffi.new('uint8_t[]', size) | |
handle = ffi.cast('struct spa_handle *', buffer) | |
res = self._ptr.init(self._ptr, handle, info, support, n_support) | |
if res < 0: | |
raise RuntimeError('failed to initialize handle') | |
return Handle(self, buffer) | |
class Handle: | |
def __init__(self, factory: Factory, buffer) -> None: | |
# Hold onto Factory so it can't be garbage collected | |
self._factory = factory | |
self._buffer = buffer # Take ownership of handle memory | |
self._ptr = ffi.cast('struct spa_handle *', buffer) | |
def __repr__(self) -> str: | |
return f'Handle<{self._factory.name}>' | |
def get_interface(self, interface_type) -> 'Interface': | |
interface_p = ffi.new('struct spa_interface **'); | |
ret = self._ptr.get_interface(self._ptr, interface_type.C_TYPE, ffi.cast('void **', interface_p)) | |
if ret < 0: | |
raise RuntimeError(f'Failed to get {interface_type.TYPE} interface on {self}') | |
return interface_type(self, interface_p[0]) | |
class Methods: | |
def __init__(self, methods_type: str, handle_ptr, interface_ptr) -> None: | |
self.__methods = ffi.cast(methods_type, interface_ptr.cb.funcs) | |
self.__ptr = handle_ptr | |
def __getattr__(self, name: str) -> callable: | |
method = getattr(self.__methods, name) | |
return functools.partial(method, self.__ptr) | |
class Interface: | |
TYPE = None | |
C_TYPE = None | |
METHOD_TYPE = None | |
def __init__(self, handle: Handle, ptr) -> None: | |
# Hold onto Handle so it can't be garbage collected | |
self._handle = handle | |
self._ptr = ptr | |
@property | |
def type(self) -> str: | |
return ffi.string(self._ptr.type).decode('utf-8') | |
@property | |
def _methods(self): | |
return Methods(self.METHOD_TYPE, self._handle._ptr, self._ptr) | |
def __repr__(self) -> str: | |
return f'{self.__class__.__name__}<{self.type}>' | |
class LogInterface(Interface): | |
TYPE = 'Spa:Pointer:Interface:Log' | |
C_TYPE = ffi.new('char[]', TYPE.encode('utf-8')) | |
METHOD_TYPE = 'struct spa_log_methods *' | |
LOG_LEVEL_NONE = 0 | |
LOG_LEVEL_ERROR = 1 | |
LOG_LEVEL_WARN = 2 | |
LOG_LEVEL_INFO = 3 | |
LOG_LEVEL_DEBUG = 4 | |
LOG_LEVEL_TRACE = 5 | |
def error(self, msg): | |
file = ffi.new('char[]', b'file.c') | |
func = ffi.new('char[]', b'func') | |
fmt = ffi.new('char[]', b'%s') | |
msg = ffi.new('char[]', msg.encode('utf-8')) | |
self._methods.log(self.LOG_LEVEL_ERROR, file, 0, func, fmt, msg) | |
class LoopInterface(Interface): | |
TYPE = 'Spa:Pointer:Interface:Loop' | |
C_TYPE = ffi.new('char[]', TYPE.encode('utf-8')) | |
METHOD_TYPE = 'struct spa_loop_methods *' | |
class LoopControlInterface(Interface): | |
TYPE = 'Spa:Pointer:Interface:LoopControl' | |
C_TYPE = ffi.new('char[]', TYPE.encode('utf-8')) | |
METHOD_TYPE = 'struct spa_loop_control_methods *' | |
def __enter__(self): | |
self.enter() | |
def __exit__(self, *_exc_info): | |
self.leave() | |
def enter(self): | |
print('LoopControl.enter') | |
self._methods.enter() | |
def leave(self): | |
print('LoopControl.leave') | |
self._methods.leave() | |
def iterate(self, timeout_ms: int = -1): | |
ret = self._methods.iterate(timeout_ms) | |
if ret < 0: | |
raise RuntimeError(f'LoopControl.iterate failed: {ret}') | |
def check(self) -> bool: | |
ret = self._methods.check() | |
if ret < 0: | |
raise RuntimeError(f'LoopControl.check failed: {ret}') | |
return bool(ret) | |
class LoopUtilsInterface(Interface): | |
TYPE = 'Spa:Pointer:Interface:LoopUtils' | |
C_TYPE = ffi.new('char[]', TYPE.encode('utf-8')) | |
METHOD_TYPE = 'struct spa_loop_utils_methods *' | |
class NodeInterface(Interface): | |
TYPE = 'Spa:Pointer:Interface:Node' | |
C_TYPE = ffi.new('char[]', TYPE.encode('utf-8')) | |
METHOD_TYPE = 'struct spa_node_methods *' | |
def set_callbacks(self, callbacks, data) -> None: | |
ret = self._methods.set_callbacks(callbacks, data) | |
if ret < 0: | |
raise RuntimeError(f'Failed to set Node callbacks: {ret}') | |
def add_listener(self, listener, node_events, data) -> None: | |
ret = self._methods.add_listener(listener, node_events, data) | |
if ret < 0: | |
raise RuntimeError(f'Failed to add Node listener: {ret}') | |
def port_set_io(self, direction: int, port: int, io_type: int, io): | |
ret = self._methods.port_set_io(direction, port, io_type, io, ffi.sizeof(io[0])) | |
if ret < 0: | |
raise RuntimeError(f'Failed to set Node port {port} IO: {ret}') | |
def port_use_buffers(self, direction: int, port: int, flags: int, spa_buffers, n_buffers: int): | |
ret = self._methods.port_use_buffers(direction, port, flags, spa_buffers, n_buffers) | |
if ret < 0: | |
raise RuntimeError(f'Failed to set Node port {port} buffers: {ret}') | |
def port_set_param(self, direction: int, port: int, param_type: int, flags: int, format): | |
ret = self._methods.port_set_param(direction, port, param_type, flags, format) | |
if ret < 0: | |
raise RuntimeError(f'Failed to set Node port={port!r} param_type={param_type!r}: {ret}') | |
def port_reuse_buffer(self, port: int, buffer_id: id) -> int: | |
ret = self._methods.port_reuse_buffer(port, buffer_id) | |
if ret < 0: | |
raise RuntimeError(f'Node.port_reuse_buffer failed port={port!r}: {ret}') | |
def send_command(self, command: int): | |
command_buffer = ffi.new('uint8_t[]', bytes(PodObject(SPA_TYPE_COMMAND__NODE, command))) | |
ret = self._methods.send_command(ffi.cast('struct spa_command *', command_buffer)) | |
if ret < 0: | |
raise RuntimeError(f'Failed to send command: {ret}') | |
def process(self) -> int: | |
ret = self._methods.process() | |
if ret < 0: | |
raise RuntimeError(f'Node process failed: {ret}') | |
return ret |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# auto-generated file | |
import _cffi_backend | |
ffi = _cffi_backend.FFI('spa._cffi', | |
_version = 0x2601, | |
_types = b'\x00\x00\x40\x0D\x00\x00\xD0\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x01\x11\x00\x00\xA7\x03\x00\x00\x19\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x0D\x03\x00\x00\x11\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\xD1\x03\x00\x00\x01\x11\x00\x00\xCD\x03\x00\x00\xEB\x03\x00\x00\x16\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x0D\x11\x00\x00\xD5\x03\x00\x00\x0A\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\xF4\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x00\x0B\x00\x00\x16\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\x0F\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\xC1\x03\x00\x00\x16\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\xE9\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x19\x11\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x07\x01\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x34\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x07\x01\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x34\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\xC8\x03\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\xD2\x03\x00\x00\xE4\x03\x00\x00\x19\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\xE3\x03\x00\x00\x19\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x16\x01\x00\x00\x16\x01\x00\x00\x34\x11\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x16\x01\x00\x00\x19\x11\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x40\x0D\x00\x00\x19\x11\x00\x00\x18\x01\x00\x00\x18\x01\x00\x00\xE9\x03\x00\x00\x00\x0F\x00\x00\x3C\x0D\x00\x00\x0D\x11\x00\x00\x0F\x11\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x5A\x11\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\x1D\x11\x00\x00\x16\x01\x00\x00\xEA\x03\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\x02\x0B\x00\x00\x05\x11\x00\x00\x07\x01\x00\x00\x05\x11\x00\x00\x05\x11\x00\x00\x01\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x16\x01\x00\x00\xF4\x03\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\xCE\x03\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\x5A\x11\x00\x00\xDC\x03\x00\x00\x19\x11\x00\x00\x00\x0F\x00\x00\xF4\x0D\x00\x00\x19\x11\x00\x00\xE5\x03\x00\x00\x00\x0F\x00\x00\x02\x01\x00\x00\x01\x0B\x00\x00\x00\x03\x00\x00\x03\x03\x00\x00\x0C\x03\x00\x00\x13\x03\x00\x00\x18\x03\x00\x00\x1B\x03\x00\x00\x20\x03\x00\x00\x26\x03\x00\x00\x2E\x03\x00\x00\x36\x03\x00\x00\x3E\x03\x00\x00\x42\x03\x00\x00\x4C\x03\x00\x00\x54\x03\x00\x00\x58\x03\x00\x00\x5E\x03\x00\x00\x63\x03\x00\x00\x68\x03\x00\x00\x6E\x03\x00\x00\x74\x03\x00\x00\x15\x01\x00\x00\x17\x01\x00\x00\x7A\x03\x00\x00\x00\x09\x00\x00\xC2\x03\x00\x00\x01\x09\x00\x00\x02\x09\x00\x00\xC5\x03\x00\x00\x03\x09\x00\x00\xC5\x05\x00\x00\x00\x01\x00\x00\x04\x09\x00\x00\xCA\x03\x00\x00\x05\x09\x00\x00\xCA\x05\x00\x00\x00\x01\x00\x00\x06\x09\x00\x00\x07\x09\x00\x00\x08\x09\x00\x00\x09\x09\x00\x00\x0A\x09\x00\x00\x0B\x09\x00\x00\x0C\x09\x00\x00\x0D\x09\x00\x00\xD6\x03\x00\x00\x0E\x09\x00\x00\x0F\x09\x00\x00\xD9\x03\x00\x00\x10\x09\x00\x00\x11\x09\x00\x00\x12\x09\x00\x00\x13\x09\x00\x00\x14\x09\x00\x00\xDF\x03\x00\x00\x15\x09\x00\x00\xDF\x05\x00\x00\x00\x01\x00\x00\x16\x09\x00\x00\x17\x09\x00\x00\x18\x09\x00\x00\x19\x09\x00\x00\x1A\x09\x00\x00\xE8\x03\x00\x00\x1B\x09\x00\x00\x1C\x09\x00\x00\x1D\x09\x00\x00\x1E\x09\x00\x00\x7E\x03\x00\x00\x81\x03\x00\x00\x84\x03\x00\x00\x8A\x03\x00\x00\x92\x03\x00\x00\x99\x03\x00\x00\x9D\x03\x00\x00\xA3\x03\x00\x00\x00\x01', | |
_globals = (b'\xFF\xFF\xFF\x1FSPA_CHUNK_FLAG_CORRUPTED',1,b'\xFF\xFF\xFF\x1FSPA_CHUNK_FLAG_EMPTY',2,b'\xFF\xFF\xFF\x1FSPA_CHUNK_FLAG_NONE',0,b'\xFF\xFF\xFF\x1FSPA_DATA_FLAG_DYNAMIC',4,b'\xFF\xFF\xFF\x1FSPA_DATA_FLAG_NONE',0,b'\xFF\xFF\xFF\x1FSPA_DATA_FLAG_READABLE',1,b'\xFF\xFF\xFF\x1FSPA_DATA_FLAG_READWRITE',3,b'\xFF\xFF\xFF\x1FSPA_DATA_FLAG_WRITABLE',2,b'\xFF\xFF\xFF\x0BSPA_DIRECTION_INPUT',0,b'\xFF\xFF\xFF\x0BSPA_DIRECTION_OUTPUT',1,b'\xFF\xFF\xFF\x0BSPA_IO_Buffers',1,b'\xFF\xFF\xFF\x0BSPA_IO_Clock',3,b'\xFF\xFF\xFF\x0BSPA_IO_Control',5,b'\xFF\xFF\xFF\x0BSPA_IO_Invalid',0,b'\xFF\xFF\xFF\x0BSPA_IO_Latency',4,b'\xFF\xFF\xFF\x0BSPA_IO_Memory',9,b'\xFF\xFF\xFF\x0BSPA_IO_Notify',6,b'\xFF\xFF\xFF\x0BSPA_IO_Position',7,b'\xFF\xFF\xFF\x0BSPA_IO_Range',2,b'\xFF\xFF\xFF\x0BSPA_IO_RateMatch',8,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_DEBUG',4,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_ERROR',1,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_INFO',3,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_NONE',0,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_TRACE',5,b'\xFF\xFF\xFF\x0BSPA_LOG_LEVEL_WARN',2,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_CORRUPTED',2,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_DELTA_UNIT',32,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_DISCONT',1,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_GAP',16,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_HEADER',8,b'\xFF\xFF\xFF\x1FSPA_META_HEADER_FLAG_MARKER',4,b'\xFF\xFF\xFF\x1FSPA_PORT_CHANGE_MASK_FLAGS',1,b'\xFF\xFF\xFF\x1FSPA_PORT_CHANGE_MASK_PARAMS',8,b'\xFF\xFF\xFF\x1FSPA_PORT_CHANGE_MASK_PROPS',4,b'\xFF\xFF\xFF\x1FSPA_PORT_CHANGE_MASK_RATE',2,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_CAN_ALLOC_BUFFERS',4,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_DYNAMIC_DATA',256,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_IN_PLACE',8,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_LIVE',32,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_NO_REF',16,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_OPTIONAL',2,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_PHYSICAL',64,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_REMOVABLE',1,b'\xFF\xFF\xFF\x1FSPA_PORT_FLAG_TERMINAL',128,b'\xFF\xFF\xFF\x1FSPA_STATUS_DRAINED',8,b'\xFF\xFF\xFF\x1FSPA_STATUS_HAVE_DATA',2,b'\xFF\xFF\xFF\x1FSPA_STATUS_NEED_DATA',1,b'\xFF\xFF\xFF\x1FSPA_STATUS_OK',0,b'\xFF\xFF\xFF\x1FSPA_STATUS_STOPPED',4,b'\xFF\xFF\xFF\x1FSPA_VERSION_HANDLE',0,b'\xFF\xFF\xFF\x1FSPA_VERSION_LOOP_CONTROL_METHODS',1,b'\xFF\xFF\xFF\x1FSPA_VERSION_NODE_CALLBACKS',0,b'\xFF\xFF\xFF\x1FSPA_VERSION_NODE_EVENTS',0,b'\xFF\xFF\xFF\x1FSPA_VERSION_NODE_METHODS',0,b'\x00\x00\x08\x23spa_handle_factory_enum',0), | |
_struct_unions = ((b'\x00\x00\x00\xC0\x00\x00\x00\x02buffer',b'\x00\x00\x11\x11id',b'\x00\x00\xC2\x11buffer',b'\x00\x00\xE0\x11metas',b'\x00\x00\xE2\x11header',b'\x00\x00\xCB\x11datas',b'\x00\x00\xC6\x11chunks',b'\x00\x00\x19\x11data',b'\x00\x00\x3C\x11data_size'),(b'\x00\x00\x00\xC2\x00\x00\x00\x02spa_buffer',b'\x00\x00\x11\x11n_metas',b'\x00\x00\x11\x11n_datas',b'\x00\x00\xDE\x11metas',b'\x00\x00\xC9\x11datas'),(b'\x00\x00\x00\xC3\x00\x00\x00\x02spa_callbacks',b'\x00\x00\x97\x11funcs',b'\x00\x00\x19\x11data'),(b'\x00\x00\x00\xC5\x00\x00\x00\x02spa_chunk',b'\x00\x00\x11\x11offset',b'\x00\x00\x11\x11size',b'\x00\x00\xBD\x11stride',b'\x00\x00\xBD\x11flags'),(b'\x00\x00\x00\xC8\x00\x00\x00\x10spa_command',),(b'\x00\x00\x00\xCA\x00\x00\x00\x02spa_data',b'\x00\x00\x11\x11type',b'\x00\x00\x11\x11flags',b'\x00\x00\xBE\x11fd',b'\x00\x00\x11\x11mapoffset',b'\x00\x00\x11\x11maxsize',b'\x00\x00\x19\x11data',b'\x00\x00\xC4\x11chunk'),(b'\x00\x00\x00\xCD\x00\x00\x00\x10spa_dict',),(b'\x00\x00\x00\xCE\x00\x00\x00\x10spa_event',),(b'\x00\x00\x00\xCF\x00\x00\x00\x02spa_fraction',b'\x00\x00\x11\x11num',b'\x00\x00\x11\x11denom'),(b'\x00\x00\x00\xD0\x00\x00\x00\x02spa_handle',b'\x00\x00\x11\x11version',b'\x00\x00\xAA\x11get_interface',b'\x00\x00\xA9\x11clear'),(b'\x00\x00\x00\xD1\x00\x00\x00\x02spa_handle_factory',b'\x00\x00\x11\x11version',b'\x00\x00\x05\x11name',b'\x00\x00\x0F\x11info',b'\x00\x00\xBF\x11get_size',b'\x00\x00\xAB\x11init',b'\x00\x00\xAC\x11enum_interface_info'),(b'\x00\x00\x00\xD2\x00\x00\x00\x02spa_hook',b'\x00\x00\xD9\x11link',b'\x00\x00\xC3\x11cb',b'\x00\x00\xEC\x11removed',b'\x00\x00\x19\x11priv'),(b'\x00\x00\x00\xD3\x00\x00\x00\x02spa_hook_list',b'\x00\x00\xD9\x11list'),(b'\x00\x00\x00\xD4\x00\x00\x00\x02spa_interface',b'\x00\x00\x05\x11type',b'\x00\x00\x11\x11version',b'\x00\x00\xC3\x11cb'),(b'\x00\x00\x00\xD6\x00\x00\x00\x10spa_interface_info',),(b'\x00\x00\x00\xD7\x00\x00\x00\x02spa_io_buffers',b'\x00\x00\xBD\x11status',b'\x00\x00\x11\x11buffer_id'),(b'\x00\x00\x00\xD9\x00\x00\x00\x02spa_list',b'\x00\x00\xD8\x11next',b'\x00\x00\xD8\x11prev'),(b'\x00\x00\x00\xDA\x00\x00\x00\x02spa_log',b'\x00\x00\xD4\x11iface',b'\x00\x00\x8C\x11level'),(b'\x00\x00\x00\xDB\x00\x00\x00\x02spa_log_methods',b'\x00\x00\x11\x11version',b'\x00\x00\xEF\x11log'),(b'\x00\x00\x00\xDC\x00\x00\x00\x10spa_loop_control_hooks',),(b'\x00\x00\x00\xDD\x00\x00\x00\x02spa_loop_control_methods',b'\x00\x00\x11\x11version',b'\x00\x00\xAD\x11get_fd',b'\x00\x00\xF2\x11add_hook',b'\x00\x00\xED\x11enter',b'\x00\x00\xED\x11leave',b'\x00\x00\xB3\x11iterate',b'\x00\x00\xAD\x11check'),(b'\x00\x00\x00\xDF\x00\x00\x00\x02spa_meta',b'\x00\x00\x11\x11type',b'\x00\x00\x11\x11size',b'\x00\x00\x19\x11data'),(b'\x00\x00\x00\xE2\x00\x00\x00\x02spa_meta_header',b'\x00\x00\x11\x11flags',b'\x00\x00\x11\x11offset',b'\x00\x00\xBE\x11pts',b'\x00\x00\xBE\x11dts_offset',b'\x00\x00\x76\x11seq'),(b'\x00\x00\x00\xE3\x00\x00\x00\x02spa_node_callbacks',b'\x00\x00\x11\x11version',b'\x00\x00\xB3\x11ready',b'\x00\x00\xB9\x11reuse_buffer',b'\x00\x00\xBC\x11xrun'),(b'\x00\x00\x00\xE4\x00\x00\x00\x02spa_node_events',b'\x00\x00\x11\x11version',b'\x00\x00\xF3\x11info',b'\x00\x00\xEE\x11port_info',b'\x00\x00\xF0\x11result',b'\x00\x00\xF1\x11event'),(b'\x00\x00\x00\xE5\x00\x00\x00\x10spa_node_info',),(b'\x00\x00\x00\xE6\x00\x00\x00\x02spa_node_methods',b'\x00\x00\x11\x11version',b'\x00\x00\xB7\x11add_listener',b'\x00\x00\xB8\x11set_callbacks',b'\x00\x00\xB3\x11sync',b'\x00\x00\xB5\x11enum_params',b'\x00\x00\xBA\x11set_param',b'\x00\x00\xBB\x11set_io',b'\x00\x00\xB6\x11send_command',b'\x00\x00\xAF\x11add_port',b'\x00\x00\xAE\x11remove_port',b'\x00\x00\xB4\x11port_enum_params',b'\x00\x00\xB1\x11port_set_param',b'\x00\x00\xB0\x11port_use_buffers',b'\x00\x00\xB2\x11port_set_io',b'\x00\x00\xB9\x11port_reuse_buffer',b'\x00\x00\xAD\x11process'),(b'\x00\x00\x00\xE8\x00\x00\x00\x10spa_param_info',),(b'\x00\x00\x00\xE9\x00\x00\x00\x10spa_pod',),(b'\x00\x00\x00\xEA\x00\x00\x00\x02spa_port_info',b'\x00\x00\x76\x11change_mask',b'\x00\x00\x76\x11flags',b'\x00\x00\xCF\x11rate',b'\x00\x00\x0F\x11props',b'\x00\x00\xE7\x11params',b'\x00\x00\x11\x11n_params'),(b'\x00\x00\x00\xEB\x00\x00\x00\x02spa_support',b'\x00\x00\x05\x11type',b'\x00\x00\x19\x11data')), | |
_enums = (b'\x00\x00\x00\x1D\x00\x00\x00\x16spa_direction\x00SPA_DIRECTION_INPUT,SPA_DIRECTION_OUTPUT',b'\x00\x00\x00\xA8\x00\x00\x00\x16spa_io_type\x00SPA_IO_Invalid,SPA_IO_Buffers,SPA_IO_Range,SPA_IO_Clock,SPA_IO_Latency,SPA_IO_Control,SPA_IO_Notify,SPA_IO_Position,SPA_IO_RateMatch,SPA_IO_Memory',b'\x00\x00\x00\x8C\x00\x00\x00\x16spa_log_level\x00SPA_LOG_LEVEL_NONE,SPA_LOG_LEVEL_ERROR,SPA_LOG_LEVEL_WARN,SPA_LOG_LEVEL_INFO,SPA_LOG_LEVEL_DEBUG,SPA_LOG_LEVEL_TRACE'), | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Build out-of-line CFFI "ABI mode" module for SPA | |
# Because this uses ABI mode, it does not depend on an actual compiler, | |
# but not all C definitions may be recognized. | |
from cffi import FFI | |
ffi = FFI() | |
ffi.set_source('spa._cffi', None) | |
ffi.cdef(r''' | |
struct spa_dict; | |
struct spa_interface_info; | |
struct spa_fraction { | |
uint32_t num; | |
uint32_t denom; | |
}; | |
/** | |
* Extra supporting infrastructure passed to the init() function of | |
* a factory. It can be extra information or interfaces such as logging. | |
*/ | |
struct spa_support { | |
const char *type; /*< the type of the support item */ | |
void *data; /*< specific data for the item */ | |
}; | |
struct spa_handle { | |
#define SPA_VERSION_HANDLE 0 | |
uint32_t version; | |
/** | |
* Get the interface provided by \a handle with \a type. | |
* | |
* \a interface is always a struct spa_interface but depending on | |
* \a type, the struct might contain other information. | |
* | |
* \param handle a spa_handle | |
* \param type the interface type | |
* \param interface result to hold the interface. | |
* \return 0 on success | |
* -ENOTSUP when there are no interfaces | |
* -EINVAL when handle or info is NULL | |
*/ | |
int (*get_interface) (struct spa_handle *handle, const char *type, void **interface); | |
/** | |
* Clean up the memory of \a handle. After this, \a handle should not be used | |
* anymore. | |
* | |
* \param handle a pointer to memory | |
* \return 0 on success | |
*/ | |
int (*clear) (struct spa_handle *handle); | |
}; | |
struct spa_handle_factory { | |
uint32_t version; | |
const char *name; | |
const struct spa_dict *info; | |
size_t (*get_size) (const struct spa_handle_factory *factory, | |
const struct spa_dict *params); | |
int (*init) (const struct spa_handle_factory *factory, | |
struct spa_handle *handle, | |
const struct spa_dict *info, | |
const struct spa_support *support, | |
uint32_t n_support); | |
int (*enum_interface_info) (const struct spa_handle_factory *factory, | |
const struct spa_interface_info **info, | |
uint32_t *index); | |
}; | |
int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index); | |
struct spa_callbacks { | |
const void *funcs; | |
void *data; | |
}; | |
struct spa_list { | |
struct spa_list *next; | |
struct spa_list *prev; | |
}; | |
/** \struct spa_hook_list | |
* A list of hooks. This struct is primarily used by | |
* implementation that use multiple caller-provided \ref spa_hook. */ | |
struct spa_hook_list { | |
struct spa_list list; | |
}; | |
/** \struct spa_hook | |
* A hook, contains the structure with functions and the data passed | |
* to the functions. | |
* | |
* A hook should be treated as opaque by the caller. | |
*/ | |
struct spa_hook { | |
struct spa_list link; | |
struct spa_callbacks cb; | |
/** callback and data for the hook list, private to the | |
* hook_list implementor */ | |
void (*removed) (struct spa_hook *hook); | |
void *priv; | |
}; | |
struct spa_interface { | |
const char *type; | |
uint32_t version; | |
struct spa_callbacks cb; | |
}; | |
''') | |
# Log interface | |
ffi.cdef(r''' | |
enum spa_log_level { | |
SPA_LOG_LEVEL_NONE = 0, | |
SPA_LOG_LEVEL_ERROR, | |
SPA_LOG_LEVEL_WARN, | |
SPA_LOG_LEVEL_INFO, | |
SPA_LOG_LEVEL_DEBUG, | |
SPA_LOG_LEVEL_TRACE, | |
}; | |
struct spa_log { | |
struct spa_interface iface; | |
enum spa_log_level level; | |
}; | |
struct spa_log_methods { | |
uint32_t version; | |
void (*log) (void *object, | |
enum spa_log_level level, | |
const char *file, | |
int line, | |
const char *func, | |
const char *fmt, ...); | |
// ... | |
}; | |
''') | |
# Loop interfaces | |
ffi.cdef(r''' | |
/** | |
* Control an event loop | |
*/ | |
struct spa_loop_control_methods { | |
/* the version of this structure. This can be used to expand this | |
* structure in the future */ | |
#define SPA_VERSION_LOOP_CONTROL_METHODS 1 | |
uint32_t version; | |
int (*get_fd) (void *object); | |
/** Add a hook | |
* \param ctrl the control to change | |
* \param hooks the hooks to add | |
* | |
* Adds hooks to the loop controlled by \a ctrl. | |
*/ | |
void (*add_hook) (void *object, | |
struct spa_hook *hook, | |
const struct spa_loop_control_hooks *hooks, | |
void *data); | |
/** Enter a loop | |
* \param ctrl the control | |
* | |
* Start an iteration of the loop. This function should be called | |
* before calling iterate and is typically used to capture the thread | |
* that this loop will run in. | |
*/ | |
void (*enter) (void *object); | |
/** Leave a loop | |
* \param ctrl the control | |
* | |
* Ends the iteration of a loop. This should be called after calling | |
* iterate. | |
*/ | |
void (*leave) (void *object); | |
/** Perform one iteration of the loop. | |
* \param ctrl the control | |
* \param timeout an optional timeout in milliseconds. | |
* 0 for no timeout, -1 for infinite timeout. | |
* | |
* This function will block | |
* up to \a timeout milliseconds and then dispatch the fds with activity. | |
* The number of dispatched fds is returned. | |
*/ | |
int (*iterate) (void *object, int timeout); | |
/** Check context of the loop | |
* \param ctrl the control | |
* | |
* This function will check if the current thread is currently the | |
* one that did the enter call. Since version 1:1. | |
* | |
* returns 1 on success, 0 or negative errno value on error. | |
*/ | |
int (*check) (void *object); | |
}; | |
''') | |
# Node interface | |
ffi.cdef(r''' | |
struct spa_io_buffers { | |
#define SPA_STATUS_OK 0 | |
#define SPA_STATUS_NEED_DATA 1 | |
#define SPA_STATUS_HAVE_DATA 2 | |
#define SPA_STATUS_STOPPED 4 | |
#define SPA_STATUS_DRAINED 8 | |
int32_t status; /**< the status code */ | |
uint32_t buffer_id; /**< a buffer id */ | |
}; | |
enum spa_direction { | |
SPA_DIRECTION_INPUT = 0, | |
SPA_DIRECTION_OUTPUT = 1, | |
}; | |
/** Different IO area types */ | |
enum spa_io_type { | |
SPA_IO_Invalid, | |
SPA_IO_Buffers, /**< area to exchange buffers, struct spa_io_buffers */ | |
SPA_IO_Range, /**< expected byte range, struct spa_io_range */ | |
SPA_IO_Clock, /**< area to update clock information, struct spa_io_clock */ | |
SPA_IO_Latency, /**< latency reporting, struct spa_io_latency */ | |
SPA_IO_Control, /**< area for control messages, struct spa_io_sequence */ | |
SPA_IO_Notify, /**< area for notify messages, struct spa_io_sequence */ | |
SPA_IO_Position, /**< position information in the graph, struct spa_io_position */ | |
SPA_IO_RateMatch, /**< rate matching between nodes, struct spa_io_rate_match */ | |
SPA_IO_Memory, /**< memory pointer, struct spa_io_memory */ | |
}; | |
/** | |
* Node methods | |
*/ | |
struct spa_node_methods { | |
/* the version of the node methods. This can be used to expand this | |
* structure in the future */ | |
#define SPA_VERSION_NODE_METHODS 0 | |
uint32_t version; | |
/** | |
* Adds an event listener on \a node. | |
* | |
* Setting the events will trigger the info event and a | |
* port_info event for each managed port on the new | |
* listener. | |
* | |
* \param node a #spa_node | |
* \param listener a listener | |
* \param events a struct \ref spa_node_events | |
* \param data data passed as first argument in functions of \a events | |
* \return 0 on success | |
* < 0 errno on error | |
*/ | |
int (*add_listener) (void *object, | |
struct spa_hook *listener, | |
const struct spa_node_events *events, | |
void *data); | |
/** | |
* Set callbacks to on \a node. | |
* if \a callbacks is NULL, the current callbacks are removed. | |
* | |
* This function must be called from the main thread. | |
* | |
* All callbacks are called from the data thread. | |
* | |
* \param node a spa_node | |
* \param callbacks callbacks to set | |
* \return 0 on success | |
* -EINVAL when node is NULL | |
*/ | |
int (*set_callbacks) (void *object, | |
const struct spa_node_callbacks *callbacks, | |
void *data); | |
/** | |
* Perform a sync operation. | |
* | |
* This method will emit the result event with the given sequence | |
* number synchronously or with the returned async return value | |
* asynchronously. | |
* | |
* Because all methods are serialized in the node, this can be used | |
* to wait for completion of all previous method calls. | |
* | |
* \param seq a sequence number | |
* \return 0 on success | |
* -EINVAL when node is NULL | |
* an async result | |
*/ | |
int (*sync) (void *object, int seq); | |
/** | |
* Enumerate the parameters of a node. | |
* | |
* Parameters are identified with an \a id. Some parameters can have | |
* multiple values, see the documentation of the parameter id. | |
* | |
* Parameters can be filtered by passing a non-NULL \a filter. | |
* | |
* The function will emit the result event up to \a max times with | |
* the result value. The seq in the result will either be the \a seq | |
* number when executed synchronously or the async return value of | |
* this function when executed asynchronously. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param node a \ref spa_node | |
* \param seq a sequence number to pass to the result event when | |
* this method is executed synchronously. | |
* \param id the param id to enumerate | |
* \param start the index of enumeration, pass 0 for the first item | |
* \param max the maximum number of parameters to enumerate | |
* \param filter and optional filter to use | |
* | |
* \return 0 when no more items can be iterated. | |
* -EINVAL when invalid arguments are given | |
* -ENOENT the parameter \a id is unknown | |
* -ENOTSUP when there are no parameters | |
* implemented on \a node | |
* an async return value when the result event will be | |
* emitted later. | |
*/ | |
int (*enum_params) (void *object, int seq, | |
uint32_t id, uint32_t start, uint32_t max, | |
const struct spa_pod *filter); | |
/** | |
* Set the configurable parameter in \a node. | |
* | |
* Usually, \a param will be obtained from enum_params and then | |
* modified but it is also possible to set another spa_pod | |
* as long as its keys and types match a supported object. | |
* | |
* Objects with property keys that are not known are ignored. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param node a \ref spa_node | |
* \param id the parameter id to configure | |
* \param flags additional flags | |
* \param param the parameter to configure | |
* | |
* \return 0 on success | |
* -EINVAL when node is NULL | |
* -ENOTSUP when there are no parameters implemented on \a node | |
* -ENOENT the parameter is unknown | |
*/ | |
int (*set_param) (void *object, | |
uint32_t id, uint32_t flags, | |
const struct spa_pod *param); | |
/** | |
* Configure the given memory area with \a id on \a node. This | |
* structure is allocated by the host and is used to exchange | |
* data and parameters with the node. | |
* | |
* Setting an \a io of NULL will disable the node io. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param id the id of the io area, the available ids can be | |
* enumerated with the node parameters. | |
* \param data a io area memory | |
* \param size the size of \a data | |
* \return 0 on success | |
* -EINVAL when invalid input is given | |
* -ENOENT when \a id is unknown | |
* -ENOSPC when \a size is too small | |
*/ | |
int (*set_io) (void *object, | |
uint32_t id, void *data, size_t size); | |
/** | |
* Send a command to a node. | |
* | |
* Upon completion, a command might change the state of a node. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param node a spa_node | |
* \param command a spa_command | |
* \return 0 on success | |
* -EINVAL when node or command is NULL | |
* -ENOTSUP when this node can't process commands | |
* -EINVAL \a command is an invalid command | |
*/ | |
int (*send_command) (void *object, const struct spa_command *command); | |
/** | |
* Make a new port with \a port_id. The caller should use the lowest unused | |
* port id for the given \a direction. | |
* | |
* Port ids should be between 0 and max_ports as obtained from the info | |
* event. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param node a spa_node | |
* \param direction a enum \ref spa_direction | |
* \param port_id an unused port id | |
* \param props extra properties | |
* \return 0 on success | |
* -EINVAL when node is NULL | |
*/ | |
int (*add_port) (void *object, | |
enum spa_direction direction, uint32_t port_id, | |
const struct spa_dict *props); | |
/** | |
* Remove a port with \a port_id. | |
* | |
* \param node a spa_node | |
* \param direction a enum \ref spa_direction | |
* \param port_id a port id | |
* \return 0 on success | |
* -EINVAL when node is NULL or when port_id is unknown or | |
* when the port can't be removed. | |
*/ | |
int (*remove_port) (void *object, | |
enum spa_direction direction, uint32_t port_id); | |
/** | |
* Enumerate all possible parameters of \a id on \a port_id of \a node | |
* that are compatible with \a filter. | |
* | |
* The result parameters can be queried and modified and ultimately be used | |
* to call port_set_param. | |
* | |
* The function will emit the result event up to \a max times with | |
* the result value. The seq in the result event will either be the | |
* \a seq number when executed synchronously or the async return | |
* value of this function when executed asynchronously. | |
* | |
* This function must be called from the main thread. | |
* | |
* \param node a spa_node | |
* \param seq a sequence number to pass to the result event when | |
* this method is executed synchronously. | |
* \param direction an spa_direction | |
* \param port_id the port to query | |
* \param id the parameter id to query | |
* \param start the first index to query, 0 to get the first item | |
* \param max the maximum number of params to query | |
* \param filter a parameter filter or NULL for no filter | |
* | |
* \return 0 when no more items can be iterated. | |
* -EINVAL when invalid parameters are given | |
* -ENOENT when \a id is unknown | |
* an async return value when the result event will be | |
* emitted later. | |
*/ | |
int (*port_enum_params) (void *object, int seq, | |
enum spa_direction direction, uint32_t port_id, | |
uint32_t id, uint32_t start, uint32_t max, | |
const struct spa_pod *filter); | |
/** | |
* Set a parameter on \a port_id of \a node. | |
* | |
* When \a param is NULL, the parameter will be unset. | |
* | |
* This function must be called from the main thread. The node muse be paused | |
* or the port SPA_IO_Buffers area is NULL when this function is called with | |
* a param that changes the processing state (like a format change). | |
* | |
* \param node a struct \ref spa_node | |
* \param direction a enum \ref spa_direction | |
* \param port_id the port to configure | |
* \param id the parameter id to set | |
* \param flags optional flags | |
* \param param a struct \ref spa_pod with the parameter to set | |
* \return 0 on success | |
* 1 on success, the value of \a param might have been | |
* changed depending on \a flags and the final value can be found by | |
* doing port_enum_params. | |
* -EINVAL when node is NULL or invalid arguments are given | |
* -ESRCH when one of the mandatory param | |
* properties is not specified and SPA_NODE_PARAM_FLAG_FIXATE was | |
* not set in \a flags. | |
* -ESRCH when the type or size of a property is not correct. | |
* -ENOENT when the param id is not found | |
*/ | |
int (*port_set_param) (void *object, | |
enum spa_direction direction, | |
uint32_t port_id, | |
uint32_t id, uint32_t flags, | |
const struct spa_pod *param); | |
/** | |
* Tell the port to use the given buffers | |
* | |
* When \a flags contains SPA_NODE_BUFFERS_FLAG_ALLOC, the data | |
* in the buffers should point to an array of at least 1 data entry | |
* with the desired supported type that will be filled by this function. | |
* | |
* The port should also have a spa_io_buffers io area configured to exchange | |
* the buffers with the port. | |
* | |
* For an input port, all the buffers will remain dequeued. | |
* Once a buffer has been queued on a port in the spa_io_buffers, | |
* it should not be reused until the reuse_buffer callback is notified | |
* or when the buffer has been returned in the spa_io_buffers of | |
* the port. | |
* | |
* For output ports, all buffers will be queued in the port. When process | |
* returns SPA_STATUS_HAVE_DATA, buffers are available in one or more | |
* of the spa_io_buffers areas. | |
* | |
* When a buffer can be reused, port_reuse_buffer() should be called or the | |
* buffer_id should be placed in the spa_io_buffers area before calling | |
* process. | |
* | |
* Passing NULL as \a buffers will remove the reference that the port has | |
* on the buffers. | |
* | |
* When this function returns async, use the spa_node_sync operation to | |
* wait for completion. | |
* | |
* This function must be called from the main thread. The node muse be paused | |
* or the port SPA_IO_Buffers area is NULL when this function is called. | |
* | |
* \param object an object implementing the interface | |
* \param direction a port direction | |
* \param port_id a port id | |
* \param flags extra flags | |
* \param buffers an array of buffer pointers | |
* \param n_buffers number of elements in \a buffers | |
* \return 0 on success | |
*/ | |
int (*port_use_buffers) (void *object, | |
enum spa_direction direction, | |
uint32_t port_id, | |
uint32_t flags, | |
struct spa_buffer **buffers, | |
uint32_t n_buffers); | |
/** | |
* Configure the given memory area with \a id on \a port_id. This | |
* structure is allocated by the host and is used to exchange | |
* data and parameters with the port. | |
* | |
* Setting an \a io of NULL will disable the port io. | |
* | |
* This function must be called from the main thread. | |
* | |
* This function can be called when the node is running and the node | |
* must be prepared to handle changes in io areas while running. This | |
* is normally done by synchronizing the port io updates with the | |
* data processing loop. | |
* | |
* \param direction a spa_direction | |
* \param port_id a port id | |
* \param id the id of the io area, the available ids can be | |
* enumerated with the port parameters. | |
* \param data a io area memory | |
* \param size the size of \a data | |
* \return 0 on success | |
* -EINVAL when invalid input is given | |
* -ENOENT when \a id is unknown | |
* -ENOSPC when \a size is too small | |
*/ | |
int (*port_set_io) (void *object, | |
enum spa_direction direction, | |
uint32_t port_id, | |
uint32_t id, | |
void *data, size_t size); | |
/** | |
* Tell an output port to reuse a buffer. | |
* | |
* This function must be called from the data thread. | |
* | |
* \param node a spa_node | |
* \param port_id a port id | |
* \param buffer_id a buffer id to reuse | |
* \return 0 on success | |
* -EINVAL when node is NULL | |
*/ | |
int (*port_reuse_buffer) (void *object, uint32_t port_id, uint32_t buffer_id); | |
/** | |
* Process the node | |
* | |
* This function must be called from the data thread. | |
* | |
* Output io areas with SPA_STATUS_NEED_DATA will recycle the | |
* buffers if any. | |
* | |
* Input areas with SPA_STATUS_HAVE_DATA are consumed if possible | |
* and the status is set to SPA_STATUS_NEED_DATA or SPA_STATUS_OK. | |
* | |
* When the node has new output buffers, the SPA_STATUS_HAVE_DATA | |
* bit will be set. | |
* | |
* When the node can accept new input in the next cycle, the | |
* SPA_STATUS_NEED_DATA bit will be set. | |
* | |
* Note that the node might return SPA_STATUS_NEED_DATA even when | |
* no input ports have this status. This means that the amount of | |
* data still available on the input ports is likely not going to | |
* be enough for the next cycle and the host might need to prefetch | |
* data for the next cycle. | |
*/ | |
int (*process) (void *object); | |
}; | |
/** Node callbacks | |
* | |
* Callbacks are called from the real-time data thread. Only | |
* one callback structure can be set on an spa_node. | |
*/ | |
struct spa_node_callbacks { | |
#define SPA_VERSION_NODE_CALLBACKS 0 | |
uint32_t version; | |
/** | |
* \param node a spa_node | |
* | |
* The node is ready for processing. | |
* | |
* When this function is NULL, synchronous operation is requested | |
* on the ports. | |
*/ | |
int (*ready) (void *data, int state); | |
/** | |
* \param node a spa_node | |
* \param port_id an input port_id | |
* \param buffer_id the buffer id to be reused | |
* | |
* The node has a buffer that can be reused. | |
* | |
* When this function is NULL, the buffers to reuse will be set in | |
* the io area of the input ports. | |
*/ | |
int (*reuse_buffer) (void *data, | |
uint32_t port_id, | |
uint32_t buffer_id); | |
/** | |
* \param data user data | |
* \param trigger the timestamp in microseconds when the xrun happened | |
* \param delay the amount of microseconds of xrun. | |
* \param info an object with extra info (NULL for now) | |
* | |
* The node has encountered an over or underrun | |
* | |
* The info contains an object with more information | |
*/ | |
int (*xrun) (void *data, uint64_t trigger, uint64_t delay, | |
struct spa_pod *info); | |
}; | |
/** events from the spa_node. | |
* | |
* All event are called from the main thread and multiple | |
* listeners can be registered for the events with | |
* spa_node_add_listener(). | |
*/ | |
struct spa_node_events { | |
#define SPA_VERSION_NODE_EVENTS 0 | |
uint32_t version; /**< version of this structure */ | |
/** Emitted when info changes */ | |
void (*info) (void *data, const struct spa_node_info *info); | |
/** Emitted when port info changes, NULL when port is removed */ | |
void (*port_info) (void *data, | |
enum spa_direction direction, uint32_t port, | |
const struct spa_port_info *info); | |
/** notify a result. | |
* | |
* Some methods will trigger a result event with an optional | |
* result of the given type. Look at the documentation of the | |
* method to know when to expect a result event. | |
* | |
* The result event can be called synchronously, as an event | |
* called from inside the method itself, in which case the seq | |
* number passed to the method will be passed unchanged. | |
* | |
* The result event will be called asynchronously when the | |
* method returned an async return value. In this case, the seq | |
* number in the result will match the async return value of | |
* the method call. Users should match the seq number from | |
* request to the reply. | |
*/ | |
void (*result) (void *data, int seq, int res, | |
uint32_t type, const void *result); | |
/** | |
* \param node a spa_node | |
* \param event the event that was emitted | |
* | |
* This will be called when an out-of-bound event is notified | |
* on \a node. | |
*/ | |
void (*event) (void *data, const struct spa_event *event); | |
}; | |
/** | |
* Port information structure | |
* | |
* Contains the basic port information. | |
*/ | |
struct spa_port_info { | |
#define SPA_PORT_CHANGE_MASK_FLAGS 1 | |
#define SPA_PORT_CHANGE_MASK_RATE 2 | |
#define SPA_PORT_CHANGE_MASK_PROPS 4 | |
#define SPA_PORT_CHANGE_MASK_PARAMS 8 | |
uint64_t change_mask; | |
#define SPA_PORT_FLAG_REMOVABLE 1 /**< port can be removed */ | |
#define SPA_PORT_FLAG_OPTIONAL 2 /**< processing on port is optional */ | |
#define SPA_PORT_FLAG_CAN_ALLOC_BUFFERS 4 /**< the port can allocate buffer data */ | |
#define SPA_PORT_FLAG_IN_PLACE 8 /**< the port can process data in-place and | |
* will need a writable input buffer */ | |
#define SPA_PORT_FLAG_NO_REF 16 /**< the port does not keep a ref on the buffer. | |
* This means the node will always completely | |
* consume the input buffer and it will be | |
* recycled after process. */ | |
#define SPA_PORT_FLAG_LIVE 32 /**< output buffers from this port are | |
* timestamped against a live clock. */ | |
#define SPA_PORT_FLAG_PHYSICAL 64 /**< connects to some device */ | |
#define SPA_PORT_FLAG_TERMINAL 128 /**< data was not created from this port | |
* or will not be made available on another | |
* port */ | |
#define SPA_PORT_FLAG_DYNAMIC_DATA 256 /**< data pointer on buffers can be changed. | |
* Only the buffer data marked as DYNAMIC | |
* can be changed. */ | |
uint64_t flags; /**< port flags */ | |
struct spa_fraction rate; /**< rate of sequence numbers on port */ | |
const struct spa_dict *props; /**< extra port properties */ | |
struct spa_param_info *params; /**< parameter information */ | |
uint32_t n_params; /**< number of items in \a params */ | |
}; | |
''') | |
# Custom defines | |
ffi.cdef(r''' | |
/** | |
* A metadata element. | |
* | |
* This structure is available on the buffer structure and contains | |
* the type of the metadata and a pointer/size to the actual metadata | |
* itself. | |
*/ | |
struct spa_meta { | |
uint32_t type; /**< metadata type, one of enum spa_meta_type */ | |
uint32_t size; /**< size of metadata */ | |
void *data; /**< pointer to metadata */ | |
}; | |
/** | |
* Describes essential buffer header metadata such as flags and | |
* timestamps. | |
*/ | |
struct spa_meta_header { | |
#define SPA_META_HEADER_FLAG_DISCONT 1u /**< data is not continuous with previous buffer */ | |
#define SPA_META_HEADER_FLAG_CORRUPTED 2u /**< data might be corrupted */ | |
#define SPA_META_HEADER_FLAG_MARKER 4u /**< media specific marker */ | |
#define SPA_META_HEADER_FLAG_HEADER 8u /**< data contains a codec specific header */ | |
#define SPA_META_HEADER_FLAG_GAP 16u /**< data contains media neutral data */ | |
#define SPA_META_HEADER_FLAG_DELTA_UNIT 32u /**< cannot be decoded independently */ | |
uint32_t flags; /**< flags */ | |
uint32_t offset; /**< offset in current cycle */ | |
int64_t pts; /**< presentation timestamp in nanoseconds */ | |
int64_t dts_offset; /**< decoding timestamp as a difference with pts */ | |
uint64_t seq; /**< sequence number, increments with a | |
* media specific frequency */ | |
}; | |
/** Chunk of memory, can change for each buffer */ | |
struct spa_chunk { | |
uint32_t offset; /**< offset of valid data. Should be taken | |
* modulo the data maxsize to get the offset | |
* in the data memory. */ | |
uint32_t size; /**< size of valid data. Should be clamped to | |
* maxsize. */ | |
int32_t stride; /**< stride of valid data */ | |
#define SPA_CHUNK_FLAG_NONE 0u | |
#define SPA_CHUNK_FLAG_CORRUPTED 1u /**< chunk data is corrupted in some way */ | |
#define SPA_CHUNK_FLAG_EMPTY 2u /**< chunk data is empty with media specific | |
* neutral data such as silence or black. This | |
* could be used to optimize processing. */ | |
int32_t flags; /**< chunk flags */ | |
}; | |
/** Data for a buffer this stays constant for a buffer */ | |
struct spa_data { | |
uint32_t type; /**< memory type, one of enum spa_data_type, when | |
* allocating memory, the type contains a bitmask | |
* of allowed types. SPA_ID_INVALID is a special | |
* value for the allocator to indicate that the | |
* other side did not explicitly specify any | |
* supported data types. It should probably use | |
* a memory type that does not require special | |
* handling in addition to simple mmap/munmap. */ | |
#define SPA_DATA_FLAG_NONE 0u | |
#define SPA_DATA_FLAG_READABLE 1u /**< data is readable */ | |
#define SPA_DATA_FLAG_WRITABLE 2u /**< data is writable */ | |
#define SPA_DATA_FLAG_DYNAMIC 4u /**< data pointer can be changed */ | |
#define SPA_DATA_FLAG_READWRITE 3u | |
uint32_t flags; /**< data flags */ | |
int64_t fd; /**< optional fd for data */ | |
uint32_t mapoffset; /**< offset to map fd at */ | |
uint32_t maxsize; /**< max size of data */ | |
void *data; /**< optional data pointer */ | |
struct spa_chunk *chunk; /**< valid chunk of memory */ | |
}; | |
/** A Buffer */ | |
struct spa_buffer { | |
uint32_t n_metas; /**< number of metadata */ | |
uint32_t n_datas; /**< number of data members */ | |
struct spa_meta *metas; /**< array of metadata */ | |
struct spa_data *datas; /**< array of data members */ | |
}; | |
struct buffer { | |
uint32_t id; | |
// Owned buffer | |
struct spa_buffer buffer; | |
struct spa_meta metas[1]; | |
struct spa_meta_header header; | |
struct spa_data datas[1]; | |
struct spa_chunk chunks[1]; | |
// Data | |
void *data; | |
size_t data_size; | |
}; | |
''') | |
if __name__ == "__main__": | |
ffi.compile(verbose=True) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SPA constants | |
# These could also be pulled from CFFI. | |
SPA_STATUS_OK = 0 | |
SPA_STATUS_NEED_DATA = 1 | |
SPA_STATUS_HAVE_DATA = 2 | |
SPA_TYPE_OBJECT__FORMAT = 0x00040003 | |
SPA_PARAM_ENUM_FORMAT = 3 | |
SPA_PARAM__FORMAT = 4 | |
SPA_FORMAT__MEDIA_TYPE = 1 | |
SPA_FORMAT__MEDIA_SUBTYPE = 2 | |
SPA_MEDIA_TYPE__VIDEO = 2 | |
SPA_MEDIA_SUBTYPE__RAW = 1 | |
SPA_FORMAT_VIDEO__FORMAT = 0x00020001 | |
SPA_FORMAT_VIDEO__SIZE = 0x00020003 | |
SPA_FORMAT_VIDEO__FRAMERATE = 0x00020004 | |
SPA_VIDEO_FORMAT__RGB = 15 | |
SPA_TYPE_COMMAND__NODE = 0x00030002 | |
SPA_NODE_COMMAND__PAUSE = 1 | |
SPA_NODE_COMMAND__START = 2 | |
SPA_DIRECTION_INPUT = 0 | |
SPA_DIRECTION_OUTPUT = 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SPA POD types | |
# See https://docs.pipewire.org/page_spa_pod.html for specification. | |
import struct | |
from typing import Any | |
class Pod: | |
TYPE = NotImplemented | |
def __repr__(self) -> str: | |
return f'Pod<{self.TYPE}>' | |
def __bytes__(self) -> bytes: | |
data = self._serialize_body() | |
padding = -len(data) % 8 | |
return b''.join([ | |
struct.pack('=II', len(data), self.TYPE), | |
data, | |
b'\0' * padding, | |
]) | |
def _serialize_body(self) -> bytes: | |
"""Serialize the POD body for this type.""" | |
raise NotImplementedError() | |
class PodNone(Pod): | |
TYPE = 1 | |
def _serialize_body(self) -> bytes: | |
return b'' | |
class PodBool(Pod): | |
TYPE = 2 | |
def __init__(self, value: bool) -> None: | |
self.value = bool(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=I', int(self.value)) | |
class PodId(Pod): | |
TYPE = 3 | |
def __init__(self, value: int) -> None: | |
self.value = int(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=I', self.value) | |
class PodInt(Pod): | |
TYPE = 4 | |
def __init__(self, value: int) -> None: | |
self.value = int(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=i', self.value) | |
class PodLong(Pod): | |
TYPE = 5 | |
def __init__(self, value: int) -> None: | |
self.value = int(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=q', self.value) | |
class PodFloat(Pod): | |
TYPE = 6 | |
def __init__(self, value: float) -> None: | |
self.value = float(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=f', self.value) | |
class PodDouble(Pod): | |
TYPE = 7 | |
def __init__(self, value: float) -> None: | |
self.value = float(value) | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=d', self.value) | |
class PodString(Pod): | |
TYPE = 8 | |
def __init__(self, value: str) -> None: | |
self.value = str(value) | |
def _serialize_body(self) -> bytes: | |
return self.value.encode('utf-8') + b'\0' | |
class PodBytes(Pod): | |
TYPE = 9 | |
def __init__(self, value: bytes) -> None: | |
self.value = value | |
def _serialize_body(self) -> bytes: | |
return self.value | |
class PodRectangle(Pod): | |
TYPE = 10 | |
def __init__(self, width: int, height: int) -> None: | |
self.width = width | |
self.height = height | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=II', self.width, self.height) | |
class PodFraction(Pod): | |
TYPE = 11 | |
def __init__(self, numerator: int, denominator: int) -> None: | |
self.numerator = numerator | |
self.denominator = denominator | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=II', self.numerator, self.denominator) | |
class PodBitmap(Pod): | |
TYPE = 12 | |
def __init__(self, value: bytes) -> None: | |
self.value = bytes(value) | |
def _serialize_body(self) -> bytes: | |
return self.value | |
class PodArray(Pod): | |
TYPE = 13 | |
# Not implemented | |
class PodStruct(Pod): | |
TYPE = 14 | |
# Not implemented | |
class PodObject(Pod): | |
TYPE = 15 | |
def __init__(self, object_type: int, object_id: int, properties: dict = None) -> None: | |
self.object_type = object_type | |
self.object_id = object_id | |
self.properties = properties or {} | |
def _serialize_body(self) -> bytes: | |
data = [struct.pack('=II', self.object_type, self.object_id)] | |
for key, value in self.properties.items(): | |
if not isinstance(value, Pod): | |
raise TypeError(f'{value!r} is not a Pod type') | |
flags = 0 # Not supported | |
data.extend([ | |
struct.pack('=II', key, flags), | |
bytes(value), | |
]) | |
return b''.join(data) | |
class PodSequence(Pod): | |
TYPE = 16 | |
# Not implemented | |
class PodPointer(Pod): | |
TYPE = 17 | |
def __init__(self, pointer_type: int, pointer_address: int) -> None: | |
self.type = pointer_type | |
self.address = pointer_address | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=IIp', self.pointer_type, 0, self.pointer_address) | |
class PodFd(Pod): | |
TYPE = 18 | |
def __init__(self, value: int) -> None: | |
self.value = value | |
def _serialize_body(self) -> bytes: | |
return struct.pack('=Q', self.value) | |
class PodChoice(Pod): | |
TYPE = 19 | |
# Not implemented | |
class PodPod(Pod): | |
TYPE = 20 | |
# Not implemented |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Example of using SPA from Python | |
import errno | |
import signal | |
import threading | |
from spa import Plugin, LogInterface, LoopInterface, LoopControlInterface, LoopUtilsInterface, NodeInterface, ffi | |
from spa.constants import * | |
from spa.pod import PodObject, PodId, PodFraction, PodRectangle | |
N_BUFFERS = 8 | |
WIDTH = 1280 | |
HEIGHT = 720 | |
import ctypes | |
import sdl2 | |
import sdl2.ext | |
def main(): | |
sdl2.ext.common.init() | |
window = sdl2.ext.Window("videotestsrc", (WIDTH, HEIGHT), flags=sdl2.SDL_WINDOW_SHOWN) | |
renderer = sdl2.ext.renderer.Renderer(window) | |
# Support plugins | |
n_support = 0 | |
support = ffi.new('struct spa_support[]', 8) | |
support_plugin = Plugin.load_plugin('support/libspa-support.so') | |
log_factory = support_plugin.get_factory('support.log') | |
log_handle = log_factory.build(ffi.NULL, ffi.NULL, 0) | |
log = log_handle.get_interface(LogInterface) | |
print(f'Got {log!r}') | |
support[n_support].type = LogInterface.C_TYPE | |
support[n_support].data = log._ptr | |
n_support += 1 | |
loop_factory = support_plugin.get_factory('support.loop') | |
loop_handle = loop_factory.build(ffi.NULL, support, n_support) | |
loop = loop_handle.get_interface(LoopInterface) | |
print(f'Got {loop!r}') | |
# This loop is the data loop | |
data_loop_type = ffi.new('char[]', b'Spa:Pointer:Interface:DataLoop') | |
support[n_support].type = data_loop_type | |
support[n_support].data = loop._ptr | |
n_support += 1 | |
loop_control = loop_handle.get_interface(LoopControlInterface) | |
print(f'Got {loop_control!r}') | |
support[n_support].type = LoopControlInterface.C_TYPE | |
support[n_support].data = loop_control._ptr | |
n_support += 1 | |
loop_utils = loop_handle.get_interface(LoopUtilsInterface) | |
print(f'Got {loop_utils!r}') | |
support[n_support].type = LoopUtilsInterface.C_TYPE | |
support[n_support].data = loop_utils._ptr | |
n_support += 1 | |
log.error('Hello, world!') | |
# IO area | |
io = ffi.new('struct spa_io_buffers *', (0, 0xFFFFFFFF)) | |
source_plugin = Plugin.load_plugin('videotestsrc/libspa-videotestsrc.so') | |
source_factory = source_plugin.get_factory('videotestsrc') | |
source_handle = source_factory.build(ffi.NULL, support, n_support) | |
source_node = source_handle.get_interface(NodeInterface) | |
print(f'Got {source_node!r}') | |
# Allocate buffer structures | |
bp = ffi.new('struct spa_buffer *[]', N_BUFFERS) | |
buffers = ffi.new('struct buffer[]', N_BUFFERS) | |
# extern "Python" int on_source_ready(void *, int); | |
@ffi.callback('int(void *, int)') | |
def on_source_ready(_data, _status) -> int: | |
res = source_node.process() | |
if res == SPA_STATUS_HAVE_DATA: | |
buffer_id = io[0].buffer_id | |
# FIXME: There has got to be a better way to get a ctypes pointer | |
addr = int(ffi.cast('uintptr_t', bp[buffer_id].datas[0].data)) | |
data = ctypes.POINTER(ctypes.c_ubyte)(ctypes.c_ubyte.from_address(addr)) | |
stride = bp[buffer_id].datas[0].chunk.stride | |
surface = sdl2.SDL_CreateRGBSurfaceWithFormatFrom(data, WIDTH, HEIGHT, 8, stride, sdl2.SDL_PIXELFORMAT_RGB24) | |
if not surface: | |
print(f'SDL_CreateRGBSurfaceWithFormatFrom failed: {sdl2.SDL_GetError()}') | |
io[0].status = SPA_STATUS_NEED_DATA | |
source_node.port_reuse_buffer(0, buffer_id) | |
return -errno.EIO | |
try: | |
texture = sdl2.ext.renderer.Texture(renderer, surface) | |
finally: | |
sdl2.SDL_FreeSurface(surface) | |
renderer.clear() | |
renderer.copy(texture) | |
renderer.present() | |
texture.destroy() | |
# Hand the buffer immediately back | |
io[0].status = SPA_STATUS_NEED_DATA | |
source_node.port_reuse_buffer(0, buffer_id) | |
return 0 | |
# extern "Python" void on_source_port_info(void *, enum spa_direction, uint32_t, const struct spa_port_info *); | |
@ffi.callback('void(void *, enum spa_direction, uint32_t, const struct spa_port_info *)') | |
def on_source_port_info(_data, _direction, _port, info) -> None: | |
print(f'on_source_port_info: flags={info.flags}') | |
source_callbacks = ffi.new('struct spa_node_callbacks *', {'version': 0, 'ready': on_source_ready}) | |
source_node.set_callbacks(source_callbacks, ffi.NULL) | |
print('set_callbacks') | |
source_listener = ffi.new('struct spa_hook *') | |
source_events = ffi.new('struct spa_node_events *', {'version': 0, 'port_info': on_source_port_info}) | |
source_node.add_listener(source_listener, source_events, ffi.NULL) | |
print('add_listener') | |
source_node.port_set_io(source_plugin.lib.SPA_DIRECTION_OUTPUT, 0, source_plugin.lib.SPA_IO_Buffers, io) | |
print(f'port_set_io') | |
format_buffer = ffi.new('uint8_t[]', bytes( | |
PodObject(SPA_TYPE_OBJECT__FORMAT, SPA_PARAM_ENUM_FORMAT, { | |
SPA_FORMAT__MEDIA_TYPE: PodId(SPA_MEDIA_TYPE__VIDEO), | |
SPA_FORMAT__MEDIA_SUBTYPE: PodId(SPA_MEDIA_SUBTYPE__RAW), | |
SPA_FORMAT_VIDEO__FORMAT: PodId(SPA_VIDEO_FORMAT__RGB), | |
SPA_FORMAT_VIDEO__SIZE: PodRectangle(1280, 720), | |
SPA_FORMAT_VIDEO__FRAMERATE: PodFraction(25, 1), | |
}) | |
)) | |
source_node.port_set_param(SPA_DIRECTION_OUTPUT, 0, SPA_PARAM__FORMAT, 0, ffi.cast('struct spa_pod *', format_buffer)) | |
buffers_data = [] | |
for i in range(N_BUFFERS): | |
b = buffers[i] | |
b.data_size = 1280 * 720 * 4 # 32-bits per pixel | |
buffers_data.append(ffi.new('uint8_t[]', b.data_size)) | |
b.data = buffers_data[-1] | |
# Essential buffer metadata (1 total) | |
b.metas[0].type = 1 # SPA_META_Header | |
b.metas[0].data = ffi.addressof(b.header) | |
b.metas[0].size = ffi.sizeof(b.header) | |
# Data chunks (1 total) | |
b.datas[0].type = 1 # SPA_DATA_MemPtr | |
b.datas[0].flags = 0 | |
b.datas[0].fd = -1 | |
b.datas[0].mapoffset = 0 | |
b.datas[0].maxsize = 0 | |
b.datas[0].data = b.data | |
b.datas[0].chunk = ffi.addressof(b.chunks[0]) | |
b.datas[0].chunk.offset = 0 | |
b.datas[0].chunk.size = b.data_size | |
b.datas[0].chunk.stride = 0 | |
# Update `spa_buf`` | |
b.buffer.metas = b.metas | |
b.buffer.n_metas = 1 | |
b.buffer.datas = b.datas | |
b.buffer.n_datas = 1 | |
bp[i] = ffi.addressof(b.buffer) | |
source_node.port_use_buffers(SPA_DIRECTION_OUTPUT, 0, 0, bp, N_BUFFERS) | |
running = True | |
def on_signal(signum, _frame): | |
nonlocal running | |
if signum == signal.SIGINT: | |
running = False | |
signal.signal(signal.SIGINT, on_signal) | |
def spa_event_loop(): | |
nonlocal running | |
with loop_control: | |
source_node.send_command(SPA_NODE_COMMAND__START) | |
while running: | |
loop_control.iterate(-1) | |
source_node.send_command(SPA_NODE_COMMAND__PAUSE) | |
def sdl_event_loop(): | |
nonlocal running | |
while running: | |
for event in sdl2.ext.common.get_events(): | |
if event.type == sdl2.SDL_QUIT: | |
print("Quit") | |
running = False | |
break | |
window.refresh() | |
# FIXME: SPA event loop does not exit cleanly, requring forceful termination | |
t = threading.Thread(target=spa_event_loop) | |
t.start() | |
sdl_event_loop() | |
t.join() | |
window.close() | |
sdl2.common.quit() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment