/* * 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 #endif #include #include #include #include #include #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 "); 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)