2696 lines
73 KiB
Diff
2696 lines
73 KiB
Diff
From 1e4022a90a81434c7496bc48d62257fca7667688 Mon Sep 17 00:00:00 2001
|
|
From: Jeffy Chen <jeffy.chen@rock-chips.com>
|
|
Date: Fri, 21 Jun 2024 14:49:54 +0800
|
|
Subject: [PATCH 49/49] uvcgadget: Port from 1.24.5
|
|
|
|
Based on:
|
|
3c66f10e21 Release 1.24.5
|
|
|
|
Signed-off-by: Jeffy Chen <jeffy.chen@rock-chips.com>
|
|
---
|
|
meson_options.txt | 1 +
|
|
sys/meson.build | 1 +
|
|
sys/uvcgadget/configfs.c | 1006 ++++++++++++++++++++++++++++++++++++
|
|
sys/uvcgadget/configfs.h | 119 +++++
|
|
sys/uvcgadget/gstuvcsink.c | 942 +++++++++++++++++++++++++++++++++
|
|
sys/uvcgadget/gstuvcsink.h | 79 +++
|
|
sys/uvcgadget/meson.build | 40 ++
|
|
sys/uvcgadget/uvc.c | 420 +++++++++++++++
|
|
8 files changed, 2608 insertions(+)
|
|
create mode 100644 sys/uvcgadget/configfs.c
|
|
create mode 100644 sys/uvcgadget/configfs.h
|
|
create mode 100644 sys/uvcgadget/gstuvcsink.c
|
|
create mode 100644 sys/uvcgadget/gstuvcsink.h
|
|
create mode 100644 sys/uvcgadget/meson.build
|
|
create mode 100644 sys/uvcgadget/uvc.c
|
|
|
|
diff --git a/meson_options.txt b/meson_options.txt
|
|
index 6c8855a..f6d4d9a 100644
|
|
--- a/meson_options.txt
|
|
+++ b/meson_options.txt
|
|
@@ -184,6 +184,7 @@ option('zxing', type : 'feature', value : 'auto', description : 'Barcode image s
|
|
option('wpe', type : 'feature', value : 'auto', description : 'WPE Web browser plugin')
|
|
option('magicleap', type : 'feature', value : 'auto', description : 'Magic Leap platform support')
|
|
option('v4l2codecs', type : 'feature', value : 'auto', description : 'Video4Linux Stateless CODECs support')
|
|
+option('uvcgadget', type : 'feature', value : 'auto', description : 'uvc video gadget plugin')
|
|
option('isac', type : 'feature', value : 'auto', description : 'iSAC plugin')
|
|
|
|
# HLS plugin options
|
|
diff --git a/sys/meson.build b/sys/meson.build
|
|
index cfe1778..2ad1b22 100644
|
|
--- a/sys/meson.build
|
|
+++ b/sys/meson.build
|
|
@@ -22,6 +22,7 @@ subdir('shm')
|
|
subdir('tinyalsa')
|
|
subdir('uvch264')
|
|
subdir('v4l2codecs')
|
|
+subdir('uvcgadget')
|
|
subdir('va')
|
|
subdir('wasapi')
|
|
subdir('wasapi2')
|
|
diff --git a/sys/uvcgadget/configfs.c b/sys/uvcgadget/configfs.c
|
|
new file mode 100644
|
|
index 0000000..e611e82
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/configfs.c
|
|
@@ -0,0 +1,1006 @@
|
|
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
+/*
|
|
+ * ConfigFS Gadget device handling
|
|
+ *
|
|
+ * Copyright (C) 2018 Kieran Bingham
|
|
+ *
|
|
+ * Contact: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
+ */
|
|
+
|
|
+/* To provide asprintf from the GNU library. */
|
|
+#define _GNU_SOURCE
|
|
+
|
|
+#include <dirent.h>
|
|
+#include <errno.h>
|
|
+#include <fcntl.h>
|
|
+#include <glob.h>
|
|
+#include <stdbool.h>
|
|
+#include <stdio.h>
|
|
+#include <stdlib.h>
|
|
+#include <string.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+#include "linux/videodev2.h"
|
|
+
|
|
+#include "configfs.h"
|
|
+
|
|
+/* ------------------------------------------------------------------------
|
|
+ * GUIDs and formats
|
|
+ */
|
|
+
|
|
+#define UVC_GUID_FORMAT_MJPEG \
|
|
+ { 'M', 'J', 'P', 'G', 0x00, 0x00, 0x10, 0x00, \
|
|
+ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
|
+#define UVC_GUID_FORMAT_YUY2 \
|
|
+ { 'Y', 'U', 'Y', '2', 0x00, 0x00, 0x10, 0x00, \
|
|
+ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
|
+
|
|
+struct uvc_function_format_info
|
|
+{
|
|
+ uint8_t guid[16];
|
|
+ uint32_t fcc;
|
|
+};
|
|
+
|
|
+static struct uvc_function_format_info uvc_formats[] = {
|
|
+ {
|
|
+ .guid = UVC_GUID_FORMAT_YUY2,
|
|
+ .fcc = V4L2_PIX_FMT_YUYV,
|
|
+ },
|
|
+ {
|
|
+ .guid = UVC_GUID_FORMAT_MJPEG,
|
|
+ .fcc = V4L2_PIX_FMT_MJPEG,
|
|
+ },
|
|
+};
|
|
+
|
|
+/* -----------------------------------------------------------------------------
|
|
+ * Path handling and support
|
|
+ */
|
|
+
|
|
+static char *
|
|
+path_join (const char *dirname, const char *name)
|
|
+{
|
|
+ char *path;
|
|
+ int ret;
|
|
+
|
|
+ ret = asprintf (&path, "%s/%s", dirname, name);
|
|
+
|
|
+ /*
|
|
+ * asprintf returns -1 on allocation or other errors, leaving 'path'
|
|
+ * undefined. We shouldn't even call free(path) here. We want to return
|
|
+ * NULL on error, so we must manually set it.
|
|
+ */
|
|
+ if (ret < 0)
|
|
+ path = NULL;
|
|
+
|
|
+ return path;
|
|
+}
|
|
+
|
|
+static char *
|
|
+path_glob_first_match (const char *g)
|
|
+{
|
|
+ glob_t globbuf;
|
|
+ char *match = NULL;
|
|
+
|
|
+ glob (g, 0, NULL, &globbuf);
|
|
+
|
|
+ if (globbuf.gl_pathc)
|
|
+ match = strdup (globbuf.gl_pathv[0]);
|
|
+
|
|
+ globfree (&globbuf);
|
|
+
|
|
+ return match;
|
|
+}
|
|
+
|
|
+/*
|
|
+ * Find and return the full path of the first directory entry that satisfies
|
|
+ * the match function.
|
|
+ */
|
|
+static char *
|
|
+dir_first_match (const char *dir, int (*match) (const struct dirent *))
|
|
+{
|
|
+ struct dirent **entries;
|
|
+ unsigned int i;
|
|
+ int n_entries;
|
|
+ char *path;
|
|
+
|
|
+ n_entries = scandir (dir, &entries, match, alphasort);
|
|
+ if (n_entries < 0)
|
|
+ return NULL;
|
|
+
|
|
+ if (n_entries == 0) {
|
|
+ free (entries);
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ path = path_join (dir, entries[0]->d_name);
|
|
+
|
|
+ for (i = 0; i < (unsigned int) n_entries; ++i)
|
|
+ free (entries[i]);
|
|
+
|
|
+ free (entries);
|
|
+
|
|
+ return path;
|
|
+}
|
|
+
|
|
+/* -----------------------------------------------------------------------------
|
|
+ * Attribute handling
|
|
+ */
|
|
+
|
|
+static int
|
|
+attribute_read (const char *path, const char *file, void *buf, unsigned int len)
|
|
+{
|
|
+ char *f;
|
|
+ int ret;
|
|
+ int fd;
|
|
+
|
|
+ f = path_join (path, file);
|
|
+ if (!f)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ fd = open (f, O_RDONLY);
|
|
+ free (f);
|
|
+ if (fd == -1) {
|
|
+ printf ("Failed to open attribute %s: %s\n", file, strerror (errno));
|
|
+ return -ENOENT;
|
|
+ }
|
|
+
|
|
+ ret = read (fd, buf, len);
|
|
+ close (fd);
|
|
+
|
|
+ if (ret < 0) {
|
|
+ printf ("Failed to read attribute %s: %s\n", file, strerror (errno));
|
|
+ return -ENODATA;
|
|
+ }
|
|
+
|
|
+ return len;
|
|
+}
|
|
+
|
|
+static int
|
|
+attribute_read_uint (const char *path, const char *file, unsigned int *val)
|
|
+{
|
|
+ /* 4,294,967,295 */
|
|
+ char buf[11];
|
|
+ char *endptr;
|
|
+ int ret;
|
|
+
|
|
+ ret = attribute_read (path, file, buf, sizeof (buf) - 1);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ buf[ret] = '\0';
|
|
+
|
|
+ errno = 0;
|
|
+
|
|
+ /* base 0: Autodetect hex, octal, decimal. */
|
|
+ *val = strtoul (buf, &endptr, 0);
|
|
+ if (errno)
|
|
+ return -errno;
|
|
+
|
|
+ if (endptr == buf)
|
|
+ return -ENODATA;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static char *
|
|
+attribute_read_str (const char *path, const char *file)
|
|
+{
|
|
+ char buf[1024];
|
|
+ char *p;
|
|
+ int ret;
|
|
+
|
|
+ ret = attribute_read (path, file, buf, sizeof (buf) - 1);
|
|
+ if (ret < 0)
|
|
+ return NULL;
|
|
+
|
|
+ buf[ret] = '\0';
|
|
+
|
|
+ p = strrchr (buf, '\n');
|
|
+ if (p != buf)
|
|
+ *p = '\0';
|
|
+
|
|
+ return strdup (buf);
|
|
+}
|
|
+
|
|
+/* -----------------------------------------------------------------------------
|
|
+ * UDC parsing
|
|
+ */
|
|
+
|
|
+/*
|
|
+ * udc_find_video_device - Find the video device node for a UVC function
|
|
+ * @udc: The UDC name
|
|
+ * @function: The UVC function name
|
|
+ *
|
|
+ * This function finds the video device node corresponding to a UVC function as
|
|
+ * specified by a @function name and @udc name.
|
|
+ *
|
|
+ * The @function parameter specifies the name of the USB function, usually in
|
|
+ * the form "uvc.%u". If NULL the first function found will be used.
|
|
+ *
|
|
+ * The @udc parameter specifies the name of the UDC. If NULL any UDC that
|
|
+ * contains a function matching the @function name will be used.
|
|
+ *
|
|
+ * Return a pointer to a newly allocated string containing the video device node
|
|
+ * full path if the function is found. Otherwise return NULL. The returned
|
|
+ * pointer must be freed by the caller with a call to free().
|
|
+ */
|
|
+static char *
|
|
+udc_find_video_device (const char *udc, const char *function)
|
|
+{
|
|
+ char *vpath;
|
|
+ char *video = NULL;
|
|
+ glob_t globbuf;
|
|
+ unsigned int i;
|
|
+ int ret;
|
|
+
|
|
+ ret = asprintf (&vpath,
|
|
+ "/sys/class/udc/%s/device/gadget*/video4linux/video*", udc ? udc : "*");
|
|
+ if (!ret)
|
|
+ return NULL;
|
|
+
|
|
+ glob (vpath, 0, NULL, &globbuf);
|
|
+ free (vpath);
|
|
+
|
|
+ for (i = 0; i < globbuf.gl_pathc; ++i) {
|
|
+ char *config;
|
|
+ bool match;
|
|
+
|
|
+ /* Match on the first if no search string. */
|
|
+ if (!function)
|
|
+ break;
|
|
+
|
|
+ config = attribute_read_str (globbuf.gl_pathv[i], "function_name");
|
|
+ match = strcmp (function, config) == 0;
|
|
+
|
|
+ free (config);
|
|
+
|
|
+ if (match)
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (i < globbuf.gl_pathc) {
|
|
+ gchar *v = g_path_get_basename (globbuf.gl_pathv[i]);
|
|
+
|
|
+ video = path_join ("/dev", v);
|
|
+ g_free (v);
|
|
+ }
|
|
+
|
|
+ globfree (&globbuf);
|
|
+
|
|
+ return video;
|
|
+}
|
|
+
|
|
+/* -----------------------------------------------------------------------------
|
|
+ * Legacy g_webcam support
|
|
+ */
|
|
+
|
|
+static const struct uvc_function_config g_webcam_config = {
|
|
+ .control = {
|
|
+ .intf = {
|
|
+ .bInterfaceNumber = 0,
|
|
+ },
|
|
+ },
|
|
+ .streaming = {
|
|
+ .intf = {
|
|
+ .bInterfaceNumber = 1,
|
|
+ },
|
|
+ .ep = {
|
|
+ .bInterval = 1,
|
|
+ .bMaxBurst = 0,
|
|
+ .wMaxPacketSize = 1024,
|
|
+ },
|
|
+ .num_formats = 2,
|
|
+ .formats = (struct uvc_function_config_format[]) {
|
|
+ {
|
|
+ .index = 1,
|
|
+ .guid = UVC_GUID_FORMAT_YUY2,
|
|
+ .fcc = V4L2_PIX_FMT_YUYV,
|
|
+ .num_frames = 2,
|
|
+ .frames = (struct uvc_function_config_frame[]) {
|
|
+ {
|
|
+ .index = 1,
|
|
+ .width = 640,
|
|
+ .height = 360,
|
|
+ .num_intervals = 3,
|
|
+ .intervals = (unsigned int[]) {
|
|
+ 666666,
|
|
+ 10000000,
|
|
+ 50000000,
|
|
+ },
|
|
+ }, {
|
|
+ .index = 2,
|
|
+ .width = 1280,
|
|
+ .height = 720,
|
|
+ .num_intervals = 1,
|
|
+ .intervals = (unsigned int[]) {
|
|
+ 50000000,
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+ }, {
|
|
+ .index = 2,
|
|
+ .guid = UVC_GUID_FORMAT_MJPEG,
|
|
+ .fcc = V4L2_PIX_FMT_MJPEG,
|
|
+ .num_frames = 2,
|
|
+ .frames = (struct uvc_function_config_frame[]) {
|
|
+ {
|
|
+ .index = 1,
|
|
+ .width = 640,
|
|
+ .height = 360,
|
|
+ .num_intervals = 3,
|
|
+ .intervals = (unsigned int[]) {
|
|
+ 666666,
|
|
+ 10000000,
|
|
+ 50000000,
|
|
+ },
|
|
+ }, {
|
|
+ .index = 2,
|
|
+ .width = 1280,
|
|
+ .height = 720,
|
|
+ .num_intervals = 1,
|
|
+ .intervals = (unsigned int[]) {
|
|
+ 50000000,
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+static void *
|
|
+memdup (const void *src, size_t size)
|
|
+{
|
|
+ void *dst;
|
|
+
|
|
+ dst = malloc (size);
|
|
+ if (!dst)
|
|
+ return NULL;
|
|
+ memcpy (dst, src, size);
|
|
+ return dst;
|
|
+}
|
|
+
|
|
+static int
|
|
+parse_legacy_g_webcam (const char *udc, struct uvc_function_config *fc)
|
|
+{
|
|
+ unsigned int i, j;
|
|
+ size_t size;
|
|
+
|
|
+ *fc = g_webcam_config;
|
|
+
|
|
+ /*
|
|
+ * We need to duplicate all sub-structures as the
|
|
+ * configfs_free_uvc_function() function expects them to be dynamically
|
|
+ * allocated.
|
|
+ */
|
|
+ size = sizeof *fc->streaming.formats * fc->streaming.num_formats;
|
|
+ fc->streaming.formats = memdup (fc->streaming.formats, size);
|
|
+
|
|
+ for (i = 0; i < fc->streaming.num_formats; ++i) {
|
|
+ struct uvc_function_config_format *format = &fc->streaming.formats[i];
|
|
+
|
|
+ size = sizeof *format->frames * format->num_frames;
|
|
+ format->frames = memdup (format->frames, size);
|
|
+
|
|
+ for (j = 0; j < format->num_frames; ++j) {
|
|
+ struct uvc_function_config_frame *frame = &format->frames[j];
|
|
+
|
|
+ size = sizeof *frame->intervals * frame->num_intervals;
|
|
+ frame->intervals = memdup (frame->intervals, size);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fc->video = udc_find_video_device (udc, NULL);
|
|
+
|
|
+ return fc->video ? 0 : -ENODEV;
|
|
+}
|
|
+
|
|
+/* -----------------------------------------------------------------------------
|
|
+ * ConfigFS support
|
|
+ */
|
|
+
|
|
+/*
|
|
+ * configfs_mount_point - Identify the ConfigFS mount location
|
|
+ *
|
|
+ * Return a pointer to a newly allocated string containing the fully qualified
|
|
+ * path to the ConfigFS mount point if located. Returns NULL if the ConfigFS
|
|
+ * mount point can not be identified.
|
|
+ */
|
|
+static char *
|
|
+configfs_mount_point (void)
|
|
+{
|
|
+ FILE *mounts;
|
|
+ char *line = NULL;
|
|
+ char *path = NULL;
|
|
+ size_t len = 0;
|
|
+
|
|
+ mounts = fopen ("/proc/mounts", "r");
|
|
+ if (mounts == NULL)
|
|
+ return NULL;
|
|
+
|
|
+ while (getline (&line, &len, mounts) != -1) {
|
|
+ if (strstr (line, "configfs")) {
|
|
+ char *saveptr;
|
|
+ char *token;
|
|
+
|
|
+ /* Obtain the second token. */
|
|
+ token = strtok_r (line, " ", &saveptr);
|
|
+ token = strtok_r (NULL, " ", &saveptr);
|
|
+
|
|
+ if (token)
|
|
+ path = strdup (token);
|
|
+
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ free (line);
|
|
+ fclose (mounts);
|
|
+
|
|
+ return path;
|
|
+}
|
|
+
|
|
+/*
|
|
+ * configfs_find_uvc_function - Find the ConfigFS full path for a UVC function
|
|
+ * @function: The UVC function name
|
|
+ *
|
|
+ * Return a pointer to a newly allocated string containing the full ConfigFS
|
|
+ * path to the function if the function is found. Otherwise return NULL. The
|
|
+ * returned pointer must be freed by the caller with a call to free().
|
|
+ */
|
|
+static char *
|
|
+configfs_find_uvc_function (const char *function)
|
|
+{
|
|
+ const char *target = function ? function : "*";
|
|
+ const char *format;
|
|
+ char *configfs;
|
|
+ char *func_path;
|
|
+ char *path;
|
|
+ int ret;
|
|
+
|
|
+ configfs = configfs_mount_point ();
|
|
+ if (!configfs)
|
|
+ printf ("Failed to locate configfs mount point, using default\n");
|
|
+
|
|
+ /*
|
|
+ * The function description can be provided as a path from the
|
|
+ * usb_gadget root "g1/functions/uvc.0", or if there is no ambiguity
|
|
+ * over the gadget name, a shortcut "uvc.0" can be provided.
|
|
+ */
|
|
+ if (!strchr (target, '/'))
|
|
+ format = "%s/usb_gadget/*/functions/%s";
|
|
+ else
|
|
+ format = "%s/usb_gadget/%s";
|
|
+
|
|
+ ret = asprintf (&path, format, configfs ? configfs : "/sys/kernel/config",
|
|
+ target);
|
|
+ free (configfs);
|
|
+ if (!ret)
|
|
+ return NULL;
|
|
+
|
|
+ func_path = path_glob_first_match (path);
|
|
+ free (path);
|
|
+
|
|
+ return func_path;
|
|
+}
|
|
+
|
|
+/*
|
|
+ * configfs_free_uvc_function - Free a uvc_function_config object
|
|
+ * @fc: The uvc_function_config to be freed
|
|
+ *
|
|
+ * Free the given @fc function previously allocated by a call to
|
|
+ * configfs_parse_uvc_function().
|
|
+ */
|
|
+void
|
|
+configfs_free_uvc_function (struct uvc_function_config *fc)
|
|
+{
|
|
+ unsigned int i, j;
|
|
+
|
|
+ free (fc->udc);
|
|
+ free (fc->video);
|
|
+
|
|
+ for (i = 0; i < fc->streaming.num_formats; ++i) {
|
|
+ struct uvc_function_config_format *format = &fc->streaming.formats[i];
|
|
+
|
|
+ for (j = 0; j < format->num_frames; ++j) {
|
|
+ struct uvc_function_config_frame *frame = &format->frames[j];
|
|
+
|
|
+ free (frame->intervals);
|
|
+ }
|
|
+
|
|
+ free (format->frames);
|
|
+ }
|
|
+
|
|
+ free (fc->streaming.formats);
|
|
+ free (fc);
|
|
+}
|
|
+
|
|
+#define configfs_parse_child(parent, child, cfg, parse) \
|
|
+({ \
|
|
+ char *__path; \
|
|
+ int __ret; \
|
|
+ \
|
|
+ __path = path_join((parent), (child)); \
|
|
+ if (__path) { \
|
|
+ __ret = parse(__path, (cfg)); \
|
|
+ free(__path); \
|
|
+ } else { \
|
|
+ __ret = -ENOMEM; \
|
|
+ } \
|
|
+ \
|
|
+ __ret; \
|
|
+})
|
|
+
|
|
+static int
|
|
+configfs_parse_interface (const char *path,
|
|
+ struct uvc_function_config_interface *cfg)
|
|
+{
|
|
+ int ret;
|
|
+
|
|
+ ret = attribute_read_uint (path, "bInterfaceNumber", &cfg->bInterfaceNumber);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_control (const char *path,
|
|
+ struct uvc_function_config_control *cfg)
|
|
+{
|
|
+ int ret;
|
|
+
|
|
+ ret = configfs_parse_interface (path, &cfg->intf);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_streaming_frame (const char *path,
|
|
+ struct uvc_function_config_frame *frame)
|
|
+{
|
|
+ char *intervals;
|
|
+ char *p;
|
|
+ int ret = 0;
|
|
+
|
|
+ ret = ret ? : attribute_read_uint (path, "bFrameIndex", &frame->index);
|
|
+ ret = ret ? : attribute_read_uint (path, "wWidth", &frame->width);
|
|
+ ret = ret ? : attribute_read_uint (path, "wHeight", &frame->height);
|
|
+ ret = ret ? : attribute_read_uint (path, "dwMaxVideoFrameBufferSize",
|
|
+ &frame->maxvideofbsize);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ intervals = attribute_read_str (path, "dwFrameInterval");
|
|
+ if (!intervals)
|
|
+ return -EINVAL;
|
|
+
|
|
+ for (p = intervals; *p;) {
|
|
+ unsigned int interval;
|
|
+ unsigned int *mem;
|
|
+ char *endp;
|
|
+ size_t size;
|
|
+
|
|
+ interval = strtoul (p, &endp, 10);
|
|
+ if (*endp != '\0' && *endp != '\n') {
|
|
+ ret = -EINVAL;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ p = *endp ? endp + 1 : endp;
|
|
+
|
|
+ size = sizeof *frame->intervals * (frame->num_intervals + 1);
|
|
+ mem = realloc (frame->intervals, size);
|
|
+ if (!mem) {
|
|
+ ret = -ENOMEM;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ frame->intervals = mem;
|
|
+ frame->intervals[frame->num_intervals++] = interval;
|
|
+ }
|
|
+
|
|
+ free (intervals);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+frame_filter (const struct dirent *ent)
|
|
+{
|
|
+ /* Accept all directories but "." and "..". */
|
|
+ if (ent->d_type != DT_DIR)
|
|
+ return 0;
|
|
+ if (!strcmp (ent->d_name, "."))
|
|
+ return 0;
|
|
+ if (!strcmp (ent->d_name, ".."))
|
|
+ return 0;
|
|
+ return 1;
|
|
+}
|
|
+
|
|
+static int
|
|
+frame_compare (const void *a, const void *b)
|
|
+{
|
|
+ const struct uvc_function_config_frame *fa = a;
|
|
+ const struct uvc_function_config_frame *fb = b;
|
|
+
|
|
+ if (fa->index < fb->index)
|
|
+ return -1;
|
|
+ else if (fa->index == fb->index)
|
|
+ return 0;
|
|
+ else
|
|
+ return 1;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_streaming_format (const char *path,
|
|
+ struct uvc_function_config_format *format)
|
|
+{
|
|
+ struct dirent **entries;
|
|
+ char link_target[1024];
|
|
+ char *segment;
|
|
+ unsigned int i;
|
|
+ int n_entries;
|
|
+ int ret;
|
|
+
|
|
+ ret = attribute_read_uint (path, "bFormatIndex", &format->index);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ ret = readlink (path, link_target, sizeof (link_target) - 1);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ link_target[ret] = '\0';
|
|
+
|
|
+ /*
|
|
+ * Extract the second-to-last path component of the link target,
|
|
+ * which contains the format descriptor type name as exposed by
|
|
+ * the UVC function driver.
|
|
+ */
|
|
+ segment = strrchr (link_target, '/');
|
|
+ if (!segment)
|
|
+ return -EINVAL;
|
|
+ *segment = '\0';
|
|
+ segment = strrchr (link_target, '/');
|
|
+ if (!segment)
|
|
+ return -EINVAL;
|
|
+ segment++;
|
|
+
|
|
+ if (!strcmp (segment, "mjpeg")) {
|
|
+ static const uint8_t guid[16] = UVC_GUID_FORMAT_MJPEG;
|
|
+ memcpy (format->guid, guid, 16);
|
|
+ } else if (!strcmp (segment, "uncompressed")) {
|
|
+ ret = attribute_read (path, "guidFormat", format->guid,
|
|
+ sizeof (format->guid));
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+ } else {
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ for (i = 0; i < G_N_ELEMENTS (uvc_formats); ++i) {
|
|
+ if (!memcmp (uvc_formats[i].guid, format->guid, 16)) {
|
|
+ format->fcc = uvc_formats[i].fcc;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Find all entries corresponding to a frame and parse them. */
|
|
+ n_entries = scandir (path, &entries, frame_filter, alphasort);
|
|
+ if (n_entries < 0)
|
|
+ return -errno;
|
|
+
|
|
+ if (n_entries == 0) {
|
|
+ free (entries);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ format->num_frames = n_entries;
|
|
+ format->frames = calloc (sizeof *format->frames, format->num_frames);
|
|
+ if (!format->frames)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ for (i = 0; i < (unsigned int) n_entries; ++i) {
|
|
+ char *frame;
|
|
+
|
|
+ frame = path_join (path, entries[i]->d_name);
|
|
+ if (!frame) {
|
|
+ ret = -ENOMEM;
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ ret = configfs_parse_streaming_frame (frame, &format->frames[i]);
|
|
+ free (frame);
|
|
+ if (ret < 0)
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ /* Sort the frames by index. */
|
|
+ qsort (format->frames, format->num_frames, sizeof *format->frames,
|
|
+ frame_compare);
|
|
+
|
|
+done:
|
|
+ for (i = 0; i < (unsigned int) n_entries; ++i)
|
|
+ free (entries[i]);
|
|
+ free (entries);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+format_filter (const struct dirent *ent)
|
|
+{
|
|
+ char *path;
|
|
+ bool valid;
|
|
+
|
|
+ /*
|
|
+ * Accept all links that point to a directory containing a
|
|
+ * "bFormatIndex" file.
|
|
+ */
|
|
+ if (ent->d_type != DT_LNK)
|
|
+ return 0;
|
|
+
|
|
+ /* HACK: Support mjpeg and uncompressed only */
|
|
+ if (strcmp(ent->d_name, "m") && strcmp(ent->d_name, "u"))
|
|
+ return 0;
|
|
+
|
|
+ path = path_join (ent->d_name, "bFormatIndex");
|
|
+ if (!path)
|
|
+ return 0;
|
|
+
|
|
+ valid = access (path, R_OK);
|
|
+ free (path);
|
|
+ return valid;
|
|
+}
|
|
+
|
|
+static int
|
|
+format_compare (const void *a, const void *b)
|
|
+{
|
|
+ const struct uvc_function_config_format *fa = a;
|
|
+ const struct uvc_function_config_format *fb = b;
|
|
+
|
|
+ if (fa->index < fb->index)
|
|
+ return -1;
|
|
+ else if (fa->index == fb->index)
|
|
+ return 0;
|
|
+ else
|
|
+ return 1;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_streaming_header (const char *path,
|
|
+ struct uvc_function_config_streaming *cfg)
|
|
+{
|
|
+ struct dirent **entries;
|
|
+ unsigned int i;
|
|
+ int n_entries;
|
|
+ int ret;
|
|
+
|
|
+ /* Find all entries corresponding to a format and parse them. */
|
|
+ n_entries = scandir (path, &entries, format_filter, alphasort);
|
|
+ if (n_entries < 0)
|
|
+ return -errno;
|
|
+
|
|
+ if (n_entries == 0) {
|
|
+ free (entries);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ cfg->num_formats = n_entries;
|
|
+ cfg->formats = calloc (sizeof *cfg->formats, cfg->num_formats);
|
|
+ if (!cfg->formats)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ for (i = 0; i < (unsigned int) n_entries; ++i) {
|
|
+ char *format;
|
|
+
|
|
+ format = path_join (path, entries[i]->d_name);
|
|
+ if (!format) {
|
|
+ ret = -ENOMEM;
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ ret = configfs_parse_streaming_format (format, &cfg->formats[i]);
|
|
+ free (format);
|
|
+ if (ret < 0)
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ /* Sort the formats by index. */
|
|
+ qsort (cfg->formats, cfg->num_formats, sizeof *cfg->formats, format_compare);
|
|
+
|
|
+done:
|
|
+ for (i = 0; i < (unsigned int) n_entries; ++i)
|
|
+ free (entries[i]);
|
|
+ free (entries);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+link_filter (const struct dirent *ent)
|
|
+{
|
|
+ /* Accept all links. */
|
|
+ return ent->d_type == DT_LNK;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_streaming (const char *path,
|
|
+ struct uvc_function_config_streaming *cfg)
|
|
+{
|
|
+ char *header;
|
|
+ char *class;
|
|
+ int ret;
|
|
+
|
|
+ ret = configfs_parse_interface (path, &cfg->intf);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ /*
|
|
+ * Handle the high-speed class descriptors only for now. Find the first
|
|
+ * link to the class descriptors.
|
|
+ */
|
|
+ class = path_join (path, "class/hs");
|
|
+ if (!class)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ header = dir_first_match (class, link_filter);
|
|
+ free (class);
|
|
+ if (!header)
|
|
+ return -EINVAL;
|
|
+
|
|
+ ret = configfs_parse_streaming_header (header, cfg);
|
|
+ free (header);
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int
|
|
+configfs_parse_uvc (const char *fpath, struct uvc_function_config *fc)
|
|
+{
|
|
+ int ret = 0;
|
|
+
|
|
+ ret = ret ? : configfs_parse_child (fpath, "control", &fc->control,
|
|
+ configfs_parse_control);
|
|
+ ret = ret ? : configfs_parse_child (fpath, "streaming", &fc->streaming,
|
|
+ configfs_parse_streaming);
|
|
+
|
|
+ /*
|
|
+ * These parameters should be part of the streaming interface in
|
|
+ * ConfigFS, but for legacy reasons they are located directly in the
|
|
+ * function directory.
|
|
+ */
|
|
+ ret = ret ? : attribute_read_uint (fpath, "streaming_interval",
|
|
+ &fc->streaming.ep.bInterval);
|
|
+ ret = ret ? : attribute_read_uint (fpath, "streaming_maxburst",
|
|
+ &fc->streaming.ep.bMaxBurst);
|
|
+ ret = ret ? : attribute_read_uint (fpath, "streaming_maxpacket",
|
|
+ &fc->streaming.ep.wMaxPacketSize);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+/*
|
|
+ * configfs_parse_uvc_function - Parse a UVC function configuration in ConfigFS
|
|
+ * @function: The function name
|
|
+ *
|
|
+ * This function locates and parse the configuration of a UVC function in
|
|
+ * ConfigFS as specified by the @function name argument. The function name can
|
|
+ * be fully qualified with a gadget name (e.g. "g%u/functions/uvc.%u"), or as a
|
|
+ * shortcut can be an unqualified function name (e.g. "uvc.%u"). When the
|
|
+ * function name is unqualified, the first function matching the name in any
|
|
+ * UDC will be returned.
|
|
+ *
|
|
+ * Return a pointer to a newly allocated UVC function configuration structure
|
|
+ * that contains configuration parameters for the function, if the function is
|
|
+ * found. Otherwise return NULL. The returned pointer must be freed by the
|
|
+ * caller with a call to free().
|
|
+ */
|
|
+struct uvc_function_config *
|
|
+configfs_parse_uvc_function (const char *function)
|
|
+{
|
|
+ struct uvc_function_config *fc;
|
|
+ char *fpath;
|
|
+ gchar *bname;
|
|
+ int ret = 0;
|
|
+
|
|
+ fc = malloc (sizeof *fc);
|
|
+ if (fc == NULL)
|
|
+ return NULL;
|
|
+
|
|
+ memset (fc, 0, sizeof *fc);
|
|
+
|
|
+ /* Find the function in ConfigFS. */
|
|
+ fpath = configfs_find_uvc_function (function);
|
|
+ if (!fpath) {
|
|
+ /*
|
|
+ * If the function can't be found attempt legacy parsing to
|
|
+ * support the g_webcam gadget. The function parameter contains
|
|
+ * a UDC name in that case.
|
|
+ */
|
|
+ ret = parse_legacy_g_webcam (function, fc);
|
|
+ if (ret) {
|
|
+ configfs_free_uvc_function (fc);
|
|
+ fc = NULL;
|
|
+ }
|
|
+
|
|
+ return fc;
|
|
+ }
|
|
+
|
|
+ /*
|
|
+ * Parse the function configuration. Remove the gadget name qualifier
|
|
+ * from the function name, if any.
|
|
+ */
|
|
+ bname = g_path_get_basename (function);
|
|
+
|
|
+ fc->udc = attribute_read_str (fpath, "../../UDC");
|
|
+ fc->video = udc_find_video_device (fc->udc, bname);
|
|
+ if (!fc->video) {
|
|
+ ret = -ENODEV;
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ ret = configfs_parse_uvc (fpath, fc);
|
|
+
|
|
+done:
|
|
+ if (ret) {
|
|
+ configfs_free_uvc_function (fc);
|
|
+ fc = NULL;
|
|
+ }
|
|
+
|
|
+ free (fpath);
|
|
+ g_free (bname);
|
|
+
|
|
+ return fc;
|
|
+}
|
|
+
|
|
+static char *
|
|
+video_find_config_name (const char *video)
|
|
+{
|
|
+ char *vpath;
|
|
+ glob_t globbuf;
|
|
+ char *config;
|
|
+ int ret;
|
|
+
|
|
+ ret = asprintf (&vpath,
|
|
+ "/sys/class/udc/*/device/gadget*/video4linux/%s", video ? video : "*");
|
|
+ if (!ret)
|
|
+ return NULL;
|
|
+
|
|
+ glob (vpath, 0, NULL, &globbuf);
|
|
+ free (vpath);
|
|
+
|
|
+ if (globbuf.gl_pathc != 1)
|
|
+ return NULL;
|
|
+
|
|
+ config = attribute_read_str (globbuf.gl_pathv[0], "function_name");
|
|
+
|
|
+ globfree (&globbuf);
|
|
+
|
|
+ return config;
|
|
+}
|
|
+
|
|
+struct uvc_function_config *
|
|
+configfs_parse_uvc_videodev (int fd, const char *video)
|
|
+{
|
|
+ struct uvc_function_config *fc;
|
|
+ char *function = NULL;
|
|
+ char rpath[PATH_MAX];
|
|
+ char *res;
|
|
+ gchar *bname;
|
|
+
|
|
+ res = realpath (video, rpath);
|
|
+ if (!res)
|
|
+ return NULL;
|
|
+
|
|
+ bname = g_path_get_basename (rpath);
|
|
+ function = video_find_config_name (bname);
|
|
+ g_free (bname);
|
|
+
|
|
+ if (!function)
|
|
+ return NULL;
|
|
+
|
|
+ fc = configfs_parse_uvc_function (function);
|
|
+
|
|
+ free (function);
|
|
+
|
|
+ return fc;
|
|
+}
|
|
diff --git a/sys/uvcgadget/configfs.h b/sys/uvcgadget/configfs.h
|
|
new file mode 100644
|
|
index 0000000..1cbca78
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/configfs.h
|
|
@@ -0,0 +1,119 @@
|
|
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
+/*
|
|
+ * ConfigFS Gadget device handling
|
|
+ *
|
|
+ * Copyright (C) 2018 Kieran Bingham
|
|
+ *
|
|
+ * Contact: Kieran Bingham <kieran.bingham@ideasonboard.com>
|
|
+ */
|
|
+
|
|
+#ifndef __CONFIGFS_H__
|
|
+#define __CONFIGFS_H__
|
|
+
|
|
+#include <stdint.h>
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_endpoint - Endpoint parameters
|
|
+ * @bInterval: Transfer interval (interrupt and isochronous only)
|
|
+ * @bMaxBurst: Transfer burst size (super-speed only)
|
|
+ * @wMaxPacketSize: Maximum packet size (including the multiplier)
|
|
+ */
|
|
+struct uvc_function_config_endpoint
|
|
+{
|
|
+ unsigned int bInterval;
|
|
+ unsigned int bMaxBurst;
|
|
+ unsigned int wMaxPacketSize;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_interface - Interface parameters
|
|
+ * @bInterfaceNumber: Interface number
|
|
+ */
|
|
+struct uvc_function_config_interface
|
|
+{
|
|
+ unsigned int bInterfaceNumber;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_control - Control interface parameters
|
|
+ * @intf: Generic interface parameters
|
|
+ */
|
|
+struct uvc_function_config_control
|
|
+{
|
|
+ struct uvc_function_config_interface intf;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_frame - Streaming frame parameters
|
|
+ * @index: Frame index in the UVC descriptors
|
|
+ * @width: Frame width in pixels
|
|
+ * @height: Frame height in pixels
|
|
+ * @num_intervals: Number of entries in the intervals array
|
|
+ * @intervals: Array of frame intervals
|
|
+ */
|
|
+struct uvc_function_config_frame
|
|
+{
|
|
+ unsigned int index;
|
|
+ unsigned int width;
|
|
+ unsigned int height;
|
|
+ unsigned int num_intervals;
|
|
+ unsigned int *intervals;
|
|
+ unsigned int maxvideofbsize;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_format - Streaming format parameters
|
|
+ * @index: Format index in the UVC descriptors
|
|
+ * @guid: Format GUID
|
|
+ * @fcc: V4L2 pixel format
|
|
+ * @num_frames: Number of entries in the frames array
|
|
+ * @frames: Array of frame descriptors
|
|
+ */
|
|
+struct uvc_function_config_format
|
|
+{
|
|
+ unsigned int index;
|
|
+ uint8_t guid[16];
|
|
+ unsigned int fcc;
|
|
+ unsigned int num_frames;
|
|
+ struct uvc_function_config_frame *frames;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config_streaming - Streaming interface parameters
|
|
+ * @intf: Generic interface parameters
|
|
+ * @ep: Endpoint parameters
|
|
+ * @num_formats: Number of entries in the formats array
|
|
+ * @formats: Array of format descriptors
|
|
+ */
|
|
+struct uvc_function_config_streaming
|
|
+{
|
|
+ struct uvc_function_config_interface intf;
|
|
+ struct uvc_function_config_endpoint ep;
|
|
+
|
|
+ unsigned int num_formats;
|
|
+ struct uvc_function_config_format *formats;
|
|
+};
|
|
+
|
|
+/*
|
|
+ * struct uvc_function_config - UVC function configuration parameters
|
|
+ * @video: Full path to the video device node
|
|
+ * @udc: UDC name
|
|
+ * @control: Control interface configuration
|
|
+ * @streaming: Streaming interface configuration
|
|
+ */
|
|
+struct uvc_function_config
|
|
+{
|
|
+ char *video;
|
|
+ char *udc;
|
|
+
|
|
+ struct uvc_function_config_control control;
|
|
+ struct uvc_function_config_streaming streaming;
|
|
+};
|
|
+
|
|
+struct uvc_function_config *configfs_parse_uvc_function (const char *function);
|
|
+void configfs_free_uvc_function (struct uvc_function_config *fc);
|
|
+
|
|
+struct uvc_function_config *configfs_parse_uvc_videodev (int fd,
|
|
+ const char *video);
|
|
+
|
|
+#endif
|
|
diff --git a/sys/uvcgadget/gstuvcsink.c b/sys/uvcgadget/gstuvcsink.c
|
|
new file mode 100644
|
|
index 0000000..b921e23
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/gstuvcsink.c
|
|
@@ -0,0 +1,942 @@
|
|
+/*
|
|
+ * SPDX-License-Identifier: LGPL-2.0-or-later
|
|
+ *
|
|
+ * Copyright (C) 2023 Pengutronix e.K. - www.pengutronix.de
|
|
+ *
|
|
+ */
|
|
+
|
|
+/**
|
|
+ * SECTION:plugin-uvcgadget
|
|
+ * @title: uvcgadget
|
|
+ *
|
|
+ * Since: 1.24
|
|
+ */
|
|
+
|
|
+/**
|
|
+ * SECTION:element-uvcsink
|
|
+ * @title: uvcsink
|
|
+ *
|
|
+ * uvcsink can be used to push frames to the Linux UVC Gadget
|
|
+ * driver.
|
|
+ *
|
|
+ * ## Example launch lines
|
|
+ * |[
|
|
+ * gst-launch videotestsrc ! uvcsink v4l2sink::device=/dev/video1
|
|
+ * ]|
|
|
+ * This pipeline streams a test pattern on UVC gadget /dev/video1.
|
|
+ *
|
|
+ * Before starting the pipeline, the linux system needs an uvc gadget
|
|
+ * configured on the udc (usb device controller)
|
|
+ *
|
|
+ * Either by using the legacy g_webcam gadget or by preconfiguring it
|
|
+ * with configfs. One way to configure is to use the example script from
|
|
+ * uvc-gadget:
|
|
+ *
|
|
+ * https://git.ideasonboard.org/uvc-gadget.git/blob/HEAD:/scripts/uvc-gadget.sh
|
|
+ *
|
|
+ * A modern way of configuring the gadget with an scheme file that gets
|
|
+ * loaded by gadget-tool (gt) using libusbgx.
|
|
+ *
|
|
+ * https://github.com/linux-usb-gadgets/libusbgx
|
|
+ * https://github.com/linux-usb-gadgets/gt
|
|
+ *
|
|
+ * Since: 1.24
|
|
+ */
|
|
+#define _GNU_SOURCE
|
|
+
|
|
+#ifdef HAVE_CONFIG_H
|
|
+#include <config.h>
|
|
+#endif
|
|
+
|
|
+#include <sys/ioctl.h>
|
|
+#include <fcntl.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+#include <gst/gst.h>
|
|
+#include <gst/pbutils/pbutils.h>
|
|
+
|
|
+#include "gstuvcsink.h"
|
|
+
|
|
+GST_DEBUG_CATEGORY (uvcsink_debug);
|
|
+#define GST_CAT_DEFAULT uvcsink_debug
|
|
+
|
|
+/**
|
|
+ * gst_util_simplify_fraction:
|
|
+ * @numerator: First value as #gint
|
|
+ * @denominator: Second value as #gint
|
|
+ * @n_terms: non-significative terms (typical value: 8)
|
|
+ * @threshold: threshold (typical value: 333)
|
|
+ *
|
|
+ * Calculates the simpler representation of @numerator and @denominator and
|
|
+ * update both values with the resulting simplified fraction.
|
|
+ *
|
|
+ * Simplify a fraction using a simple continued fraction decomposition.
|
|
+ * The idea here is to convert fractions such as 333333/10000000 to 1/30
|
|
+ * using 32 bit arithmetic only. The algorithm is not perfect and relies
|
|
+ * upon two arbitrary parameters to remove non-significative terms from
|
|
+ * the simple continued fraction decomposition. Using 8 and 333 for
|
|
+ * @n_terms and @threshold respectively seems to give nice results.
|
|
+ *
|
|
+ * Since: 1.24
|
|
+ */
|
|
+static void
|
|
+gst_util_simplify_fraction (gint * numerator, gint * denominator,
|
|
+ guint n_terms, guint threshold)
|
|
+{
|
|
+ guint *an;
|
|
+ guint x, y, r;
|
|
+ guint i, n;
|
|
+
|
|
+ an = g_malloc_n (n_terms, sizeof (*an));
|
|
+ if (an == NULL)
|
|
+ return;
|
|
+
|
|
+ /*
|
|
+ * Convert the fraction to a simple continued fraction. See
|
|
+ * https://en.wikipedia.org/wiki/Continued_fraction
|
|
+ * Stop if the current term is bigger than or equal to the given
|
|
+ * threshold.
|
|
+ */
|
|
+ x = *numerator;
|
|
+ y = *denominator;
|
|
+
|
|
+ for (n = 0; n < n_terms && y != 0; ++n) {
|
|
+ an[n] = x / y;
|
|
+ if (an[n] >= threshold) {
|
|
+ if (n < 2)
|
|
+ n++;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ r = x - an[n] * y;
|
|
+ x = y;
|
|
+ y = r;
|
|
+ }
|
|
+
|
|
+ /* Expand the simple continued fraction back to an integer fraction. */
|
|
+ x = 0;
|
|
+ y = 1;
|
|
+
|
|
+ for (i = n; i > 0; --i) {
|
|
+ r = y;
|
|
+ y = an[i - 1] * y + x;
|
|
+ x = r;
|
|
+ }
|
|
+
|
|
+ *numerator = y;
|
|
+ *denominator = x;
|
|
+ g_free (an);
|
|
+}
|
|
+
|
|
+enum
|
|
+{
|
|
+ ARG_0,
|
|
+ PROP_STREAMING,
|
|
+ PROP_LAST
|
|
+};
|
|
+
|
|
+#define gst_uvc_sink_parent_class parent_class
|
|
+G_DEFINE_TYPE (GstUvcSink, gst_uvc_sink, GST_TYPE_BIN);
|
|
+GST_ELEMENT_REGISTER_DEFINE (uvcsink,
|
|
+ "uvcsink", GST_RANK_NONE, GST_TYPE_UVCSINK);
|
|
+
|
|
+/* GstElement methods: */
|
|
+static GstStateChangeReturn gst_uvc_sink_change_state (GstElement *
|
|
+ element, GstStateChange transition);
|
|
+
|
|
+static void gst_uvc_sink_dispose (GObject * object);
|
|
+static gboolean gst_uvc_sink_prepare_configfs (GstUvcSink * self);
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_get_property (GObject * object, guint prop_id,
|
|
+ GValue * value, GParamSpec * pspec)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (object);
|
|
+
|
|
+ switch (prop_id) {
|
|
+ case PROP_STREAMING:
|
|
+ g_value_set_boolean (value, g_atomic_int_get (&self->streaming));
|
|
+ break;
|
|
+ default:
|
|
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
+ break;
|
|
+ }
|
|
+}
|
|
+
|
|
+static GstStructure *
|
|
+gst_v4l2uvc_fourcc_to_bare_struct (guint32 fourcc)
|
|
+{
|
|
+ GstStructure *structure = NULL;
|
|
+
|
|
+ /* Since MJPEG and YUY2 are currently the only one supported
|
|
+ * we limit the function to parse only these fourccs
|
|
+ */
|
|
+ switch (fourcc) {
|
|
+ case V4L2_PIX_FMT_MJPEG: /* Motion-JPEG */
|
|
+ case V4L2_PIX_FMT_JPEG: /* JFIF JPEG */
|
|
+ structure = gst_structure_new_empty ("image/jpeg");
|
|
+ break;
|
|
+ case V4L2_PIX_FMT_YUYV:{
|
|
+ GstVideoFormat format = GST_VIDEO_FORMAT_YUY2;
|
|
+ if (format != GST_VIDEO_FORMAT_UNKNOWN)
|
|
+ structure = gst_structure_new ("video/x-raw",
|
|
+ "format", G_TYPE_STRING, gst_video_format_to_string (format), NULL);
|
|
+ break;
|
|
+ }
|
|
+ break;
|
|
+ default:
|
|
+ GST_DEBUG ("Unsupported fourcc 0x%08x %" GST_FOURCC_FORMAT,
|
|
+ fourcc, GST_FOURCC_ARGS (fourcc));
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ return structure;
|
|
+}
|
|
+
|
|
+/* The UVC EVENT_DATA from the host, which is committing the currently
|
|
+ * selected configuration (format+resolution+framerate) is only selected
|
|
+ * by some index values (except the framerate). We have to transform
|
|
+ * those values to an valid caps string that we can return on the caps
|
|
+ * query.
|
|
+ */
|
|
+static GstCaps *
|
|
+gst_uvc_sink_get_configured_caps (GstUvcSink * self)
|
|
+{
|
|
+ struct v4l2_fmtdesc format;
|
|
+ struct v4l2_frmsizeenum size;
|
|
+ struct v4l2_frmivalenum ival;
|
|
+ guint32 width, height;
|
|
+ gint device_fd;
|
|
+ GstStructure *s;
|
|
+ gint numerator, denominator;
|
|
+ GValue framerate = { 0, };
|
|
+
|
|
+ g_object_get (G_OBJECT (self->v4l2sink), "device-fd", &device_fd, NULL);
|
|
+
|
|
+ format.index = self->cur.bFormatIndex - 1;
|
|
+ format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
|
|
+
|
|
+ if (ioctl (device_fd, VIDIOC_ENUM_FMT, &format) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ, ("Linux kernel error"),
|
|
+ ("VIDIOC_ENUM_FMT failed: %s (%d)", g_strerror (errno), errno));
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ s = gst_v4l2uvc_fourcc_to_bare_struct (format.pixelformat);
|
|
+
|
|
+ memset (&size, 0, sizeof (struct v4l2_frmsizeenum));
|
|
+ size.index = self->cur.bFrameIndex - 1;
|
|
+ size.pixel_format = format.pixelformat;
|
|
+
|
|
+ if (ioctl (device_fd, VIDIOC_ENUM_FRAMESIZES, &size) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ, ("Linux kernel error"),
|
|
+ ("VIDIOC_ENUM_FRAMESIZES failed: %s (%d)", g_strerror (errno), errno));
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ GST_LOG_OBJECT (self, "got discrete frame size %dx%d",
|
|
+ size.discrete.width, size.discrete.height);
|
|
+
|
|
+ width = MIN (size.discrete.width, G_MAXINT);
|
|
+ height = MIN (size.discrete.height, G_MAXINT);
|
|
+
|
|
+ if (!width || !height)
|
|
+ return NULL;
|
|
+
|
|
+ g_value_init (&framerate, GST_TYPE_FRACTION);
|
|
+
|
|
+ memset (&ival, 0, sizeof (struct v4l2_frmivalenum));
|
|
+
|
|
+ ival.index = 0;
|
|
+ ival.pixel_format = format.pixelformat;
|
|
+ ival.width = width;
|
|
+ ival.height = height;
|
|
+
|
|
+#define SIMPLIFY_FRACTION_N_TERMS 8
|
|
+#define SIMPLIFY_FRACTION_THRESHOLD 333
|
|
+
|
|
+ numerator = self->cur.dwFrameInterval;
|
|
+ denominator = 10000000;
|
|
+ gst_util_simplify_fraction (&numerator, &denominator,
|
|
+ SIMPLIFY_FRACTION_N_TERMS, SIMPLIFY_FRACTION_THRESHOLD);
|
|
+
|
|
+ if (ioctl (device_fd, VIDIOC_ENUM_FRAMEINTERVALS, &ival) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ, ("Linux kernel error"),
|
|
+ ("VIDIOC_ENUM_FRAMEINTERVALS failed: %s (%d)",
|
|
+ g_strerror (errno), errno));
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ do {
|
|
+ gint inumerator = ival.discrete.numerator;
|
|
+ gint idenominator = ival.discrete.denominator;
|
|
+
|
|
+ gst_util_simplify_fraction (&inumerator, &idenominator,
|
|
+ SIMPLIFY_FRACTION_N_TERMS, SIMPLIFY_FRACTION_THRESHOLD);
|
|
+
|
|
+ if (numerator == inumerator && denominator == idenominator)
|
|
+ /* swap to get the framerate */
|
|
+ gst_value_set_fraction (&framerate, denominator, numerator);
|
|
+
|
|
+ ival.index++;
|
|
+ } while (ioctl (device_fd, VIDIOC_ENUM_FRAMEINTERVALS, &ival) >= 0);
|
|
+
|
|
+ gst_structure_set (s, "width", G_TYPE_INT, (gint) width,
|
|
+ "height", G_TYPE_INT, (gint) height, NULL);
|
|
+
|
|
+ gst_structure_take_value (s, "framerate", &framerate);
|
|
+
|
|
+ return gst_caps_new_full (s, NULL);
|
|
+}
|
|
+
|
|
+static gboolean gst_uvc_sink_to_fakesink (GstUvcSink * self);
|
|
+static gboolean gst_uvc_sink_to_v4l2sink (GstUvcSink * self);
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_update_streaming (GstUvcSink * self)
|
|
+{
|
|
+ if (self->streamon && !self->streaming)
|
|
+ GST_ERROR_OBJECT (self, "Unexpected STREAMON");
|
|
+ if (self->streamoff && self->streaming)
|
|
+ GST_ERROR_OBJECT (self, "Unexpected STREAMOFF");
|
|
+
|
|
+ if (self->streamon)
|
|
+ gst_uvc_sink_to_v4l2sink (self);
|
|
+
|
|
+ g_atomic_int_set (&self->streamon, FALSE);
|
|
+ g_atomic_int_set (&self->streamoff, FALSE);
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (parent);
|
|
+
|
|
+ switch (GST_EVENT_TYPE (event)) {
|
|
+ case GST_EVENT_CAPS:
|
|
+ GST_DEBUG_OBJECT (self, "Handling %" GST_PTR_FORMAT, event);
|
|
+
|
|
+ /* EVENT CAPS signals that the buffers after the event will use new caps.
|
|
+ * If the UVC host requested a new format, we now must start the stream.
|
|
+ */
|
|
+ if (self->caps_changed) {
|
|
+ if (self->streamon || self->streamoff)
|
|
+ g_atomic_int_set (&self->caps_changed, FALSE);
|
|
+
|
|
+ gst_uvc_sink_update_streaming (self);
|
|
+ }
|
|
+ break;
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ return gst_pad_event_default (pad, parent, event);
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_query (GstPad * pad, GstObject * parent, GstQuery * query)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (parent);
|
|
+
|
|
+ switch (GST_QUERY_TYPE (query)) {
|
|
+ case GST_QUERY_CAPS:
|
|
+ {
|
|
+ if (gst_caps_is_empty (self->cur_caps))
|
|
+ return gst_pad_query_default (pad, parent, query);
|
|
+
|
|
+ GST_DEBUG_OBJECT (self, "caps %" GST_PTR_FORMAT, self->cur_caps);
|
|
+ gst_query_set_caps_result (query, self->cur_caps);
|
|
+
|
|
+ if (!self->caps_changed)
|
|
+ gst_uvc_sink_update_streaming (self);
|
|
+
|
|
+ return TRUE;
|
|
+ }
|
|
+ case GST_QUERY_ALLOCATION:
|
|
+ return TRUE;
|
|
+ default:
|
|
+ return gst_pad_query_default (pad, parent, query);
|
|
+ }
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_class_init (GstUvcSinkClass * klass)
|
|
+{
|
|
+ GObjectClass *gobject_class;
|
|
+ GstElementClass *element_class;
|
|
+
|
|
+ element_class = GST_ELEMENT_CLASS (klass);
|
|
+
|
|
+ element_class->change_state = gst_uvc_sink_change_state;
|
|
+
|
|
+ gst_element_class_set_metadata (element_class,
|
|
+ "UVC Sink", "Sink/Video",
|
|
+ "Streams Video via UVC Gadget", "Michael Grzeschik <mgr@pengutronix.de>");
|
|
+
|
|
+ GST_DEBUG_CATEGORY_INIT (uvcsink_debug, "uvcsink", 0, "uvc sink element");
|
|
+
|
|
+ gobject_class = G_OBJECT_CLASS (klass);
|
|
+ gobject_class->dispose = gst_uvc_sink_dispose;
|
|
+ gobject_class->get_property = gst_uvc_sink_get_property;
|
|
+
|
|
+ /**
|
|
+ * uvcsink:streaming:
|
|
+ *
|
|
+ * The stream status of the host.
|
|
+ *
|
|
+ * Since: 1.24
|
|
+ */
|
|
+ g_object_class_install_property (gobject_class, PROP_STREAMING,
|
|
+ g_param_spec_boolean ("streaming", "streaming",
|
|
+ "The stream status of the host", 0,
|
|
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_to_fakesink (GstUvcSink * self)
|
|
+{
|
|
+ if (gst_pad_is_linked (self->fakesinkpad)) {
|
|
+ GST_DEBUG_OBJECT (self, "v4l2sink already disabled");
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ GST_DEBUG_OBJECT (self, "switching to fakesink");
|
|
+ gst_ghost_pad_set_target (GST_GHOST_PAD (self->sinkpad), self->fakesinkpad);
|
|
+ gst_element_set_state (GST_ELEMENT (self->fakesink), GST_STATE_PLAYING);
|
|
+
|
|
+ /* going to state READY makes v4l2sink lose its reference to the clock */
|
|
+ self->v4l2_clock = gst_element_get_clock (self->v4l2sink);
|
|
+
|
|
+ gst_pad_query (self->v4l2sinkpad, gst_query_new_drain ());
|
|
+
|
|
+ gst_element_set_state (GST_ELEMENT (self->v4l2sink), GST_STATE_READY);
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_to_v4l2sink (GstUvcSink * self)
|
|
+{
|
|
+ if (gst_pad_is_linked (self->v4l2sinkpad)) {
|
|
+ GST_DEBUG_OBJECT (self, "fakesink already disabled");
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ if (self->v4l2_clock) {
|
|
+ gst_element_set_clock (self->v4l2sink, self->v4l2_clock);
|
|
+ gst_object_unref (self->v4l2_clock);
|
|
+ }
|
|
+
|
|
+ GST_DEBUG_OBJECT (self, "switching to v4l2sink");
|
|
+
|
|
+ gst_ghost_pad_set_target (GST_GHOST_PAD (self->sinkpad), self->v4l2sinkpad);
|
|
+ gst_element_set_state (GST_ELEMENT (self->v4l2sink), GST_STATE_PLAYING);
|
|
+
|
|
+ gst_pad_query (self->fakesinkpad, gst_query_new_drain ());
|
|
+
|
|
+ gst_element_set_state (GST_ELEMENT (self->fakesink), GST_STATE_READY);
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static GstPadProbeReturn
|
|
+gst_uvc_sink_sinkpad_buffer_peer_probe (GstPad * pad,
|
|
+ GstPadProbeInfo * info, gpointer user_data)
|
|
+{
|
|
+ GstUvcSink *self = user_data;
|
|
+
|
|
+ if (self->streamon || self->streamoff)
|
|
+ return GST_PAD_PROBE_DROP;
|
|
+
|
|
+ self->buffer_peer_probe_id = 0;
|
|
+
|
|
+ return GST_PAD_PROBE_REMOVE;
|
|
+}
|
|
+
|
|
+static GstPadProbeReturn
|
|
+gst_uvc_sink_sinkpad_idle_probe (GstPad * pad,
|
|
+ GstPadProbeInfo * info, gpointer user_data)
|
|
+{
|
|
+ GstUvcSink *self = user_data;
|
|
+
|
|
+ if (self->streamon || self->streamoff) {
|
|
+ /* Drop all incoming buffers until the streamoff or streamon is done. */
|
|
+ self->buffer_peer_probe_id =
|
|
+ gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BUFFER,
|
|
+ gst_uvc_sink_sinkpad_buffer_peer_probe, self, NULL);
|
|
+
|
|
+ GST_DEBUG_OBJECT (self, "Send reconfigure");
|
|
+ gst_pad_push_event (self->sinkpad, gst_event_new_reconfigure ());
|
|
+ }
|
|
+
|
|
+ if (self->streamoff)
|
|
+ gst_uvc_sink_to_fakesink (self);
|
|
+
|
|
+ return GST_PAD_PROBE_PASS;
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_remove_buffer_peer_probe (GstUvcSink * self)
|
|
+{
|
|
+ GstPad *peerpad = gst_pad_get_peer (self->sinkpad);
|
|
+ if (peerpad && self->buffer_peer_probe_id) {
|
|
+ gst_pad_remove_probe (peerpad, self->buffer_peer_probe_id);
|
|
+ gst_object_unref (peerpad);
|
|
+ self->buffer_peer_probe_id = 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_create_idle_probe (GstUvcSink * self)
|
|
+{
|
|
+ GstPad *peerpad = gst_pad_get_peer (self->sinkpad);
|
|
+ if (peerpad) {
|
|
+ self->idle_probe_id =
|
|
+ gst_pad_add_probe (peerpad, GST_PAD_PROBE_TYPE_IDLE,
|
|
+ gst_uvc_sink_sinkpad_idle_probe, self, NULL);
|
|
+ gst_object_unref (peerpad);
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_remove_idle_probe (GstUvcSink * self)
|
|
+{
|
|
+ GstPad *peerpad = gst_pad_get_peer (self->sinkpad);
|
|
+ if (peerpad && self->idle_probe_id) {
|
|
+ gst_pad_remove_probe (peerpad, self->idle_probe_id);
|
|
+ gst_object_unref (peerpad);
|
|
+ self->idle_probe_id = 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_open (GstUvcSink * self)
|
|
+{
|
|
+ if (!self->v4l2sink) {
|
|
+ gst_element_post_message (GST_ELEMENT_CAST (self),
|
|
+ gst_missing_element_message_new (GST_ELEMENT_CAST (self), "v4l2sink"));
|
|
+
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("No v4l2sink element found"), ("Check your plugin installation"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ if (!self->fakesink) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("No fakesink element found"), ("Check your plugin installation"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_init (GstUvcSink * self)
|
|
+{
|
|
+ /* add the v4l2sink element */
|
|
+ self->v4l2sink = gst_element_factory_make ("v4l2sink", "v4l2sink");
|
|
+ if (!self->v4l2sink)
|
|
+ return;
|
|
+
|
|
+ g_object_set (G_OBJECT (self->v4l2sink), "async", FALSE, NULL);
|
|
+ gst_bin_add (GST_BIN (self), self->v4l2sink);
|
|
+
|
|
+ /* add the fakesink element */
|
|
+ self->fakesink = gst_element_factory_make ("fakesink", "fakesink");
|
|
+ if (!self->fakesink)
|
|
+ return;
|
|
+
|
|
+ g_object_set (G_OBJECT (self->fakesink), "sync", TRUE, NULL);
|
|
+ gst_bin_add (GST_BIN (self), self->fakesink);
|
|
+
|
|
+ self->v4l2sinkpad = gst_element_get_static_pad (self->v4l2sink, "sink");
|
|
+ g_return_if_fail (self->v4l2sinkpad != NULL);
|
|
+
|
|
+ self->fakesinkpad = gst_element_get_static_pad (self->fakesink, "sink");
|
|
+ g_return_if_fail (self->fakesinkpad != NULL);
|
|
+
|
|
+ /* create ghost pad sink */
|
|
+ self->sinkpad = gst_ghost_pad_new ("sink", self->fakesinkpad);
|
|
+ gst_element_add_pad (GST_ELEMENT (self), self->sinkpad);
|
|
+
|
|
+ g_atomic_int_set (&self->streaming, FALSE);
|
|
+
|
|
+ g_atomic_int_set (&self->streamon, FALSE);
|
|
+ g_atomic_int_set (&self->streamoff, FALSE);
|
|
+
|
|
+ gst_pad_set_query_function (self->sinkpad, gst_uvc_sink_query);
|
|
+ gst_pad_set_event_function (self->sinkpad, gst_uvc_sink_event);
|
|
+
|
|
+ self->cur_caps = gst_caps_new_empty ();
|
|
+}
|
|
+
|
|
+static void
|
|
+gst_uvc_sink_dispose (GObject * object)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (object);
|
|
+
|
|
+ if (self->sinkpad) {
|
|
+ gst_uvc_sink_remove_buffer_peer_probe (self);
|
|
+
|
|
+ gst_pad_set_active (self->sinkpad, FALSE);
|
|
+ gst_element_remove_pad (GST_ELEMENT (self), self->sinkpad);
|
|
+ self->sinkpad = NULL;
|
|
+ }
|
|
+
|
|
+ G_OBJECT_CLASS (parent_class)->dispose (object);
|
|
+}
|
|
+
|
|
+static gboolean gst_uvc_sink_unwatch (GstUvcSink * self);
|
|
+
|
|
+/* the thread where everything happens */
|
|
+static void
|
|
+gst_uvc_sink_task (gpointer data)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (data);
|
|
+ GstClockTime timeout = GST_CLOCK_TIME_NONE;
|
|
+ struct uvc_request_data resp;
|
|
+ gboolean ret = TRUE;
|
|
+
|
|
+ /* Since the plugin needs to be able to start immidiatly in PLAYING
|
|
+ state we ensure the pipeline is not blocked while we poll for
|
|
+ events.
|
|
+ */
|
|
+ GST_PAD_STREAM_UNLOCK (self->sinkpad);
|
|
+
|
|
+ ret = gst_poll_wait (self->poll, timeout);
|
|
+ if (G_UNLIKELY (ret < 0))
|
|
+ return;
|
|
+
|
|
+ timeout = GST_CLOCK_TIME_NONE;
|
|
+
|
|
+ if (gst_poll_fd_has_closed (self->poll, &self->pollfd)) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ, ("videofd was closed"), NULL);
|
|
+ gst_uvc_sink_unwatch (self);
|
|
+ gst_element_set_state (GST_ELEMENT (self), GST_STATE_NULL);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (gst_poll_fd_has_error (self->poll, &self->pollfd)) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ, ("videofd has error"), NULL);
|
|
+ gst_uvc_sink_unwatch (self);
|
|
+ gst_element_set_state (GST_ELEMENT (self), GST_STATE_NULL);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* PRI is used to signal that events are available */
|
|
+ if (gst_poll_fd_has_pri (self->poll, &self->pollfd)) {
|
|
+ struct v4l2_event event = { 0, };
|
|
+ struct uvc_event *uvc_event = (void *) &event.u.data;
|
|
+
|
|
+ if (ioctl (self->pollfd.fd, VIDIOC_DQEVENT, &event) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ,
|
|
+ ("could not dequeue event"), NULL);
|
|
+ gst_uvc_sink_unwatch (self);
|
|
+ gst_element_set_state (GST_ELEMENT (self), GST_STATE_NULL);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ switch (event.type) {
|
|
+ case UVC_EVENT_STREAMON:
|
|
+ GST_DEBUG_OBJECT (self, "UVC_EVENT_STREAMON");
|
|
+ GST_STATE_LOCK (GST_ELEMENT (self));
|
|
+ g_atomic_int_set (&self->streaming, TRUE);
|
|
+ g_atomic_int_set (&self->streamon, TRUE);
|
|
+ GST_STATE_UNLOCK (GST_ELEMENT (self));
|
|
+ g_object_notify (G_OBJECT (self), "streaming");
|
|
+ break;
|
|
+ case UVC_EVENT_STREAMOFF:
|
|
+ case UVC_EVENT_DISCONNECT:
|
|
+ GST_DEBUG_OBJECT (self, "UVC_EVENT_STREAMOFF");
|
|
+ GST_STATE_LOCK (GST_ELEMENT (self));
|
|
+ g_atomic_int_set (&self->streaming, FALSE);
|
|
+ g_atomic_int_set (&self->streamoff, TRUE);
|
|
+ GST_STATE_UNLOCK (GST_ELEMENT (self));
|
|
+ g_object_notify (G_OBJECT (self), "streaming");
|
|
+ break;
|
|
+ case UVC_EVENT_SETUP:
|
|
+ GST_DEBUG_OBJECT (self, "UVC_EVENT_SETUP");
|
|
+
|
|
+ memset (&resp, 0, sizeof (resp));
|
|
+ resp.length = -EL2HLT;
|
|
+
|
|
+ uvc_events_process_setup (self, &uvc_event->req, &resp);
|
|
+
|
|
+ if (ioctl (self->pollfd.fd, UVCIOC_SEND_RESPONSE, &resp) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("UVCIOC_SEND_RESPONSE failed"),
|
|
+ ("UVCIOC_SEND_RESPONSE on UVC_EVENT_SETUP failed"));
|
|
+ gst_uvc_sink_unwatch (self);
|
|
+ gst_element_set_state (GST_ELEMENT (self), GST_STATE_NULL);
|
|
+ return;
|
|
+ }
|
|
+ break;
|
|
+ case UVC_EVENT_DATA:
|
|
+ GST_DEBUG_OBJECT (self, "UVC_EVENT_DATA");
|
|
+ uvc_events_process_data (self, &uvc_event->data);
|
|
+ if (self->control == UVC_VS_COMMIT_CONTROL) {
|
|
+ GstCaps *configured_caps;
|
|
+ GstCaps *prev_caps;
|
|
+
|
|
+ /* The configured caps are not sufficient for negotiation. Select caps
|
|
+ * from the probed caps that match the configured caps.
|
|
+ */
|
|
+ configured_caps = gst_uvc_sink_get_configured_caps (self);
|
|
+ gst_clear_caps (&self->cur_caps);
|
|
+ self->cur_caps =
|
|
+ gst_caps_intersect_full (self->probed_caps, configured_caps,
|
|
+ GST_CAPS_INTERSECT_FIRST);
|
|
+ GST_INFO_OBJECT (self, "UVC host selected %" GST_PTR_FORMAT,
|
|
+ self->cur_caps);
|
|
+ gst_caps_unref (configured_caps);
|
|
+
|
|
+ prev_caps = gst_pad_get_current_caps (self->sinkpad);
|
|
+ if (!gst_caps_is_subset (prev_caps, self->cur_caps)) {
|
|
+ self->caps_changed = TRUE;
|
|
+ GST_DEBUG_OBJECT (self,
|
|
+ "caps changed from %" GST_PTR_FORMAT, prev_caps);
|
|
+ }
|
|
+ gst_caps_unref (prev_caps);
|
|
+ }
|
|
+ break;
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_watch (GstUvcSink * self)
|
|
+{
|
|
+ struct v4l2_event_subscription sub = {.type = UVC_EVENT_STREAMON };
|
|
+ gboolean ret = TRUE;
|
|
+ gint device_fd;
|
|
+ gint fd;
|
|
+
|
|
+ ret = gst_uvc_sink_prepare_configfs (self);
|
|
+ if (!ret) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("could not parse configfs"), ("Check your configfs setup"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ g_object_get (G_OBJECT (self->v4l2sink), "device-fd", &device_fd, NULL);
|
|
+
|
|
+ fd = dup3 (device_fd, device_fd + 1, O_CLOEXEC);
|
|
+ if (fd < 0)
|
|
+ return FALSE;
|
|
+
|
|
+ self->poll = gst_poll_new (TRUE);
|
|
+ gst_poll_fd_init (&self->pollfd);
|
|
+ self->pollfd.fd = fd;
|
|
+ gst_poll_add_fd (self->poll, &self->pollfd);
|
|
+ gst_poll_fd_ctl_pri (self->poll, &self->pollfd, TRUE);
|
|
+
|
|
+ if (ioctl (device_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to subscribe event"),
|
|
+ ("UVC_EVENT_STREAMON could not be subscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_STREAMOFF;
|
|
+ if (ioctl (device_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to subscribe event"),
|
|
+ ("UVC_EVENT_STREAMOFF could not be subscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_DISCONNECT;
|
|
+ if (ioctl (device_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to subscribe event"),
|
|
+ ("UVC_EVENT_DISCONNECT could not be subscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_SETUP;
|
|
+ if (ioctl (device_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to subscribe event"),
|
|
+ ("UVC_EVENT_SETUP could not be subscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_DATA;
|
|
+ if (ioctl (device_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to subscribe event"),
|
|
+ ("UVC_EVENT_DATA could not be subscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ if (!gst_pad_start_task (GST_PAD (self->sinkpad),
|
|
+ (GstTaskFunction) gst_uvc_sink_task, self, NULL)) {
|
|
+ GST_ELEMENT_ERROR (self, CORE, THREAD, ("Could not start pad task"),
|
|
+ ("Could not start pad task"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_unwatch (GstUvcSink * self)
|
|
+{
|
|
+ struct v4l2_event_subscription sub = {.type = UVC_EVENT_DATA };
|
|
+ gint device_fd;
|
|
+
|
|
+ gst_poll_set_flushing (self->poll, TRUE);
|
|
+ gst_pad_stop_task (GST_PAD (self->sinkpad));
|
|
+
|
|
+ g_object_get (G_OBJECT (self->v4l2sink), "device-fd", &device_fd, NULL);
|
|
+
|
|
+ if (ioctl (device_fd, VIDIOC_UNSUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to unsubscribe event"),
|
|
+ ("UVC_EVENT_DATA could not be unsubscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_SETUP;
|
|
+ if (ioctl (device_fd, VIDIOC_UNSUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to unsubscribe event"),
|
|
+ ("UVC_EVENT_SETUP could not be unsubscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_STREAMON;
|
|
+ if (ioctl (device_fd, VIDIOC_UNSUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to unsubscribe event"),
|
|
+ ("UVC_EVENT_STREAMON could not be unsubscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_STREAMOFF;
|
|
+ if (ioctl (device_fd, VIDIOC_UNSUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to unsubscribe event"),
|
|
+ ("UVC_EVENT_STREAMOFF could not be unsubscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ sub.type = UVC_EVENT_DISCONNECT;
|
|
+ if (ioctl (device_fd, VIDIOC_UNSUBSCRIBE_EVENT, &sub) < 0) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to unsubscribe event"),
|
|
+ ("UVC_EVENT_DISCONNECT could not be unsubscribed"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_prepare_configfs (GstUvcSink * self)
|
|
+{
|
|
+ gint device_fd;
|
|
+ GValue device = G_VALUE_INIT;
|
|
+
|
|
+ g_object_get (G_OBJECT (self->v4l2sink), "device-fd", &device_fd, NULL);
|
|
+ g_object_get_property (G_OBJECT (self->v4l2sink), "device", &device);
|
|
+
|
|
+ self->fc = configfs_parse_uvc_videodev (device_fd,
|
|
+ g_value_get_string (&device));
|
|
+ if (!self->fc) {
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
+ ("Failed to identify function configuration"),
|
|
+ ("Check your configfs setup"));
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ uvc_fill_streaming_control (self, &self->probe, self->cur.bFrameIndex,
|
|
+ self->cur.bFormatIndex, self->cur.dwFrameInterval);
|
|
+ uvc_fill_streaming_control (self, &self->commit, self->cur.bFrameIndex,
|
|
+ self->cur.bFormatIndex, self->cur.dwFrameInterval);
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+gst_uvc_sink_query_probed_caps (GstUvcSink * self)
|
|
+{
|
|
+ GstQuery *caps_query = gst_query_new_caps (NULL);
|
|
+ GstCaps *query_caps;
|
|
+
|
|
+ gst_clear_caps (&self->probed_caps);
|
|
+ if (!gst_pad_query (self->v4l2sinkpad, caps_query))
|
|
+ return FALSE;
|
|
+
|
|
+ gst_query_parse_caps_result (caps_query, &query_caps);
|
|
+ gst_query_unref (caps_query);
|
|
+
|
|
+ self->probed_caps = gst_caps_copy (query_caps);
|
|
+
|
|
+ gst_caps_replace (&self->cur_caps, self->probed_caps);
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static GstStateChangeReturn
|
|
+gst_uvc_sink_change_state (GstElement * element, GstStateChange transition)
|
|
+{
|
|
+ GstUvcSink *self = GST_UVCSINK (element);
|
|
+ int bret = GST_STATE_CHANGE_SUCCESS;
|
|
+
|
|
+ GST_DEBUG_OBJECT (self, "%s -> %s",
|
|
+ gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
|
|
+ gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
|
|
+
|
|
+ switch (transition) {
|
|
+ case GST_STATE_CHANGE_NULL_TO_READY:
|
|
+ if (!gst_uvc_sink_open (self))
|
|
+ return GST_STATE_CHANGE_FAILURE;
|
|
+ break;
|
|
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
|
|
+ gst_uvc_sink_create_idle_probe (self);
|
|
+ break;
|
|
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
|
|
+ gst_element_sync_state_with_parent (GST_ELEMENT (self->fakesink));
|
|
+ gst_uvc_sink_remove_idle_probe (self);
|
|
+ break;
|
|
+ case GST_STATE_CHANGE_READY_TO_NULL:
|
|
+ if (!gst_uvc_sink_unwatch (self))
|
|
+ return GST_STATE_CHANGE_FAILURE;
|
|
+ break;
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ bret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
|
+
|
|
+ switch (transition) {
|
|
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
|
|
+ break;
|
|
+ case GST_STATE_CHANGE_NULL_TO_READY:
|
|
+ if (!gst_uvc_sink_watch (self))
|
|
+ return GST_STATE_CHANGE_FAILURE;
|
|
+ if (!gst_uvc_sink_query_probed_caps (self))
|
|
+ return GST_STATE_CHANGE_FAILURE;
|
|
+ break;
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ return bret;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+plugin_init (GstPlugin * plugin)
|
|
+{
|
|
+ return GST_ELEMENT_REGISTER (uvcsink, plugin);
|
|
+}
|
|
+
|
|
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
|
|
+ GST_VERSION_MINOR,
|
|
+ uvcgadget,
|
|
+ "gstuvcgadget plugin",
|
|
+ plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
|
|
diff --git a/sys/uvcgadget/gstuvcsink.h b/sys/uvcgadget/gstuvcsink.h
|
|
new file mode 100644
|
|
index 0000000..27e594e
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/gstuvcsink.h
|
|
@@ -0,0 +1,79 @@
|
|
+/*
|
|
+ * SPDX-License-Identifier: LGPL-2.0-or-later
|
|
+ *
|
|
+ * Copyright (C) 2023 Pengutronix e.K. - www.pengutronix.de
|
|
+ *
|
|
+ */
|
|
+
|
|
+#pragma once
|
|
+
|
|
+#include "linux/usb/g_uvc.h"
|
|
+#include "linux/usb/video.h"
|
|
+#include "linux/videodev2.h"
|
|
+
|
|
+#include <gst/gst.h>
|
|
+
|
|
+#include "configfs.h"
|
|
+
|
|
+G_BEGIN_DECLS GST_DEBUG_CATEGORY_EXTERN (uvcsink_debug);
|
|
+
|
|
+#define GST_TYPE_UVCSINK (gst_uvc_sink_get_type())
|
|
+G_DECLARE_FINAL_TYPE (GstUvcSink, gst_uvc_sink, GST, UVCSINK, GstBin)
|
|
+
|
|
+GST_ELEMENT_REGISTER_DECLARE (uvcsink);
|
|
+
|
|
+struct _GstUvcSink
|
|
+{
|
|
+ GstBin bin;
|
|
+ GstElement *fakesink;
|
|
+ GstElement *v4l2sink;
|
|
+ GstPad *sinkpad;
|
|
+ GstPad *fakesinkpad;
|
|
+ GstPad *v4l2sinkpad;
|
|
+
|
|
+ /* streaming status */
|
|
+ gboolean streaming;
|
|
+
|
|
+ GstCaps *probed_caps;
|
|
+ GstCaps *cur_caps;
|
|
+
|
|
+ /* a poll for video_fd */
|
|
+ GstPoll *poll;
|
|
+ GstPollFD pollfd;
|
|
+
|
|
+ struct uvc_function_config *fc;
|
|
+
|
|
+ struct {
|
|
+ int bFrameIndex;
|
|
+ int bFormatIndex;
|
|
+ unsigned int dwFrameInterval;
|
|
+ } cur;
|
|
+
|
|
+ struct uvc_streaming_control probe;
|
|
+ struct uvc_streaming_control commit;
|
|
+
|
|
+ int control;
|
|
+
|
|
+ /* probes */
|
|
+ int buffer_peer_probe_id;
|
|
+ int idle_probe_id;
|
|
+
|
|
+ GstClock *v4l2_clock;
|
|
+
|
|
+ int caps_changed;
|
|
+ int streamon;
|
|
+ int streamoff;
|
|
+};
|
|
+
|
|
+#define UVCSINK_MSG_LOCK(v) g_mutex_lock(&(v)->msg_lock)
|
|
+#define UVCSINK_MSG_UNLOCK(v) g_mutex_unlock(&(v)->msg_lock)
|
|
+
|
|
+int uvc_events_process_data(GstUvcSink * self,
|
|
+ const struct uvc_request_data *data);
|
|
+int uvc_events_process_setup(GstUvcSink * self,
|
|
+ const struct usb_ctrlrequest *ctrl,
|
|
+ struct uvc_request_data *resp);
|
|
+int uvc_fill_streaming_control(GstUvcSink * self,
|
|
+ struct uvc_streaming_control *ctrl,
|
|
+ int iframe, int iformat, unsigned int dwival);
|
|
+G_END_DECLS
|
|
diff --git a/sys/uvcgadget/meson.build b/sys/uvcgadget/meson.build
|
|
new file mode 100644
|
|
index 0000000..0d3cbf4
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/meson.build
|
|
@@ -0,0 +1,40 @@
|
|
+uvcgadget_sources = [
|
|
+ 'gstuvcsink.c',
|
|
+ 'configfs.c',
|
|
+ 'uvc.c'
|
|
+]
|
|
+
|
|
+libgudev_dep = dependency('gudev-1.0', required: get_option('uvcgadget'))
|
|
+
|
|
+if get_option('uvcgadget').disabled()
|
|
+ have_v4l2 = false
|
|
+ message('UVCSink plugin is disabled')
|
|
+else
|
|
+ # Should only be built on Linux, check for Linux kernel headers even though
|
|
+ # we have our own copy.
|
|
+ have_v4l2 = cc.has_header('linux/videodev2.h') or cc.has_header('sys/videodev2.h') or cc.has_header('sys/videoio.h')
|
|
+ if get_option('uvcgadget').enabled() and not have_v4l2
|
|
+ error('UVCGADGET is requested but kernel headers were not found')
|
|
+ endif
|
|
+
|
|
+ # Find makedev in various header files. Different operating systems put the
|
|
+ # macro in different header files.
|
|
+ foreach name: ['mkdev', 'sysmacros', 'types']
|
|
+ have_makedev = cc.has_header_symbol('sys/@0@.h'.format(name), 'makedev')
|
|
+ cdata.set10('HAVE_MAKEDEV_IN_' + name.to_upper(), have_makedev)
|
|
+ endforeach
|
|
+endif
|
|
+
|
|
+if have_v4l2 and libgudev_dep.found()
|
|
+ gstuvcgadget = library('gstuvcgadget',
|
|
+ uvcgadget_sources,
|
|
+ c_args : gst_plugins_bad_args,
|
|
+ cpp_args: gst_plugins_bad_args,
|
|
+ include_directories : [configinc, include_directories('../v4l2codecs')],
|
|
+ dependencies : [gstbase_dep, gstvideo_dep, gstallocators_dep, libgudev_dep, gstpbutils_dep,],
|
|
+ install : true,
|
|
+ install_dir : plugins_install_dir,
|
|
+ )
|
|
+ pkgconfig.generate(gstuvcgadget, install_dir : plugins_pkgconfig_install_dir)
|
|
+ plugins += [gstuvcgadget]
|
|
+endif
|
|
diff --git a/sys/uvcgadget/uvc.c b/sys/uvcgadget/uvc.c
|
|
new file mode 100644
|
|
index 0000000..e00a5a2
|
|
--- /dev/null
|
|
+++ b/sys/uvcgadget/uvc.c
|
|
@@ -0,0 +1,420 @@
|
|
+/*
|
|
+ * SPDX-License-Identifier: LGPL-2.0-or-later
|
|
+ *
|
|
+ * Copyright (C) 2023 Pengutronix e.K. - www.pengutronix.de
|
|
+ *
|
|
+ */
|
|
+
|
|
+#define _GNU_SOURCE
|
|
+
|
|
+#include <endian.h>
|
|
+#include <glob.h>
|
|
+#include <stdio.h>
|
|
+#include <fcntl.h>
|
|
+#include <stdint.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+#include "gstuvcsink.h"
|
|
+#include "configfs.h"
|
|
+
|
|
+#define GST_CAT_DEFAULT uvcsink_debug
|
|
+
|
|
+#define UVC_STRING_CONTROL_IDX 0
|
|
+#define UVC_STRING_STREAMING_IDX 1
|
|
+
|
|
+/* --------------------------------------------------------------------------
|
|
+ * Control requests
|
|
+ */
|
|
+
|
|
+static const char *
|
|
+uvc_request_name (uint8_t req)
|
|
+{
|
|
+ switch (req) {
|
|
+ case UVC_SET_CUR:
|
|
+ return "SET_CUR";
|
|
+ case UVC_GET_CUR:
|
|
+ return "GET_CUR";
|
|
+ case UVC_GET_MIN:
|
|
+ return "GET_MIN";
|
|
+ case UVC_GET_MAX:
|
|
+ return "GET_MAX";
|
|
+ case UVC_GET_RES:
|
|
+ return "GET_RES";
|
|
+ case UVC_GET_LEN:
|
|
+ return "GET_LEN";
|
|
+ case UVC_GET_INFO:
|
|
+ return "GET_INFO";
|
|
+ case UVC_GET_DEF:
|
|
+ return "GET_DEF";
|
|
+ default:
|
|
+ return "<invalid>";
|
|
+ }
|
|
+}
|
|
+
|
|
+static const char *
|
|
+uvc_video_control_interface_control_selector_name (uint8_t cs)
|
|
+{
|
|
+ switch (cs) {
|
|
+ case UVC_VC_CONTROL_UNDEFINED:
|
|
+ return "UVC_VC_CONTROL_UNDEFINED";
|
|
+ case UVC_VC_VIDEO_POWER_MODE_CONTROL:
|
|
+ return "UVC_VC_VIDEO_POWER_MODE_CONTROL";
|
|
+ case UVC_VC_REQUEST_ERROR_CODE_CONTROL:
|
|
+ return "UVC_VC_REQUEST_ERROR_CODE_CONTROL";
|
|
+ default:
|
|
+ return "<unknown video control interface control selector>";
|
|
+ }
|
|
+}
|
|
+
|
|
+static const char *
|
|
+uvc_camera_terminal_control_selector_name (uint8_t cs)
|
|
+{
|
|
+ switch (cs) {
|
|
+ case UVC_CT_CONTROL_UNDEFINED:
|
|
+ return "UVC_CT_CONTROL_UNDEFINED";
|
|
+ case UVC_CT_SCANNING_MODE_CONTROL:
|
|
+ return "UVC_CT_SCANNING_MODE_CONTROL";
|
|
+ case UVC_CT_AE_MODE_CONTROL:
|
|
+ return "UVC_CT_AE_MODE_CONTROL";
|
|
+ case UVC_CT_AE_PRIORITY_CONTROL:
|
|
+ return "UVC_CT_AE_PRIORITY_CONTROL";
|
|
+ case UVC_CT_EXPOSURE_TIME_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_EXPOSURE_TIME_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_EXPOSURE_TIME_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_EXPOSURE_TIME_RELATIVE_CONTROL";
|
|
+ case UVC_CT_FOCUS_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_FOCUS_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_FOCUS_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_FOCUS_RELATIVE_CONTROL";
|
|
+ case UVC_CT_FOCUS_AUTO_CONTROL:
|
|
+ return "UVC_CT_FOCUS_AUTO_CONTROL";
|
|
+ case UVC_CT_IRIS_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_IRIS_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_IRIS_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_IRIS_RELATIVE_CONTROL";
|
|
+ case UVC_CT_ZOOM_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_ZOOM_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_ZOOM_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_ZOOM_RELATIVE_CONTROL";
|
|
+ case UVC_CT_PANTILT_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_PANTILT_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_PANTILT_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_PANTILT_RELATIVE_CONTROL";
|
|
+ case UVC_CT_ROLL_ABSOLUTE_CONTROL:
|
|
+ return "UVC_CT_ROLL_ABSOLUTE_CONTROL";
|
|
+ case UVC_CT_ROLL_RELATIVE_CONTROL:
|
|
+ return "UVC_CT_ROLL_RELATIVE_CONTROL";
|
|
+ case UVC_CT_PRIVACY_CONTROL:
|
|
+ return "UVC_CT_PRIVACY_CONTROL";
|
|
+ default:
|
|
+ return "<unknown camera terminal control selector>";
|
|
+ }
|
|
+}
|
|
+
|
|
+static const char *
|
|
+uvc_processing_unit_control_selector_name (uint8_t cs)
|
|
+{
|
|
+ switch (cs) {
|
|
+ case UVC_PU_CONTROL_UNDEFINED:
|
|
+ return "UVC_PU_CONTROL_UNDEFINED";
|
|
+ case UVC_PU_BACKLIGHT_COMPENSATION_CONTROL:
|
|
+ return "UVC_PU_BACKLIGHT_COMPENSATION_CONTROL";
|
|
+ case UVC_PU_BRIGHTNESS_CONTROL:
|
|
+ return "UVC_PU_BRIGHTNESS_CONTROL";
|
|
+ case UVC_PU_CONTRAST_CONTROL:
|
|
+ return "UVC_PU_CONTRAST_CONTROL";
|
|
+ case UVC_PU_GAIN_CONTROL:
|
|
+ return "UVC_PU_GAIN_CONTROL";
|
|
+ case UVC_PU_POWER_LINE_FREQUENCY_CONTROL:
|
|
+ return "UVC_PU_POWER_LINE_FREQUENCY_CONTROL";
|
|
+ case UVC_PU_HUE_CONTROL:
|
|
+ return "UVC_PU_HUE_CONTROL";
|
|
+ case UVC_PU_SATURATION_CONTROL:
|
|
+ return "UVC_PU_SATURATION_CONTROL";
|
|
+ case UVC_PU_SHARPNESS_CONTROL:
|
|
+ return "UVC_PU_SHARPNESS_CONTROL";
|
|
+ case UVC_PU_GAMMA_CONTROL:
|
|
+ return "UVC_PU_GAMMA_CONTROL";
|
|
+ case UVC_PU_WHITE_BALANCE_TEMPERATURE_CONTROL:
|
|
+ return "UVC_PU_WHITE_BALANCE_TEMPERATURE_CONTROL";
|
|
+ case UVC_PU_WHITE_BALANCE_TEMPERATURE_AUTO_CONTROL:
|
|
+ return "UVC_PU_WHITE_BALANCE_TEMPERATURE_AUTO_CONTROL";
|
|
+ case UVC_PU_WHITE_BALANCE_COMPONENT_CONTROL:
|
|
+ return "UVC_PU_WHITE_BALANCE_COMPONENT_CONTROL";
|
|
+ case UVC_PU_WHITE_BALANCE_COMPONENT_AUTO_CONTROL:
|
|
+ return "UVC_PU_WHITE_BALANCE_COMPONENT_AUTO_CONTROL";
|
|
+ case UVC_PU_DIGITAL_MULTIPLIER_CONTROL:
|
|
+ return "UVC_PU_DIGITAL_MULTIPLIER_CONTROL";
|
|
+ case UVC_PU_DIGITAL_MULTIPLIER_LIMIT_CONTROL:
|
|
+ return "UVC_PU_DIGITAL_MULTIPLIER_LIMIT_CONTROL";
|
|
+ case UVC_PU_HUE_AUTO_CONTROL:
|
|
+ return "UVC_PU_HUE_AUTO_CONTROL";
|
|
+ case UVC_PU_ANALOG_VIDEO_STANDARD_CONTROL:
|
|
+ return "UVC_PU_ANALOG_VIDEO_STANDARD_CONTROL";
|
|
+ case UVC_PU_ANALOG_LOCK_STATUS_CONTROL:
|
|
+ return "UVC_PU_ANALOG_LOCK_STATUS_CONTROL";
|
|
+ default:
|
|
+ return "<unknown processing unit control selector>";
|
|
+ }
|
|
+}
|
|
+
|
|
+static const char *
|
|
+uvc_video_streaming_interface_control_selector_name (uint8_t cs)
|
|
+{
|
|
+ switch (cs) {
|
|
+ case UVC_VS_CONTROL_UNDEFINED:
|
|
+ return "UVC_VS_CONTROL_UNDEFINED";
|
|
+ case UVC_VS_PROBE_CONTROL:
|
|
+ return "UVC_VS_PROBE_CONTROL";
|
|
+ case UVC_VS_COMMIT_CONTROL:
|
|
+ return "UVC_VS_COMMIT_CONTROL";
|
|
+ case UVC_VS_STILL_PROBE_CONTROL:
|
|
+ return "UVC_VS_STILL_PROBE_CONTROL";
|
|
+ case UVC_VS_STILL_COMMIT_CONTROL:
|
|
+ return "UVC_VS_STILL_COMMIT_CONTROL";
|
|
+ case UVC_VS_STILL_IMAGE_TRIGGER_CONTROL:
|
|
+ return "UVC_VS_STILL_IMAGE_TRIGGER_CONTROL";
|
|
+ case UVC_VS_STREAM_ERROR_CODE_CONTROL:
|
|
+ return "UVC_VS_STREAM_ERROR_CODE_CONTROL";
|
|
+ case UVC_VS_GENERATE_KEY_FRAME_CONTROL:
|
|
+ return "UVC_VS_GENERATE_KEY_FRAME_CONTROL";
|
|
+ case UVC_VS_UPDATE_FRAME_SEGMENT_CONTROL:
|
|
+ return "UVC_VS_UPDATE_FRAME_SEGMENT_CONTROL";
|
|
+ case UVC_VS_SYNC_DELAY_CONTROL:
|
|
+ return "UVC_VS_SYNC_DELAY_CONTROL";
|
|
+ default:
|
|
+ return "<unknown video streaming interface control selector>";
|
|
+ }
|
|
+}
|
|
+
|
|
+int
|
|
+uvc_fill_streaming_control (GstUvcSink * self,
|
|
+ struct uvc_streaming_control *ctrl,
|
|
+ int iframe, int iformat, unsigned int dwival)
|
|
+{
|
|
+ const struct uvc_function_config_format *format;
|
|
+ const struct uvc_function_config_frame *frame;
|
|
+ unsigned int i;
|
|
+
|
|
+ if (!self->fc)
|
|
+ return -ENOENT;
|
|
+ /*
|
|
+ * Restrict the iformat, iframe and ival to valid values. Negative
|
|
+ * values for iformat or iframe will result in the maximum valid value
|
|
+ * being selected.
|
|
+ */
|
|
+ iformat = CLAMP ((unsigned int) iformat, 1U, self->fc->streaming.num_formats);
|
|
+ format = &self->fc->streaming.formats[iformat - 1];
|
|
+
|
|
+ iframe = CLAMP ((unsigned int) iframe, 1U, format->num_frames);
|
|
+ frame = &format->frames[iframe - 1];
|
|
+
|
|
+ for (i = 0; i < frame->num_intervals; ++i) {
|
|
+ if (dwival <= frame->intervals[i]) {
|
|
+ dwival = frame->intervals[i];
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (i == frame->num_intervals)
|
|
+ dwival = frame->intervals[frame->num_intervals - 1];
|
|
+
|
|
+ memset (ctrl, 0, sizeof (*ctrl));
|
|
+
|
|
+ ctrl->bmHint = 1;
|
|
+ ctrl->bFormatIndex = iformat;
|
|
+ ctrl->bFrameIndex = iframe;
|
|
+ ctrl->dwFrameInterval = dwival;
|
|
+
|
|
+ switch (format->fcc) {
|
|
+ case V4L2_PIX_FMT_YUYV:
|
|
+ ctrl->dwMaxVideoFrameSize = frame->width * frame->height * 2;
|
|
+ break;
|
|
+ case V4L2_PIX_FMT_MJPEG:
|
|
+ ctrl->dwMaxVideoFrameSize = frame->maxvideofbsize;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ ctrl->dwMaxPayloadTransferSize = self->fc->streaming.ep.wMaxPacketSize;
|
|
+ ctrl->bmFramingInfo = 3;
|
|
+ ctrl->bPreferedVersion = 1;
|
|
+ ctrl->bMaxVersion = 1;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+int
|
|
+uvc_events_process_data (GstUvcSink * self, const struct uvc_request_data *data)
|
|
+{
|
|
+ const struct uvc_streaming_control *ctrl =
|
|
+ (const struct uvc_streaming_control *) &data->data;
|
|
+ struct uvc_streaming_control *target;
|
|
+ int ret;
|
|
+
|
|
+ switch (self->control) {
|
|
+ case UVC_VS_PROBE_CONTROL:
|
|
+ GST_DEBUG_OBJECT (self, "setting probe control");
|
|
+ target = &self->probe;
|
|
+ break;
|
|
+
|
|
+ case UVC_VS_COMMIT_CONTROL:
|
|
+ GST_DEBUG_OBJECT (self, "setting commit control");
|
|
+ target = &self->commit;
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ GST_ELEMENT_ERROR (self, RESOURCE, READ,
|
|
+ ("setting unknown control, %d", self->control), NULL);
|
|
+ return -EOPNOTSUPP;
|
|
+ }
|
|
+
|
|
+ ret = uvc_fill_streaming_control (self, target, ctrl->bFrameIndex,
|
|
+ ctrl->bFormatIndex, ctrl->dwFrameInterval);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ if (self->control == UVC_VS_COMMIT_CONTROL) {
|
|
+ self->cur.bFrameIndex = ctrl->bFrameIndex;
|
|
+ self->cur.bFormatIndex = ctrl->bFormatIndex;
|
|
+ self->cur.dwFrameInterval = ctrl->dwFrameInterval;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int
|
|
+uvc_events_process_streaming (GstUvcSink * self, uint8_t req, uint8_t cs,
|
|
+ struct uvc_request_data *resp)
|
|
+{
|
|
+ struct uvc_streaming_control *ctrl;
|
|
+ int ret;
|
|
+
|
|
+ GST_DEBUG_OBJECT (self,
|
|
+ "%s: %s",
|
|
+ uvc_video_streaming_interface_control_selector_name (cs),
|
|
+ uvc_request_name (req));
|
|
+
|
|
+ if (cs != UVC_VS_PROBE_CONTROL && cs != UVC_VS_COMMIT_CONTROL)
|
|
+ return 0;
|
|
+
|
|
+ ctrl = (struct uvc_streaming_control *) &resp->data;
|
|
+ resp->length = sizeof (*ctrl);
|
|
+
|
|
+ switch (req) {
|
|
+ case UVC_SET_CUR:
|
|
+ self->control = cs;
|
|
+ resp->length = 34;
|
|
+ break;
|
|
+
|
|
+ case UVC_GET_CUR:
|
|
+ if (cs == UVC_VS_PROBE_CONTROL)
|
|
+ memcpy (ctrl, &self->probe, sizeof (*ctrl));
|
|
+ else
|
|
+ memcpy (ctrl, &self->commit, sizeof (*ctrl));
|
|
+ break;
|
|
+
|
|
+ case UVC_GET_MIN:
|
|
+ case UVC_GET_MAX:
|
|
+ case UVC_GET_DEF:
|
|
+ if (req == UVC_GET_MAX)
|
|
+ ret = uvc_fill_streaming_control (self, ctrl, -1, -1, UINT_MAX);
|
|
+ else
|
|
+ ret = uvc_fill_streaming_control (self, ctrl, 1, 1, 0);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+ break;
|
|
+
|
|
+ case UVC_GET_RES:
|
|
+ memset (ctrl, 0, sizeof (*ctrl));
|
|
+ break;
|
|
+
|
|
+ case UVC_GET_LEN:
|
|
+ resp->data[0] = 0x00;
|
|
+ resp->data[1] = 0x22;
|
|
+ resp->length = 2;
|
|
+ break;
|
|
+
|
|
+ case UVC_GET_INFO:
|
|
+ resp->data[0] = 0x03;
|
|
+ resp->length = 1;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void
|
|
+uvc_events_parse_control (GstUvcSink * self, uint8_t req,
|
|
+ uint8_t cs, uint8_t entity_id, uint8_t len, struct uvc_request_data *resp)
|
|
+{
|
|
+ switch (entity_id) {
|
|
+ case 0:
|
|
+ GST_DEBUG_OBJECT (self, "%s",
|
|
+ uvc_video_control_interface_control_selector_name (cs));
|
|
+ break;
|
|
+
|
|
+ case 1:
|
|
+ GST_DEBUG_OBJECT (self, "%s: %s",
|
|
+ uvc_camera_terminal_control_selector_name (cs),
|
|
+ uvc_request_name (req));
|
|
+ break;
|
|
+
|
|
+ case 2:
|
|
+ GST_DEBUG_OBJECT (self, "%s: %s",
|
|
+ uvc_processing_unit_control_selector_name (cs),
|
|
+ uvc_request_name (req));
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ GST_DEBUG_OBJECT (self,
|
|
+ "Unknown entity ID (0x%02x), CS: 0x%02x, Request %s (0x%02x)",
|
|
+ entity_id, cs, uvc_request_name (req), req);
|
|
+ break;
|
|
+ }
|
|
+}
|
|
+
|
|
+static int
|
|
+uvc_events_process_class (GstUvcSink * self,
|
|
+ const struct usb_ctrlrequest *ctrl, struct uvc_request_data *resp)
|
|
+{
|
|
+ unsigned int interface = le16toh (ctrl->wIndex) & 0xff;
|
|
+
|
|
+ if ((ctrl->bRequestType & USB_RECIP_MASK) != USB_RECIP_INTERFACE)
|
|
+ return -EINVAL;
|
|
+
|
|
+ if (interface == UVC_STRING_CONTROL_IDX) {
|
|
+ uvc_events_parse_control (self, ctrl->bRequest, ctrl->wValue >> 8,
|
|
+ ctrl->wIndex >> 8, ctrl->wLength, resp);
|
|
+ return -EOPNOTSUPP;
|
|
+ } else if (interface == UVC_STRING_STREAMING_IDX) {
|
|
+ return uvc_events_process_streaming (self, ctrl->bRequest,
|
|
+ le16toh (ctrl->wValue) >> 8, resp);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+int
|
|
+uvc_events_process_setup (GstUvcSink * self,
|
|
+ const struct usb_ctrlrequest *ctrl, struct uvc_request_data *resp)
|
|
+{
|
|
+ self->control = 0;
|
|
+
|
|
+ GST_DEBUG_OBJECT (self,
|
|
+ "bRequestType %02x bRequest %02x wValue %04x wIndex %04x wLength %04x",
|
|
+ ctrl->bRequestType, ctrl->bRequest, ctrl->wValue,
|
|
+ ctrl->wIndex, ctrl->wLength);
|
|
+
|
|
+ switch (ctrl->bRequestType & USB_TYPE_MASK) {
|
|
+ case USB_TYPE_STANDARD:
|
|
+ return -EOPNOTSUPP;
|
|
+
|
|
+ case USB_TYPE_CLASS:
|
|
+ return uvc_events_process_class (self, ctrl, resp);
|
|
+
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
--
|
|
2.20.1
|
|
|