From patchwork Mon Mar 14 15:46:31 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15445 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 44091BF415 for ; Mon, 14 Mar 2022 15:47:00 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id CD3B8632E6; Mon, 14 Mar 2022 16:46:58 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1647272818; bh=9h9puR1p8Ohv5Zh7QQicbwYZkzp92RG6dfnzmnpgXSE=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=Ex1OBjw0y8iRNbYQ2ijocqumL9Kx7g9za0mCgNivGUV8PbpHX1mlghdkvlKTk851i aiNbzJVI/0v9EdQympg3PDvY+qcJkc9R/M6ZgOoHLhLgJpyeUSvXHKk1XhacAzVMdr 0qF7TXGntbWLtaSe6r1pbQ4yviO/EV+urINnqGCOOmp7/CLffqbUYck6iwNnIlAxwS x9cT3B0Hh3OBoli5IW+UKvnK3bFPxJ0vkIZ/RH86iIBV+VriX39c7Sh3CsOYAo4LfY HDz/RZVRBZGqpDnAYxqSskaLX3Gj27Pqb627hXtWExt4f8ZTQt0cO7F8+5YUw/Zsou keRg+ap0gO4gg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 41CD0604EA for ; Mon, 14 Mar 2022 16:46:57 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="S1ZVHbEe"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id A714230B; Mon, 14 Mar 2022 16:46:56 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1647272817; bh=9h9puR1p8Ohv5Zh7QQicbwYZkzp92RG6dfnzmnpgXSE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=S1ZVHbEeSfxaLnVxrxmfvVPUahY5DjkVYNlMervpAuLjebRBns3mSfnE/E5W/+0Zh 8hdeBaSmnZptKoXBzBf/oyNKC4q6Oyq/K0zHL90/0D+fsz+OZ9kXtoV5ymuB8mpBo8 Y28FXjCmeHMME0ajKNlHYaWiib13dcAUVPuOxAqM= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart Date: Mon, 14 Mar 2022 17:46:31 +0200 Message-Id: <20220314154633.506026-2-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220314154633.506026-1-tomi.valkeinen@ideasonboard.com> References: <20220314154633.506026-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v5 1/3] Add Python bindings X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add libcamera Python bindings. pybind11 is used to generate the C++ <-> Python layer. We use pybind11 'smart_holder' version to avoid issues with private destructors and shared_ptr. There is also an alternative solution here: https://github.com/pybind/pybind11/pull/2067 Only a subset of libcamera classes are exposed. Implementing and testing the wrapper classes is challenging, and as such only classes that I have needed have been added so far. Signed-off-by: Tomi Valkeinen --- meson.build | 1 + meson_options.txt | 5 + src/meson.build | 1 + src/py/libcamera/__init__.py | 10 + src/py/libcamera/meson.build | 43 ++++ src/py/libcamera/pyenums.cpp | 53 ++++ src/py/libcamera/pymain.cpp | 453 +++++++++++++++++++++++++++++++++++ src/py/meson.build | 1 + subprojects/.gitignore | 3 +- subprojects/pybind11.wrap | 6 + 10 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 src/py/libcamera/__init__.py create mode 100644 src/py/libcamera/meson.build create mode 100644 src/py/libcamera/pyenums.cpp create mode 100644 src/py/libcamera/pymain.cpp create mode 100644 src/py/meson.build create mode 100644 subprojects/pybind11.wrap diff --git a/meson.build b/meson.build index 29d8542d..ff6c2ad6 100644 --- a/meson.build +++ b/meson.build @@ -179,6 +179,7 @@ summary({ 'Tracing support': tracing_enabled, 'Android support': android_enabled, 'GStreamer support': gst_enabled, + 'Python bindings': pycamera_enabled, 'V4L2 emulation support': v4l2_enabled, 'cam application': cam_enabled, 'qcam application': qcam_enabled, diff --git a/meson_options.txt b/meson_options.txt index 2c80ad8b..ca00c78e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -58,3 +58,8 @@ option('v4l2', type : 'boolean', value : false, description : 'Compile the V4L2 compatibility layer') + +option('pycamera', + type : 'feature', + value : 'auto', + description : 'Enable libcamera Python bindings (experimental)') diff --git a/src/meson.build b/src/meson.build index e0ea9c35..34663a6f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,4 +37,5 @@ subdir('cam') subdir('qcam') subdir('gstreamer') +subdir('py') subdir('v4l2') diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py new file mode 100644 index 00000000..b30bf33a --- /dev/null +++ b/src/py/libcamera/__init__.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from ._libcamera import * +import mmap + +def __FrameBuffer__mmap(self, plane): + return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ) + +FrameBuffer.mmap = __FrameBuffer__mmap diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build new file mode 100644 index 00000000..82388efb --- /dev/null +++ b/src/py/libcamera/meson.build @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: CC0-1.0 + +py3_dep = dependency('python3', required : get_option('pycamera')) + +if not py3_dep.found() + pycamera_enabled = false + subdir_done() +endif + +pycamera_enabled = true + +pybind11_proj = subproject('pybind11') +pybind11_dep = pybind11_proj.get_variable('pybind11_dep') + +pycamera_sources = files([ + 'pymain.cpp', + 'pyenums.cpp', +]) + +pycamera_deps = [ + libcamera_public, + py3_dep, + pybind11_dep, +] + +pycamera_args = ['-fvisibility=hidden'] +pycamera_args += ['-Wno-shadow'] +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT'] + +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/libcamera' + +pycamera = shared_module('_libcamera', + pycamera_sources, + install : true, + install_dir : destdir, + name_prefix : '', + dependencies : pycamera_deps, + cpp_args : pycamera_args) + +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py', + meson.current_build_dir() / '__init__.py') + +install_data(['__init__.py'], install_dir : destdir) diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp new file mode 100644 index 00000000..af6151c8 --- /dev/null +++ b/src/py/libcamera/pyenums.cpp @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + */ + +#include + +#include + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums(py::module& m) +{ + py::enum_(m, "ConfigurationStatus") + .value("Valid", CameraConfiguration::Valid) + .value("Adjusted", CameraConfiguration::Adjusted) + .value("Invalid", CameraConfiguration::Invalid); + + py::enum_(m, "StreamRole") + .value("StillCapture", StreamRole::StillCapture) + .value("Raw", StreamRole::Raw) + .value("VideoRecording", StreamRole::VideoRecording) + .value("Viewfinder", StreamRole::Viewfinder); + + py::enum_(m, "RequestStatus") + .value("Pending", Request::RequestPending) + .value("Complete", Request::RequestComplete) + .value("Cancelled", Request::RequestCancelled); + + py::enum_(m, "FrameMetadataStatus") + .value("Success", FrameMetadata::FrameSuccess) + .value("Error", FrameMetadata::FrameError) + .value("Cancelled", FrameMetadata::FrameCancelled); + + py::enum_(m, "ReuseFlag") + .value("Default", Request::ReuseFlag::Default) + .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers); + + py::enum_(m, "ControlType") + .value("None", ControlType::ControlTypeNone) + .value("Bool", ControlType::ControlTypeBool) + .value("Byte", ControlType::ControlTypeByte) + .value("Integer32", ControlType::ControlTypeInteger32) + .value("Integer64", ControlType::ControlTypeInteger64) + .value("Float", ControlType::ControlTypeFloat) + .value("String", ControlType::ControlTypeString) + .value("Rectangle", ControlType::ControlTypeRectangle) + .value("Size", ControlType::ControlTypeSize); +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp new file mode 100644 index 00000000..7701da40 --- /dev/null +++ b/src/py/libcamera/pymain.cpp @@ -0,0 +1,453 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + */ + +/* + * To generate pylibcamera stubs: + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace py = pybind11; + +using namespace std; +using namespace libcamera; + +template +static py::object ValueOrTuple(const ControlValue &cv) +{ + if (cv.isArray()) { + const T *v = reinterpret_cast(cv.data().data()); + auto t = py::tuple(cv.numElements()); + + for (size_t i = 0; i < cv.numElements(); ++i) + t[i] = v[i]; + + return t; + } + + return py::cast(cv.get()); +} + +static py::object ControlValueToPy(const ControlValue &cv) +{ + switch (cv.type()) { + case ControlTypeBool: + return ValueOrTuple(cv); + case ControlTypeByte: + return ValueOrTuple(cv); + case ControlTypeInteger32: + return ValueOrTuple(cv); + case ControlTypeInteger64: + return ValueOrTuple(cv); + case ControlTypeFloat: + return ValueOrTuple(cv); + case ControlTypeString: + return py::cast(cv.get()); + case ControlTypeRectangle: { + const Rectangle *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->x, v->y, v->width, v->height); + } + case ControlTypeSize: { + const Size *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->width, v->height); + } + case ControlTypeNone: + default: + throw runtime_error("Unsupported ControlValue type"); + } +} + +static ControlValue PyToControlValue(const py::object &ob, ControlType type) +{ + switch (type) { + case ControlTypeBool: + return ControlValue(ob.cast()); + case ControlTypeByte: + return ControlValue(ob.cast()); + case ControlTypeInteger32: + return ControlValue(ob.cast()); + case ControlTypeInteger64: + return ControlValue(ob.cast()); + case ControlTypeFloat: + return ControlValue(ob.cast()); + case ControlTypeString: + return ControlValue(ob.cast()); + case ControlTypeRectangle: + case ControlTypeSize: + case ControlTypeNone: + default: + throw runtime_error("Control type not implemented"); + } +} + +static weak_ptr g_camera_manager; +static int g_eventfd; +static mutex g_reqlist_mutex; +static vector g_reqlist; + +static void handleRequestCompleted(Request *req) +{ + { + lock_guard guard(g_reqlist_mutex); + g_reqlist.push_back(req); + } + + uint64_t v = 1; + write(g_eventfd, &v, 8); +} + +void init_pyenums(py::module& m); + +PYBIND11_MODULE(_libcamera, m) +{ + init_pyenums(m); + + /* Forward declarations */ + + /* + * We need to declare all the classes here so that Python docstrings + * can be generated correctly. + * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings + */ + + auto pyCameraManager = py::class_(m, "CameraManager"); + auto pyCamera = py::class_(m, "Camera"); + auto pyCameraConfiguration = py::class_(m, "CameraConfiguration"); + auto pyStreamConfiguration = py::class_(m, "StreamConfiguration"); + auto pyStreamFormats = py::class_(m, "StreamFormats"); + auto pyFrameBufferAllocator = py::class_(m, "FrameBufferAllocator"); + auto pyFrameBuffer = py::class_(m, "FrameBuffer"); + auto pyStream = py::class_(m, "Stream"); + auto pyControlId = py::class_(m, "ControlId"); + auto pyRequest = py::class_(m, "Request"); + auto pyFrameMetadata = py::class_(m, "FrameMetadata"); + + /* Global functions */ + m.def("logSetLevel", &logSetLevel); + + /* Classes */ + pyCameraManager + .def_static("singleton", []() { + shared_ptr cm = g_camera_manager.lock(); + if (cm) + return cm; + + int fd = eventfd(0, 0); + if (fd == -1) + throw std::system_error(errno, std::generic_category(), "Failed to create eventfd"); + + cm = shared_ptr(new CameraManager, [](auto p) { + close(g_eventfd); + g_eventfd = -1; + delete p; + }); + + g_eventfd = fd; + g_camera_manager = cm; + + int ret = cm->start(); + if (ret) + throw std::system_error(-ret, std::generic_category(), "Failed to start CameraManager"); + + return cm; + }) + + .def_property_readonly("version", &CameraManager::version) + + .def_property_readonly("efd", [](CameraManager &) { + return g_eventfd; + }) + + .def("getReadyRequests", [](CameraManager &) { + vector v; + + { + lock_guard guard(g_reqlist_mutex); + swap(v, g_reqlist); + } + + vector ret; + + for (Request *req : v) { + py::object o = py::cast(req); + /* decrease the ref increased in Camera::queueRequest() */ + o.dec_ref(); + ret.push_back(o); + } + + return ret; + }) + + .def("get", py::overload_cast(&CameraManager::get), py::keep_alive<0, 1>()) + + .def("find", [](CameraManager &self, string str) { + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + + for (auto c : self.cameras()) { + string id = c->id(); + + std::transform(id.begin(), id.end(), id.begin(), ::tolower); + + if (id.find(str) != string::npos) + return c; + } + + return shared_ptr(); + }, py::keep_alive<0, 1>()) + + /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */ + .def_property_readonly("cameras", [](CameraManager &self) { + py::list l; + + for (auto &c : self.cameras()) { + py::object py_cm = py::cast(self); + py::object py_cam = py::cast(c); + py::detail::keep_alive_impl(py_cam, py_cm); + l.append(py_cam); + } + + return l; + }); + + pyCamera + .def_property_readonly("id", &Camera::id) + .def("acquire", &Camera::acquire) + .def("release", &Camera::release) + .def("start", [](Camera &self) { + self.requestCompleted.connect(handleRequestCompleted); + + int ret = self.start(); + if (ret) + self.requestCompleted.disconnect(handleRequestCompleted); + + return ret; + }) + + .def("stop", [](Camera &self) { + int ret = self.stop(); + if (!ret) + self.requestCompleted.disconnect(handleRequestCompleted); + + return ret; + }) + + .def("__repr__", [](Camera &self) { + return ""; + }) + + /* Keep the camera alive, as StreamConfiguration contains a Stream* */ + .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) + .def("configure", &Camera::configure) + + .def("createRequest", &Camera::createRequest, py::arg("cookie") = 0) + + .def("queueRequest", [](Camera &self, Request *req) { + py::object py_req = py::cast(req); + + py_req.inc_ref(); + + int ret = self.queueRequest(req); + if (ret) + py_req.dec_ref(); + + return ret; + }) + + .def_property_readonly("streams", [](Camera &self) { + py::set set; + for (auto &s : self.streams()) { + py::object py_self = py::cast(self); + py::object py_s = py::cast(s); + py::detail::keep_alive_impl(py_s, py_self); + set.add(py_s); + } + return set; + }) + + .def("find_control", [](Camera &self, const string &name) { + const auto &controls = self.controls(); + + auto it = find_if(controls.begin(), controls.end(), + [&name](const auto &kvp) { return kvp.first->name() == name; }); + + if (it == controls.end()) + throw runtime_error("Control not found"); + + return it->first; + }, py::return_value_policy::reference_internal) + + .def_property_readonly("controls", [](Camera &self) { + py::dict ret; + + for (const auto &[id, ci] : self.controls()) { + ret[id->name().c_str()] = make_tuple(ControlValueToPy(ci.min()), + ControlValueToPy(ci.max()), + ControlValueToPy(ci.def())); + } + + return ret; + }) + + .def_property_readonly("properties", [](Camera &self) { + py::dict ret; + + for (const auto &[key, cv] : self.properties()) { + const ControlId *id = properties::properties.at(key); + py::object ob = ControlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }); + + pyCameraConfiguration + .def("__iter__", [](CameraConfiguration &self) { + return py::make_iterator(self); + }, py::keep_alive<0, 1>()) + .def("__len__", [](CameraConfiguration &self) { + return self.size(); + }) + .def("validate", &CameraConfiguration::validate) + .def("at", py::overload_cast(&CameraConfiguration::at), py::return_value_policy::reference_internal) + .def_property_readonly("size", &CameraConfiguration::size) + .def_property_readonly("empty", &CameraConfiguration::empty); + + pyStreamConfiguration + .def("toString", &StreamConfiguration::toString) + .def_property_readonly("stream", &StreamConfiguration::stream, py::return_value_policy::reference_internal) + .def_property( + "size", + [](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); }, + [](StreamConfiguration &self, tuple size) { self.size.width = get<0>(size); self.size.height = get<1>(size); }) + .def_property( + "pixelFormat", + [](StreamConfiguration &self) { return self.pixelFormat.toString(); }, + [](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); }) + .def_readwrite("stride", &StreamConfiguration::stride) + .def_readwrite("frameSize", &StreamConfiguration::frameSize) + .def_readwrite("bufferCount", &StreamConfiguration::bufferCount) + .def_property_readonly("formats", &StreamConfiguration::formats, py::return_value_policy::reference_internal); + ; + + pyStreamFormats + .def_property_readonly("pixelFormats", [](StreamFormats &self) { + vector fmts; + for (auto &fmt : self.pixelformats()) + fmts.push_back(fmt.toString()); + return fmts; + }) + .def("sizes", [](StreamFormats &self, const string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + vector> fmts; + for (const auto &s : self.sizes(fmt)) + fmts.push_back(make_tuple(s.width, s.height)); + return fmts; + }) + .def("range", [](StreamFormats &self, const string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + const auto &range = self.range(fmt); + return make_tuple(make_tuple(range.hStep, range.vStep), + make_tuple(range.min.width, range.min.height), + make_tuple(range.max.width, range.max.height)); + }); + + pyFrameBufferAllocator + .def(py::init>(), py::keep_alive<1, 2>()) + .def("allocate", &FrameBufferAllocator::allocate) + .def_property_readonly("allocated", &FrameBufferAllocator::allocated) + /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */ + .def("buffers", [](FrameBufferAllocator &self, Stream *stream) { + py::object py_self = py::cast(self); + py::list l; + for (auto &ub : self.buffers(stream)) { + py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self); + l.append(py_buf); + } + return l; + }); + + pyFrameBuffer + /* TODO: implement FrameBuffer::Plane properly */ + .def(py::init([](vector> planes, unsigned int cookie) { + vector v; + for (const auto &t : planes) + v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) }); + return new FrameBuffer(v, cookie); + })) + .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal) + .def("length", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.length; + }) + .def("fd", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.fd.get(); + }) + .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); + + pyStream + .def_property_readonly("configuration", &Stream::configuration); + + pyControlId + .def_property_readonly("id", &ControlId::id) + .def_property_readonly("name", &ControlId::name) + .def_property_readonly("type", &ControlId::type); + + pyRequest + /* Fence is not supported, so we cannot expose addBuffer() directly */ + .def("addBuffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) { + return self.addBuffer(stream, buffer); + }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */ + .def_property_readonly("status", &Request::status) + .def_property_readonly("buffers", &Request::buffers) + .def_property_readonly("cookie", &Request::cookie) + .def_property_readonly("hasPendingBuffers", &Request::hasPendingBuffers) + .def("set_control", [](Request &self, ControlId& id, py::object value) { + self.controls().set(id.id(), PyToControlValue(value, id.type())); + }) + .def_property_readonly("metadata", [](Request &self) { + py::dict ret; + + for (const auto &[key, cv] : self.metadata()) { + const ControlId *id = controls::controls.at(key); + py::object ob = ControlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }) + /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */ + .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }); + + pyFrameMetadata + .def_readonly("status", &FrameMetadata::status) + .def_readonly("sequence", &FrameMetadata::sequence) + .def_readonly("timestamp", &FrameMetadata::timestamp) + /* temporary helper, to be removed */ + .def_property_readonly("bytesused", [](FrameMetadata &self) { + vector v; + v.resize(self.planes().size()); + transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); + return v; + }); +} diff --git a/src/py/meson.build b/src/py/meson.build new file mode 100644 index 00000000..4ce9668c --- /dev/null +++ b/src/py/meson.build @@ -0,0 +1 @@ +subdir('libcamera') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 391fde2c..757bb072 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1,3 +1,4 @@ /googletest-release* /libyuv -/packagecache \ No newline at end of file +/packagecache +/pybind11*/ diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap new file mode 100644 index 00000000..ebf942ff --- /dev/null +++ b/subprojects/pybind11.wrap @@ -0,0 +1,6 @@ +[wrap-git] +url = https://github.com/tomba/pybind11.git +revision = smart_holder + +[provide] +pybind11 = pybind11_dep