rtspsrc: Implement ONVIF backchannel support

Set backchannel=onvif to enable, and use the 'push-backchannel-sample'
action signal with the correct stream id.
diff --git a/configure.ac b/configure.ac
index 90e8f1a..6736c4b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1294,6 +1294,7 @@
 tests/examples/jack/Makefile
 tests/examples/level/Makefile
 tests/examples/rtp/Makefile
+tests/examples/rtsp/Makefile
 tests/examples/shapewipe/Makefile
 tests/examples/spectrum/Makefile
 tests/examples/v4l2/Makefile
diff --git a/gst/rtsp/gstrtspsrc.c b/gst/rtsp/gstrtspsrc.c
index d636073..e424f47 100644
--- a/gst/rtsp/gstrtspsrc.c
+++ b/gst/rtsp/gstrtspsrc.c
@@ -126,6 +126,7 @@
   SIGNAL_REQUEST_RTCP_KEY,
   SIGNAL_ACCEPT_CERTIFICATE,
   SIGNAL_BEFORE_SEND,
+  SIGNAL_PUSH_BACKCHANNEL_BUFFER,
   LAST_SIGNAL
 };
 
@@ -200,6 +201,32 @@
   return ntp_time_source_type;
 }
 
+enum _GstRtspBackchannel
+{
+  BACKCHANNEL_NONE,
+  BACKCHANNEL_ONVIF
+};
+
+#define GST_TYPE_RTSP_BACKCHANNEL (gst_rtsp_backchannel_get_type())
+static GType
+gst_rtsp_backchannel_get_type (void)
+{
+  static GType backchannel_type = 0;
+  static const GEnumValue backchannel_values[] = {
+    {BACKCHANNEL_NONE, "No backchannel", "none"},
+    {BACKCHANNEL_ONVIF, "ONVIF audio backchannel", "onvif"},
+    {0, NULL, NULL},
+  };
+
+  if (G_UNLIKELY (backchannel_type == 0)) {
+    backchannel_type =
+        g_enum_register_static ("GstRTSPBackchannel", backchannel_values);
+  }
+  return backchannel_type;
+}
+
+#define BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL "www.onvif.org/ver20/backchannel"
+
 #define DEFAULT_LOCATION         NULL
 #define DEFAULT_PROTOCOLS        GST_RTSP_LOWER_TRANS_UDP | GST_RTSP_LOWER_TRANS_UDP_MCAST | GST_RTSP_LOWER_TRANS_TCP
 #define DEFAULT_DEBUG            FALSE
@@ -236,6 +263,7 @@
 #define DEFAULT_MAX_TS_OFFSET_ADJUSTMENT   G_GUINT64_CONSTANT(0)
 #define DEFAULT_MAX_TS_OFFSET   G_GINT64_CONSTANT(3000000000)
 #define DEFAULT_VERSION         GST_RTSP_VERSION_1_0
+#define DEFAULT_BACKCHANNEL  GST_RTSP_BACKCHANNEL_NONE
 
 enum
 {
@@ -279,6 +307,7 @@
   PROP_MAX_TS_OFFSET_ADJUSTMENT,
   PROP_MAX_TS_OFFSET,
   PROP_DEFAULT_VERSION,
+  PROP_BACKCHANNEL,
 };
 
 #define GST_TYPE_RTSP_NAT_METHOD (gst_rtsp_nat_method_get_type())
@@ -359,6 +388,9 @@
 static void
 gst_rtspsrc_print_sdp_message (GstRTSPSrc * src, const GstSDPMessage * msg);
 
