mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-25 11:11:08 +00:00
4670e1c809
The uvcsink was limited to only transfer YUY2 and MJPEG. For the uncompressed formats there is no technical reason not to support them. Since gst_video_format_to_string is already supporting more fourcc than only YUY2 we use the default path in gst_v4l2uvc_fourcc_to_bare_struct to create structures for more formats and bail out if the returned format is not from the uncompressed type. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/6037>
1025 lines
29 KiB
C
1025 lines
29 KiB
C
/*
|
|
* 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
|
|
|
|
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 GstVideoFormat
|
|
gst_v4l2_object_v4l2fourcc_to_video_format (guint32 fourcc)
|
|
{
|
|
GstVideoFormat format;
|
|
|
|
switch (fourcc) {
|
|
case V4L2_PIX_FMT_GREY: /* 8 Greyscale */
|
|
format = GST_VIDEO_FORMAT_GRAY8;
|
|
break;
|
|
case V4L2_PIX_FMT_Y16:
|
|
format = GST_VIDEO_FORMAT_GRAY16_LE;
|
|
break;
|
|
case V4L2_PIX_FMT_Y16_BE:
|
|
format = GST_VIDEO_FORMAT_GRAY16_BE;
|
|
break;
|
|
case V4L2_PIX_FMT_XRGB555:
|
|
case V4L2_PIX_FMT_RGB555:
|
|
format = GST_VIDEO_FORMAT_RGB15;
|
|
break;
|
|
case V4L2_PIX_FMT_XRGB555X:
|
|
case V4L2_PIX_FMT_RGB555X:
|
|
format = GST_VIDEO_FORMAT_BGR15;
|
|
break;
|
|
case V4L2_PIX_FMT_RGB565:
|
|
format = GST_VIDEO_FORMAT_RGB16;
|
|
break;
|
|
case V4L2_PIX_FMT_RGB24:
|
|
format = GST_VIDEO_FORMAT_RGB;
|
|
break;
|
|
case V4L2_PIX_FMT_BGR24:
|
|
format = GST_VIDEO_FORMAT_BGR;
|
|
break;
|
|
case V4L2_PIX_FMT_XRGB32:
|
|
case V4L2_PIX_FMT_RGB32:
|
|
format = GST_VIDEO_FORMAT_xRGB;
|
|
break;
|
|
case V4L2_PIX_FMT_RGBX32:
|
|
format = GST_VIDEO_FORMAT_RGBx;
|
|
break;
|
|
case V4L2_PIX_FMT_XBGR32:
|
|
case V4L2_PIX_FMT_BGR32:
|
|
format = GST_VIDEO_FORMAT_BGRx;
|
|
break;
|
|
case V4L2_PIX_FMT_BGRX32:
|
|
format = GST_VIDEO_FORMAT_xBGR;
|
|
break;
|
|
case V4L2_PIX_FMT_ABGR32:
|
|
format = GST_VIDEO_FORMAT_BGRA;
|
|
break;
|
|
case V4L2_PIX_FMT_BGRA32:
|
|
format = GST_VIDEO_FORMAT_ABGR;
|
|
break;
|
|
case V4L2_PIX_FMT_RGBA32:
|
|
format = GST_VIDEO_FORMAT_RGBA;
|
|
break;
|
|
case V4L2_PIX_FMT_ARGB32:
|
|
format = GST_VIDEO_FORMAT_ARGB;
|
|
break;
|
|
case V4L2_PIX_FMT_NV12:
|
|
case V4L2_PIX_FMT_NV12M:
|
|
format = GST_VIDEO_FORMAT_NV12;
|
|
break;
|
|
case V4L2_PIX_FMT_NV12MT:
|
|
format = GST_VIDEO_FORMAT_NV12_64Z32;
|
|
break;
|
|
case V4L2_PIX_FMT_MM21:
|
|
format = GST_VIDEO_FORMAT_NV12_16L32S;
|
|
break;
|
|
case V4L2_PIX_FMT_NV12M_8L128:
|
|
format = GST_VIDEO_FORMAT_NV12_8L128;
|
|
break;
|
|
case V4L2_PIX_FMT_NV12M_10BE_8L128:
|
|
format = GST_VIDEO_FORMAT_NV12_10BE_8L128;
|
|
break;
|
|
case V4L2_PIX_FMT_NV21:
|
|
case V4L2_PIX_FMT_NV21M:
|
|
format = GST_VIDEO_FORMAT_NV21;
|
|
break;
|
|
case V4L2_PIX_FMT_YVU410:
|
|
format = GST_VIDEO_FORMAT_YVU9;
|
|
break;
|
|
case V4L2_PIX_FMT_YUV410:
|
|
format = GST_VIDEO_FORMAT_YUV9;
|
|
break;
|
|
case V4L2_PIX_FMT_YUV420:
|
|
case V4L2_PIX_FMT_YUV420M:
|
|
format = GST_VIDEO_FORMAT_I420;
|
|
break;
|
|
case V4L2_PIX_FMT_YUYV:
|
|
format = GST_VIDEO_FORMAT_YUY2;
|
|
break;
|
|
case V4L2_PIX_FMT_YVU420:
|
|
case V4L2_PIX_FMT_YVU420M:
|
|
format = GST_VIDEO_FORMAT_YV12;
|
|
break;
|
|
case V4L2_PIX_FMT_UYVY:
|
|
format = GST_VIDEO_FORMAT_UYVY;
|
|
break;
|
|
case V4L2_PIX_FMT_YUV411P:
|
|
format = GST_VIDEO_FORMAT_Y41B;
|
|
break;
|
|
case V4L2_PIX_FMT_YUV422P:
|
|
format = GST_VIDEO_FORMAT_Y42B;
|
|
break;
|
|
case V4L2_PIX_FMT_YVYU:
|
|
format = GST_VIDEO_FORMAT_YVYU;
|
|
break;
|
|
case V4L2_PIX_FMT_NV16:
|
|
case V4L2_PIX_FMT_NV16M:
|
|
format = GST_VIDEO_FORMAT_NV16;
|
|
break;
|
|
case V4L2_PIX_FMT_NV61:
|
|
case V4L2_PIX_FMT_NV61M:
|
|
format = GST_VIDEO_FORMAT_NV61;
|
|
break;
|
|
case V4L2_PIX_FMT_NV24:
|
|
format = GST_VIDEO_FORMAT_NV24;
|
|
break;
|
|
default:
|
|
format = GST_VIDEO_FORMAT_UNKNOWN;
|
|
break;
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
static GstStructure *
|
|
gst_v4l2uvc_fourcc_to_bare_struct (guint32 fourcc)
|
|
{
|
|
GstStructure *structure = NULL;
|
|
GstVideoFormat format = GST_VIDEO_FORMAT_UNKNOWN;
|
|
|
|
switch (fourcc) {
|
|
case V4L2_PIX_FMT_MJPEG: /* Motion-JPEG */
|
|
case V4L2_PIX_FMT_JPEG: /* JFIF JPEG */
|
|
structure = gst_structure_new_empty ("image/jpeg");
|
|
break;
|
|
default:
|
|
format = gst_v4l2_object_v4l2fourcc_to_video_format (fourcc);
|
|
if (format == GST_VIDEO_FORMAT_UNKNOWN) {
|
|
GST_DEBUG ("Unsupported fourcc 0x%08x %" GST_FOURCC_FORMAT,
|
|
fourcc, GST_FOURCC_ARGS (fourcc));
|
|
break;
|
|
}
|
|
structure = gst_structure_new ("video/x-raw",
|
|
"format", G_TYPE_STRING, gst_video_format_to_string (format), NULL);
|
|
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);
|
|
if (!s)
|
|
return NULL;
|
|
|
|
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);
|
|
|
|
/* If the UVC host did not yet commit a format, the cur_caps may contain
|
|
* all probed caps. In this case, the element is not able to detect, if
|
|
* the caps have changed when the stream is enabled. Take the caps from
|
|
* upstream to be able to detect a change.
|
|
*/
|
|
if (!GST_CAPS_IS_SIMPLE (self->cur_caps)) {
|
|
GstCaps *caps;
|
|
|
|
gst_event_parse_caps (event, &caps);
|
|
gst_caps_replace (&self->cur_caps, caps);
|
|
|
|
GST_DEBUG_OBJECT (self,
|
|
"UVC host didn't select a format, yet. Using upstream %"
|
|
GST_PTR_FORMAT, self->cur_caps);
|
|
}
|
|
|
|
/* 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);
|
|
if (!configured_caps) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, WRITE,
|
|
("gst_uvc_sink_get_configured_caps failed"),
|
|
("gst_uvc_sink_get_configured_caps on current format failed"));
|
|
gst_uvc_sink_unwatch (self);
|
|
gst_element_set_state (GST_ELEMENT (self), GST_STATE_NULL);
|
|
return;
|
|
}
|
|
|
|
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 (prev_caps) {
|
|
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)
|