Show a patch.

GET /api/1.1/patches/20903/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 20903,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/20903/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/20903/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20240813124722.22425-4-jaslo@ziska.de>",
    "date": "2024-08-13T12:25:07",
    "name": "[v2,3/3] gstreamer: Generate controls from control_ids_*.yaml files",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "0fb73f79606eff72e64e2907f66600523f4ab27b",
    "submitter": {
        "id": 173,
        "url": "https://patchwork.libcamera.org/api/1.1/people/173/?format=api",
        "name": "Jaslo Ziska",
        "email": "jaslo@ziska.de"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/20903/mbox/",
    "series": [
        {
            "id": 4515,
            "url": "https://patchwork.libcamera.org/api/1.1/series/4515/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4515",
            "date": "2024-08-13T12:25:04",
            "name": "gstreamer: Generate controls from control_ids_*.yaml files",
            "version": 2,
            "mbox": "https://patchwork.libcamera.org/series/4515/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/20903/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/20903/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 5455BC32A9\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 13 Aug 2024 12:48:33 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D1616633BE;\n\tTue, 13 Aug 2024 14:48:32 +0200 (CEST)",
            "from mo4-p00-ob.smtp.rzone.de (mo4-p00-ob.smtp.rzone.de\n\t[85.215.255.21])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6970463398\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 13 Aug 2024 14:48:31 +0200 (CEST)",
            "from archlinux.fritz.box by smtp.strato.de (RZmta 51.1.0 AUTH)\n\twith ESMTPSA id zb9f0a07DCmUU7j\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256 bits))\n\t(Client did not present a certificate);\n\tTue, 13 Aug 2024 14:48:30 +0200 (CEST)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key;\n\tunprotected) header.d=ziska.de header.i=@ziska.de header.b=\"fcDRvIqO\";\n\tdkim=permerror (0-bit key) header.d=ziska.de header.i=@ziska.de\n\theader.b=\"GDErX7Ab\"; dkim-atps=neutral",
        "ARC-Seal": "i=1; a=rsa-sha256; t=1723553311; cv=none;\n\td=strato.com; s=strato-dkim-0002;\n\tb=IBs6zkW3Rux7PsBb9m7pgU65v2dpqH4fhxLSuYl3FD1CQm+XWu499Jup2umbrLKd9a\n\tudn3+VFZ2By05atFxnaw3kY8e7zJT68o271/lGmG7aKcOEECoeEBs/fgW5gSoaKOq/CE\n\tOj4A6w+rgz7grllBF4FfctYnz1oDOKTCBtF2rvtSxc+zJLok867WyhFKQzA6nMX+JQbX\n\tktnGoZjvgQ8vOR6n1PIr1cgz0sAEwsZis3ApTF1c0zZLPOA2NBNyjO5f17oNxEIUT+ML\n\tq8GBwt3UZO0DkQQcV8LjQuSwgo2bSU3Dntvt0q2XKsK3FVwnXDcr9073F2SFQCT7r//P\n\t4mYg==",
        "ARC-Message-Signature": "i=1; a=rsa-sha256; c=relaxed/relaxed; t=1723553311;\n\ts=strato-dkim-0002; d=strato.com;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=v22c7NEinm69XCLgjn1/tsBCz3NPw9gI+IowkF3eajk=;\n\tb=TJ+3q13EFv+aFFK1gQK8rGy4pdSywwOdXPsymV8Djs/+JwFLcwvMPeTukp1SCzCB3C\n\tAKt2AuvO61WjidsNwS1mB9oQCyfJ/Cy+B/6DdINbfL4uYD7+r+TrNfA+WqM6UBE+Oftk\n\t5LmDHvRnm30V4WIXGO0FflVSmK5Vn8yEC+Rhb5HQwfBRq4xr4GkjSt8Rn6spC+J/2ajH\n\t1ipfc8EYXGUPa8c6h3bCTE2UEbapQcSf3o3Nyt/0M4ODMB80wdgTADyXPkU8a4lgiKiw\n\tjVfQy5Bg29cWIDpK6LKPzvnoPW1gGhA9686OyQyqmkq+pkhhAZP9Femglbd9ae5ih6n7\n\tsviA==",
        "ARC-Authentication-Results": "i=1; strato.com;\n    arc=none;\n    dkim=none",
        "X-RZG-CLASS-ID": "mo00",
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/relaxed; t=1723553311;\n\ts=strato-dkim-0002; d=ziska.de;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=v22c7NEinm69XCLgjn1/tsBCz3NPw9gI+IowkF3eajk=;\n\tb=fcDRvIqO4T2gKYGJWpiGVqDu1G+cy9yI4r6wyV4vaKqII7aW7T/Qpb8/hI0JlCq6zC\n\tEasV+Gr1yXUld/1zLWszZNLH5wKRaBpb/bUm2Kh03zyQinF/vmPFx4P5CQPuKoKEngkL\n\tDd6B35abpxHXUSphkvOj85VUisA0BZreip3Qqd0UObzLxnFhNCQVM7uSlYOHtFhep5kh\n\tAB039WGVacaYQxyQLX/R3O1uFIZDTpszEDrifoY/Ochce/BGi7Dsn7q1gHTCLoD4Osfm\n\tL3l9ecoBdJg/Fk5GMAlj/qc0x0XqdlX8MkSL5e0QaKIEkdszH4pKaX1MSYzKEZlFj1lU\n\tVC7A==",
            "v=1; a=ed25519-sha256; c=relaxed/relaxed; t=1723553311;\n\ts=strato-dkim-0003; d=ziska.de;\n\th=References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=v22c7NEinm69XCLgjn1/tsBCz3NPw9gI+IowkF3eajk=;\n\tb=GDErX7AbRy6A9NUV2jcVkaKy1VbWYw+/eTVCsm2MwovJIHFW6lcJWNxd4uL6zqF/Ch\n\tR/u8wRYWtpFOTgBHU1Bw=="
        ],
        "X-RZG-AUTH": "\":Jm0XeU+IYfb0x77LHmrjN5Wlb7TBwusDqIM6Hizy8VdfzvKi4yoFC9cGhoqwVPJQb1HfVSED9d9Z5psVXjkhV67rRpA=\"",
        "From": "Jaslo Ziska <jaslo@ziska.de>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Jaslo Ziska <jaslo@ziska.de>",
        "Subject": "[PATCH v2 3/3] gstreamer: Generate controls from control_ids_*.yaml\n\tfiles",
        "Date": "Tue, 13 Aug 2024 14:25:07 +0200",
        "Message-ID": "<20240813124722.22425-4-jaslo@ziska.de>",
        "X-Mailer": "git-send-email 2.46.0",
        "In-Reply-To": "<20240813124722.22425-1-jaslo@ziska.de>",
        "References": "<20240813124722.22425-1-jaslo@ziska.de>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Content-Type": "text/plain; charset=\"us-ascii\"",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "This commit implements gstreamer controls for the libcamera element by\ngenerating the controls from the control_ids_*.yaml files using a new\ngen-gst-controls.py script. The appropriate meson files are also changed\nto automatically run the script when building.\n\nThe gen-gst-controls.py script works similar to the gen-controls.py\nscript by parsing the control_ids_*.yaml files and generating C++ code\nfor each control.\nFor the controls to be used as gstreamer properties the type for each\ncontrol needs to be translated to the appropriate glib type and a\nGEnumValue is generated for each enum control. Then a\ng_object_install_property(), _get_property() and _set_property()\nfunction is generated for each control.\nThe vendor controls get prefixed with \"$vendor-\" in the final gstreamer\nproperty name.\n\nThe C++ code generated by the gen-gst-controls.py script is written into\nthe template gstlibcamerasrc-controls.cpp.in file. The matching\ngstlibcamerasrc-controls.h header defines the GstCameraControls class\nwhich handles the installation of the gstreamer properties as well as\nkeeping track of the control values and setting and getting the\ncontrols. The content of these functions is generated in the Python\nscript.\n\nFinally the libcamerasrc element itself is edited to make use of the new\nGstCameraControls class. The way this works is by defining a PROP_LAST\nenum variant which is passed to the installProperties() function so the\nproperties are defined with the appropriate offset. When getting or\nsetting a property PROP_LAST is subtracted from the requested property\nto translate the control back into a libcamera::controls:: enum\nvariant.\n\nSigned-off-by: Jaslo Ziska <jaslo@ziska.de>\n---\n src/gstreamer/gstlibcamera-controls.cpp.in | 296 +++++++++++++++++++++\n src/gstreamer/gstlibcamera-controls.h      |  43 +++\n src/gstreamer/gstlibcamerasrc.cpp          |  22 +-\n src/gstreamer/meson.build                  |  10 +\n utils/codegen/controls.py                  |   8 +\n utils/codegen/gen-gst-controls.py          | 151 +++++++++++\n utils/codegen/meson.build                  |   1 +\n 7 files changed, 528 insertions(+), 3 deletions(-)\n create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in\n create mode 100644 src/gstreamer/gstlibcamera-controls.h\n create mode 100755 utils/codegen/gen-gst-controls.py",
    "diff": "diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in\nnew file mode 100644\nindex 00000000..aab7ae24\n--- /dev/null\n+++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n@@ -0,0 +1,296 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Collabora Ltd.\n+ *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n+ *\n+ * GStreamer Camera Controls\n+ *\n+ * This file is auto-generated. Do not edit.\n+ */\n+\n+#include <vector>\n+\n+#include <libcamera/control_ids.h>\n+\n+#include \"gstlibcamera-controls.h\"\n+\n+using namespace libcamera;\n+\n+{% for vendor, ctrls in controls %}\n+{%- for ctrl in ctrls if ctrl.is_enum %}\n+static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {\n+{%- for enum in ctrl.enum_values %}\n+\t{\n+\t\tcontrols::{{ ctrl.namespace }}{{ enum.name }},\n+\t\t{{ enum.description|format_description|indent('\\t\\t') }},\n+\t\t\"{{ enum.gst_name }}\"\n+\t},\n+{%- endfor %}\n+\t{0, NULL, NULL}\n+};\n+\n+#define TYPE_{{ ctrl.name|snake_case|upper }} \\\n+\t({{ ctrl.name|snake_case }}_get_type())\n+static GType {{ ctrl.name|snake_case }}_get_type()\n+{\n+\tstatic GType {{ ctrl.name|snake_case }}_type = 0;\n+\n+\tif (!{{ ctrl.name|snake_case }}_type)\n+\t\t{{ ctrl.name|snake_case }}_type =\n+\t\t\tg_enum_register_static(\"{{ ctrl.name }}\",\n+\t\t\t\t\t       {{ ctrl.name|snake_case }}_types);\n+\n+\treturn {{ ctrl.name|snake_case }}_type;\n+}\n+{% endfor %}\n+{%- endfor %}\n+\n+void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)\n+{\n+{%- for vendor, ctrls in controls %}\n+{%- for ctrl in ctrls %}\n+\n+{%- set spec %}\n+{%- if ctrl.is_rectangle -%}\n+gst_param_spec_array(\n+{%- else -%}\n+g_param_spec_{{ ctrl.gtype }}(\n+{%- endif -%}\n+{%- if ctrl.is_array %}\n+\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value\",\n+\t\"{{ ctrl.name }} Value\",\n+\t\"One {{ ctrl.name }} element value\",\n+{%- else %}\n+\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n+\t\"{{ ctrl.name }}\",\n+\t{{ ctrl.description|format_description|indent('\\t') }},\n+{%- endif %}\n+{%- if ctrl.is_enum %}\n+\tTYPE_{{ ctrl.name|snake_case|upper }},\n+\t{{ ctrl.default }},\n+{%- elif ctrl.is_rectangle %}\n+\tg_param_spec_int(\n+\t\t\"rectangle-value\",\n+\t\t\"Rectangle Value\",\n+\t\t\"One rectangle value, either x, y, width or height.\",\n+\t\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n+\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n+\t\t\t       G_PARAM_STATIC_STRINGS)\n+\t),\n+{%- elif ctrl.gtype == 'boolean' %}\n+\t{{ ctrl.default }},\n+{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}\n+\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n+{%- endif %}\n+\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n+\t\t       G_PARAM_STATIC_STRINGS)\n+)\n+{%- endset %}\n+\n+\tg_object_class_install_property(\n+\t\tklass,\n+\t\tlastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},\n+{%- if ctrl.is_array %}\n+\t\tgst_param_spec_array(\n+\t\t\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n+\t\t\t\"{{ ctrl.name }}\",\n+\t\t\t{{ ctrl.description|format_description|indent('\\t\\t\\t') }},\n+\t\t\t{{ spec|indent('\\t\\t\\t') }},\n+\t\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE |\n+\t\t\t\t       G_PARAM_READWRITE |\n+\t\t\t\t       G_PARAM_STATIC_STRINGS)\n+\t\t)\n+{%- else %}\n+\t\t{{ spec|indent('\\t\\t') }}\n+{%- endif %}\n+\t);\n+{%- endfor %}\n+{%- endfor %}\n+}\n+\n+bool GstCameraControls::getProperty(guint propId, GValue *value,\n+\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n+{\n+\tswitch (propId) {\n+{%- for vendor, ctrls in controls %}\n+{%- for ctrl in ctrls %}\n+\n+{%- set value_set %}\n+{%- if ctrl.is_rectangle -%}\n+Point top_left = val.topLeft();\n+Size size = val.size();\n+\n+GValue x = G_VALUE_INIT;\n+g_value_init(&x, G_TYPE_INT);\n+g_value_set_int(&x, top_left.x);\n+gst_value_array_append_and_take_value(&element, &x);\n+\n+GValue y = G_VALUE_INIT;\n+g_value_init(&y, G_TYPE_INT);\n+g_value_set_int(&y, top_left.y);\n+gst_value_array_append_and_take_value(&element, &y);\n+\n+GValue width = G_VALUE_INIT;\n+g_value_init(&width, G_TYPE_INT);\n+g_value_set_int(&width, size.width);\n+gst_value_array_append_and_take_value(&element, &width);\n+\n+GValue height = G_VALUE_INIT;\n+g_value_init(&height, G_TYPE_INT);\n+g_value_set_int(&x, size.height);\n+gst_value_array_append_and_take_value(&element, &height);\n+{%- else -%}\n+g_value_set_{{ ctrl.gtype }}(&element, val);\n+{%- endif -%}\n+{%- endset %}\n+\n+\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n+\t\tauto control = metadata_.get(controls::{{ ctrl.namespace }}{{ ctrl.name }});\n+\t\tcontrol = control ? control :\n+\t\t\t  controls_.get(controls::{{ ctrl.namespace }}{{ ctrl.name }});\n+\t\tif (!control) {\n+\t\t\tGST_WARNING(\"Control '%s' is not available, default value will be returned\",\n+\t\t\t\t    controls::{{ ctrl.namespace }}{{ ctrl.name }}.name().c_str());\n+\t\t\treturn true;\n+\t\t}\n+\n+{%- if ctrl.is_array %}\n+\t\tfor (size_t i = 0; i < control->size(); ++i) {\n+\t\t\tGValue element = G_VALUE_INIT;\n+{%- if ctrl.is_rectangle %}\n+\t\t\tg_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);\n+{%- else %}\n+\t\t\tg_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});\n+{%- endif %}\n+\t\t\tauto val = (*control)[i];\n+\t\t\t{{ value_set|indent('\\t\\t\\t\\t') }}\n+\t\t\tgst_value_array_append_and_take_value(value, &element);\n+\t\t}\n+{%- else %}\n+\t\tGValue element = *value;\n+\t\tauto val = *control;\n+\t\t{{ value_set|indent('\\t\\t\\t') }}\n+{%- endif %}\n+\n+\t\treturn true;\n+\t}\n+{%- endfor %}\n+{%- endfor %}\n+\tdefault:\n+\t\treturn false;\n+\t}\n+}\n+\n+bool GstCameraControls::setProperty(guint propId, const GValue *value,\n+\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n+{\n+\t// check whether the camera capabilities are available\n+\tif (!capabilities_.empty()) {\n+\t\t// if so, check that the control is supported\n+\t\tconst ControlId *cid = capabilities_.idmap().at(propId);\n+\t\tauto info = capabilities_.find(cid);\n+\n+\t\tif (info == capabilities_.end()) {\n+\t\t\tGST_WARNING(\"Control '%s' is not supported by the camera and will be ignored\",\n+\t\t\t\t    cid->name().c_str());\n+\t\t\treturn true;\n+\t\t}\n+\t}\n+\n+\tswitch (propId) {\n+{%- for vendor, ctrls in controls %}\n+{%- for ctrl in ctrls %}\n+\n+{%- set value_get %}\n+{%- if ctrl.is_rectangle -%}\n+if (gst_value_array_get_size(element) != 4) {\n+\tGST_ERROR(\"Rectangle must be an array of size 4\");\n+\treturn true;\n+}\n+\n+const GValue *r;\n+r = gst_value_array_get_value(element, 0);\n+int x = g_value_get_int(r);\n+r = gst_value_array_get_value(element, 1);\n+int y = g_value_get_int(r);\n+r = gst_value_array_get_value(element, 2);\n+int w = g_value_get_int(r);\n+r = gst_value_array_get_value(element, 3);\n+int h = g_value_get_int(r);\n+\n+auto val = Rectangle(x, y, w, h);\n+{%- else -%}\n+auto val = g_value_get_{{ ctrl.gtype }}(element);\n+{%- endif -%}\n+{%- endset %}\n+\n+\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n+{%- if ctrl.is_array %}\n+\t\tsize_t size = gst_value_array_get_size(value);\n+{%- if ctrl.size != 0 %}\n+\t\tif (size != {{ ctrl.size }}) {\n+\t\t\tGST_ERROR(\"Incorrect array size, must be of size {{ ctrl.size }}\");\n+\t\t\treturn true;\n+\t\t}\n+{%- endif %}\n+\n+\t\tstd::vector<{{ ctrl.element_type }}> values(size);\n+\t\tfor (size_t i = 0; i < size; ++i) {\n+\t\t\tconst GValue *element = gst_value_array_get_value(value, i);\n+\t\t\t{{ value_get|indent('\\t\\t\\t') }}\n+\t\t\tvalues[i] = val;\n+\t\t}\n+\t\tcontrols_.set(controls::{{ ctrl.namespace }}{{ ctrl.name }},\n+{%- if ctrl.size == 0 %}\n+\t\t\t      Span<const {{ ctrl.element_type }}>(values.data(), size));\n+{%- else %}\n+\t\t\t      Span<const {{ ctrl.element_type }}, {{ ctrl.size }}>(values.data(), {{ ctrl.size }}));\n+{%- endif %}\n+{%- else %}\n+\t\tconst GValue *element = value;\n+\t\t{{ value_get|indent('\\t\\t') }}\n+\t\tcontrols_.set(controls::{{ ctrl.namespace }}{{ ctrl.name }}, val);\n+{%- endif %}\n+\t\treturn true;\n+\t}\n+{%- endfor %}\n+{%- endfor %}\n+\tdefault:\n+\t\treturn false;\n+\t}\n+}\n+\n+void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)\n+{\n+\tcapabilities_ = cam->controls();\n+\n+\t// check the controls which were set before the camera capabilities were known\n+\tControlList new_controls;\n+\tfor (auto control = controls_.begin(); control != controls_.end(); ++control) {\n+\t\tunsigned int id = control->first;\n+\t\tControlValue value = control->second;\n+\n+\t\tconst ControlId *cid = capabilities_.idmap().at(id);\n+\t\tauto info = capabilities_.find(cid);\n+\n+\t\t// only add controls which are supported\n+\t\tif (info != capabilities_.end()) {\n+\t\t\tnew_controls.set(id, value);\n+\t\t} else {\n+\t\t\tGST_WARNING(\"Control '%s' is not supported by the camera and will be ignored\",\n+\t\t\t\t    cid->name().c_str());\n+\t\t}\n+\t}\n+\n+\tcontrols_ = new_controls;\n+}\n+\n+void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)\n+{\n+\trequest->controls().merge(controls_);\n+}\n+\n+void GstCameraControls::readMetadata(libcamera::Request *request)\n+{\n+\tmetadata_ = request->metadata();\n+}\ndiff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h\nnew file mode 100644\nindex 00000000..89b616ad\n--- /dev/null\n+++ b/src/gstreamer/gstlibcamera-controls.h\n@@ -0,0 +1,43 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Collabora Ltd.\n+ *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n+ *\n+ * GStreamer Camera Controls\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+\n+#include <libcamera/camera.h>\n+#include <libcamera/controls.h>\n+#include <libcamera/request.h>\n+\n+#include \"gstlibcamerasrc.h\"\n+\n+namespace libcamera {\n+\n+class GstCameraControls\n+{\n+public:\n+\tstatic void installProperties(GObjectClass *klass, int lastProp);\n+\n+\tbool getProperty(guint propId, GValue *value, GParamSpec *pspec);\n+\tbool setProperty(guint propId, const GValue *value, GParamSpec *pspec);\n+\n+\tvoid setCamera(const std::shared_ptr<libcamera::Camera> &cam);\n+\n+\tvoid applyControls(std::unique_ptr<libcamera::Request> &request);\n+\tvoid readMetadata(libcamera::Request *request);\n+\n+private:\n+\t/* supported controls and limits of camera */\n+\tControlInfoMap capabilities_;\n+\t/* set of user modified controls */\n+\tControlList controls_;\n+\t/* metadata returned by the camera */\n+\tControlList metadata_;\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp\nindex 40b787c8..8efa25f4 100644\n--- a/src/gstreamer/gstlibcamerasrc.cpp\n+++ b/src/gstreamer/gstlibcamerasrc.cpp\n@@ -37,10 +37,11 @@\n \n #include <gst/base/base.h>\n \n+#include \"gstlibcamera-controls.h\"\n+#include \"gstlibcamera-utils.h\"\n #include \"gstlibcameraallocator.h\"\n #include \"gstlibcamerapad.h\"\n #include \"gstlibcamerapool.h\"\n-#include \"gstlibcamera-utils.h\"\n \n using namespace libcamera;\n \n@@ -128,6 +129,7 @@ struct GstLibcameraSrcState {\n \n \tControlList initControls_;\n \tguint group_id_;\n+\tGstCameraControls controls_;\n \n \tint queueRequest();\n \tvoid requestCompleted(Request *request);\n@@ -153,6 +155,7 @@ struct _GstLibcameraSrc {\n enum {\n \tPROP_0,\n \tPROP_CAMERA_NAME,\n+\tPROP_LAST\n };\n \n static void gst_libcamera_src_child_proxy_init(gpointer g_iface,\n@@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()\n \tif (!request)\n \t\treturn -ENOMEM;\n \n+\t/* Apply controls */\n+\tcontrols_.applyControls(request);\n+\n \tstd::unique_ptr<RequestWrap> wrap =\n \t\tstd::make_unique<RequestWrap>(std::move(request));\n \n@@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)\n \n \t{\n \t\tGLibLocker locker(&lock_);\n+\n+\t\tcontrols_.readMetadata(request);\n+\n \t\twrap = std::move(queuedRequests_.front());\n \t\tqueuedRequests_.pop();\n \t}\n@@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)\n \t\treturn false;\n \t}\n \n+\tself->state->controls_.setCamera(cam);\n+\n \tcam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);\n \n \t/* No need to lock here, we didn't start our threads yet. */\n@@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n {\n \tGLibLocker lock(GST_OBJECT(object));\n \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n+\tGstLibcameraSrcState *state = self->state;\n \n \tswitch (prop_id) {\n \tcase PROP_CAMERA_NAME:\n@@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n \t\tself->camera_name = g_value_dup_string(value);\n \t\tbreak;\n \tdefault:\n-\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n+\t\tif (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))\n+\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n \t\tbreak;\n \t}\n }\n@@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,\n {\n \tGLibLocker lock(GST_OBJECT(object));\n \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n+\tGstLibcameraSrcState *state = self->state;\n \n \tswitch (prop_id) {\n \tcase PROP_CAMERA_NAME:\n \t\tg_value_set_string(value, self->camera_name);\n \t\tbreak;\n \tdefault:\n-\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n+\t\tif (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))\n+\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n \t\tbreak;\n \t}\n }\n@@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)\n \t\t\t\t\t\t\t     | G_PARAM_STATIC_STRINGS));\n \tg_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);\n \n+\tGstCameraControls::installProperties(object_class, PROP_LAST);\n }\n \n /* GstChildProxy implementation */\ndiff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build\nindex c2a01e7b..6b7e53b5 100644\n--- a/src/gstreamer/meson.build\n+++ b/src/gstreamer/meson.build\n@@ -25,6 +25,16 @@ libcamera_gst_sources = [\n     'gstlibcamerasrc.cpp',\n ]\n \n+# Generate gstreamer control properties\n+\n+gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')\n+libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',\n+                                       input : controls_files,\n+                                       output : 'gstlibcamera-controls.cpp',\n+                                       command : [gen_gst_controls, '-o', '@OUTPUT@',\n+                                                  '-t', gen_gst_controls_template, '@INPUT@'],\n+                                       env : py_build_env)\n+\n libcamera_gst_cpp_args = [\n     '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n     '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\ndiff --git a/utils/codegen/controls.py b/utils/codegen/controls.py\nindex 7bafee59..03c77cc6 100644\n--- a/utils/codegen/controls.py\n+++ b/utils/codegen/controls.py\n@@ -110,3 +110,11 @@ class Control(object):\n             return f\"Span<const {typ}, {self.__size}>\"\n         else:\n             return f\"Span<const {typ}>\"\n+\n+    @property\n+    def element_type(self):\n+        return self.__data.get('type')\n+\n+    @property\n+    def size(self):\n+        return self.__size\ndiff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py\nnew file mode 100755\nindex 00000000..5ac8981f\n--- /dev/null\n+++ b/utils/codegen/gen-gst-controls.py\n@@ -0,0 +1,151 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2019, Google Inc.\n+# Copyright (C) 2024, Jaslo Ziska\n+#\n+# Authors:\n+# Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n+# Jaslo Ziska <jaslo@ziska.de>\n+#\n+# Generate gstreamer control properties from YAML\n+\n+import argparse\n+import jinja2\n+import re\n+import sys\n+import yaml\n+\n+from controls import Control\n+\n+\n+def find_common_prefix(strings):\n+    prefix = strings[0]\n+\n+    for string in strings[1:]:\n+        while string[:len(prefix)] != prefix and prefix:\n+            prefix = prefix[:len(prefix) - 1]\n+        if not prefix:\n+            break\n+\n+    return prefix\n+\n+\n+def format_description(description):\n+    # Substitute doxygen keywords \\sa (see also) and \\todo\n+    description = re.sub(r'\\\\sa((?: \\w+)+)',\n+                         lambda match: 'See also: ' + ', '.join(\n+                             map(kebab_case, match.group(1).strip().split(' '))\n+                         ) + '.', description)\n+    description = re.sub(r'\\\\todo', 'Todo:', description)\n+\n+    description = description.strip().split('\\n')\n+    return '\\n'.join([\n+        '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + ' \"' for line in description if line\n+    ]).rstrip()\n+\n+\n+def snake_case(s):\n+    return ''.join([\n+        c.isupper() and ('_' + c.lower()) or c for c in s\n+    ]).strip('_')\n+\n+\n+def kebab_case(s):\n+    return snake_case(s).replace('_', '-')\n+\n+\n+def extend_control(ctrl):\n+    if ctrl.vendor != 'libcamera':\n+        ctrl.namespace = f'{ctrl.vendor}::'\n+        ctrl.vendor_prefix = f'{ctrl.vendor}-'\n+    else:\n+        ctrl.namespace = ''\n+        ctrl.vendor_prefix = ''\n+\n+    ctrl.is_array = ctrl.size is not None\n+\n+    if ctrl.is_enum:\n+        # remove common prefix from enum variant names\n+        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])\n+        for enum in ctrl.enum_values:\n+            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))\n+\n+        ctrl.gtype = 'enum'\n+        ctrl.default = '0'\n+    elif ctrl.element_type == 'bool':\n+        ctrl.gtype = 'boolean'\n+        ctrl.default = 'false'\n+    elif ctrl.element_type == 'float':\n+        ctrl.gtype = 'float'\n+        ctrl.default = '0'\n+        ctrl.min = '-G_MAXFLOAT'\n+        ctrl.max = 'G_MAXFLOAT'\n+    elif ctrl.element_type == 'int32_t':\n+        ctrl.gtype = 'int'\n+        ctrl.default = '0'\n+        ctrl.min = 'G_MININT'\n+        ctrl.max = 'G_MAXINT'\n+    elif ctrl.element_type == 'int64_t':\n+        ctrl.gtype = 'int64'\n+        ctrl.default = '0'\n+        ctrl.min = 'G_MININT64'\n+        ctrl.max = 'G_MAXINT64'\n+    elif ctrl.element_type == 'uint8_t':\n+        ctrl.gtype = 'uchar'\n+        ctrl.default = '0'\n+        ctrl.min = '0'\n+        ctrl.max = 'G_MAXUINT8'\n+    elif ctrl.element_type == 'Rectangle':\n+        ctrl.is_rectangle = True\n+        ctrl.default = '0'\n+        ctrl.min = '0'\n+        ctrl.max = 'G_MAXINT'\n+    else:\n+        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')\n+\n+    return ctrl\n+\n+\n+def main(argv):\n+    # Parse command line arguments\n+    parser = argparse.ArgumentParser()\n+    parser.add_argument('--output', '-o', metavar='file', type=str,\n+                        help='Output file name. Defaults to standard output if not specified.')\n+    parser.add_argument('--template', '-t', dest='template', type=str, required=True,\n+                        help='Template file name.')\n+    parser.add_argument('input', type=str, nargs='+',\n+                        help='Input file name.')\n+\n+    args = parser.parse_args(argv[1:])\n+\n+    controls = {}\n+    for input in args.input:\n+        data = yaml.safe_load(open(input, 'rb').read())\n+\n+        vendor = data['vendor']\n+        ctrls = controls.setdefault(vendor, [])\n+\n+        for ctrl in data['controls']:\n+            ctrl = Control(*ctrl.popitem(), vendor)\n+            ctrls.append(extend_control(ctrl))\n+\n+    data = {'controls': list(controls.items())}\n+\n+    env = jinja2.Environment()\n+    env.filters['format_description'] = format_description\n+    env.filters['snake_case'] = snake_case\n+    env.filters['kebab_case'] = kebab_case\n+    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())\n+    string = template.render(data)\n+\n+    if args.output:\n+        with open(args.output, 'w', encoding='utf-8') as output:\n+            output.write(string)\n+    else:\n+        sys.stdout.write(string)\n+\n+    return 0\n+\n+\n+if __name__ == '__main__':\n+    sys.exit(main(sys.argv))\ndiff --git a/utils/codegen/meson.build b/utils/codegen/meson.build\nindex adf33bba..904dd66d 100644\n--- a/utils/codegen/meson.build\n+++ b/utils/codegen/meson.build\n@@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']\n \n gen_controls = files('gen-controls.py')\n gen_formats = files('gen-formats.py')\n+gen_gst_controls = files('gen-gst-controls.py')\n gen_header = files('gen-header.sh')\n gen_ipa_pub_key = files('gen-ipa-pub-key.py')\n gen_tracepoints = files('gen-tp-header.py')\n",
    "prefixes": [
        "v2",
        "3/3"
    ]
}