+static GstFlowReturn gst_rtspsrc_push_backchannel_buffer (GstRTSPSrc * src,
+    guint id, GstSample * sample);
+
 typedef struct
 {
   guint8 pt;
@@ -830,6 +862,20 @@
           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
   /**
+   * GstRtpSrc:backchannel
+   *
+   * Select a type of backchannel to setup with the RTSP server.
+   * Default value is "none". Allowed values are "none" and "onvif".
+   *
+   * Since: 1.14
+   */
+  g_object_class_install_property (gobject_class, PROP_BACKCHANNEL,
+      g_param_spec_enum ("backchannel", "Backchannel type",
+          "The type of backchannel to setup. Default is 'none'.",
+          GST_TYPE_RTSP_BACKCHANNEL, BACKCHANNEL_NONE,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
    * GstRTSPSrc::handle-request:
    * @rtspsrc: a #GstRTSPSrc
    * @request: a #GstRTSPMessage
@@ -965,6 +1011,19 @@
       g_cclosure_marshal_generic, G_TYPE_BOOLEAN,
       1, GST_TYPE_RTSP_MESSAGE | G_SIGNAL_TYPE_STATIC_SCOPE);
 
+  /**
+   * GstRTSPSrc::push-backchannel-buffer:
+   * @rtspsrc: a #GstRTSPSrc
+   * @buffer: RTP buffer to send back
+   *
+   *
+   */
+  gst_rtspsrc_signals[SIGNAL_PUSH_BACKCHANNEL_BUFFER] =
+      g_signal_new ("push-backchannel-buffer", G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GstRTSPSrcClass,
+          push_backchannel_buffer), NULL, NULL, NULL, GST_TYPE_FLOW_RETURN, 2,
+      G_TYPE_UINT, GST_TYPE_BUFFER);
+
   gstelement_class->send_event = gst_rtspsrc_send_event;
   gstelement_class->provide_clock = gst_rtspsrc_provide_clock;
   gstelement_class->change_state = gst_rtspsrc_change_state;
@@ -980,6 +1039,8 @@
 
   gstbin_class->handle_message = gst_rtspsrc_handle_message;
 
+  klass->push_backchannel_buffer = gst_rtspsrc_push_backchannel_buffer;
+
   gst_rtsp_ext_list_init ();
 }
 
@@ -1335,6 +1396,9 @@
     case PROP_DEFAULT_VERSION:
       rtspsrc->default_version = g_value_get_enum (value);
       break;
+    case PROP_BACKCHANNEL:
+      rtspsrc->backchannel = g_value_get_enum (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -1705,7 +1769,9 @@
   else
     goto unknown_proto;
 
-  if (gst_sdp_media_get_attribute_val (media, "recvonly") != NULL)
+  if (gst_sdp_media_get_attribute_val (media, "recvonly") != NULL &&
+      /* We want to setup caps for streams configured as backchannel */
+      !stream->is_backchannel)
     goto recvonly_media;
 
   /* Parse global SDP attributes once */
@@ -1779,7 +1845,7 @@
   }
 recvonly_media:
   {
-    GST_DEBUG_OBJECT (src, "recvonly media ignored");
+    GST_WARNING_OBJECT (src, "recvonly media ignored, no backchannel");
     return;
   }
 }
@@ -1839,10 +1905,16 @@
   stream->ptmap = g_array_new (FALSE, FALSE, sizeof (PtMapItem));
   stream->mikey = NULL;
   stream->stream_id = NULL;
+  stream->is_backchannel = FALSE;
   g_mutex_init (&stream->conninfo.send_lock);
   g_mutex_init (&stream->conninfo.recv_lock);
   g_array_set_clear_func (stream->ptmap, (GDestroyNotify) clear_ptmap_item);
 
+  /* stream is recvonly and onvif backchannel is requested */
+  if (gst_sdp_media_get_attribute_val (media, "recvonly") != NULL &&
+      src->backchannel != BACKCHANNEL_NONE)
+    stream->is_backchannel = TRUE;
+
   /* collect bandwidth information for this steam. FIXME, configure in the RTP
    * session manager to scale RTCP. */
   gst_rtspsrc_collect_bandwidth (src, sdp, media, stream);
@@ -1940,10 +2012,10 @@
       gst_object_unref (stream->udpsink[i]);
     }
   }
