[v3,3/3] gstreamer: Generate controls from control_ids_*.yaml files
diff mbox series

Message ID 20241017172331.16377-4-jaslo@ziska.de
State New
Headers show
Series
  • gstreamer: Generate controls from control_ids_*.yaml files
Related show

Commit Message

Jaslo Ziska Oct. 17, 2024, 5:04 p.m. UTC
This commit implements gstreamer controls for the libcamera element by
generating the controls from the control_ids_*.yaml files using a new
gen-gst-controls.py script. The appropriate meson files are also changed
to automatically run the script when building.

The gen-gst-controls.py script works similar to the gen-controls.py
script by parsing the control_ids_*.yaml files and generating C++ code
for each exposed control.
For the controls to be used as gstreamer properties the type for each
control needs to be translated to the appropriate glib type and a
GEnumValue is generated for each enum control. Then a
g_object_install_property(), _get_property() and _set_property()
function is generated for each control.
The vendor controls get prefixed with "$vendor-" in the final gstreamer
property name.

The C++ code generated by the gen-gst-controls.py script is written into
the template gstlibcamerasrc-controls.cpp.in file. The matching
gstlibcamerasrc-controls.h header defines the GstCameraControls class
which handles the installation of the gstreamer properties as well as
keeping track of the control values and setting and getting the
controls. The content of these functions is generated in the Python
script.

Finally the libcamerasrc element itself is edited to make use of the new
GstCameraControls class. The way this works is by defining a PROP_LAST
enum variant which is passed to the installProperties() function so the
properties are defined with the appropriate offset. When getting or
setting a property PROP_LAST is subtracted from the requested property
to translate the control back into a libcamera::controls:: enum
variant.

Signed-off-by: Jaslo Ziska <jaslo@ziska.de>
---
 src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++
 src/gstreamer/gstlibcamera-controls.h      |  43 +++
 src/gstreamer/gstlibcamerasrc.cpp          |  22 +-
 src/gstreamer/meson.build                  |  10 +
 utils/codegen/controls.py                  |   8 +
 utils/codegen/gen-gst-controls.py          | 166 +++++++++++
 utils/codegen/meson.build                  |   1 +
 7 files changed, 579 insertions(+), 3 deletions(-)
 create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in
 create mode 100644 src/gstreamer/gstlibcamera-controls.h
 create mode 100755 utils/codegen/gen-gst-controls.py

Comments

Kieran Bingham Oct. 18, 2024, 2:20 p.m. UTC | #1
Quoting Jaslo Ziska (2024-10-17 18:04:15)
> This commit implements gstreamer controls for the libcamera element by
> generating the controls from the control_ids_*.yaml files using a new
> gen-gst-controls.py script. The appropriate meson files are also changed
> to automatically run the script when building.
> 
> The gen-gst-controls.py script works similar to the gen-controls.py
> script by parsing the control_ids_*.yaml files and generating C++ code
> for each exposed control.
> For the controls to be used as gstreamer properties the type for each
> control needs to be translated to the appropriate glib type and a
> GEnumValue is generated for each enum control. Then a
> g_object_install_property(), _get_property() and _set_property()
> function is generated for each control.
> The vendor controls get prefixed with "$vendor-" in the final gstreamer
> property name.
> 
> The C++ code generated by the gen-gst-controls.py script is written into
> the template gstlibcamerasrc-controls.cpp.in file. The matching
> gstlibcamerasrc-controls.h header defines the GstCameraControls class
> which handles the installation of the gstreamer properties as well as
> keeping track of the control values and setting and getting the
> controls. The content of these functions is generated in the Python
> script.
> 
> Finally the libcamerasrc element itself is edited to make use of the new
> GstCameraControls class. The way this works is by defining a PROP_LAST
> enum variant which is passed to the installProperties() function so the
> properties are defined with the appropriate offset. When getting or
> setting a property PROP_LAST is subtracted from the requested property
> to translate the control back into a libcamera::controls:: enum
> variant.
> 
> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>

I know there was a build failure in CI to sort out - but I'll be very
happy to hear this 'works' from some testing once the build is fixed in
the next version. There's so much templating in here I'm not sure how
effectively I can review it, but I'd be fine merging when we know it
produces the correct results!

--
Kieran


