/*
 * 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->request_error_code = REQUEST_ERROR_CODE_NO_ERROR;

  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);
      self->request_error_code = REQUEST_ERROR_CODE_NO_ERROR;
      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)