-  if (stream->fakesrc) {
-    gst_element_set_state (stream->fakesrc, GST_STATE_NULL);
-    gst_bin_remove (GST_BIN_CAST (src), stream->fakesrc);
-    gst_object_unref (stream->fakesrc);
+  if (stream->rtpsrc) {
+    gst_element_set_state (stream->rtpsrc, GST_STATE_NULL);
+    gst_bin_remove (GST_BIN_CAST (src), stream->rtpsrc);
+    gst_object_unref (stream->rtpsrc);
   }
   if (stream->srcpad) {
     gst_pad_set_active (stream->srcpad, FALSE);
@@ -2763,6 +2835,32 @@
   return res;
 }
 
+static GstFlowReturn
+gst_rtspsrc_push_backchannel_buffer (GstRTSPSrc * src, guint id,
+    GstSample * sample)
+{
+  GstFlowReturn res = GST_FLOW_OK;
+  GstRTSPStream *stream;
+
+  if (!src->conninfo.connected || src->state != GST_RTSP_STATE_PLAYING)
+    goto out;
+
+  stream = find_stream (src, &id, (gpointer) find_stream_by_id);
+  if (stream == NULL) {
+    GST_ERROR_OBJECT (src, "no stream with id %u", id);
+    goto out;
+  }
+
+  g_signal_emit_by_name (stream->rtpsrc, "push-sample", sample, &res);
+  GST_DEBUG_OBJECT (src, "sent backchannel RTP sample %p: %s", sample,
+      gst_flow_get_name (res));
+
+out:
+  gst_sample_unref (sample);
+
+  return res;
+}
+
 static GstPadProbeReturn
 pad_blocked (GstPad * pad, GstPadProbeInfo * info, gpointer user_data)
 {
@@ -2801,6 +2899,35 @@
   return TRUE;
 }
 
+static gboolean
+add_backchannel_fakesink (GstRTSPSrc * src, GstRTSPStream * stream,
+    GstPad * srcpad)
+{
+  GstPad *sinkpad;
+  GstElement *fakesink;
+
+  fakesink = gst_element_factory_make ("fakesink", NULL);
+  if (fakesink == NULL) {
+    GST_ERROR_OBJECT (src, "no fakesink");
+    return FALSE;
+  }
+
+  sinkpad = gst_element_get_static_pad (fakesink, "sink");
+
+  GST_DEBUG_OBJECT (src, "backchannel stream %p, hooking fakesink", stream);
+
+  gst_bin_add (GST_BIN_CAST (src), fakesink);
+  if (gst_pad_link (srcpad, sinkpad) != GST_PAD_LINK_OK) {
+    GST_WARNING_OBJECT (src, "could not link to fakesink");
+    return FALSE;
+  }
+
+  gst_object_unref (sinkpad);
+
+  gst_element_sync_state_with_parent (fakesink);
+  return TRUE;
+}
+
 /* this callback is called when the session manager generated a new src pad with
  * payloaded RTP packets. We simply ghost the pad here. */
 static void
@@ -2868,7 +2995,12 @@
   gst_pad_set_query_function (stream->srcpad, gst_rtspsrc_handle_src_query);
   gst_pad_set_active (stream->srcpad, TRUE);
   gst_pad_sticky_events_foreach (pad, copy_sticky_events, stream->srcpad);
-  gst_element_add_pad (GST_ELEMENT_CAST (src), stream->srcpad);
+
+  /* don't add the srcpad if this is a recvonly stream */
+  if (stream->is_backchannel)
+    add_backchannel_fakesink (src, stream, stream->srcpad);
+  else
+    gst_element_add_pad (GST_ELEMENT_CAST (src), stream->srcpad);
 
   if (all_added) {
     GST_DEBUG_OBJECT (src, "We added all streams");
@@ -3898,7 +4030,7 @@
     goto no_destination;
 
   /* try to construct the fakesrc to the RTP port of the server to open up any
-   * NAT firewalls */
+   * NAT firewalls or, if backchannel, construct an appsrc */
   if (do_rtp) {
     GST_DEBUG_OBJECT (src, "configure RTP UDP sink for %s:%d", destination,
         rtp_port);
@@ -3932,25 +4064,36 @@
       g_object_unref (socket);
     }
 
-    /* the source for the dummy packets to open up NAT */
-    stream->fakesrc = gst_element_factory_make ("fakesrc", NULL);
-    if (stream->fakesrc == NULL)
-      goto no_fakesrc_element;
+    if (stream->is_backchannel) {
+      /* appsrc is for the app to shovel data using push-backchannel-buffer */
+      stream->rtpsrc = gst_element_factory_make ("appsrc", NULL);
+      if (stream->rtpsrc == NULL)
+        goto no_appsrc_element;
 
-    /* random data in 5 buffers, a size of 200 bytes should be fine */
-    g_object_set (G_OBJECT (stream->fakesrc), "filltype", 3, "num-buffers", 5,
-        "sizetype", 2, "sizemax", 200, "silent", TRUE, NULL);
+      /* interal use only, don't emit signals */
+      g_object_set (G_OBJECT (stream->rtpsrc), "emit-signals", TRUE,
+          "is-live", TRUE, NULL);
+    } else {
+      /* the source for the dummy packets to open up NAT */
+      stream->rtpsrc = gst_element_factory_make ("fakesrc", NULL);
+      if (stream->rtpsrc == NULL)
+        goto no_fakesrc_element;
+
+      /* random data in 5 buffers, a size of 200 bytes should be fine */
+      g_object_set (G_OBJECT (stream->rtpsrc), "filltype", 3, "num-buffers", 5,
+          "sizetype", 2, "sizemax", 200, "silent", TRUE, NULL);
+    }
 
     /* keep everything locked */
     gst_element_set_locked_state (stream->udpsink[0], TRUE);
-    gst_element_set_locked_state (stream->fakesrc, TRUE);
+    gst_element_set_locked_state (stream->rtpsrc, TRUE);
 
     gst_object_ref (stream->udpsink[0]);
     gst_bin_add (GST_BIN_CAST (src), stream->udpsink[0]);
-    gst_object_ref (stream->fakesrc);
-    gst_bin_add (GST_BIN_CAST (src), stream->fakesrc);
+    gst_object_ref (stream->rtpsrc);
+    gst_bin_add (GST_BIN_CAST (src), stream->rtpsrc);
 
-    gst_element_link_pads_full (stream->fakesrc, "src", stream->udpsink[0],
+    gst_element_link_pads_full (stream->rtpsrc, "src", stream->udpsink[0],
         "sink", GST_PAD_LINK_CHECK_NOTHING);
   }
   if (do_rtcp) {
@@ -4021,6 +4164,11 @@
     GST_ERROR_OBJECT (src, "no UDP sink element found");
     return FALSE;
   }
+no_appsrc_element:
+  {
+    GST_ERROR_OBJECT (src, "no appsrc element found");
+    return FALSE;
+  }
 no_fakesrc_element:
   {
     GST_ERROR_OBJECT (src, "no fakesrc element found");
@@ -4094,8 +4242,8 @@
     case GST_RTSP_LOWER_TRANS_UDP:
       if (!gst_rtspsrc_stream_configure_udp (src, stream, transport, &outpad))
         goto transport_failed;
-      /* configure udpsinks back to the server for RTCP messages and for the
-       * dummy RTP messages to open NAT. */
+      /* configure udpsinks back to the server for RTCP messages, for the
+       * dummy RTP messages to open NAT, and for the backchannel */
       if (!gst_rtspsrc_stream_configure_udp_sinks (src, stream, transport))
         goto transport_failed;
       break;
@@ -4103,8 +4251,12 @@
       goto unknown_transport;
   }
 
-  if (outpad) {
-    GST_DEBUG_OBJECT (src, "creating ghostpad");
+  /* using backchannel and no manager, hence no srcpad for this stream */
+  if (outpad && stream->is_backchannel) {
+    add_backchannel_fakesink (src, stream, outpad);
+    gst_object_unref (outpad);
+  } else if (outpad) {
+    GST_DEBUG_OBJECT (src, "creating ghostpad for stream %p", stream);
 
     gst_pad_use_fixed_caps (outpad);
 
@@ -4128,17 +4280,17 @@
   /* ERRORS */
 transport_failed:
   {
-    GST_DEBUG_OBJECT (src, "failed to configure transport");
+    GST_WARNING_OBJECT (src, "failed to configure transport");
     return FALSE;
   }
 unknown_transport:
   {
-    GST_DEBUG_OBJECT (src, "unknown transport");
+    GST_WARNING_OBJECT (src, "unknown transport");
     return FALSE;
   }
 no_manager:
   {
-    GST_DEBUG_OBJECT (src, "cannot get a session manager");
+    GST_WARNING_OBJECT (src, "cannot get a session manager");
     return FALSE;
   }
 }
@@ -4157,13 +4309,18 @@
   for (walk = src->streams; walk; walk = g_list_next (walk)) {
     GstRTSPStream *stream = (GstRTSPStream *) walk->data;
 
-    if (stream->fakesrc && stream->udpsink[0]) {
+    if (!stream->rtpsrc || !stream->udpsink[0])
+      continue;
+
+    if (stream->is_backchannel)
+      GST_DEBUG_OBJECT (src, "starting backchannel stream %p", stream);
+    else
       GST_DEBUG_OBJECT (src, "sending dummy packet to stream %p", stream);
-      gst_element_set_state (stream->udpsink[0], GST_STATE_NULL);
-      gst_element_set_state (stream->fakesrc, GST_STATE_NULL);
-      gst_element_set_state (stream->udpsink[0], GST_STATE_PLAYING);
-      gst_element_set_state (stream->fakesrc, GST_STATE_PLAYING);
-    }
+
+    gst_element_set_state (stream->udpsink[0], GST_STATE_NULL);
+    gst_element_set_state (stream->rtpsrc, GST_STATE_NULL);
+    gst_element_set_state (stream->udpsink[0], GST_STATE_PLAYING);
+    gst_element_set_state (stream->rtpsrc, GST_STATE_PLAYING);
   }
   return TRUE;
 }
@@ -4205,7 +4362,10 @@
       /* add the pad */
       if (!stream->added) {
         GST_DEBUG_OBJECT (src, "adding stream pad %p", stream);
-        gst_element_add_pad (GST_ELEMENT_CAST (src), stream->srcpad);
+        if (stream->is_backchannel)
+          add_backchannel_fakesink (src, stream, stream->srcpad);
+        else
+          gst_element_add_pad (GST_ELEMENT_CAST (src), stream->srcpad);
         stream->added = TRUE;
       }
     }
@@ -6529,7 +6689,7 @@
 
     caps = stream_get_caps_for_pt (stream, stream->default_pt);
     if (caps == NULL) {
-      GST_DEBUG_OBJECT (src, "skipping stream %p, no caps", stream);
+      GST_WARNING_OBJECT (src, "skipping stream %p, no caps", stream);
       continue;
     }
 
@@ -6574,13 +6734,14 @@
 
     /* skip setup if we have no URL for it */
     if (stream->conninfo.location == NULL) {
-      GST_DEBUG_OBJECT (src, "skipping stream %p, no setup", stream);
+      GST_WARNING_OBJECT (src, "skipping stream %p, no setup", stream);
       continue;
     }
 
     if (src->conninfo.connection == NULL) {
       if (!gst_rtsp_conninfo_connect (src, &stream->conninfo, async)) {
-        GST_DEBUG_OBJECT (src, "skipping stream %p, failed to connect", stream);
+        GST_WARNING_OBJECT (src, "skipping stream %p, failed to connect",
+            stream);
         continue;
       }
       conninfo = &stream->conninfo;
@@ -6653,6 +6814,10 @@
     /* select transport */
     gst_rtsp_message_take_header (&request, GST_RTSP_HDR_TRANSPORT, transports);
 
+    if (stream->is_backchannel && src->backchannel == BACKCHANNEL_ONVIF)
+      gst_rtsp_message_add_header (&request, GST_RTSP_HDR_REQUIRE,
+          BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL);
+
     /* set up keys */
     if (stream->profile == GST_RTSP_PROFILE_SAVP ||
         stream->profile == GST_RTSP_PROFILE_SAVPF) {
@@ -7168,6 +7333,11 @@
   gst_rtsp_message_add_header (&request, GST_RTSP_HDR_ACCEPT,
       "application/sdp");
 
+  if (src->backchannel == BACKCHANNEL_ONVIF)
+    gst_rtsp_message_add_header (&request, GST_RTSP_HDR_REQUIRE,
+        BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL);
+  /* TODO: Handle the case when backchannel is unsupported and goto restart */
+
   /* send DESCRIBE */
   GST_DEBUG_OBJECT (src, "send describe...");
 
@@ -7395,6 +7565,10 @@
     if (res < 0)
       goto create_request_failed;
 
+    if (stream->is_backchannel && src->backchannel == BACKCHANNEL_ONVIF)
+      gst_rtsp_message_add_header (&request, GST_RTSP_HDR_REQUIRE,
+          BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL);
+
     if (async)
       GST_ELEMENT_PROGRESS (src, CONTINUE, "close", ("Closing stream"));
 
@@ -7736,6 +7910,13 @@
       gst_rtsp_message_add_header (&request, GST_RTSP_HDR_SEEK_STYLE,
           seek_style);
 
+    /* when we have an ONVIF audio backchannel, the PLAY request must have the
+     * Require: header when doing either aggregate or non-aggregate control */
+    if (src->backchannel == BACKCHANNEL_ONVIF &&
+        (control || stream->is_backchannel))
+      gst_rtsp_message_add_header (&request, GST_RTSP_HDR_REQUIRE,
+          BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL);
+
     if (async)
       GST_ELEMENT_PROGRESS (src, CONTINUE, "request", ("Sending PLAY request"));
 
@@ -7856,17 +8037,17 @@
   /* ERRORS */
 open_failed:
   {
-    GST_DEBUG_OBJECT (src, "failed to open stream");
+    GST_WARNING_OBJECT (src, "failed to open stream");
     goto done;
   }
 not_supported:
   {
-    GST_DEBUG_OBJECT (src, "PLAY is not supported");
+    GST_WARNING_OBJECT (src, "PLAY is not supported");
     goto done;
   }
 was_playing:
   {
-    GST_DEBUG_OBJECT (src, "we were already PLAYING");
+    GST_WARNING_OBJECT (src, "we were already PLAYING");
     goto done;
   }
 create_request_failed:
@@ -7950,6 +8131,13 @@
                 setup_url)) < 0)
       goto create_request_failed;
 