> ---
>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++
>  src/gstreamer/gstlibcamera-controls.h      |  43 +++
>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-
>  src/gstreamer/meson.build                  |  10 +
>  utils/codegen/controls.py                  |   8 +
>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++
>  utils/codegen/meson.build                  |   1 +
>  7 files changed, 579 insertions(+), 3 deletions(-)
>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in
>  create mode 100644 src/gstreamer/gstlibcamera-controls.h
>  create mode 100755 utils/codegen/gen-gst-controls.py
> 
> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in
> new file mode 100644
> index 00000000..6861f5d1
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in
> @@ -0,0 +1,332 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Jaslo Ziska
> + *
> + * GStreamer Camera Controls
> + *
> + * This file is auto-generated. Do not edit.
> + */
> +
> +#include <vector>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/geometry.h>
> +
> +#include "gstlibcamera-controls.h"
> +
> +using namespace libcamera;
> +
> +static void value_set_rectangle(GValue *value, const Rectangle &rect)
> +{
> +       Point top_left = rect.topLeft();
> +       Size size = rect.size();
> +
> +       GValue x = G_VALUE_INIT;
> +       g_value_init(&x, G_TYPE_INT);
> +       g_value_set_int(&x, top_left.x);
> +       gst_value_array_append_and_take_value(value, &x);
> +
> +       GValue y = G_VALUE_INIT;
> +       g_value_init(&y, G_TYPE_INT);
> +       g_value_set_int(&y, top_left.y);
> +       gst_value_array_append_and_take_value(value, &y);
> +
> +       GValue width = G_VALUE_INIT;
> +       g_value_init(&width, G_TYPE_INT);
> +       g_value_set_int(&width, size.width);
> +       gst_value_array_append_and_take_value(value, &width);
> +
> +       GValue height = G_VALUE_INIT;
> +       g_value_init(&height, G_TYPE_INT);
> +       g_value_set_int(&x, size.height);
> +       gst_value_array_append_and_take_value(value, &height);
> +}
> +
> +static Rectangle value_get_rectangle(const GValue *value)
> +{
> +       const GValue *r;
> +       r = gst_value_array_get_value(value, 0);
> +       int x = g_value_get_int(r);
> +       r = gst_value_array_get_value(value, 1);
> +       int y = g_value_get_int(r);
> +       r = gst_value_array_get_value(value, 2);
> +       int w = g_value_get_int(r);
> +       r = gst_value_array_get_value(value, 3);
> +       int h = g_value_get_int(r);
> +
> +       return Rectangle(x, y, w, h);
> +}
> +
> +{% for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls if ctrl.is_enum %}
> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {
> +{%- for enum in ctrl.enum_values %}
> +       {
> +               controls::{{ ctrl.namespace }}{{ enum.name }},
> +               {{ enum.description|format_description|indent('\t\t') }},
> +               "{{ enum.gst_name }}"
> +       },
> +{%- endfor %}
> +       {0, NULL, NULL}
> +};
> +
> +#define TYPE_{{ ctrl.name|snake_case|upper }} \
> +       ({{ ctrl.name|snake_case }}_get_type())
> +static GType {{ ctrl.name|snake_case }}_get_type()
> +{
> +       static GType {{ ctrl.name|snake_case }}_type = 0;
> +
> +       if (!{{ ctrl.name|snake_case }}_type)
> +               {{ ctrl.name|snake_case }}_type =
> +                       g_enum_register_static("{{ ctrl.name }}",
> +                                              {{ ctrl.name|snake_case }}_types);
> +
> +       return {{ ctrl.name|snake_case }}_type;
> +}
> +{% endfor %}
> +{%- endfor %}
> +
> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)
> +{
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +{%- set spec %}
> +{%- if ctrl.is_rectangle -%}
> +gst_param_spec_array(
> +{%- else -%}
> +g_param_spec_{{ ctrl.gtype }}(
> +{%- endif -%}
> +{%- if ctrl.is_array %}
> +       "{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value",
> +       "{{ ctrl.name }} Value",
> +       "One {{ ctrl.name }} element value",
> +{%- else %}
> +       "{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +       "{{ ctrl.name }}",
> +       {{ ctrl.description|format_description|indent('\t') }},
> +{%- endif %}
> +{%- if ctrl.is_enum %}
> +       TYPE_{{ ctrl.name|snake_case|upper }},
> +       {{ ctrl.default }},
> +{%- elif ctrl.is_rectangle %}
> +       g_param_spec_int(
> +               "rectangle-value",
> +               "Rectangle Value",
> +               "One rectangle value, either x, y, width or height.",
> +               {{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +               (GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +                              G_PARAM_STATIC_STRINGS)
> +       ),
> +{%- elif ctrl.gtype == 'boolean' %}
> +       {{ ctrl.default }},
> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}
> +       {{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +{%- endif %}
> +       (GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +                      G_PARAM_STATIC_STRINGS)
> +)
> +{%- endset %}
> +
> +       g_object_class_install_property(
> +               klass,
> +               lastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},
> +{%- if ctrl.is_array %}
> +               gst_param_spec_array(
> +                       "{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +                       "{{ ctrl.name }}",
> +                       {{ ctrl.description|format_description|indent('\t\t\t') }},
> +                       {{ spec|indent('\t\t\t') }},
> +                       (GParamFlags) (GST_PARAM_CONTROLLABLE |
> +                                      G_PARAM_READWRITE |
> +                                      G_PARAM_STATIC_STRINGS)
> +               )
> +{%- else %}
> +               {{ spec|indent('\t\t') }}
> +{%- endif %}
> +       );
> +{%- endfor %}
> +{%- endfor %}
> +}
> +
> +bool GstCameraControls::getProperty(guint propId, GValue *value,
> +                                   [[maybe_unused]] GParamSpec *pspec)
> +{
> +       if (!controls_acc_.contains(propId)) {
> +               GST_WARNING("Control '%s' is not available, default value will "
> +                           "be returned",
> +                           controls::controls.at(propId)->name().c_str());
> +               return true;
> +       }
> +       const ControlValue &cv = controls_acc_.get(propId);
> +
> +       switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +       case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +               auto control = cv.get<{{ ctrl.type }}>();
> +
> +{%- if ctrl.is_array %}
> +               for (size_t i = 0; i < control.size(); ++i) {
> +                       GValue element = G_VALUE_INIT;
> +{%- if ctrl.is_rectangle %}
> +                       g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);
> +                       value_set_rectangle(&element, control[i]);
> +{%- else %}
> +                       g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});
> +                       g_value_set_{{ ctrl.gtype }}(&element, control[i]);
> +{%- endif %}
> +                       gst_value_array_append_and_take_value(value, &element);
> +               }
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +               value_set_rectangle(value, control);
> +{%- else %}
> +               g_value_set_{{ ctrl.gtype }}(value, control);
> +{%- endif %}
> +{%- endif %}
> +
> +               return true;
> +       }
> +{%- endfor %}
> +{%- endfor %}
> +
> +       default:
> +               return false;
> +       }
> +}
> +
> +bool GstCameraControls::setProperty(guint propId, const GValue *value,
> +                                   [[maybe_unused]] GParamSpec *pspec)
> +{
> +       /*
> +        * Check whether the camera capabilities are already available.
> +        * They might not be available if the pipeline has not started yet.
> +        */
> +       if (!capabilities_.empty()) {
> +               /* If so, check that the control is supported by the camera. */
> +               const ControlId *cid = capabilities_.idmap().at(propId);
> +               auto info = capabilities_.find(cid);
> +
> +               if (info == capabilities_.end()) {
> +                       GST_WARNING("Control '%s' is not supported by the "
> +                                   "camera and will be ignored",
> +                                   cid->name().c_str());
> +                       return true;
> +               }
> +       }
> +
> +       switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +       case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +               ControlValue control;
> +{%- if ctrl.is_array %}
> +               size_t size = gst_value_array_get_size(value);
> +{%- if ctrl.size != 0 %}
> +               if (size != {{ ctrl.size }}) {
> +                       GST_ERROR("Incorrect array size for control "
> +                                 "'{{ ctrl.name|kebab_case }}', must be of "
> +                                 "size {{ ctrl.size }}");
> +                       return true;
> +               }
> +{%- endif %}
> +
> +               std::vector<{{ ctrl.element_type }}> values(size);
> +               for (size_t i = 0; i < size; ++i) {
> +                       const GValue *element =
> +                               gst_value_array_get_value(value, i);
> +{%- if ctrl.is_rectangle %}
> +                       if (gst_value_array_get_size(element) != 4) {
> +                               GST_ERROR("Rectangle in control "
> +                                         "'{{ ctrl.name|kebab_case }}' at"
> +                                         "index %lu must be an array of size 4",
> +                                         i);
> +                               return true;
> +                       }
> +                       values[i] = value_get_rectangle(element);
> +{%- else %}
> +                       values[i] = g_value_get_{{ ctrl.gtype }}(element);
> +{%- endif %}
> +               }
> +
> +{%- if ctrl.size == 0 %}
> +               control.set(Span<const {{ ctrl.element_type }}>(values.data(),
> +                                                               size));
> +{%- else %}
> +               control.set(Span<const {{ ctrl.element_type }},
> +                                {{ ctrl.size }}>(values.data(),
> +                                                 {{ ctrl.size }}));
> +{%- endif %}
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +               if (gst_value_array_get_size(value) != 4) {
> +                       GST_ERROR("Rectangle in control "
> +                                 "'{{ ctrl.name|kebab_case }}' must be an "
> +                                 "array of size 4");
> +                       return true;
> +               }
> +               Rectangle val = value_get_rectangle(value);
> +{%- else %}
> +               auto val = g_value_get_{{ ctrl.gtype }}(value);
> +{%- endif %}
> +               control.set(val);
> +{%- endif %}
> +               controls_.set(propId, control);
> +               controls_acc_.set(propId, control);
> +               return true;
> +       }
> +{%- endfor %}
> +{%- endfor %}
> +
> +       default:
> +               return false;
> +       }
> +}
> +
> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)
> +{
> +       capabilities_ = cam->controls();
> +
> +       /*
> +        * Check the controls which were set before the camera capabilities were
> +        * known. This is required because GStreamer may set properties before
> +        * the pipeline has started and thus before the camera was known.
> +        */
> +       ControlList new_controls;
> +       for (auto control = controls_acc_.begin();
> +            control != controls_acc_.end();
> +            ++control) {
> +               unsigned int id = control->first;
> +               ControlValue value = control->second;
> +
> +               const ControlId *cid = capabilities_.idmap().at(id);
> +               auto info = capabilities_.find(cid);
> +
> +               /* Only add controls which are supported. */
> +               if (info != capabilities_.end())
> +                       new_controls.set(id, value);
> +               else
> +                       GST_WARNING("Control '%s' is not supported by the "
> +                                   "camera and will be ignored",
> +                                   cid->name().c_str());
> +       }
> +
> +       controls_acc_ = new_controls;
> +       controls_ = new_controls;
> +}
> +
> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)
> +{
> +       request->controls().merge(controls_);
> +       controls_.clear();
> +}
> +
> +void GstCameraControls::readMetadata(libcamera::Request *request)
> +{
> +       controls_acc_.merge(request->metadata(),
> +                           ControlList::MergePolicy::OverwriteExisting);
> +}
> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h
> new file mode 100644
> index 00000000..749220b5
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.h
> @@ -0,0 +1,43 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2023, Collabora Ltd.
> + *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
> + *
> + * GStreamer Camera Controls
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/camera.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/request.h>
> +
> +#include "gstlibcamerasrc.h"
> +
> +namespace libcamera {
> +
> +class GstCameraControls
> +{
> +public:
> +       static void installProperties(GObjectClass *klass, int lastProp);
> +
> +       bool getProperty(guint propId, GValue *value, GParamSpec *pspec);
> +       bool setProperty(guint propId, const GValue *value, GParamSpec *pspec);
> +
> +       void setCamera(const std::shared_ptr<libcamera::Camera> &cam);
> +
> +       void applyControls(std::unique_ptr<libcamera::Request> &request);
> +       void readMetadata(libcamera::Request *request);
> +
> +private:
> +       /* Supported controls and limits of camera. */
> +       ControlInfoMap capabilities_;
> +       /* Set of user modified controls. */
> +       ControlList controls_;
> +       /* Accumulator of all controls ever set and metadata returned by camera */
> +       ControlList controls_acc_;
> +};
> +
> +} /* namespace libcamera */
> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
> index 40b787c8..8efa25f4 100644
> --- a/src/gstreamer/gstlibcamerasrc.cpp
> +++ b/src/gstreamer/gstlibcamerasrc.cpp
> @@ -37,10 +37,11 @@
>  
>  #include <gst/base/base.h>
>  
> +#include "gstlibcamera-controls.h"
> +#include "gstlibcamera-utils.h"
>  #include "gstlibcameraallocator.h"
>  #include "gstlibcamerapad.h"
>  #include "gstlibcamerapool.h"
> -#include "gstlibcamera-utils.h"
>  
>  using namespace libcamera;
>  
> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {
>  
>         ControlList initControls_;
>         guint group_id_;
> +       GstCameraControls controls_;
>  
>         int queueRequest();
>         void requestCompleted(Request *request);
> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {
>  enum {
>         PROP_0,
>         PROP_CAMERA_NAME,
> +       PROP_LAST
>  };
>  
>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,
> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()
>         if (!request)
>                 return -ENOMEM;
>  
> +       /* Apply controls */
> +       controls_.applyControls(request);
> +
>         std::unique_ptr<RequestWrap> wrap =
>                 std::make_unique<RequestWrap>(std::move(request));
>  
> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)
>  
>         {
>                 GLibLocker locker(&lock_);
> +
> +               controls_.readMetadata(request);
> +
>                 wrap = std::move(queuedRequests_.front());
>                 queuedRequests_.pop();
>         }
> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)
>                 return false;
>         }
>  
> +       self->state->controls_.setCamera(cam);
> +
>         cam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);
>  
>         /* No need to lock here, we didn't start our threads yet. */
> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>  {
>         GLibLocker lock(GST_OBJECT(object));
>         GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +       GstLibcameraSrcState *state = self->state;
>  
>         switch (prop_id) {
>         case PROP_CAMERA_NAME:
> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>                 self->camera_name = g_value_dup_string(value);
>                 break;
>         default:
> -               G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +               if (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))
> +                       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>                 break;
>         }
>  }
> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,
>  {
>         GLibLocker lock(GST_OBJECT(object));
>         GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +       GstLibcameraSrcState *state = self->state;
>  
>         switch (prop_id) {
>         case PROP_CAMERA_NAME:
>                 g_value_set_string(value, self->camera_name);
>                 break;
>         default:
> -               G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +               if (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))
> +                       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>                 break;
>         }
>  }
> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)
>                                                              | G_PARAM_STATIC_STRINGS));
>         g_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);
>  
> +       GstCameraControls::installProperties(object_class, PROP_LAST);
>  }
>  
>  /* GstChildProxy implementation */
> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build
> index c2a01e7b..6b7e53b5 100644
> --- a/src/gstreamer/meson.build
> +++ b/src/gstreamer/meson.build
> @@ -25,6 +25,16 @@ libcamera_gst_sources = [
>      'gstlibcamerasrc.cpp',
>  ]
>  
> +# Generate gstreamer control properties
> +
> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')
> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',
> +                                       input : controls_files,
> +                                       output : 'gstlibcamera-controls.cpp',
> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',
> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],
> +                                       env : py_build_env)
> +
>  libcamera_gst_cpp_args = [
>      '-DVERSION="@0@"'.format(libcamera_git_version),
>      '-DPACKAGE="@0@"'.format(meson.project_name()),
> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py
> index 7bafee59..03c77cc6 100644
> --- a/utils/codegen/controls.py
> +++ b/utils/codegen/controls.py
> @@ -110,3 +110,11 @@ class Control(object):
>              return f"Span<const {typ}, {self.__size}>"
>          else:
>              return f"Span<const {typ}>"
> +
> +    @property
> +    def element_type(self):
> +        return self.__data.get('type')
> +
> +    @property
> +    def size(self):
> +        return self.__size
> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py
> new file mode 100755
> index 00000000..0b899258
> --- /dev/null
> +++ b/utils/codegen/gen-gst-controls.py
> @@ -0,0 +1,166 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2019, Google Inc.
> +# Copyright (C) 2024, Jaslo Ziska
> +#
> +# Authors:
> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> +# Jaslo Ziska <jaslo@ziska.de>
> +#
> +# Generate gstreamer control properties from YAML
> +
> +import argparse
> +import jinja2
> +import re
> +import sys
> +import yaml
> +
> +from controls import Control
> +
> +
> +exposed_controls = [
> +    "AeEnable", "AeLocked", "AeMeteringMode", "AeConstraintMode",
> +    "AeExposureMode", "ExposureValue", "ExposureTime", "AnalogueGain",
> +    "AeFlickerMode", "AeFlickerPeriod", "AeFlickerDetected", "Brightness",
> +    "Contrast", "Lux", "AwbEnable", "AwbMode", "AwbLocked", "ColourGains",
> +    "ColourTemperature", "Saturation", "SensorBlackLevels", "Sharpness",
> +    "FocusFoM", "ColourCorrectionMatrix", "ScalerCrop", "DigitalGain",
> +    "FrameDuration", "SensorTemperature", "SensorTimestamp", "AfMode",
> +    "AfRange", "AfSpeed", "AfMetering", "AfWindows", "LensPosition", "AfState",
> +    "AfPauseState", "Gamma",
> +]
> +
> +
> +def find_common_prefix(strings):
> +    prefix = strings[0]
> +
> +    for string in strings[1:]:
> +        while string[:len(prefix)] != prefix and prefix:
> +            prefix = prefix[:len(prefix) - 1]
> +        if not prefix:
> +            break
> +
> +    return prefix
> +
> +
> +def format_description(description):
> +    # Substitute doxygen keywords \sa (see also) and \todo
> +    description = re.sub(r'\\sa((?: \w+)+)',
> +                         lambda match: 'See also: ' + ', '.join(
> +                             map(kebab_case, match.group(1).strip().split(' '))
> +                         ) + '.', description)
> +    description = re.sub(r'\\todo', 'Todo:', description)
> +
> +    description = description.strip().split('\n')
> +    return '\n'.join([
> +        '"' + line.replace('\\', r'\\').replace('"', r'\"') + ' "' for line in description if line
> +    ]).rstrip()
> +
> +
> +def snake_case(s):
> +    return ''.join([
> +        c.isupper() and ('_' + c.lower()) or c for c in s
> +    ]).strip('_')
> +
> +
> +def kebab_case(s):
> +    return snake_case(s).replace('_', '-')
> +
> +
> +def extend_control(ctrl):
> +    if ctrl.vendor != 'libcamera':
> +        ctrl.namespace = f'{ctrl.vendor}::'
> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'
> +    else:
> +        ctrl.namespace = ''
> +        ctrl.vendor_prefix = ''
> +
> +    ctrl.is_array = ctrl.size is not None
> +
> +    if ctrl.is_enum:
> +        # Remove common prefix from enum variant names
> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])
> +        for enum in ctrl.enum_values:
> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))
> +
> +        ctrl.gtype = 'enum'
> +        ctrl.default = '0'
> +    elif ctrl.element_type == 'bool':
> +        ctrl.gtype = 'boolean'
> +        ctrl.default = 'false'
> +    elif ctrl.element_type == 'float':
> +        ctrl.gtype = 'float'
> +        ctrl.default = '0'
> +        ctrl.min = '-G_MAXFLOAT'
> +        ctrl.max = 'G_MAXFLOAT'
> +    elif ctrl.element_type == 'int32_t':
> +        ctrl.gtype = 'int'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT'
> +        ctrl.max = 'G_MAXINT'
> +    elif ctrl.element_type == 'int64_t':
> +        ctrl.gtype = 'int64'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT64'
> +        ctrl.max = 'G_MAXINT64'
> +    elif ctrl.element_type == 'uint8_t':
> +        ctrl.gtype = 'uchar'
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXUINT8'
> +    elif ctrl.element_type == 'Rectangle':
> +        ctrl.is_rectangle = True
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXINT'
> +    else:
> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')
> +
> +    return ctrl
> +
> +
> +def main(argv):
> +    # Parse command line arguments
> +    parser = argparse.ArgumentParser()
> +    parser.add_argument('--output', '-o', metavar='file', type=str,
> +                        help='Output file name. Defaults to standard output if not specified.')
> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,
> +                        help='Template file name.')
> +    parser.add_argument('input', type=str, nargs='+',
> +                        help='Input file name.')
> +
> +    args = parser.parse_args(argv[1:])
> +
> +    controls = {}
> +    for input in args.input:
> +        data = yaml.safe_load(open(input, 'rb').read())
> +
> +        vendor = data['vendor']
> +        ctrls = controls.setdefault(vendor, [])
> +
> +        for ctrl in data['controls']:
> +            ctrl = Control(*ctrl.popitem(), vendor)
> +
> +            if ctrl.name in exposed_controls:
> +                ctrls.append(extend_control(ctrl))
> +
> +    data = {'controls': list(controls.items())}
> +
> +    env = jinja2.Environment()
> +    env.filters['format_description'] = format_description
> +    env.filters['snake_case'] = snake_case
> +    env.filters['kebab_case'] = kebab_case
> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())
> +    string = template.render(data)
> +
> +    if args.output:
> +        with open(args.output, 'w', encoding='utf-8') as output:
> +            output.write(string)
> +    else:
> +        sys.stdout.write(string)
> +
> +    return 0
> +
> +
> +if __name__ == '__main__':
> +    sys.exit(main(sys.argv))
> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build
> index adf33bba..904dd66d 100644
> --- a/utils/codegen/meson.build
> +++ b/utils/codegen/meson.build
> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']
>  
>  gen_controls = files('gen-controls.py')
>  gen_formats = files('gen-formats.py')
> +gen_gst_controls = files('gen-gst-controls.py')
>  gen_header = files('gen-header.sh')
>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')
>  gen_tracepoints = files('gen-tp-header.py')
> -- 
> 2.46.2
>
Nicolas Dufresne Oct. 18, 2024, 6:19 p.m. UTC | #2
Le jeudi 17 octobre 2024 à 19:04 +0200, Jaslo Ziska a écrit :
> This commit implements gstreamer controls for the libcamera element by
> generating the controls from the control_ids_*.yaml files using a new
> gen-gst-controls.py script. The appropriate meson files are also changed
> to automatically run the script when building.
> 
> The gen-gst-controls.py script works similar to the gen-controls.py
> script by parsing the control_ids_*.yaml files and generating C++ code
> for each exposed control.
> For the controls to be used as gstreamer properties the type for each
> control needs to be translated to the appropriate glib type and a
> GEnumValue is generated for each enum control. Then a
> g_object_install_property(), _get_property() and _set_property()
> function is generated for each control.
> The vendor controls get prefixed with "$vendor-" in the final gstreamer
> property name.
> 
> The C++ code generated by the gen-gst-controls.py script is written into
> the template gstlibcamerasrc-controls.cpp.in file. The matching
> gstlibcamerasrc-controls.h header defines the GstCameraControls class
> which handles the installation of the gstreamer properties as well as
> keeping track of the control values and setting and getting the
> controls. The content of these functions is generated in the Python
> script.
> 
> Finally the libcamerasrc element itself is edited to make use of the new
> GstCameraControls class. The way this works is by defining a PROP_LAST
> enum variant which is passed to the installProperties() function so the
> properties are defined with the appropriate offset. When getting or
> setting a property PROP_LAST is subtracted from the requested property
> to translate the control back into a libcamera::controls:: enum
> variant.
> 
> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>
> ---
>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++
>  src/gstreamer/gstlibcamera-controls.h      |  43 +++
>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-
>  src/gstreamer/meson.build                  |  10 +
>  utils/codegen/controls.py                  |   8 +
>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++
>  utils/codegen/meson.build                  |   1 +
>  7 files changed, 579 insertions(+), 3 deletions(-)
>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in
>  create mode 100644 src/gstreamer/gstlibcamera-controls.h
>  create mode 100755 utils/codegen/gen-gst-controls.py
> 
> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in
> new file mode 100644
> index 00000000..6861f5d1
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in
> @@ -0,0 +1,332 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Jaslo Ziska
> + *
> + * GStreamer Camera Controls
> + *
> + * This file is auto-generated. Do not edit.
> + */
> +
> +#include <vector>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/geometry.h>
> +
> +#include "gstlibcamera-controls.h"
> +
> +using namespace libcamera;
> +
> +static void value_set_rectangle(GValue *value, const Rectangle &rect)
> +{
> +	Point top_left = rect.topLeft();
> +	Size size = rect.size();
> +
> +	GValue x = G_VALUE_INIT;
> +	g_value_init(&x, G_TYPE_INT);
> +	g_value_set_int(&x, top_left.x);
> +	gst_value_array_append_and_take_value(value, &x);
> +
> +	GValue y = G_VALUE_INIT;
> +	g_value_init(&y, G_TYPE_INT);
> +	g_value_set_int(&y, top_left.y);
> +	gst_value_array_append_and_take_value(value, &y);
> +
> +	GValue width = G_VALUE_INIT;
> +	g_value_init(&width, G_TYPE_INT);
> +	g_value_set_int(&width, size.width);
> +	gst_value_array_append_and_take_value(value, &width);
> +
> +	GValue height = G_VALUE_INIT;
> +	g_value_init(&height, G_TYPE_INT);
> +	g_value_set_int(&x, size.height);
> +	gst_value_array_append_and_take_value(value, &height);
> +}
> +
> +static Rectangle value_get_rectangle(const GValue *value)
> +{
> +	const GValue *r;
> +	r = gst_value_array_get_value(value, 0);
> +	int x = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 1);
> +	int y = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 2);
> +	int w = g_value_get_int(r);
> +	r = gst_value_array_get_value(value, 3);
> +	int h = g_value_get_int(r);
> +
> +	return Rectangle(x, y, w, h);
> +}
> +
> +{% for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls if ctrl.is_enum %}
> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {
> +{%- for enum in ctrl.enum_values %}
> +	{
> +		controls::{{ ctrl.namespace }}{{ enum.name }},
> +		{{ enum.description|format_description|indent('\t\t') }},
> +		"{{ enum.gst_name }}"
> +	},
> +{%- endfor %}
> +	{0, NULL, NULL}
> +};
> +
> +#define TYPE_{{ ctrl.name|snake_case|upper }} \
> +	({{ ctrl.name|snake_case }}_get_type())
> +static GType {{ ctrl.name|snake_case }}_get_type()
> +{
> +	static GType {{ ctrl.name|snake_case }}_type = 0;
> +
> +	if (!{{ ctrl.name|snake_case }}_type)
> +		{{ ctrl.name|snake_case }}_type =
> +			g_enum_register_static("{{ ctrl.name }}",
> +					       {{ ctrl.name|snake_case }}_types);
> +
> +	return {{ ctrl.name|snake_case }}_type;
> +}
> +{% endfor %}
> +{%- endfor %}
> +
> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)
> +{
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +{%- set spec %}
> +{%- if ctrl.is_rectangle -%}
> +gst_param_spec_array(
> +{%- else -%}
> +g_param_spec_{{ ctrl.gtype }}(
> +{%- endif -%}
> +{%- if ctrl.is_array %}
> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value",
> +	"{{ ctrl.name }} Value",
> +	"One {{ ctrl.name }} element value",
> +{%- else %}
> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +	"{{ ctrl.name }}",
> +	{{ ctrl.description|format_description|indent('\t') }},
> +{%- endif %}
> +{%- if ctrl.is_enum %}
> +	TYPE_{{ ctrl.name|snake_case|upper }},
> +	{{ ctrl.default }},
> +{%- elif ctrl.is_rectangle %}
> +	g_param_spec_int(
> +		"rectangle-value",
> +		"Rectangle Value",
> +		"One rectangle value, either x, y, width or height.",
> +		{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +		(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +			       G_PARAM_STATIC_STRINGS)
> +	),
> +{%- elif ctrl.gtype == 'boolean' %}
> +	{{ ctrl.default }},
> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}
> +	{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
> +{%- endif %}
> +	(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
> +		       G_PARAM_STATIC_STRINGS)
> +)
> +{%- endset %}
> +
> +	g_object_class_install_property(
> +		klass,
> +		lastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},
> +{%- if ctrl.is_array %}
> +		gst_param_spec_array(
> +			"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
> +			"{{ ctrl.name }}",
> +			{{ ctrl.description|format_description|indent('\t\t\t') }},
> +			{{ spec|indent('\t\t\t') }},
> +			(GParamFlags) (GST_PARAM_CONTROLLABLE |
> +				       G_PARAM_READWRITE |
> +				       G_PARAM_STATIC_STRINGS)
> +		)
> +{%- else %}
> +		{{ spec|indent('\t\t') }}
> +{%- endif %}
> +	);
> +{%- endfor %}
> +{%- endfor %}
> +}
> +
> +bool GstCameraControls::getProperty(guint propId, GValue *value,
> +				    [[maybe_unused]] GParamSpec *pspec)
> +{
> +	if (!controls_acc_.contains(propId)) {
> +		GST_WARNING("Control '%s' is not available, default value will "
> +			    "be returned",
> +			    controls::controls.at(propId)->name().c_str());
> +		return true;
> +	}
> +	const ControlValue &cv = controls_acc_.get(propId);
> +
> +	switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +		auto control = cv.get<{{ ctrl.type }}>();
> +
> +{%- if ctrl.is_array %}
> +		for (size_t i = 0; i < control.size(); ++i) {
> +			GValue element = G_VALUE_INIT;
> +{%- if ctrl.is_rectangle %}
> +			g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);
> +			value_set_rectangle(&element, control[i]);
> +{%- else %}
> +			g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});
> +			g_value_set_{{ ctrl.gtype }}(&element, control[i]);
> +{%- endif %}
> +			gst_value_array_append_and_take_value(value, &element);
> +		}
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +		value_set_rectangle(value, control);
> +{%- else %}
> +		g_value_set_{{ ctrl.gtype }}(value, control);
> +{%- endif %}
> +{%- endif %}
> +
> +		return true;
> +	}
> +{%- endfor %}
> +{%- endfor %}
> +
> +	default:
> +		return false;
> +	}
> +}
> +
> +bool GstCameraControls::setProperty(guint propId, const GValue *value,
> +				    [[maybe_unused]] GParamSpec *pspec)
> +{
> +	/*
> +	 * Check whether the camera capabilities are already available.
> +	 * They might not be available if the pipeline has not started yet.
> +	 */
> +	if (!capabilities_.empty()) {
> + 		/* If so, check that the control is supported by the camera. */
> +		const ControlId *cid = capabilities_.idmap().at(propId);
> +		auto info = capabilities_.find(cid);
> +
> +		if (info == capabilities_.end()) {
> +			GST_WARNING("Control '%s' is not supported by the "
> +				    "camera and will be ignored",
> +				    cid->name().c_str());
> +			return true;
> +		}
> +	}
> +
> +	switch (propId) {
> +{%- for vendor, ctrls in controls %}
> +{%- for ctrl in ctrls %}
> +
> +	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
> +		ControlValue control;
> +{%- if ctrl.is_array %}
> +		size_t size = gst_value_array_get_size(value);
> +{%- if ctrl.size != 0 %}
> +		if (size != {{ ctrl.size }}) {
> +			GST_ERROR("Incorrect array size for control "
> +				  "'{{ ctrl.name|kebab_case }}', must be of "
> +				  "size {{ ctrl.size }}");
> +			return true;
> +		}
> +{%- endif %}
> +
> +		std::vector<{{ ctrl.element_type }}> values(size);
> +		for (size_t i = 0; i < size; ++i) {
> +			const GValue *element =
> +				gst_value_array_get_value(value, i);
> +{%- if ctrl.is_rectangle %}
> +			if (gst_value_array_get_size(element) != 4) {
> +				GST_ERROR("Rectangle in control "
> +					  "'{{ ctrl.name|kebab_case }}' at"
> +					  "index %lu must be an array of size 4",
> +					  i);
> +				return true;
> +			}
> +			values[i] = value_get_rectangle(element);
> +{%- else %}
> +			values[i] = g_value_get_{{ ctrl.gtype }}(element);
> +{%- endif %}
> +		}
> +
> +{%- if ctrl.size == 0 %}
> +		control.set(Span<const {{ ctrl.element_type }}>(values.data(),
> +								size));
> +{%- else %}
> +		control.set(Span<const {{ ctrl.element_type }},
> +			         {{ ctrl.size }}>(values.data(),
> +						  {{ ctrl.size }}));
> +{%- endif %}
> +{%- else %}
> +{%- if ctrl.is_rectangle %}
> +		if (gst_value_array_get_size(value) != 4) {
> +			GST_ERROR("Rectangle in control "
> +				  "'{{ ctrl.name|kebab_case }}' must be an "
> +				  "array of size 4");
> +			return true;
> +		}
> +		Rectangle val = value_get_rectangle(value);
> +{%- else %}
> +		auto val = g_value_get_{{ ctrl.gtype }}(value);
> +{%- endif %}
> +		control.set(val);
> +{%- endif %}
> +		controls_.set(propId, control);
> +		controls_acc_.set(propId, control);
> +		return true;
> +	}
> +{%- endfor %}
> +{%- endfor %}
> +
> +	default:
> +		return false;
> +	}
> +}
> +
> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)
> +{
> +	capabilities_ = cam->controls();
> +
> +	/*
> +	 * Check the controls which were set before the camera capabilities were
> +	 * known. This is required because GStreamer may set properties before
> +	 * the pipeline has started and thus before the camera was known.
> +	 */
> +	ControlList new_controls;
> +	for (auto control = controls_acc_.begin();
> +	     control != controls_acc_.end();
> +	     ++control) {
> +		unsigned int id = control->first;
> +		ControlValue value = control->second;
> +
> +		const ControlId *cid = capabilities_.idmap().at(id);
> +		auto info = capabilities_.find(cid);
> +
> +		/* Only add controls which are supported. */
> +		if (info != capabilities_.end())
> +			new_controls.set(id, value);
> +		else
> +			GST_WARNING("Control '%s' is not supported by the "
> +				    "camera and will be ignored",
> +				    cid->name().c_str());
> +	}
> +
> +	controls_acc_ = new_controls;
> +	controls_ = new_controls;
> +}
> +
> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)
> +{
> +	request->controls().merge(controls_);
> +	controls_.clear();
> +}
> +
> +void GstCameraControls::readMetadata(libcamera::Request *request)
> +{
> +	controls_acc_.merge(request->metadata(),
> +			    ControlList::MergePolicy::OverwriteExisting);
> +}
> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h
> new file mode 100644
> index 00000000..749220b5
> --- /dev/null
> +++ b/src/gstreamer/gstlibcamera-controls.h
> @@ -0,0 +1,43 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2023, Collabora Ltd.
> + *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
> + *
> + * GStreamer Camera Controls
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/camera.h>
> +#include <libcamera/controls.h>
> +#include <libcamera/request.h>
> +
> +#include "gstlibcamerasrc.h"
> +
> +namespace libcamera {
> +
> +class GstCameraControls
> +{
> +public:
> +	static void installProperties(GObjectClass *klass, int lastProp);
> +
> +	bool getProperty(guint propId, GValue *value, GParamSpec *pspec);
> +	bool setProperty(guint propId, const GValue *value, GParamSpec *pspec);
> +
> +	void setCamera(const std::shared_ptr<libcamera::Camera> &cam);
> +
> +	void applyControls(std::unique_ptr<libcamera::Request> &request);
> +	void readMetadata(libcamera::Request *request);
> +
> +private:
> +	/* Supported controls and limits of camera. */
> +	ControlInfoMap capabilities_;
> +	/* Set of user modified controls. */
> +	ControlList controls_;
> +	/* Accumulator of all controls ever set and metadata returned by camera */
> +	ControlList controls_acc_;
> +};
> +
> +} /* namespace libcamera */
> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
> index 40b787c8..8efa25f4 100644
> --- a/src/gstreamer/gstlibcamerasrc.cpp
> +++ b/src/gstreamer/gstlibcamerasrc.cpp
> @@ -37,10 +37,11 @@
>  
>  #include <gst/base/base.h>
>  
> +#include "gstlibcamera-controls.h"
> +#include "gstlibcamera-utils.h"
>  #include "gstlibcameraallocator.h"
>  #include "gstlibcamerapad.h"
>  #include "gstlibcamerapool.h"
> -#include "gstlibcamera-utils.h"
>  
>  using namespace libcamera;
>  
> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {
>  
>  	ControlList initControls_;
>  	guint group_id_;
> +	GstCameraControls controls_;
>  
>  	int queueRequest();
>  	void requestCompleted(Request *request);
> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {
>  enum {
>  	PROP_0,
>  	PROP_CAMERA_NAME,
> +	PROP_LAST
>  };
>  
>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,
> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()
>  	if (!request)
>  		return -ENOMEM;
>  
> +	/* Apply controls */
> +	controls_.applyControls(request);
> +
>  	std::unique_ptr<RequestWrap> wrap =
>  		std::make_unique<RequestWrap>(std::move(request));
>  
> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)
>  
>  	{
>  		GLibLocker locker(&lock_);
> +
> +		controls_.readMetadata(request);
> +
>  		wrap = std::move(queuedRequests_.front());
>  		queuedRequests_.pop();
>  	}
> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)
>  		return false;
>  	}
>  
> +	self->state->controls_.setCamera(cam);
> +
>  	cam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);
>  
>  	/* No need to lock here, we didn't start our threads yet. */
> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>  {
>  	GLibLocker lock(GST_OBJECT(object));
>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +	GstLibcameraSrcState *state = self->state;
>  
>  	switch (prop_id) {
>  	case PROP_CAMERA_NAME:
> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,
>  		self->camera_name = g_value_dup_string(value);
>  		break;
>  	default:
> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +		if (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))
> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>  		break;
>  	}
>  }
> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,
>  {
>  	GLibLocker lock(GST_OBJECT(object));
>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
> +	GstLibcameraSrcState *state = self->state;
>  
>  	switch (prop_id) {
>  	case PROP_CAMERA_NAME:
>  		g_value_set_string(value, self->camera_name);
>  		break;
>  	default:
> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
> +		if (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))
> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
>  		break;
>  	}
>  }
> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)
>  							     | G_PARAM_STATIC_STRINGS));
>  	g_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);
>  
> +	GstCameraControls::installProperties(object_class, PROP_LAST);
>  }
>  
>  /* GstChildProxy implementation */
> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build
> index c2a01e7b..6b7e53b5 100644
> --- a/src/gstreamer/meson.build
> +++ b/src/gstreamer/meson.build
> @@ -25,6 +25,16 @@ libcamera_gst_sources = [
>      'gstlibcamerasrc.cpp',
>  ]
>  
> +# Generate gstreamer control properties
> +
> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')
> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',
> +                                       input : controls_files,
> +                                       output : 'gstlibcamera-controls.cpp',
> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',
> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],
> +                                       env : py_build_env)
> +
>  libcamera_gst_cpp_args = [
>      '-DVERSION="@0@"'.format(libcamera_git_version),
>      '-DPACKAGE="@0@"'.format(meson.project_name()),
> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py
> index 7bafee59..03c77cc6 100644
> --- a/utils/codegen/controls.py
> +++ b/utils/codegen/controls.py
> @@ -110,3 +110,11 @@ class Control(object):
>              return f"Span<const {typ}, {self.__size}>"
>          else:
>              return f"Span<const {typ}>"
> +
> +    @property
> +    def element_type(self):
> +        return self.__data.get('type')
> +
> +    @property
> +    def size(self):
> +        return self.__size
> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py
> new file mode 100755
> index 00000000..0b899258
> --- /dev/null
> +++ b/utils/codegen/gen-gst-controls.py
> @@ -0,0 +1,166 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2019, Google Inc.
> +# Copyright (C) 2024, Jaslo Ziska
> +#
> +# Authors:
> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> +# Jaslo Ziska <jaslo@ziska.de>
> +#
> +# Generate gstreamer control properties from YAML
> +
> +import argparse
> +import jinja2
> +import re
> +import sys
> +import yaml
> +
> +from controls import Control
> +
> +
> +exposed_controls = [
> +    "AeEnable", "AeLocked", "AeMeteringMode", "AeConstraintMode",

AeLocked is not settable, until we can expose it correctly I'd drop. We might
also want something more accurate, so we can match an exact frame?

> +    "AeExposureMode", "ExposureValue", "ExposureTime", "AnalogueGain",
> +    "AeFlickerMode", "AeFlickerPeriod", "AeFlickerDetected", "Brightness",

I'm not sure about AeFlickerMode and AeFlickerDetected, and its supposed to be
read-only. Without a use case I cannot tell the proper way to expose these, I
would drop that.

> +    "Contrast", "Lux", "AwbEnable", "AwbMode", "AwbLocked", "ColourGains",

AwbLocked is similar to AeLocked. Same goes for ColourGains and
ColourTemperature, probably fine as property, but read-only.

> 
> +    "ColourTemperature", "Saturation", "SensorBlackLevels", "Sharpness",
> +    "FocusFoM", "ColourCorrectionMatrix", "ScalerCrop", "DigitalGain",
> +    "FrameDuration", "SensorTemperature", "SensorTimestamp", "AfMode",

FrameDuration and SensorTimestamp should be meta, and attached to GstBuffer, I
would drop it from the list in this serie. SensorTimestamp is clearly a fit for
GstReferenceTimestampMeta. I would need to read more about FrameDuration to see
if we should use it, perhaps could be set to GstBuffer.duration ?

> +    "AfRange", "AfSpeed", "AfMetering", "AfWindows", "LensPosition", "AfState",
> +    "AfPauseState", "Gamma",

AfState, AfPausedState are not settable, I'd remove for now until we can
generate read-only properties. 

cheers
Nicolas

> +]
> +
> +
> +def find_common_prefix(strings):
> +    prefix = strings[0]
> +
> +    for string in strings[1:]:
> +        while string[:len(prefix)] != prefix and prefix:
> +            prefix = prefix[:len(prefix) - 1]
> +        if not prefix:
> +            break
> +
> +    return prefix
> +
> +
> +def format_description(description):
> +    # Substitute doxygen keywords \sa (see also) and \todo
> +    description = re.sub(r'\\sa((?: \w+)+)',
> +                         lambda match: 'See also: ' + ', '.join(
> +                             map(kebab_case, match.group(1).strip().split(' '))
> +                         ) + '.', description)
> +    description = re.sub(r'\\todo', 'Todo:', description)
> +
> +    description = description.strip().split('\n')
> +    return '\n'.join([
> +        '"' + line.replace('\\', r'\\').replace('"', r'\"') + ' "' for line in description if line
> +    ]).rstrip()
> +
> +
> +def snake_case(s):
> +    return ''.join([
> +        c.isupper() and ('_' + c.lower()) or c for c in s
> +    ]).strip('_')
> +
> +
> +def kebab_case(s):
> +    return snake_case(s).replace('_', '-')
> +
> +
> +def extend_control(ctrl):
> +    if ctrl.vendor != 'libcamera':
> +        ctrl.namespace = f'{ctrl.vendor}::'
> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'
> +    else:
> +        ctrl.namespace = ''
> +        ctrl.vendor_prefix = ''
> +
> +    ctrl.is_array = ctrl.size is not None
> +
> +    if ctrl.is_enum:
> +        # Remove common prefix from enum variant names
> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])
> +        for enum in ctrl.enum_values:
> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))
> +
> +        ctrl.gtype = 'enum'
> +        ctrl.default = '0'
> +    elif ctrl.element_type == 'bool':
> +        ctrl.gtype = 'boolean'
> +        ctrl.default = 'false'
> +    elif ctrl.element_type == 'float':
> +        ctrl.gtype = 'float'
> +        ctrl.default = '0'
> +        ctrl.min = '-G_MAXFLOAT'
> +        ctrl.max = 'G_MAXFLOAT'
> +    elif ctrl.element_type == 'int32_t':
> +        ctrl.gtype = 'int'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT'
> +        ctrl.max = 'G_MAXINT'
> +    elif ctrl.element_type == 'int64_t':
> +        ctrl.gtype = 'int64'
> +        ctrl.default = '0'
> +        ctrl.min = 'G_MININT64'
> +        ctrl.max = 'G_MAXINT64'
> +    elif ctrl.element_type == 'uint8_t':
> +        ctrl.gtype = 'uchar'
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXUINT8'
> +    elif ctrl.element_type == 'Rectangle':
> +        ctrl.is_rectangle = True
> +        ctrl.default = '0'
> +        ctrl.min = '0'
> +        ctrl.max = 'G_MAXINT'
> +    else:
> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')
> +
> +    return ctrl
> +
> +
> +def main(argv):
> +    # Parse command line arguments
> +    parser = argparse.ArgumentParser()
> +    parser.add_argument('--output', '-o', metavar='file', type=str,
> +                        help='Output file name. Defaults to standard output if not specified.')
> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,
> +                        help='Template file name.')
> +    parser.add_argument('input', type=str, nargs='+',
> +                        help='Input file name.')
> +
> +    args = parser.parse_args(argv[1:])
> +
> +    controls = {}
> +    for input in args.input:
> +        data = yaml.safe_load(open(input, 'rb').read())
> +
> +        vendor = data['vendor']
> +        ctrls = controls.setdefault(vendor, [])
> +
> +        for ctrl in data['controls']:
> +            ctrl = Control(*ctrl.popitem(), vendor)
> +
> +            if ctrl.name in exposed_controls:
> +                ctrls.append(extend_control(ctrl))
> +
> +    data = {'controls': list(controls.items())}
> +
> +    env = jinja2.Environment()
> +    env.filters['format_description'] = format_description
> +    env.filters['snake_case'] = snake_case
> +    env.filters['kebab_case'] = kebab_case
> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())
> +    string = template.render(data)
> +
> +    if args.output:
> +        with open(args.output, 'w', encoding='utf-8') as output:
> +            output.write(string)
> +    else:
> +        sys.stdout.write(string)
> +
> +    return 0
> +
> +
> +if __name__ == '__main__':
> +    sys.exit(main(sys.argv))
> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build
> index adf33bba..904dd66d 100644
> --- a/utils/codegen/meson.build
> +++ b/utils/codegen/meson.build
> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']
>  
>  gen_controls = files('gen-controls.py')
>  gen_formats = files('gen-formats.py')
> +gen_gst_controls = files('gen-gst-controls.py')
>  gen_header = files('gen-header.sh')
>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')
>  gen_tracepoints = files('gen-tp-header.py')
Jaslo Ziska Oct. 21, 2024, 4:07 p.m. UTC | #3
Hi Nicolas,

