From 87bff56a78a183cf7347f792f8ce56c3fbb5d372 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Wed, 20 Jan 2021 14:03:08 +0300 Subject: [PATCH] python module --- .bumpversion.cfg | 4 + Makefile | 24 ++-- linters/tox.ini | 8 +- pkg/arch/PKGBUILD | 15 ++- src/python/setup.py | 22 ++++ src/python/ustreamer.c | 290 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 346 insertions(+), 17 deletions(-) create mode 100644 src/python/setup.py create mode 100644 src/python/ustreamer.c diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0b1fdd3..2d1ce57 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -10,6 +10,10 @@ serialize = search = VERSION "{current_version}" replace = VERSION "{new_version}" +[bumpversion:file:src/python/setup.py] +search = version="{current_version}" +replace = version="{new_version}" + [bumpversion:file:pkg/arch/PKGBUILD] search = pkgver={current_version} replace = pkgver={new_version} diff --git a/Makefile b/Makefile index 7d0a9ea..430fb07 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ endif # ===== -all: $(USTR) $(DUMP) +all: $(USTR) $(DUMP) python install: $(USTR) $(DUMP) @@ -89,6 +89,9 @@ install: $(USTR) $(DUMP) install -m644 man/$(DUMP).1 $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1 gzip $(DESTDIR)$(MANPREFIX)/man1/$(USTR).1 gzip $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1 +ifneq ($(call optbool,$(WITH_PYTHON)),) + cd src/python && python3 setup.py install --prefix=$(PREFIX) --root=$(DESTDIR) +endif install-strip: install @@ -96,13 +99,6 @@ install-strip: install strip $(DESTDIR)$(PREFIX)/bin/$(DUMP) -uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/$(USTR) \ - $(DESTDIR)$(PREFIX)/bin/$(DUMP) \ - $(DESTDIR)$(MANPREFIX)/man1/$(USTR).1 \ - $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1 - - regen: tools/make-jpeg-h.py src/ustreamer/data/blank.jpeg src/ustreamer/data/blank_jpeg.c BLANK tools/make-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX @@ -134,6 +130,14 @@ $(BUILD)/%.o: %.c @ $(CC) $< -o $@ $(CFLAGS) +python: +ifneq ($(call optbool,$(WITH_PYTHON)),) + cd src/python && python3 setup.py build + ln -sf src/python/build/lib.*/*.so . +else + @ true +endif + release: make clean make tox @@ -176,10 +180,10 @@ clean-all: linters clean -it $(LINTERS_IMAGE) bash -c "cd src && rm -rf linters/{.tox,.mypy_cache}" clean: rm -rf pkg/arch/pkg pkg/arch/src pkg/arch/v*.tar.gz pkg/arch/ustreamer-*.pkg.tar.{xz,zst} - rm -rf $(USTR) $(DUMP) $(BUILD) vgcore.* *.sock + rm -rf $(USTR) $(DUMP) $(BUILD) src/python/build vgcore.* *.sock *.so -.PHONY: linters +.PHONY: python linters _OBJS = $(_USTR_SRCS:%.c=$(BUILD)/%.o) $(_DUMP_SRCS:%.c=$(BUILD)/%.o) diff --git a/linters/tox.ini b/linters/tox.ini index 6e0b1da..f39b3df 100644 --- a/linters/tox.ini +++ b/linters/tox.ini @@ -24,26 +24,26 @@ commands = cppcheck \ [testenv:flake8] whitelist_externals = bash -commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' +commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' src/python/*.py deps = flake8 flake8-quotes [testenv:pylint] whitelist_externals = bash -commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py' +commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py src/python/*.py' deps = pylint [testenv:mypy] whitelist_externals = bash -commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py' +commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py src/python/*.py' deps = mypy [testenv:vulture] whitelist_externals = bash -commands = bash -c 'vulture tools/*.py' +commands = bash -c 'vulture tools/*.py src/python/*.py' deps = vulture diff --git a/pkg/arch/PKGBUILD b/pkg/arch/PKGBUILD index 9d61b8a..6b6fd53 100644 --- a/pkg/arch/PKGBUILD +++ b/pkg/arch/PKGBUILD @@ -15,6 +15,18 @@ makedepends=(gcc make) source=(${pkgname}::"git+https://github.com/pikvm/ustreamer#commit=v${pkgver}") md5sums=(SKIP) +_options="WITH_GPIO=1" +if [ -e /usr/lib/python3 ]; then + _options="$_options WITH_PYTHON=1" + depends+=(python) + makedepends+=(python-setuptools) +fi +if [ -e /opt/vc/include/IL/OMX_Core.h ]; then + depends+=(raspberrypi-firmware) + makedepends+=(raspberrypi-firmware) + _options="$_options WITH_OMX=1" +fi + build() { cd "$srcdir" @@ -22,9 +34,6 @@ build() { cp -r $pkgname $pkgname-build cd $pkgname-build - local _options="WITH_GPIO=1" - [ -e /opt/vc/include/IL/OMX_Core.h ] && _options="$_options WITH_OMX=1" - # LD does not link mmal with this option LDFLAGS="${LDFLAGS//--as-needed/}" LDFLAGS="${LDFLAGS//,,/,}" diff --git a/src/python/setup.py b/src/python/setup.py new file mode 100644 index 0000000..7b8b184 --- /dev/null +++ b/src/python/setup.py @@ -0,0 +1,22 @@ +from distutils.core import Extension +from distutils.core import setup + + +# ===== +if __name__ == "__main__": + setup( + name="ustreamer", + version="3.5", + description="uStreamer tools", + author="Maxim Devaev", + author_email="mdevaev@gmail.com", + url="https://github.com/pikvm/ustreamer", + ext_modules=[ + Extension( + "ustreamer", + libraries=["rt", "m", "pthread"], + undef_macros=["NDEBUG"], + sources=["ustreamer.c"], + ), + ], + ) diff --git a/src/python/ustreamer.c b/src/python/ustreamer.c new file mode 100644 index 0000000..7058bbd --- /dev/null +++ b/src/python/ustreamer.c @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "../libs/tools.h" // Just a header without C-sources +#include "../libs/memsinksh.h" // No sources again + + +typedef struct { + PyObject_HEAD + + char *obj; + double lock_timeout; + double wait_timeout; + + int fd; + memsink_shared_s *mem; + uint8_t *tmp_data; + size_t tmp_data_allocated; + uint64_t last_id; +} MemsinkObject; + + +static void MemsinkObject_destroy_internals(MemsinkObject *self) { + if (self->mem != NULL) { + munmap(self->mem, sizeof(memsink_shared_s)); + self->mem = NULL; + } + if (self->fd > 0) { + close(self->fd); + self->fd = -1; + } + if (self->tmp_data) { + free(self->tmp_data); + self->tmp_data = NULL; + self->tmp_data_allocated = 0; + } +} + +static int MemsinkObject_init(MemsinkObject *self, PyObject *args, PyObject *kwargs) { + self->lock_timeout = 1; + self->wait_timeout = 1; + + static char *kws[] = {"obj", "lock_timeout", "wait_timeout", NULL}; + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "s|dd", kws, + &self->obj, &self->lock_timeout, &self->wait_timeout)) { + return -1; + } + +# define SET_TIMEOUT(_timeout) { \ + if (self->_timeout <= 0) { \ + PyErr_SetString(PyExc_ValueError, #_timeout " must be > 0"); \ + return -1; \ + } \ + } + + SET_TIMEOUT(lock_timeout); + SET_TIMEOUT(wait_timeout); + +# undef CHECK_TIMEOUT + + self->tmp_data_allocated = 512 * 1024; + A_REALLOC(self->tmp_data, self->tmp_data_allocated); + + if ((self->fd = shm_open(self->obj, O_RDWR, 0)) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + if ((self->mem = mmap( + NULL, + sizeof(memsink_shared_s), + PROT_READ | PROT_WRITE, + MAP_SHARED, + self->fd, + 0 + )) == MAP_FAILED) { + PyErr_SetFromErrno(PyExc_OSError); + self->mem = NULL; + goto error; + } + if (self->mem == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Memory mapping is NULL"); \ + goto error; + } + + return 0; + + error: + MemsinkObject_destroy_internals(self); + return -1; +} + +static PyObject *MemsinkObject_repr(MemsinkObject *self) { + char repr[1024]; + snprintf(repr, 1023, "", self->obj); + return Py_BuildValue("s", repr); +} + +static void MemsinkObject_dealloc(MemsinkObject *self) { + MemsinkObject_destroy_internals(self); + PyObject_Del(self); +} + +static PyObject *MemsinkObject_close(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) { + MemsinkObject_destroy_internals(self); + Py_RETURN_NONE; +} + +static PyObject *MemsinkObject_enter(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) { + Py_INCREF(self); + return (PyObject *)self; +} + +static PyObject *MemsinkObject_exit(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) { + return PyObject_CallMethod((PyObject *)self, "close", ""); +} + +static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) { + if (self->mem == NULL || self->fd <= 0) { + PyErr_SetString(PyExc_RuntimeError, "Closed"); + return NULL; + } + + bool found = false; + bool failed = false; + + Py_BEGIN_ALLOW_THREADS + long double deadline_ts = get_now_monotonic() + self->wait_timeout; + do { + int retval = flock_timedwait_monotonic(self->fd, self->lock_timeout); + if (retval < 0 && errno != EWOULDBLOCK) { + failed = true; + break; + } else if (retval == 0) { + if ( + self->mem->magic == MEMSINK_MAGIC + && self->mem->version == MEMSINK_VERSION + && self->mem->id != self->last_id + ) { + found = true; + break; + } + if (flock(self->fd, LOCK_UN) < 0) { + failed = true; + break; + } + } + errno = 0; + usleep(1000); + if (errno == EINTR) { + failed = true; + break; + } + } while (get_now_monotonic() < deadline_ts); + Py_END_ALLOW_THREADS + + if (failed) { + return PyErr_SetFromErrno(PyExc_OSError); + } + if (!found) { + Py_RETURN_NONE; + } + +# define COPY(_type, _field) _type tmp_##_field = self->mem->_field + COPY(unsigned, width); + COPY(unsigned, height); + COPY(unsigned, format); + COPY(unsigned, stride); + COPY(bool, online); + COPY(double, grab_ts); + COPY(double, encode_begin_ts); + COPY(double, encode_end_ts); + COPY(unsigned, used); +# undef COPY + + // Временный буффер используется для скорейшего разблокирования синка + if (self->tmp_data_allocated < self->mem->used) { + size_t size = self->mem->used + (512 * 1024); + A_REALLOC(self->tmp_data, size); + self->tmp_data_allocated = size; + } + memcpy(self->tmp_data, self->mem->data, self->mem->used); + + self->mem->last_client_ts = get_now_monotonic(); + self->last_id = self->mem->id; + + if (flock(self->fd, LOCK_UN) < 0) { + return PyErr_SetFromErrno(PyExc_OSError); + } + + PyObject *frame = PyDict_New(); +# define SET_VALUE(_field, _from, _to) PyDict_SetItemString(frame, #_field, Py##_to##_From##_from(tmp_##_field)) + SET_VALUE(width, Long, Long); + SET_VALUE(height, Long, Long); + SET_VALUE(format, Long, Long); + SET_VALUE(stride, Long, Long); + SET_VALUE(online, Long, Bool); + SET_VALUE(grab_ts, Double, Float); + SET_VALUE(encode_begin_ts, Double, Float); + SET_VALUE(encode_end_ts, Double, Float); +# undef SET_VALUE + PyDict_SetItemString(frame, "data", PyBytes_FromStringAndSize((const char *)self->tmp_data, tmp_used)); + return frame; +} + +static PyObject *MemsinkObject_is_opened(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) { + return PyBool_FromLong(self->mem != NULL && self->fd > 0); +} + +#define FIELD_GETTER(_field, _from, _to) \ + static PyObject *MemsinkObject_getter_##_field(MemsinkObject *self, void *Py_UNUSED(closure)) { \ + return Py##_to##_From##_from(self->_field); \ + } + +FIELD_GETTER(obj, String, Unicode) +FIELD_GETTER(lock_timeout, Double, Float) +FIELD_GETTER(wait_timeout, Double, Float) + +#undef FIELD_GETTER + +static PyMethodDef MemsinkObject_methods[] = { +# define ADD_METHOD(_meth, _flags) {.ml_name = #_meth, .ml_meth = (PyCFunction)MemsinkObject_##_meth, .ml_flags = (_flags)} + ADD_METHOD(close, METH_NOARGS), + ADD_METHOD(enter, METH_NOARGS), + ADD_METHOD(exit, METH_VARARGS), + ADD_METHOD(wait_frame, METH_NOARGS), + ADD_METHOD(is_opened, METH_NOARGS), + {}, +# undef ADD_METHOD +}; + +static PyGetSetDef MemsinkObject_getsets[] = { +# define ADD_GETTER(_field) {.name = #_field, .get = (getter)MemsinkObject_getter_##_field} + ADD_GETTER(obj), + ADD_GETTER(lock_timeout), + ADD_GETTER(wait_timeout), + {}, +# undef ADD_GETTER +}; + +static PyTypeObject MemsinkType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "ustreamer.Memsink", + .tp_basicsize = sizeof(MemsinkObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = (initproc)MemsinkObject_init, + .tp_dealloc = (destructor)MemsinkObject_dealloc, + .tp_repr = (reprfunc)MemsinkObject_repr, + .tp_methods = MemsinkObject_methods, + .tp_getset = MemsinkObject_getsets, +}; + +static PyModuleDef ustreamer_Module = { + PyModuleDef_HEAD_INIT, + .m_name = "ustreamer", + .m_size = -1, +}; + +PyMODINIT_FUNC PyInit_ustreamer(void) { // cppcheck-suppress unusedFunction + PyObject *module = PyModule_Create(&ustreamer_Module); + if (module == NULL) { + return NULL; + } + + if (PyType_Ready(&MemsinkType) < 0) { + return NULL; + } + + Py_INCREF(&MemsinkType); + + if (PyModule_AddObject(module, "Memsink", (PyObject *)&MemsinkType) < 0) { + return NULL; + } + + return module; +}