+    /* when we have an ONVIF audio backchannel, the PAUSE request must have the
+     * Require: header when doing either aggregate or non-aggregate control */
+    if (src->backchannel == BACKCHANNEL_ONVIF &&
+        (control || stream->is_backchannel))
+      gst_rtsp_message_add_header (&request, GST_RTSP_HDR_REQUIRE,
+          BACKCHANNEL_ONVIF_HDR_REQUIRE_VAL);
+
     if ((res =
             gst_rtspsrc_send (src, conninfo, &request, &response, NULL,
                 NULL)) < 0)
diff --git a/gst/rtsp/gstrtspsrc.h b/gst/rtsp/gstrtspsrc.h
index 57921d2..4e5adef 100644
--- a/gst/rtsp/gstrtspsrc.h
+++ b/gst/rtsp/gstrtspsrc.h
@@ -123,8 +123,8 @@
   GstElement   *udpsink[2];
   GstPad       *rtcppad;
 
-  /* fakesrc for sending dummy data */
-  GstElement   *fakesrc;
+  /* fakesrc for sending dummy data or appsrc for sending backchannel data */
+  GstElement   *rtpsrc;
 
   /* state */
   guint         port;
@@ -161,6 +161,7 @@
   gchar        *destination;
   gboolean      is_multicast;
   guint         ttl;