thanks for the review.

Nicolas Dufresne <nicolas@ndufresne.ca> writes:
> Le jeudi 17 octobre 2024 à 19:04 +0200, Jaslo Ziska a écrit :
>> This commit implements gstreamer controls for the libcamera 
>> element by
>> generating the controls from the control_ids_*.yaml files using 
>> a new
>> gen-gst-controls.py script. The appropriate meson files are 
>> also changed
>> to automatically run the script when building.
>> 
>> The gen-gst-controls.py script works similar to the 
>> gen-controls.py
>> script by parsing the control_ids_*.yaml files and generating 
>> C++ code
>> for each exposed control.
>> For the controls to be used as gstreamer properties the type 
>> for each
>> control needs to be translated to the appropriate glib type and 
>> a
>> GEnumValue is generated for each enum control. Then a
>> g_object_install_property(), _get_property() and 
>> _set_property()
>> function is generated for each control.
>> The vendor controls get prefixed with "$vendor-" in the final 
>> gstreamer
>> property name.
>> 
>> The C++ code generated by the gen-gst-controls.py script is 
>> written into
>> the template gstlibcamerasrc-controls.cpp.in file. The matching
>> gstlibcamerasrc-controls.h header defines the GstCameraControls 
>> class
>> which handles the installation of the gstreamer properties as 
>> well as
>> keeping track of the control values and setting and getting the
>> controls. The content of these functions is generated in the 
>> Python
>> script.
>> 
>> Finally the libcamerasrc element itself is edited to make use 
>> of the new
>> GstCameraControls class. The way this works is by defining a 
>> PROP_LAST
>> enum variant which is passed to the installProperties() 
>> function so the
>> properties are defined with the appropriate offset. When 
>> getting or
>> setting a property PROP_LAST is subtracted from the requested 
>> property
>> to translate the control back into a libcamera::controls:: enum
>> variant.
>> 
>> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>
>> ---
>>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 
>>  +++++++++++++++++++++
>>  src/gstreamer/gstlibcamera-controls.h      |  43 +++
>>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-
>>  src/gstreamer/meson.build                  |  10 +
>>  utils/codegen/controls.py                  |   8 +
>>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++
>>  utils/codegen/meson.build                  |   1 +
>>  7 files changed, 579 insertions(+), 3 deletions(-)
>>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in
>>  create mode 100644 src/gstreamer/gstlibcamera-controls.h
>>  create mode 100755 utils/codegen/gen-gst-controls.py
>> 
>> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in 
>> b/src/gstreamer/gstlibcamera-controls.cpp.in
>> new file mode 100644
>> index 00000000..6861f5d1
>> --- /dev/null
>> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in
>> @@ -0,0 +1,332 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2024, Jaslo Ziska
>> + *
>> + * GStreamer Camera Controls
>> + *
>> + * This file is auto-generated. Do not edit.
>> + */
>> +
>> +#include <vector>
>> +
>> +#include <libcamera/control_ids.h>
>> +#include <libcamera/controls.h>
>> +#include <libcamera/geometry.h>
>> +
>> +#include "gstlibcamera-controls.h"
>> +
>> +using namespace libcamera;
>> +
>> +static void value_set_rectangle(GValue *value, const Rectangle 
>> &rect)
>> +{
>> +	Point top_left = rect.topLeft();
>> +	Size size = rect.size();
>> +
>> +	GValue x = G_VALUE_INIT;
>> +	g_value_init(&x, G_TYPE_INT);
>> +	g_value_set_int(&x, top_left.x);
>> +	gst_value_array_append_and_take_value(value, &x);
>> +
>> +	GValue y = G_VALUE_INIT;
>> +	g_value_init(&y, G_TYPE_INT);
>> +	g_value_set_int(&y, top_left.y);
>> +	gst_value_array_append_and_take_value(value, &y);
>> +
>> +	GValue width = G_VALUE_INIT;
>> +	g_value_init(&width, G_TYPE_INT);
>> +	g_value_set_int(&width, size.width);
>> +	gst_value_array_append_and_take_value(value, &width);
>> +
>> +	GValue height = G_VALUE_INIT;
>> +	g_value_init(&height, G_TYPE_INT);
>> +	g_value_set_int(&x, size.height);
>> +	gst_value_array_append_and_take_value(value, &height);
>> +}
>> +
>> +static Rectangle value_get_rectangle(const GValue *value)
>> +{
>> +	const GValue *r;
>> +	r = gst_value_array_get_value(value, 0);
>> +	int x = g_value_get_int(r);
>> +	r = gst_value_array_get_value(value, 1);
>> +	int y = g_value_get_int(r);
>> +	r = gst_value_array_get_value(value, 2);
>> +	int w = g_value_get_int(r);
>> +	r = gst_value_array_get_value(value, 3);
>> +	int h = g_value_get_int(r);
>> +
>> +	return Rectangle(x, y, w, h);
>> +}
>> +
>> +{% for vendor, ctrls in controls %}
>> +{%- for ctrl in ctrls if ctrl.is_enum %}
>> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {
>> +{%- for enum in ctrl.enum_values %}
>> +	{
>> +		controls::{{ ctrl.namespace }}{{ enum.name }},
>> +		{{ enum.description|format_description|indent('\t\t') 
>> }},
>> +		"{{ enum.gst_name }}"
>> +	},
>> +{%- endfor %}
>> +	{0, NULL, NULL}
>> +};
>> +
>> +#define TYPE_{{ ctrl.name|snake_case|upper }} \
>> +	({{ ctrl.name|snake_case }}_get_type())
>> +static GType {{ ctrl.name|snake_case }}_get_type()
>> +{
>> +	static GType {{ ctrl.name|snake_case }}_type = 0;
>> +
>> +	if (!{{ ctrl.name|snake_case }}_type)
>> +		{{ ctrl.name|snake_case }}_type =
>> +			g_enum_register_static("{{ ctrl.name }}",
>> +					       {{ ctrl.name|snake_case }}_types);
>> +
>> +	return {{ ctrl.name|snake_case }}_type;
>> +}
>> +{% endfor %}
>> +{%- endfor %}
>> +
>> +void GstCameraControls::installProperties(GObjectClass *klass, 
>> int lastPropId)
>> +{
>> +{%- for vendor, ctrls in controls %}
>> +{%- for ctrl in ctrls %}
>> +
>> +{%- set spec %}
>> +{%- if ctrl.is_rectangle -%}
>> +gst_param_spec_array(
>> +{%- else -%}
>> +g_param_spec_{{ ctrl.gtype }}(
>> +{%- endif -%}
>> +{%- if ctrl.is_array %}
>> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case 
>> }}-value",
>> +	"{{ ctrl.name }} Value",
>> +	"One {{ ctrl.name }} element value",
>> +{%- else %}
>> +	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
>> +	"{{ ctrl.name }}",
>> +	{{ ctrl.description|format_description|indent('\t') }},
>> +{%- endif %}
>> +{%- if ctrl.is_enum %}
>> +	TYPE_{{ ctrl.name|snake_case|upper }},
>> +	{{ ctrl.default }},
>> +{%- elif ctrl.is_rectangle %}
>> +	g_param_spec_int(
>> +		"rectangle-value",
>> +		"Rectangle Value",
>> +		"One rectangle value, either x, y, width or height.",
>> +		{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
>> +		(GParamFlags) (GST_PARAM_CONTROLLABLE | 
>> G_PARAM_READWRITE |
>> +			       G_PARAM_STATIC_STRINGS)
>> +	),
>> +{%- elif ctrl.gtype == 'boolean' %}
>> +	{{ ctrl.default }},
>> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}
>> +	{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
>> +{%- endif %}
>> +	(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE 
>> |
>> +		       G_PARAM_STATIC_STRINGS)
>> +)
>> +{%- endset %}
>> +
>> +	g_object_class_install_property(
>> +		klass,
>> +		lastPropId + controls::{{ ctrl.namespace }}{{ 
>> ctrl.name|snake_case|upper }},
>> +{%- if ctrl.is_array %}
>> +		gst_param_spec_array(
>> +			"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case 
>> }}",
>> +			"{{ ctrl.name }}",
>> +			{{ 
>> ctrl.description|format_description|indent('\t\t\t') }},
>> +			{{ spec|indent('\t\t\t') }},
>> +			(GParamFlags) (GST_PARAM_CONTROLLABLE |
>> +				       G_PARAM_READWRITE |
>> +				       G_PARAM_STATIC_STRINGS)
>> +		)
>> +{%- else %}
>> +		{{ spec|indent('\t\t') }}
>> +{%- endif %}
>> +	);
>> +{%- endfor %}
>> +{%- endfor %}
>> +}
>> +
>> +bool GstCameraControls::getProperty(guint propId, GValue 
>> *value,
>> +				    [[maybe_unused]] GParamSpec *pspec)
>> +{
>> +	if (!controls_acc_.contains(propId)) {
>> +		GST_WARNING("Control '%s' is not available, default 
>> value will "
>> +			    "be returned",
>> + 
>> controls::controls.at(propId)->name().c_str());
>> +		return true;
>> +	}
>> +	const ControlValue &cv = controls_acc_.get(propId);
>> +
>> +	switch (propId) {
>> +{%- for vendor, ctrls in controls %}
>> +{%- for ctrl in ctrls %}
>> +
>> +	case controls::{{ ctrl.namespace }}{{ 
>> ctrl.name|snake_case|upper }}: {
>> +		auto control = cv.get<{{ ctrl.type }}>();
>> +
>> +{%- if ctrl.is_array %}
>> +		for (size_t i = 0; i < control.size(); ++i) {
>> +			GValue element = G_VALUE_INIT;
>> +{%- if ctrl.is_rectangle %}
>> +			g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);
>> +			value_set_rectangle(&element, control[i]);
>> +{%- else %}
>> +			g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper 
>> }});
>> +			g_value_set_{{ ctrl.gtype }}(&element, 
>> control[i]);
>> +{%- endif %}
>> +			gst_value_array_append_and_take_value(value, 
>> &element);
>> +		}
>> +{%- else %}
>> +{%- if ctrl.is_rectangle %}
>> +		value_set_rectangle(value, control);
>> +{%- else %}
>> +		g_value_set_{{ ctrl.gtype }}(value, control);
>> +{%- endif %}
>> +{%- endif %}
>> +
>> +		return true;
>> +	}
>> +{%- endfor %}
>> +{%- endfor %}
>> +
>> +	default:
>> +		return false;
>> +	}
>> +}
>> +
>> +bool GstCameraControls::setProperty(guint propId, const GValue 
>> *value,
>> +				    [[maybe_unused]] GParamSpec *pspec)
>> +{
>> +	/*
>> +	 * Check whether the camera capabilities are already 
>> available.
>> +	 * They might not be available if the pipeline has not 
>> started yet.
>> +	 */
>> +	if (!capabilities_.empty()) {
>> + 		/* If so, check that the control is supported by the 
>> camera. */
>> +		const ControlId *cid = 
>> capabilities_.idmap().at(propId);
>> +		auto info = capabilities_.find(cid);
>> +
>> +		if (info == capabilities_.end()) {
>> +			GST_WARNING("Control '%s' is not supported by the 
>> "
>> +				    "camera and will be ignored",
>> +				    cid->name().c_str());
>> +			return true;
>> +		}
>> +	}
>> +
>> +	switch (propId) {
>> +{%- for vendor, ctrls in controls %}
>> +{%- for ctrl in ctrls %}
>> +
>> +	case controls::{{ ctrl.namespace }}{{ 
>> ctrl.name|snake_case|upper }}: {
>> +		ControlValue control;
>> +{%- if ctrl.is_array %}
>> +		size_t size = gst_value_array_get_size(value);
>> +{%- if ctrl.size != 0 %}
>> +		if (size != {{ ctrl.size }}) {
>> +			GST_ERROR("Incorrect array size for control "
>> +				  "'{{ ctrl.name|kebab_case }}', must be of "
>> +				  "size {{ ctrl.size }}");
>> +			return true;
>> +		}
>> +{%- endif %}
>> +
>> +		std::vector<{{ ctrl.element_type }}> values(size);
>> +		for (size_t i = 0; i < size; ++i) {
>> +			const GValue *element =
>> +				gst_value_array_get_value(value, i);
>> +{%- if ctrl.is_rectangle %}
>> +			if (gst_value_array_get_size(element) != 4) {
>> +				GST_ERROR("Rectangle in control "
>> +					  "'{{ ctrl.name|kebab_case }}' at"
>> +					  "index %lu must be an array of size 4",
>> +					  i);
>> +				return true;
>> +			}
>> +			values[i] = value_get_rectangle(element);
>> +{%- else %}
>> +			values[i] = g_value_get_{{ ctrl.gtype }}(element);
>> +{%- endif %}
>> +		}
>> +
>> +{%- if ctrl.size == 0 %}
>> +		control.set(Span<const {{ ctrl.element_type 
>> }}>(values.data(),
>> +								size));
>> +{%- else %}
>> +		control.set(Span<const {{ ctrl.element_type }},
>> +			         {{ ctrl.size }}>(values.data(),
>> +						  {{ ctrl.size }}));
>> +{%- endif %}
>> +{%- else %}
>> +{%- if ctrl.is_rectangle %}
>> +		if (gst_value_array_get_size(value) != 4) {
>> +			GST_ERROR("Rectangle in control "
>> +				  "'{{ ctrl.name|kebab_case }}' must be an "
>> +				  "array of size 4");
>> +			return true;
>> +		}
>> +		Rectangle val = value_get_rectangle(value);
>> +{%- else %}
>> +		auto val = g_value_get_{{ ctrl.gtype }}(value);
>> +{%- endif %}
>> +		control.set(val);
>> +{%- endif %}
>> +		controls_.set(propId, control);
>> +		controls_acc_.set(propId, control);
>> +		return true;
>> +	}
>> +{%- endfor %}
>> +{%- endfor %}
>> +
>> +	default:
>> +		return false;
>> +	}
>> +}
>> +
>> +void GstCameraControls::setCamera(const 
>> std::shared_ptr<libcamera::Camera> &cam)
>> +{
>> +	capabilities_ = cam->controls();
>> +
>> +	/*
>> +	 * Check the controls which were set before the camera 
>> capabilities were
>> +	 * known. This is required because GStreamer may set 
>> properties before
>> +	 * the pipeline has started and thus before the camera was 
>> known.
>> +	 */
>> +	ControlList new_controls;
>> +	for (auto control = controls_acc_.begin();
>> +	     control != controls_acc_.end();
>> +	     ++control) {
>> +		unsigned int id = control->first;
>> +		ControlValue value = control->second;
>> +
>> +		const ControlId *cid = capabilities_.idmap().at(id);
>> +		auto info = capabilities_.find(cid);
>> +
>> +		/* Only add controls which are supported. */
>> +		if (info != capabilities_.end())
>> +			new_controls.set(id, value);
>> +		else
>> +			GST_WARNING("Control '%s' is not supported by the 
>> "
>> +				    "camera and will be ignored",
>> +				    cid->name().c_str());
>> +	}
>> +
>> +	controls_acc_ = new_controls;
>> +	controls_ = new_controls;
>> +}
>> +
>> +void 
>> GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> 
>> &request)
>> +{
>> +	request->controls().merge(controls_);
>> +	controls_.clear();
>> +}
>> +
>> +void GstCameraControls::readMetadata(libcamera::Request 
>> *request)
>> +{
>> +	controls_acc_.merge(request->metadata(),
>> +			    ControlList::MergePolicy::OverwriteExisting);
>> +}
>> diff --git a/src/gstreamer/gstlibcamera-controls.h 
>> b/src/gstreamer/gstlibcamera-controls.h
>> new file mode 100644
>> index 00000000..749220b5
>> --- /dev/null
>> +++ b/src/gstreamer/gstlibcamera-controls.h
>> @@ -0,0 +1,43 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2023, Collabora Ltd.
>> + *     Author: Nicolas Dufresne 
>> <nicolas.dufresne@collabora.com>
>> + *
>> + * GStreamer Camera Controls
>> + */
>> +
>> +#pragma once
>> +
>> +#include <memory>
>> +
>> +#include <libcamera/camera.h>
>> +#include <libcamera/controls.h>
>> +#include <libcamera/request.h>
>> +
>> +#include "gstlibcamerasrc.h"
>> +
>> +namespace libcamera {
>> +
>> +class GstCameraControls
>> +{
>> +public:
>> +	static void installProperties(GObjectClass *klass, int 
>> lastProp);
>> +
>> +	bool getProperty(guint propId, GValue *value, GParamSpec 
>> *pspec);
>> +	bool setProperty(guint propId, const GValue *value, 
>> GParamSpec *pspec);
>> +
>> +	void setCamera(const std::shared_ptr<libcamera::Camera> 
>> &cam);
>> +
>> +	void applyControls(std::unique_ptr<libcamera::Request> 
>> &request);
>> +	void readMetadata(libcamera::Request *request);
>> +
>> +private:
>> +	/* Supported controls and limits of camera. */
>> +	ControlInfoMap capabilities_;
>> +	/* Set of user modified controls. */
>> +	ControlList controls_;
>> +	/* Accumulator of all controls ever set and metadata 
>> returned by camera */
>> +	ControlList controls_acc_;
>> +};
>> +
>> +} /* namespace libcamera */
>> diff --git a/src/gstreamer/gstlibcamerasrc.cpp 
>> b/src/gstreamer/gstlibcamerasrc.cpp
>> index 40b787c8..8efa25f4 100644
>> --- a/src/gstreamer/gstlibcamerasrc.cpp
>> +++ b/src/gstreamer/gstlibcamerasrc.cpp
>> @@ -37,10 +37,11 @@
>>  
>>  #include <gst/base/base.h>
>>  
>> +#include "gstlibcamera-controls.h"
>> +#include "gstlibcamera-utils.h"
>>  #include "gstlibcameraallocator.h"
>>  #include "gstlibcamerapad.h"
>>  #include "gstlibcamerapool.h"
>> -#include "gstlibcamera-utils.h"
>>  
>>  using namespace libcamera;
>>  
>> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {
>>  
>>  	ControlList initControls_;
>>  	guint group_id_;
>> +	GstCameraControls controls_;
>>  
>>  	int queueRequest();
>>  	void requestCompleted(Request *request);
>> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {
>>  enum {
>>  	PROP_0,
>>  	PROP_CAMERA_NAME,
>> +	PROP_LAST
>>  };
>>  
>>  static void gst_libcamera_src_child_proxy_init(gpointer 
>>  g_iface,
>> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()
>>  	if (!request)
>>  		return -ENOMEM;
>>  
>> +	/* Apply controls */
>> +	controls_.applyControls(request);
>> +
>>  	std::unique_ptr<RequestWrap> wrap =
>>  		std::make_unique<RequestWrap>(std::move(request));
>>  
>> @@ -226,6 +232,9 @@ 
>> GstLibcameraSrcState::requestCompleted(Request *request)
>>  
>>  	{
>>  		GLibLocker locker(&lock_);
>> +
>> +		controls_.readMetadata(request);
>> +
>>  		wrap = std::move(queuedRequests_.front());
>>  		queuedRequests_.pop();
>>  	}
>> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc 
>> *self)
>>  		return false;
>>  	}
>>  
>> +	self->state->controls_.setCamera(cam);
>> +
>>  	cam->requestCompleted.connect(self->state, 
>>  &GstLibcameraSrcState::requestCompleted);
>>  
>>  	/* No need to lock here, we didn't start our threads yet. 
>>  */
>> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject 
>> *object, guint prop_id,
>>  {
>>  	GLibLocker lock(GST_OBJECT(object));
>>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
>> +	GstLibcameraSrcState *state = self->state;
>>  
>>  	switch (prop_id) {
>>  	case PROP_CAMERA_NAME:
>> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject 
>> *object, guint prop_id,
>>  		self->camera_name = g_value_dup_string(value);
>>  		break;
>>  	default:
>> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, 
>> pspec);
>> +		if (!state->controls_.setProperty(prop_id - PROP_LAST, 
>> value, pspec))
>> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, 
>> pspec);
>>  		break;
>>  	}
>>  }
>> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject 
>> *object, guint prop_id, GValue *value,
>>  {
>>  	GLibLocker lock(GST_OBJECT(object));
>>  	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
>> +	GstLibcameraSrcState *state = self->state;
>>  
>>  	switch (prop_id) {
>>  	case PROP_CAMERA_NAME:
>>  		g_value_set_string(value, self->camera_name);
>>  		break;
>>  	default:
>> -		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, 
>> pspec);
>> +		if (!state->controls_.getProperty(prop_id - PROP_LAST, 
>> value, pspec))
>> +			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, 
>> pspec);
>>  		break;
>>  	}
>>  }
>> @@ -947,6 +962,7 @@ 
>> gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)
>>  							     | G_PARAM_STATIC_STRINGS));
>>  	g_object_class_install_property(object_class, 
>>  PROP_CAMERA_NAME, spec);
>>  
>> +	GstCameraControls::installProperties(object_class, 
>> PROP_LAST);
>>  }
>>  
>>  /* GstChildProxy implementation */
>> diff --git a/src/gstreamer/meson.build 
>> b/src/gstreamer/meson.build
>> index c2a01e7b..6b7e53b5 100644
>> --- a/src/gstreamer/meson.build
>> +++ b/src/gstreamer/meson.build
>> @@ -25,6 +25,16 @@ libcamera_gst_sources = [
>>      'gstlibcamerasrc.cpp',
>>  ]
>>  
>> +# Generate gstreamer control properties
>> +
>> +gen_gst_controls_template = 
>> files('gstlibcamera-controls.cpp.in')
>> +libcamera_gst_sources += 
>> custom_target('gstlibcamera-controls.cpp',
>> +                                       input : controls_files,
>> +                                       output : 
>> 'gstlibcamera-controls.cpp',
>> +                                       command : 
>> [gen_gst_controls, '-o', '@OUTPUT@',
>> +                                                  '-t', 
>> gen_gst_controls_template, '@INPUT@'],
>> +                                       env : py_build_env)
>> +
>>  libcamera_gst_cpp_args = [
>>      '-DVERSION="@0@"'.format(libcamera_git_version),
>>      '-DPACKAGE="@0@"'.format(meson.project_name()),
>> diff --git a/utils/codegen/controls.py 
>> b/utils/codegen/controls.py
>> index 7bafee59..03c77cc6 100644
>> --- a/utils/codegen/controls.py
>> +++ b/utils/codegen/controls.py
>> @@ -110,3 +110,11 @@ class Control(object):
>>              return f"Span<const {typ}, {self.__size}>"
>>          else:
>>              return f"Span<const {typ}>"
>> +
>> +    @property
>> +    def element_type(self):
>> +        return self.__data.get('type')
>> +
>> +    @property
>> +    def size(self):
>> +        return self.__size
>> diff --git a/utils/codegen/gen-gst-controls.py 
>> b/utils/codegen/gen-gst-controls.py
>> new file mode 100755
>> index 00000000..0b899258
>> --- /dev/null
>> +++ b/utils/codegen/gen-gst-controls.py
>> @@ -0,0 +1,166 @@
>> +#!/usr/bin/env python3
>> +# SPDX-License-Identifier: GPL-2.0-or-later
>> +# Copyright (C) 2019, Google Inc.
>> +# Copyright (C) 2024, Jaslo Ziska
>> +#
>> +# Authors:
>> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>
>> +# Jaslo Ziska <jaslo@ziska.de>
>> +#
>> +# Generate gstreamer control properties from YAML
>> +
>> +import argparse
>> +import jinja2
>> +import re
>> +import sys
>> +import yaml
>> +
>> +from controls import Control
>> +
>> +
>> +exposed_controls = [
>> +    "AeEnable", "AeLocked", "AeMeteringMode", 
>> "AeConstraintMode",
>
> AeLocked is not settable, until we can expose it correctly I'd 
> drop. We might
> also want something more accurate, so we can match an exact 
> frame?

You are right, I forgot that the read-only properties should not 
be exposed yet, I will fix that.

>> +    "AeExposureMode", "ExposureValue", "ExposureTime", 
>> "AnalogueGain",
>> +    "AeFlickerMode", "AeFlickerPeriod", "AeFlickerDetected", 
>> "Brightness",
>
> I'm not sure about AeFlickerMode and AeFlickerDetected, and its 
> supposed to be
> read-only. Without a use case I cannot tell the proper way to 
> expose these, I
> would drop that.

I think only AeFlickerDetected is read-only but I can leave them 
out for now.

>> +    "Contrast", "Lux", "AwbEnable", "AwbMode", "AwbLocked", 
>> "ColourGains",
>
> AwbLocked is similar to AeLocked. Same goes for ColourGains and
> ColourTemperature, probably fine as property, but read-only.

ColourGains is writeable, I tested that. But you are correct about 
ColourTemperature.

>> 
>> +    "ColourTemperature", "Saturation", "SensorBlackLevels", 
>> "Sharpness",
>> +    "FocusFoM", "ColourCorrectionMatrix", "ScalerCrop", 
>> "DigitalGain",
>> +    "FrameDuration", "SensorTemperature", "SensorTimestamp", 
>> "AfMode",
>
> FrameDuration and SensorTimestamp should be meta, and attached 
> to GstBuffer, I
> would drop it from the list in this serie. SensorTimestamp is 
> clearly a fit for
> GstReferenceTimestampMeta. I would need to read more about 
> FrameDuration to see
> if we should use it, perhaps could be set to GstBuffer.duration 
> ?

I will just drop them.

>> +    "AfRange", "AfSpeed", "AfMetering", "AfWindows", 
>> "LensPosition", "AfState",
>> +    "AfPauseState", "Gamma",
>
> AfState, AfPausedState are not settable, I'd remove for now 
> until we can
> generate read-only properties.

Adding the readable/writeable property and integrating that into 
the Gstreamer element will be my next goal once this is merged.

Best regards,

Jaslo

> cheers
> Nicolas
>
>> +]
>> +
>> +
>> +def find_common_prefix(strings):
>> +    prefix = strings[0]
>> +
>> +    for string in strings[1:]:
>> +        while string[:len(prefix)] != prefix and prefix:
>> +            prefix = prefix[:len(prefix) - 1]
>> +        if not prefix:
>> +            break
>> +
>> +    return prefix
>> +
>> +
>> +def format_description(description):
>> +    # Substitute doxygen keywords \sa (see also) and \todo
>> +    description = re.sub(r'\\sa((?: \w+)+)',
>> +                         lambda match: 'See also: ' + ', 
>> '.join(
>> +                             map(kebab_case, 
>> match.group(1).strip().split(' '))
>> +                         ) + '.', description)
>> +    description = re.sub(r'\\todo', 'Todo:', description)
>> +
>> +    description = description.strip().split('\n')
>> +    return '\n'.join([
>> +        '"' + line.replace('\\', r'\\').replace('"', r'\"') + 
>> ' "' for line in description if line
>> +    ]).rstrip()
>> +
>> +
>> +def snake_case(s):
>> +    return ''.join([
>> +        c.isupper() and ('_' + c.lower()) or c for c in s
>> +    ]).strip('_')
>> +
>> +
>> +def kebab_case(s):
>> +    return snake_case(s).replace('_', '-')
>> +
>> +
>> +def extend_control(ctrl):
>> +    if ctrl.vendor != 'libcamera':
>> +        ctrl.namespace = f'{ctrl.vendor}::'
>> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'
>> +    else:
>> +        ctrl.namespace = ''
>> +        ctrl.vendor_prefix = ''
>> +
>> +    ctrl.is_array = ctrl.size is not None
>> +
>> +    if ctrl.is_enum:
>> +        # Remove common prefix from enum variant names
>> +        prefix = find_common_prefix([enum.name for enum in 
>> ctrl.enum_values])
>> +        for enum in ctrl.enum_values:
>> +            enum.gst_name = 
>> kebab_case(enum.name.removeprefix(prefix))
>> +
>> +        ctrl.gtype = 'enum'
>> +        ctrl.default = '0'
>> +    elif ctrl.element_type == 'bool':
>> +        ctrl.gtype = 'boolean'
>> +        ctrl.default = 'false'
>> +    elif ctrl.element_type == 'float':
>> +        ctrl.gtype = 'float'
>> +        ctrl.default = '0'
>> +        ctrl.min = '-G_MAXFLOAT'
>> +        ctrl.max = 'G_MAXFLOAT'
>> +    elif ctrl.element_type == 'int32_t':
>> +        ctrl.gtype = 'int'
>> +        ctrl.default = '0'
>> +        ctrl.min = 'G_MININT'
>> +        ctrl.max = 'G_MAXINT'
>> +    elif ctrl.element_type == 'int64_t':
>> +        ctrl.gtype = 'int64'
>> +        ctrl.default = '0'
>> +        ctrl.min = 'G_MININT64'
>> +        ctrl.max = 'G_MAXINT64'
>> +    elif ctrl.element_type == 'uint8_t':
>> +        ctrl.gtype = 'uchar'
>> +        ctrl.default = '0'
>> +        ctrl.min = '0'
>> +        ctrl.max = 'G_MAXUINT8'
>> +    elif ctrl.element_type == 'Rectangle':
>> +        ctrl.is_rectangle = True
>> +        ctrl.default = '0'
>> +        ctrl.min = '0'
>> +        ctrl.max = 'G_MAXINT'
>> +    else:
>> +        raise RuntimeError(f'The type `{ctrl.element_type}` is 
>> unknown')
>> +
>> +    return ctrl
>> +
>> +
>> +def main(argv):
>> +    # Parse command line arguments
>> +    parser = argparse.ArgumentParser()
>> +    parser.add_argument('--output', '-o', metavar='file', 
>> type=str,
>> +                        help='Output file name. Defaults to 
>> standard output if not specified.')
>> +    parser.add_argument('--template', '-t', dest='template', 
>> type=str, required=True,
>> +                        help='Template file name.')
>> +    parser.add_argument('input', type=str, nargs='+',
>> +                        help='Input file name.')
>> +
>> +    args = parser.parse_args(argv[1:])
>> +
>> +    controls = {}
>> +    for input in args.input:
>> +        data = yaml.safe_load(open(input, 'rb').read())
>> +
>> +        vendor = data['vendor']
>> +        ctrls = controls.setdefault(vendor, [])
>> +
>> +        for ctrl in data['controls']:
>> +            ctrl = Control(*ctrl.popitem(), vendor)
>> +
>> +            if ctrl.name in exposed_controls:
>> +                ctrls.append(extend_control(ctrl))
>> +
>> +    data = {'controls': list(controls.items())}
>> +
>> +    env = jinja2.Environment()
>> +    env.filters['format_description'] = format_description
>> +    env.filters['snake_case'] = snake_case
>> +    env.filters['kebab_case'] = kebab_case
>> +    template = env.from_string(open(args.template, 'r', 
>> encoding='utf-8').read())
>> +    string = template.render(data)
>> +
>> +    if args.output:
>> +        with open(args.output, 'w', encoding='utf-8') as 
>> output:
>> +            output.write(string)
>> +    else:
>> +        sys.stdout.write(string)
>> +
>> +    return 0
>> +
>> +
>> +if __name__ == '__main__':
>> +    sys.exit(main(sys.argv))
>> diff --git a/utils/codegen/meson.build 
>> b/utils/codegen/meson.build
>> index adf33bba..904dd66d 100644
>> --- a/utils/codegen/meson.build
>> +++ b/utils/codegen/meson.build
>> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']
>>  
>>  gen_controls = files('gen-controls.py')
>>  gen_formats = files('gen-formats.py')
>> +gen_gst_controls = files('gen-gst-controls.py')
>>  gen_header = files('gen-header.sh')
>>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')
>>  gen_tracepoints = files('gen-tp-header.py')

Patch
diff mbox series

diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in
new file mode 100644
index 00000000..6861f5d1
--- /dev/null
+++ b/src/gstreamer/gstlibcamera-controls.cpp.in
@@ -0,0 +1,332 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Jaslo Ziska
+ *
+ * GStreamer Camera Controls
+ *
+ * This file is auto-generated. Do not edit.
+ */
+
+#include <vector>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+
+#include "gstlibcamera-controls.h"
+
+using namespace libcamera;
+
+static void value_set_rectangle(GValue *value, const Rectangle &rect)
+{
+	Point top_left = rect.topLeft();
+	Size size = rect.size();
+
+	GValue x = G_VALUE_INIT;
+	g_value_init(&x, G_TYPE_INT);
+	g_value_set_int(&x, top_left.x);
+	gst_value_array_append_and_take_value(value, &x);
+
+	GValue y = G_VALUE_INIT;
+	g_value_init(&y, G_TYPE_INT);
+	g_value_set_int(&y, top_left.y);
+	gst_value_array_append_and_take_value(value, &y);
+
+	GValue width = G_VALUE_INIT;
+	g_value_init(&width, G_TYPE_INT);
+	g_value_set_int(&width, size.width);
+	gst_value_array_append_and_take_value(value, &width);
+
+	GValue height = G_VALUE_INIT;
+	g_value_init(&height, G_TYPE_INT);
+	g_value_set_int(&x, size.height);
+	gst_value_array_append_and_take_value(value, &height);
+}
+
+static Rectangle value_get_rectangle(const GValue *value)
+{
+	const GValue *r;
+	r = gst_value_array_get_value(value, 0);
+	int x = g_value_get_int(r);
+	r = gst_value_array_get_value(value, 1);
+	int y = g_value_get_int(r);
+	r = gst_value_array_get_value(value, 2);
+	int w = g_value_get_int(r);
+	r = gst_value_array_get_value(value, 3);
+	int h = g_value_get_int(r);
+
+	return Rectangle(x, y, w, h);
+}
+
+{% for vendor, ctrls in controls %}
+{%- for ctrl in ctrls if ctrl.is_enum %}
+static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {
+{%- for enum in ctrl.enum_values %}
+	{
+		controls::{{ ctrl.namespace }}{{ enum.name }},
+		{{ enum.description|format_description|indent('\t\t') }},
+		"{{ enum.gst_name }}"
+	},
+{%- endfor %}
+	{0, NULL, NULL}
+};
+
+#define TYPE_{{ ctrl.name|snake_case|upper }} \
+	({{ ctrl.name|snake_case }}_get_type())
+static GType {{ ctrl.name|snake_case }}_get_type()
+{
+	static GType {{ ctrl.name|snake_case }}_type = 0;
+
+	if (!{{ ctrl.name|snake_case }}_type)
+		{{ ctrl.name|snake_case }}_type =
+			g_enum_register_static("{{ ctrl.name }}",
+					       {{ ctrl.name|snake_case }}_types);
+
+	return {{ ctrl.name|snake_case }}_type;
+}
+{% endfor %}
+{%- endfor %}
+
+void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)
+{
+{%- for vendor, ctrls in controls %}
+{%- for ctrl in ctrls %}
+
+{%- set spec %}
+{%- if ctrl.is_rectangle -%}
+gst_param_spec_array(
+{%- else -%}
+g_param_spec_{{ ctrl.gtype }}(
+{%- endif -%}
+{%- if ctrl.is_array %}
+	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value",
+	"{{ ctrl.name }} Value",
+	"One {{ ctrl.name }} element value",
+{%- else %}
+	"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
+	"{{ ctrl.name }}",
+	{{ ctrl.description|format_description|indent('\t') }},
+{%- endif %}
+{%- if ctrl.is_enum %}
+	TYPE_{{ ctrl.name|snake_case|upper }},
+	{{ ctrl.default }},
+{%- elif ctrl.is_rectangle %}
+	g_param_spec_int(
+		"rectangle-value",
+		"Rectangle Value",
+		"One rectangle value, either x, y, width or height.",
+		{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
+		(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
+			       G_PARAM_STATIC_STRINGS)
+	),
+{%- elif ctrl.gtype == 'boolean' %}
+	{{ ctrl.default }},
+{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}
+	{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},
+{%- endif %}
+	(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |
+		       G_PARAM_STATIC_STRINGS)
+)
+{%- endset %}
+
+	g_object_class_install_property(
+		klass,
+		lastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},
+{%- if ctrl.is_array %}
+		gst_param_spec_array(
+			"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}",
+			"{{ ctrl.name }}",
+			{{ ctrl.description|format_description|indent('\t\t\t') }},
+			{{ spec|indent('\t\t\t') }},
+			(GParamFlags) (GST_PARAM_CONTROLLABLE |
+				       G_PARAM_READWRITE |
+				       G_PARAM_STATIC_STRINGS)
+		)
+{%- else %}
+		{{ spec|indent('\t\t') }}
+{%- endif %}
+	);
+{%- endfor %}
+{%- endfor %}
+}
+
+bool GstCameraControls::getProperty(guint propId, GValue *value,
+				    [[maybe_unused]] GParamSpec *pspec)
+{
+	if (!controls_acc_.contains(propId)) {
+		GST_WARNING("Control '%s' is not available, default value will "
+			    "be returned",
+			    controls::controls.at(propId)->name().c_str());
+		return true;
+	}
+	const ControlValue &cv = controls_acc_.get(propId);
+
+	switch (propId) {
+{%- for vendor, ctrls in controls %}
+{%- for ctrl in ctrls %}
+
+	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
+		auto control = cv.get<{{ ctrl.type }}>();
+
+{%- if ctrl.is_array %}
+		for (size_t i = 0; i < control.size(); ++i) {
+			GValue element = G_VALUE_INIT;
+{%- if ctrl.is_rectangle %}
+			g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);
+			value_set_rectangle(&element, control[i]);
+{%- else %}
+			g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});
+			g_value_set_{{ ctrl.gtype }}(&element, control[i]);
+{%- endif %}
+			gst_value_array_append_and_take_value(value, &element);
+		}
+{%- else %}
+{%- if ctrl.is_rectangle %}
+		value_set_rectangle(value, control);
+{%- else %}
+		g_value_set_{{ ctrl.gtype }}(value, control);
+{%- endif %}
+{%- endif %}
+
+		return true;
+	}
+{%- endfor %}
+{%- endfor %}
+
+	default:
+		return false;
+	}
+}
+
+bool GstCameraControls::setProperty(guint propId, const GValue *value,
+				    [[maybe_unused]] GParamSpec *pspec)
+{
+	/*
+	 * Check whether the camera capabilities are already available.
+	 * They might not be available if the pipeline has not started yet.
+	 */
+	if (!capabilities_.empty()) {
+ 		/* If so, check that the control is supported by the camera. */
+		const ControlId *cid = capabilities_.idmap().at(propId);
+		auto info = capabilities_.find(cid);
+
+		if (info == capabilities_.end()) {
+			GST_WARNING("Control '%s' is not supported by the "
+				    "camera and will be ignored",
+				    cid->name().c_str());
+			return true;
+		}
+	}
+
+	switch (propId) {
+{%- for vendor, ctrls in controls %}
+{%- for ctrl in ctrls %}
+
+	case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {
+		ControlValue control;
+{%- if ctrl.is_array %}
+		size_t size = gst_value_array_get_size(value);
+{%- if ctrl.size != 0 %}
+		if (size != {{ ctrl.size }}) {
+			GST_ERROR("Incorrect array size for control "
+				  "'{{ ctrl.name|kebab_case }}', must be of "
+				  "size {{ ctrl.size }}");
+			return true;
+		}
+{%- endif %}
+
+		std::vector<{{ ctrl.element_type }}> values(size);
+		for (size_t i = 0; i < size; ++i) {
+			const GValue *element =
+				gst_value_array_get_value(value, i);
+{%- if ctrl.is_rectangle %}
+			if (gst_value_array_get_size(element) != 4) {
+				GST_ERROR("Rectangle in control "
+					  "'{{ ctrl.name|kebab_case }}' at"
+					  "index %lu must be an array of size 4",
+					  i);
+				return true;
+			}
+			values[i] = value_get_rectangle(element);
+{%- else %}
+			values[i] = g_value_get_{{ ctrl.gtype }}(element);
+{%- endif %}
+		}
+
+{%- if ctrl.size == 0 %}
+		control.set(Span<const {{ ctrl.element_type }}>(values.data(),
+								size));
+{%- else %}
+		control.set(Span<const {{ ctrl.element_type }},
+			         {{ ctrl.size }}>(values.data(),
+						  {{ ctrl.size }}));
+{%- endif %}
+{%- else %}
+{%- if ctrl.is_rectangle %}
+		if (gst_value_array_get_size(value) != 4) {
+			GST_ERROR("Rectangle in control "
+				  "'{{ ctrl.name|kebab_case }}' must be an "
+				  "array of size 4");
+			return true;
+		}
+		Rectangle val = value_get_rectangle(value);
+{%- else %}
+		auto val = g_value_get_{{ ctrl.gtype }}(value);
+{%- endif %}
+		control.set(val);
+{%- endif %}
+		controls_.set(propId, control);
+		controls_acc_.set(propId, control);
+		return true;
+	}
+{%- endfor %}
+{%- endfor %}
+
+	default:
+		return false;
+	}
+}
+
+void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)
+{
+	capabilities_ = cam->controls();
+
+	/*
+	 * Check the controls which were set before the camera capabilities were
+	 * known. This is required because GStreamer may set properties before
+	 * the pipeline has started and thus before the camera was known.
+	 */
+	ControlList new_controls;
+	for (auto control = controls_acc_.begin();
+	     control != controls_acc_.end();
+	     ++control) {
+		unsigned int id = control->first;
+		ControlValue value = control->second;
+
+		const ControlId *cid = capabilities_.idmap().at(id);
+		auto info = capabilities_.find(cid);
+
+		/* Only add controls which are supported. */
+		if (info != capabilities_.end())
+			new_controls.set(id, value);
+		else
+			GST_WARNING("Control '%s' is not supported by the "
+				    "camera and will be ignored",
+				    cid->name().c_str());
+	}
+
+	controls_acc_ = new_controls;
+	controls_ = new_controls;
+}
+
+void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)
+{
+	request->controls().merge(controls_);
+	controls_.clear();
+}
+
+void GstCameraControls::readMetadata(libcamera::Request *request)
+{
+	controls_acc_.merge(request->metadata(),
+			    ControlList::MergePolicy::OverwriteExisting);
+}
diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h
new file mode 100644
index 00000000..749220b5
--- /dev/null
+++ b/src/gstreamer/gstlibcamera-controls.h
@@ -0,0 +1,43 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Collabora Ltd.
+ *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
+ *
+ * GStreamer Camera Controls
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <libcamera/camera.h>
+#include <libcamera/controls.h>
+#include <libcamera/request.h>
+
+#include "gstlibcamerasrc.h"
+
+namespace libcamera {
+
+class GstCameraControls
+{
+public:
+	static void installProperties(GObjectClass *klass, int lastProp);
+
+	bool getProperty(guint propId, GValue *value, GParamSpec *pspec);
+	bool setProperty(guint propId, const GValue *value, GParamSpec *pspec);
+
+	void setCamera(const std::shared_ptr<libcamera::Camera> &cam);
+
+	void applyControls(std::unique_ptr<libcamera::Request> &request);
+	void readMetadata(libcamera::Request *request);
+
+private:
+	/* Supported controls and limits of camera. */
+	ControlInfoMap capabilities_;
+	/* Set of user modified controls. */
+	ControlList controls_;
+	/* Accumulator of all controls ever set and metadata returned by camera */
+	ControlList controls_acc_;
+};
+
+} /* namespace libcamera */
diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
index 40b787c8..8efa25f4 100644
--- a/src/gstreamer/gstlibcamerasrc.cpp
+++ b/src/gstreamer/gstlibcamerasrc.cpp
@@ -37,10 +37,11 @@ 
 
 #include <gst/base/base.h>
 
