This patch extends the GStreamer backend by implementing the libcamera (pseudo) backend.
The Libcamera backend builds on top of GStreamer and uses libcamerasrc as source element. Example usage: qemu-system-x86_64 \ -device qemu-xhci \ -videodev libcamera,id=cam0,\ camera-name="\\\_SB_.PCI0.XHC_.RHUB.HS07-7:1.0-04f2:b681",\ caps="video/x-raw^format=YUY2^width=960^height=540^framerate=15/1" \ -device usb-video,videodev=cam0 Known limitations: - libcamersrc does not expose capabilities (supported pixelformat, etc.), which makes it impossible to query them in runtime, hence we have to select format, width, height and framerate via QEMU's cmdline. Signed-off-by: David Milosevic <david.milose...@9elements.com> --- video/libcamera.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++ video/meson.build | 1 + 2 files changed, 149 insertions(+) create mode 100644 video/libcamera.c diff --git a/video/libcamera.c b/video/libcamera.c new file mode 100644 index 0000000000..f28914ee20 --- /dev/null +++ b/video/libcamera.c @@ -0,0 +1,148 @@ +/* + * Copyright 2025 9elements GmbH + * + * Authors: + * David Milosevic <david.milose...@9elements.com> + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qapi/qmp/qerror.h" +#include "qemu/option.h" +#include "video/video.h" +#include "video/gstreamer-common.h" + +#include <gst/gst.h> +#include <gst/app/gstappsink.h> + +#define TYPE_VIDEODEV_LIBCAMERA TYPE_VIDEODEV"-libcamera" + +#define VIDEO_LIBCAMERA_PIPELINE_TEMPLATE \ + "libcamerasrc name=qemu_src camera-name=\"%s\" ! capsfilter caps=\"%s\" ! videoconvert name=qemu_vc ! capsfilter name=qemu_cf ! appsink name=qemu_sink" + +struct LibcameraVideodev { + + GStreamerVideodev parent; +}; +typedef struct LibcameraVideodev LibcameraVideodev; + +DECLARE_INSTANCE_CHECKER(LibcameraVideodev, LIBCAMERA_VIDEODEV, TYPE_VIDEODEV_LIBCAMERA) + +static char *video_libcamera_pipeline_string(const char *cam_name, const char *caps) +{ + char *pipeline_desc = NULL; + size_t pipeline_template_len, pipeline_len; + + pipeline_template_len = strlen(VIDEO_LIBCAMERA_PIPELINE_TEMPLATE) - 4; // minus '%s' (x2) + pipeline_len = strlen(cam_name) + strlen(caps) + pipeline_template_len + 1; // plus '\0' + + pipeline_desc = g_malloc(pipeline_len * sizeof(char)); + if (!pipeline_desc) { + return NULL; + } + + sprintf(pipeline_desc, VIDEO_LIBCAMERA_PIPELINE_TEMPLATE, cam_name, caps); + return pipeline_desc; +} + +static int video_libcamera_open(Videodev *vd, QemuOpts *opts, Error **errp) +{ + LibcameraVideodev *lv = LIBCAMERA_VIDEODEV(vd); + GStreamerVideodev *gv = &lv->parent; + const char *cam_name = qemu_opt_get(opts, "camera-name"); + char *caps = video_gstreamer_qemu_opt_get(opts, "caps"); + char *pipeline_desc = NULL; + GError *error = NULL; + GstStateChangeReturn ret; + + if (cam_name == NULL) { + vd_error_setg(vd, errp, QERR_MISSING_PARAMETER, "camera-name"); + return VIDEODEV_RC_ERROR; + } + + if (caps == NULL) { + vd_error_setg(vd, errp, QERR_MISSING_PARAMETER, "caps"); + return VIDEODEV_RC_ERROR; + } + + pipeline_desc = video_libcamera_pipeline_string(cam_name, caps); + g_free(caps); + if (!pipeline_desc) { + vd_error_setg(vd, errp, "memory allocation failure"); + return VIDEODEV_RC_ERROR; + } + + if (!gst_is_initialized()) { + gst_init(NULL, NULL); + } + + gv->pipeline = gst_parse_bin_from_description(pipeline_desc, false, &error); + g_free(pipeline_desc); + if (error) { + vd_error_setg(vd, errp, "unable to parse pipeline: %s", error->message); + return VIDEODEV_RC_ERROR; + } + + gv->head = gst_bin_get_by_name(GST_BIN(gv->pipeline), "qemu_src"); + if (unlikely(!gv->head)) { + vd_error_setg(vd, errp, "qemu_src not found"); + return VIDEODEV_RC_ERROR; + } + + gv->tail = gst_bin_get_by_name(GST_BIN(gv->pipeline), "qemu_vc"); + if (unlikely(!gv->tail)) { + vd_error_setg(vd, errp, "qemu_vc not found"); + return VIDEODEV_RC_ERROR; + } + + gv->filter = gst_bin_get_by_name(GST_BIN(gv->pipeline), "qemu_cf"); + if (unlikely(!gv->filter)) { + vd_error_setg(vd, errp, "qemu_cf not found"); + return VIDEODEV_RC_ERROR; + } + + gv->sink = gst_bin_get_by_name(GST_BIN(gv->pipeline), "qemu_sink"); + if (unlikely(!gv->sink)) { + vd_error_setg(vd, errp, "qemu_sink not found"); + return VIDEODEV_RC_ERROR; + } + + ret = gst_element_set_state(gv->pipeline, GST_STATE_READY); + if (ret == GST_STATE_CHANGE_FAILURE) { + + vd_error_setg(vd, errp, "failed to set pipeline to READY"); + return VIDEODEV_RC_ERROR; + } + + return VIDEODEV_RC_OK; +} + +static void video_libcamera_class_init(ObjectClass *oc, const void *data) +{ + VideodevClass *vc; + video_gstreamer_class_init(oc, data); + + vc = VIDEODEV_CLASS(oc); + + /* override GStreamer class methods */ + vc->open = video_libcamera_open; + vc->enum_controls = NULL; + vc->set_control = NULL; +} + +static const TypeInfo video_libcamera_type_info = { + .name = TYPE_VIDEODEV_LIBCAMERA, + .parent = TYPE_VIDEODEV_GSTREAMER, + .instance_size = sizeof(LibcameraVideodev), + .class_init = video_libcamera_class_init, +}; + +static void register_types(void) +{ + type_register_static(&video_libcamera_type_info); +} + +type_init(register_types); diff --git a/video/meson.build b/video/meson.build index 33da556ea4..50eaf0c76e 100644 --- a/video/meson.build +++ b/video/meson.build @@ -11,6 +11,7 @@ video_modules = {} foreach m : [ ['gstreamer', gstreamer, files('gstreamer.c')], ['gstreamer-app', gstreamer_app, files('gstreamer.c')], + ['libcamera', gstreamer, files('libcamera.c', 'gstreamer.c')], ['v4l2', v4l2, files('v4l2.c')], ] if m[dep].found() -- 2.47.0