+  gboolean      is_backchannel;
 
   /* A unique and stable id we will use for the stream start event */
   gchar *stream_id;
@@ -254,6 +255,7 @@
   guint64           max_ts_offset_adjustment;
   gint64            max_ts_offset;
   gboolean          max_ts_offset_is_set;
+  gint              backchannel;
 
   /* state */
   GstRTSPState       state;
@@ -298,6 +300,8 @@
 
 struct _GstRTSPSrcClass {
   GstBinClass parent_class;
+
+  GstFlowReturn (*push_backchannel_buffer) (GstRTSPSrc *src, guint id, GstSample *sample);
 };
 
 GType gst_rtspsrc_get_type(void);
diff --git a/tests/examples/Makefile.am b/tests/examples/Makefile.am
index 3f9cee6..bc1ada4 100644
--- a/tests/examples/Makefile.am
+++ b/tests/examples/Makefile.am
@@ -17,9 +17,9 @@
 endif
 
 SUBDIRS = audiofx equalizer $(GTK_DIR) $(JACK_DIR) level \
-	rtp shapewipe spectrum v4l2 $(CAIRO_DIR)
+	rtp rtsp shapewipe spectrum v4l2 $(CAIRO_DIR)
 
 DIST_SUBDIRS = audiofx equalizer gtk jack level \