+#include "gstlibcamera-controls.h"
+#include "gstlibcamera-utils.h"
 #include "gstlibcameraallocator.h"
 #include "gstlibcamerapad.h"
 #include "gstlibcamerapool.h"
-#include "gstlibcamera-utils.h"
 
 using namespace libcamera;
 
@@ -128,6 +129,7 @@  struct GstLibcameraSrcState {
 
 	ControlList initControls_;
 	guint group_id_;
+	GstCameraControls controls_;
 
 	int queueRequest();
 	void requestCompleted(Request *request);
@@ -153,6 +155,7 @@  struct _GstLibcameraSrc {
 enum {
 	PROP_0,
 	PROP_CAMERA_NAME,
+	PROP_LAST
 };
 
 static void gst_libcamera_src_child_proxy_init(gpointer g_iface,
@@ -183,6 +186,9 @@  int GstLibcameraSrcState::queueRequest()
 	if (!request)
 		return -ENOMEM;
 
+	/* Apply controls */
+	controls_.applyControls(request);
+
 	std::unique_ptr<RequestWrap> wrap =
 		std::make_unique<RequestWrap>(std::move(request));
 
@@ -226,6 +232,9 @@  GstLibcameraSrcState::requestCompleted(Request *request)
 
 	{
 		GLibLocker locker(&lock_);
+
+		controls_.readMetadata(request);
+
 		wrap = std::move(queuedRequests_.front());
 		queuedRequests_.pop();
 	}
@@ -408,6 +417,8 @@  gst_libcamera_src_open(GstLibcameraSrc *self)
 		return false;
 	}
 
+	self->state->controls_.setCamera(cam);
+
 	cam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);
 
 	/* No need to lock here, we didn't start our threads yet. */
@@ -722,6 +733,7 @@  gst_libcamera_src_set_property(GObject *object, guint prop_id,
 {
 	GLibLocker lock(GST_OBJECT(object));
 	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
+	GstLibcameraSrcState *state = self->state;
 
 	switch (prop_id) {
 	case PROP_CAMERA_NAME:
@@ -729,7 +741,8 @@  gst_libcamera_src_set_property(GObject *object, guint prop_id,
 		self->camera_name = g_value_dup_string(value);
 		break;
 	default:
-		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+		if (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
 		break;
 	}
 }
@@ -740,13 +753,15 @@  gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,
 {
 	GLibLocker lock(GST_OBJECT(object));
 	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);
+	GstLibcameraSrcState *state = self->state;
 
 	switch (prop_id) {
 	case PROP_CAMERA_NAME:
 		g_value_set_string(value, self->camera_name);
 		break;
 	default:
-		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+		if (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
 		break;
 	}
 }
@@ -947,6 +962,7 @@  gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)
 							     | G_PARAM_STATIC_STRINGS));
 	g_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);
 