-	rtp shapewipe spectrum v4l2 cairo
+	rtp rtsp shapewipe spectrum v4l2 cairo
 
 include $(top_srcdir)/common/parallel-subdirs.mak
diff --git a/tests/examples/meson.build b/tests/examples/meson.build
index aa871a4..19f9a0d 100644
--- a/tests/examples/meson.build
+++ b/tests/examples/meson.build
@@ -4,6 +4,7 @@
 subdir('level')
 #FIXME: subdir('qt')
 subdir('rtp')
+subdir('rtsp')
 subdir('shapewipe')
 subdir('v4l2')
 
diff --git a/tests/examples/rtsp/Makefile.am b/tests/examples/rtsp/Makefile.am
new file mode 100644
index 0000000..126ce8c
--- /dev/null
+++ b/tests/examples/rtsp/Makefile.am
@@ -0,0 +1,3 @@
+noinst_PROGRAMS = test-onvif
+test_onvif_CFLAGS = $(GST_CFLAGS)
+test_onvif_LDADD = $(GST_LIBS)
diff --git a/tests/examples/rtsp/meson.build b/tests/examples/rtsp/meson.build
new file mode 100644
index 0000000..7434c8a
--- /dev/null
+++ b/tests/examples/rtsp/meson.build
@@ -0,0 +1,5 @@
+executable('onvif-test', 'onvif-test.c',
+  dependencies: [gst_dep],
+  c_args : gst_plugins_good_args,
+  include_directories : [configinc],
+  install: false)
diff --git a/tests/examples/rtsp/test-onvif.c b/tests/examples/rtsp/test-onvif.c
new file mode 100644
index 0000000..671c20f
--- /dev/null
+++ b/tests/examples/rtsp/test-onvif.c
@@ -0,0 +1,107 @@
+#include <gst/gst.h>
+
+static GMainLoop *loop = NULL;
+static GstElement *backpipe = NULL;
+static gint stream_id = -1;
+
+#define PCMU_CAPS "application/x-rtp, media=audio, payload=0, clock-rate=8000, encoding-name=PCMU"
+
+static GstFlowReturn
+new_sample (GstElement * appsink, GstElement * rtspsrc)
+{
+  GstSample *sample;
+  GstFlowReturn ret = GST_FLOW_OK;
+
+  g_assert (stream_id != -1);
+
+  g_signal_emit_by_name (appsink, "pull-sample", &sample);
+
+  if (!sample)
+    goto out;
+
+  g_signal_emit_by_name (rtspsrc, "push-backchannel-buffer", stream_id, sample,
+      &ret);
+
+out:
+  return ret;
+}
+
+static void
+setup_backchannel_shoveler (GstElement * rtspsrc, GstCaps * caps)
+{
+  GstElement *appsink;
+
+  backpipe = gst_parse_launch ("audiotestsrc is-live=true wave=red-noise ! "
+      "mulawenc ! rtppcmupay ! appsink name=out", NULL);
+  if (!backpipe)
+    g_error ("Could not setup backchannel pipeline");
+
+  appsink = gst_bin_get_by_name (GST_BIN (backpipe), "out");
+  g_object_set (G_OBJECT (appsink), "caps", caps, "emit-signals", TRUE, NULL);
+
+  g_signal_connect (appsink, "new-sample", G_CALLBACK (new_sample), rtspsrc);
+
+  g_print ("Playing backchannel shoveler\n");
+  gst_element_set_state (backpipe, GST_STATE_PLAYING);
+}
+
+static gboolean
+remove_extra_fields (GQuark field_id, GValue * value G_GNUC_UNUSED,
+    gpointer user_data G_GNUC_UNUSED)
+{
+  return !g_str_has_prefix (g_quark_to_string (field_id), "a-");
+}
+
+static gboolean
+find_backchannel (GstElement * rtspsrc, guint idx, GstCaps * caps,
+    gpointer user_data G_GNUC_UNUSED)
+{
+  GstStructure *s;
+  gchar *caps_str = gst_caps_to_string (caps);
+  g_print ("Selecting stream idx %u, caps %s\n", idx, caps_str);
+  g_free (caps_str);
+
+  s = gst_caps_get_structure (caps, 0);
+  if (gst_structure_has_field (s, "a-recvonly")) {
+    stream_id = idx;
+    caps = gst_caps_new_empty ();
+    s = gst_structure_copy (s);
+    gst_structure_set_name (s, "application/x-rtp");
+    gst_structure_filter_and_map_in_place (s, remove_extra_fields, NULL);
+    gst_caps_append_structure (caps, s);
+    setup_backchannel_shoveler (rtspsrc, caps);
+  }
+
+  return TRUE;
+}
+
+int
+main (int argc, char *argv[])
+{
+  GstElement *pipeline, *rtspsrc;
+  const gchar *location;
+
+  gst_init (&argc, &argv);
+
+  if (argc >= 2)
+    location = argv[1];
+  else
+    location = "rtsp://127.0.0.1:8554/test";
+
+  loop = g_main_loop_new (NULL, FALSE);
+
+  pipeline = gst_parse_launch ("rtspsrc backchannel=onvif debug=true name=r "
+      "r. ! queue ! decodebin ! queue ! xvimagesink async=false "
+      "r. ! queue ! decodebin ! queue ! pulsesink async=false ", NULL);
+  if (!pipeline)
+    g_error ("Failed to parse pipeline");
+
+  rtspsrc = gst_bin_get_by_name (GST_BIN (pipeline), "r");
+  g_object_set (G_OBJECT (rtspsrc), "location", location, NULL);
+  g_signal_connect (rtspsrc, "select-stream", G_CALLBACK (find_backchannel),
+      NULL);
+
+  gst_element_set_state (pipeline, GST_STATE_PLAYING);
+
+  g_main_loop_run (loop);
+}