+	GstCameraControls::installProperties(object_class, PROP_LAST);
 }
 
 /* GstChildProxy implementation */
diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build
index c2a01e7b..6b7e53b5 100644
--- a/src/gstreamer/meson.build
+++ b/src/gstreamer/meson.build
@@ -25,6 +25,16 @@  libcamera_gst_sources = [
     'gstlibcamerasrc.cpp',
 ]
 
+# Generate gstreamer control properties
+
+gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')
+libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',
+                                       input : controls_files,
+                                       output : 'gstlibcamera-controls.cpp',
+                                       command : [gen_gst_controls, '-o', '@OUTPUT@',
+                                                  '-t', gen_gst_controls_template, '@INPUT@'],
+                                       env : py_build_env)
+
 libcamera_gst_cpp_args = [
     '-DVERSION="@0@"'.format(libcamera_git_version),
     '-DPACKAGE="@0@"'.format(meson.project_name()),
diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py
index 7bafee59..03c77cc6 100644
--- a/utils/codegen/controls.py
+++ b/utils/codegen/controls.py
@@ -110,3 +110,11 @@  class Control(object):
             return f"Span<const {typ}, {self.__size}>"
         else:
             return f"Span<const {typ}>"
+
+    @property
+    def element_type(self):
+        return self.__data.get('type')
+
+    @property
+    def size(self):
+        return self.__size
diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py
new file mode 100755
index 00000000..0b899258
--- /dev/null
+++ b/utils/codegen/gen-gst-controls.py
@@ -0,0 +1,166 @@ 
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2019, Google Inc.
+# Copyright (C) 2024, Jaslo Ziska
+#
+# Authors:
+# Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+# Jaslo Ziska <jaslo@ziska.de>
+#
+# Generate gstreamer control properties from YAML
+
+import argparse
+import jinja2
+import re
+import sys
+import yaml
+
+from controls import Control
+
+
+exposed_controls = [
+    "AeEnable", "AeLocked", "AeMeteringMode", "AeConstraintMode",
+    "AeExposureMode", "ExposureValue", "ExposureTime", "AnalogueGain",
+    "AeFlickerMode", "AeFlickerPeriod", "AeFlickerDetected", "Brightness",
+    "Contrast", "Lux", "AwbEnable", "AwbMode", "AwbLocked", "ColourGains",
+    "ColourTemperature", "Saturation", "SensorBlackLevels", "Sharpness",
+    "FocusFoM", "ColourCorrectionMatrix", "ScalerCrop", "DigitalGain",
+    "FrameDuration", "SensorTemperature", "SensorTimestamp", "AfMode",
+    "AfRange", "AfSpeed", "AfMetering", "AfWindows", "LensPosition", "AfState",
+    "AfPauseState", "Gamma",
+]
+
+
+def find_common_prefix(strings):
+    prefix = strings[0]
+
+    for string in strings[1:]:
+        while string[:len(prefix)] != prefix and prefix:
+            prefix = prefix[:len(prefix) - 1]
+        if not prefix:
+            break
+
+    return prefix
+
+
+def format_description(description):
+    # Substitute doxygen keywords \sa (see also) and \todo
+    description = re.sub(r'\\sa((?: \w+)+)',
+                         lambda match: 'See also: ' + ', '.join(
+                             map(kebab_case, match.group(1).strip().split(' '))
+                         ) + '.', description)
+    description = re.sub(r'\\todo', 'Todo:', description)
+
+    description = description.strip().split('\n')
+    return '\n'.join([
+        '"' + line.replace('\\', r'\\').replace('"', r'\"') + ' "' for line in description if line
+    ]).rstrip()
+
+
+def snake_case(s):
+    return ''.join([
+        c.isupper() and ('_' + c.lower()) or c for c in s
+    ]).strip('_')
+
+
+def kebab_case(s):
+    return snake_case(s).replace('_', '-')
+
+
+def extend_control(ctrl):
+    if ctrl.vendor != 'libcamera':
+        ctrl.namespace = f'{ctrl.vendor}::'
+        ctrl.vendor_prefix = f'{ctrl.vendor}-'
+    else:
+        ctrl.namespace = ''
+        ctrl.vendor_prefix = ''
+
+    ctrl.is_array = ctrl.size is not None
+
+    if ctrl.is_enum:
+        # Remove common prefix from enum variant names
+        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])
+        for enum in ctrl.enum_values:
+            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))
+
+        ctrl.gtype = 'enum'
+        ctrl.default = '0'
+    elif ctrl.element_type == 'bool':
+        ctrl.gtype = 'boolean'
+        ctrl.default = 'false'
+    elif ctrl.element_type == 'float':
+        ctrl.gtype = 'float'
+        ctrl.default = '0'
+        ctrl.min = '-G_MAXFLOAT'
+        ctrl.max = 'G_MAXFLOAT'
+    elif ctrl.element_type == 'int32_t':
+        ctrl.gtype = 'int'
+        ctrl.default = '0'
+        ctrl.min = 'G_MININT'
+        ctrl.max = 'G_MAXINT'
+    elif ctrl.element_type == 'int64_t':
+        ctrl.gtype = 'int64'
+        ctrl.default = '0'
+        ctrl.min = 'G_MININT64'
+        ctrl.max = 'G_MAXINT64'
+    elif ctrl.element_type == 'uint8_t':
+        ctrl.gtype = 'uchar'
+        ctrl.default = '0'
+        ctrl.min = '0'
+        ctrl.max = 'G_MAXUINT8'
+    elif ctrl.element_type == 'Rectangle':
+        ctrl.is_rectangle = True
+        ctrl.default = '0'
+        ctrl.min = '0'
+        ctrl.max = 'G_MAXINT'
+    else:
+        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')
+
+    return ctrl
+
+
+def main(argv):
+    # Parse command line arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--output', '-o', metavar='file', type=str,
+                        help='Output file name. Defaults to standard output if not specified.')
+    parser.add_argument('--template', '-t', dest='template', type=str, required=True,
+                        help='Template file name.')
+    parser.add_argument('input', type=str, nargs='+',
+                        help='Input file name.')
+
+    args = parser.parse_args(argv[1:])
+
+    controls = {}
+    for input in args.input:
+        data = yaml.safe_load(open(input, 'rb').read())
+
+        vendor = data['vendor']
+        ctrls = controls.setdefault(vendor, [])
+
+        for ctrl in data['controls']:
+            ctrl = Control(*ctrl.popitem(), vendor)
+
+            if ctrl.name in exposed_controls:
+                ctrls.append(extend_control(ctrl))
+
+    data = {'controls': list(controls.items())}
+
+    env = jinja2.Environment()
+    env.filters['format_description'] = format_description
+    env.filters['snake_case'] = snake_case
+    env.filters['kebab_case'] = kebab_case
+    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())
+    string = template.render(data)
+
+    if args.output:
+        with open(args.output, 'w', encoding='utf-8') as output:
+            output.write(string)
+    else:
+        sys.stdout.write(string)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build
index adf33bba..904dd66d 100644
--- a/utils/codegen/meson.build
+++ b/utils/codegen/meson.build
@@ -11,6 +11,7 @@  py_modules += ['jinja2', 'yaml']
 
 gen_controls = files('gen-controls.py')
 gen_formats = files('gen-formats.py')
+gen_gst_controls = files('gen-gst-controls.py')
 gen_header = files('gen-header.sh')
 gen_ipa_pub_key = files('gen-ipa-pub-key.py')
 gen_tracepoints = files('gen-tp-header.py')