webrtcbin: initial support for sending and receiving simulcast streams
authorMatthew Waters <matthew@centricular.com>
Tue, 16 Nov 2021 08:27:11 +0000 (19:27 +1100)
committerGStreamer Marge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Tue, 29 Mar 2022 23:55:40 +0000 (23:55 +0000)
Input (sink pads) is the already-ssrc-muxed stream with the relevant rtp
sdes header extensions already applied:
  - mid
  - stream-id
  - repaired-stream-id

Output (src pads) have the pads separated into individual ssrc's as
that's what rtpbin gives us.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1664>

subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c
subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcstats.c
subprojects/gst-plugins-bad/ext/webrtc/transportstream.c
subprojects/gst-plugins-bad/ext/webrtc/transportstream.h
subprojects/gst-plugins-bad/ext/webrtc/webrtctransceiver.c
subprojects/gst-plugins-bad/ext/webrtc/webrtctransceiver.h
subprojects/gst-plugins-bad/tests/check/elements/webrtcbin.c

index a18fe43..0d460d9 100644 (file)
 
 #define DEFAULT_JB_LATENCY 200
 
+#define RTPHDREXT_MID GST_RTP_HDREXT_BASE "sdes:mid"
+#define RTPHDREXT_STREAM_ID GST_RTP_HDREXT_BASE "sdes:rtp-stream-id"
+#define RTPHDREXT_REPAIRED_STREAM_ID GST_RTP_HDREXT_BASE "sdes:repaired-rtp-stream-id"
+
 /**
  * SECTION: element-webrtcbin
  * title: webrtcbin
@@ -263,18 +267,21 @@ gst_webrtc_bin_pad_class_init (GstWebRTCBinPadClass * klass)
 }
 
 static void
-gst_webrtc_bin_pad_update_ssrc_event (GstWebRTCBinPad * wpad)
+gst_webrtc_bin_pad_update_tos_event (GstWebRTCBinPad * wpad)
 {
-  if (wpad->received_caps) {
-    WebRTCTransceiver *trans = (WebRTCTransceiver *) wpad->trans;
+  WebRTCTransceiver *trans = (WebRTCTransceiver *) wpad->trans;
+
+  if (wpad->received_caps && trans->parent.mid) {
     GstPad *pad = GST_PAD (wpad);
 
-    gst_event_take (&trans->ssrc_event,
+    gst_event_take (&trans->tos_event,
         gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
-            gst_structure_new ("GstWebRtcBinUpdateTos", "ssrc", G_TYPE_UINT,
-                trans->current_ssrc, NULL)));
+            gst_structure_new ("GstWebRtcBinUpdateTos", "mid", G_TYPE_STRING,
+                trans->parent.mid, NULL)));
 
-    gst_pad_send_event (pad, gst_event_ref (trans->ssrc_event));
+    GST_DEBUG_OBJECT (pad, "sending new tos event %" GST_PTR_FORMAT,
+        trans->tos_event);
+    gst_pad_send_event (pad, gst_event_ref (trans->tos_event));
   }
 }
 
@@ -311,12 +318,7 @@ gst_webrtcbin_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
         GST_PTR_FORMAT, pad, check_negotiation, caps);
 
     if (check_negotiation) {
-      WebRTCTransceiver *trans = WEBRTC_TRANSCEIVER (wpad->trans);
-      const GstStructure *s;
-
-      s = gst_caps_get_structure (caps, 0);
-      gst_structure_get_uint (s, "ssrc", &trans->current_ssrc);
-      gst_webrtc_bin_pad_update_ssrc_event (wpad);
+      gst_webrtc_bin_pad_update_tos_event (wpad);
     }
 
     /* A remote description might have been set while the pad hadn't
@@ -858,17 +860,82 @@ _find_pad_for_transceiver (GstWebRTCBin * webrtc, GstPadDirection direction,
 
 #if 0
 static gboolean
-match_for_ssrc (GstWebRTCBinPad * pad, guint * ssrc)
+match_for_pad (GstWebRTCBinPad * pad, GstWebRTCBinPad * other)
 {
-  return pad->ssrc == *ssrc;
+  return pad == other;
 }
+#endif
+
+struct SsrcMatch
+{
+  GstWebRTCRTPTransceiverDirection direction;
+  guint32 ssrc;
+};
 
 static gboolean
-match_for_pad (GstWebRTCBinPad * pad, GstWebRTCBinPad * other)
+mid_ssrc_match_for_ssrc (SsrcMapItem * entry, const struct SsrcMatch *match)
 {
-  return pad == other;
+  return entry->direction == match->direction && entry->ssrc == match->ssrc;
+}
+
+static gboolean
+mid_ssrc_remove_ssrc (SsrcMapItem * item, const struct SsrcMatch *match)
+{
+  return !mid_ssrc_match_for_ssrc (item, match);
+}
+
+static SsrcMapItem *
+find_mid_ssrc_for_ssrc (GstWebRTCBin * webrtc,
+    GstWebRTCRTPTransceiverDirection direction, guint rtp_session, guint ssrc)
+{
+  TransportStream *stream = _find_transport_for_session (webrtc, rtp_session);
+  struct SsrcMatch m = { direction, ssrc };
+
+  if (!stream)
+    return NULL;
+
+  return transport_stream_find_ssrc_map_item (stream, &m,
+      (FindSsrcMapFunc) mid_ssrc_match_for_ssrc);
+}
+
+static SsrcMapItem *
+find_or_add_ssrc_map_item (GstWebRTCBin * webrtc,
+    GstWebRTCRTPTransceiverDirection direction, guint rtp_session, guint ssrc,
+    guint media_idx)
+{
+  TransportStream *stream = _find_transport_for_session (webrtc, rtp_session);
+  struct SsrcMatch m = { direction, ssrc };
+  SsrcMapItem *item;
+
+  if (!stream)
+    return NULL;
+
+  if ((item = transport_stream_find_ssrc_map_item (stream, &m,
+              (FindSsrcMapFunc) mid_ssrc_match_for_ssrc)))
+    return item;
+
+  return transport_stream_add_ssrc_map_item (stream, direction, ssrc,
+      media_idx);
+}
+
+static void
+remove_ssrc_entry_by_ssrc (GstWebRTCBin * webrtc, guint rtp_session, guint ssrc)
+{
+  TransportStream *stream;
+
+  stream = _find_transport_for_session (webrtc, rtp_session);
+  if (stream) {
+    struct SsrcMatch m =
+        { GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, ssrc };
+
+    transport_stream_filter_ssrc_map_item (stream, &m,
+        (FindSsrcMapFunc) mid_ssrc_remove_ssrc);
+
+    m.direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY;
+    transport_stream_filter_ssrc_map_item (stream, &m,
+        (FindSsrcMapFunc) mid_ssrc_remove_ssrc);
+  }
 }
-#endif
 
 static gboolean
 _unlock_pc_thread (GMutex * lock)
@@ -1538,7 +1605,7 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
   GST_LOG_OBJECT (webrtc, "checking if negotiation is needed");
 
   /* We can't negotiate until we have received caps on all our sink pads,
-   * as we will need the ssrcs in our offer / answer */
+   * as we will need the formats in our offer / answer */
   if (!_all_sinks_have_caps (webrtc)) {
     GST_LOG_OBJECT (webrtc,
         "no negotiation possible until caps have been received on all sink pads");
@@ -1892,7 +1959,7 @@ _find_codec_preferences (GstWebRTCBin * webrtc,
 
     if (caps) {
       if (trans)
-        gst_caps_replace (&trans->last_configured_caps, caps);
+        gst_caps_replace (&trans->last_retrieved_caps, caps);
 
       ret = caps;
     }
@@ -1901,8 +1968,8 @@ _find_codec_preferences (GstWebRTCBin * webrtc,
   if (!ret) {
     if (codec_preferences)
       ret = gst_caps_ref (codec_preferences);
-    else if (trans->last_configured_caps)
-      ret = gst_caps_ref (trans->last_configured_caps);
+    else if (trans->last_retrieved_caps)
+      ret = gst_caps_ref (trans->last_retrieved_caps);
   }
 
 out:
@@ -1972,14 +2039,6 @@ _on_dtls_transport_notify_state (GstWebRTCDTLSTransport * transport,
 }
 
 static gboolean
-match_ssrc (GstWebRTCRTPTransceiver * rtp_trans, gconstpointer data)
-{
-  WebRTCTransceiver *trans = (WebRTCTransceiver *) rtp_trans;
-
-  return (trans->current_ssrc == GPOINTER_TO_UINT (data));
-}
-
-static gboolean
 _on_sending_rtcp (GObject * internal_session, GstBuffer * buffer,
     gboolean early, gpointer user_data)
 {
@@ -1993,17 +2052,28 @@ _on_sending_rtcp (GObject * internal_session, GstBuffer * buffer,
   if (gst_rtcp_buffer_get_first_packet (&rtcp, &packet)) {
     if (gst_rtcp_packet_get_type (&packet) == GST_RTCP_TYPE_SR) {
       guint32 ssrc;
-      GstWebRTCRTPTransceiver *rtp_trans;
+      GstWebRTCRTPTransceiver *rtp_trans = NULL;
       WebRTCTransceiver *trans;
+      guint rtp_session;
+      SsrcMapItem *mid;
 
       gst_rtcp_packet_sr_get_sender_info (&packet, &ssrc, NULL, NULL, NULL,
           NULL);
-
-      rtp_trans = _find_transceiver (webrtc, GUINT_TO_POINTER (ssrc),
-          match_ssrc);
+      rtp_session =
+          GPOINTER_TO_UINT (g_object_get_data (internal_session,
+              "GstWebRTCBinRTPSessionID"));
+
+      mid = find_mid_ssrc_for_ssrc (webrtc,
+          GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, rtp_session, ssrc);
+      if (mid && mid->mid) {
+        rtp_trans = _find_transceiver_for_mid (webrtc, mid->mid);
+        GST_LOG_OBJECT (webrtc, "found %" GST_PTR_FORMAT " from mid entry "
+            "using rtp session %u ssrc %u -> mid \'%s\'", rtp_trans,
+            rtp_session, ssrc, mid->mid);
+      }
       trans = (WebRTCTransceiver *) rtp_trans;
 
-      if (rtp_trans && rtp_trans->sender && trans->ssrc_event) {
+      if (rtp_trans && rtp_trans->sender && trans->tos_event) {
         GstPad *pad;
         gchar *pad_name = NULL;
 
@@ -2013,7 +2083,7 @@ _on_sending_rtcp (GObject * internal_session, GstBuffer * buffer,
         pad = gst_element_get_static_pad (webrtc->rtpbin, pad_name);
         g_free (pad_name);
         if (pad) {
-          gst_pad_push_event (pad, gst_event_ref (trans->ssrc_event));
+          gst_pad_push_event (pad, gst_event_ref (trans->tos_event));
           gst_object_unref (pad);
         }
       }
@@ -2036,6 +2106,8 @@ gst_webrtc_bin_attach_tos_to_session (GstWebRTCBin * webrtc, guint session_id)
       session_id, &internal_session);
 
   if (internal_session) {
+    g_object_set_data (internal_session, "GstWebRTCBinRTPSessionID",
+        GUINT_TO_POINTER (session_id));
     g_signal_connect (internal_session, "on-sending-rtcp",
         G_CALLBACK (_on_sending_rtcp), webrtc);
     g_object_unref (internal_session);
@@ -2063,14 +2135,13 @@ _nicesink_pad_probe (GstPad * pad, GstPadProbeInfo * info, gpointer user_data)
         gst_event_get_structure (GST_PAD_PROBE_INFO_EVENT (info));
 
     if (gst_structure_has_name (s, "GstWebRtcBinUpdateTos")) {
-      guint ssrc;
+      const char *mid;
       gint priority;
 
-      if (gst_structure_get_uint (s, "ssrc", &ssrc)) {
+      if ((mid = gst_structure_get_string (s, "mid"))) {
         GstWebRTCRTPTransceiver *rtp_trans;
 
-        rtp_trans = _find_transceiver (webrtc, GUINT_TO_POINTER (ssrc),
-            match_ssrc);
+        rtp_trans = _find_transceiver_for_mid (webrtc, mid);
         if (rtp_trans) {
           WebRTCTransceiver *trans = WEBRTC_TRANSCEIVER (rtp_trans);
           GstWebRTCICEStream *stream = _find_ice_stream_for_session (webrtc,
@@ -2710,10 +2781,12 @@ _pick_rtx_payload_types (GstWebRTCBin * webrtc, WebRTCTransceiver * trans,
 
     /* https://tools.ietf.org/html/rfc4588#section-8.6 */
 
-    str = g_strdup_printf ("%u", target_ssrc);
-    gst_structure_set (trans->local_rtx_ssrc_map, str, G_TYPE_UINT,
-        g_random_int (), NULL);
-    g_free (str);
+    if (target_ssrc != -1) {
+      str = g_strdup_printf ("%u", target_ssrc);
+      gst_structure_set (trans->local_rtx_ssrc_map, str, G_TYPE_UINT,
+          g_random_int (), NULL);
+      g_free (str);
+    }
 
     str = g_strdup_printf ("%u", pt);
     gst_sdp_media_add_format (media, str);
@@ -2979,6 +3052,49 @@ _gather_extmap (GstCaps * caps, GError ** error)
   return edata.extmap;
 }
 
+struct has_hdrext
+{
+  const char *rtphdrext;
+  gboolean result;
+};
+
+static gboolean
+structure_value_has_rtphdrext (GQuark field_id, const GValue * value,
+    gpointer user_data)
+{
+  struct has_hdrext *rtphdrext = user_data;
+
+  if (g_str_has_prefix (g_quark_to_string (field_id), "extmap-")) {
+    const char *val = NULL;
+
+    if (GST_VALUE_HOLDS_ARRAY (value) && gst_value_array_get_size (value) >= 2) {
+      value = gst_value_array_get_value (value, 1);
+    }
+    if (G_VALUE_HOLDS_STRING (value)) {
+      val = g_value_get_string (value);
+    }
+
+    if (g_strcmp0 (val, rtphdrext->rtphdrext) == 0) {
+      rtphdrext->result = TRUE;
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+static gboolean
+caps_contain_rtp_header_extension (const GstCaps * caps,
+    const char *rtphdrextname)
+{
+  const GstStructure *s = gst_caps_get_structure (caps, 0);
+  struct has_hdrext data = { rtphdrextname, FALSE };
+
+  gst_structure_foreach (s, structure_value_has_rtphdrext, &data);
+
+  return data.result;
+}
+
 static gboolean
 _copy_field (GQuark field_id, const GValue * value, GstStructure * s)
 {
@@ -3178,9 +3294,12 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
     if (!gst_structure_get_int (s, "clock-rate", &clockrate))
       GST_WARNING_OBJECT (webrtc,
           "Caps %" GST_PTR_FORMAT " are missing clock-rate", caps);
-    if (!gst_structure_get_uint (s, "ssrc", &rtx_target_ssrc))
-      GST_WARNING_OBJECT (webrtc, "Caps %" GST_PTR_FORMAT " are missing ssrc",
-          caps);
+    if (!gst_structure_get_uint (s, "ssrc", &rtx_target_ssrc)) {
+      if (!caps_contain_rtp_header_extension (caps, RTPHDREXT_MID)) {
+        GST_WARNING_OBJECT (webrtc, "Caps %" GST_PTR_FORMAT " are missing ssrc",
+            caps);
+      }
+    }
 
     _pick_fec_payload_types (webrtc, WEBRTC_TRANSCEIVER (trans), reserved_pts,
         clockrate, &rtx_target_pt, media);
@@ -3196,6 +3315,7 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
   /* Some identifier; we also add the media name to it so it's identifiable */
   if (trans->mid) {
     const char *media_mid = gst_sdp_media_get_attribute_val (media, "mid");
+
     if (!media_mid) {
       gst_sdp_media_add_attribute (media, "mid", trans->mid);
     } else if (g_strcmp0 (media_mid, trans->mid) != 0) {
@@ -6001,7 +6121,11 @@ _set_description_task (GstWebRTCBin * webrtc, struct set_description *sd)
 
           if (split[0] && sscanf (split[0], "%u", &ssrc) && split[1]
               && g_str_has_prefix (split[1], "cname:")) {
-            g_ptr_array_add (item->remote_ssrcmap, ssrcmap_item_new (ssrc, i));
+            if (!find_mid_ssrc_for_ssrc (webrtc,
+                    GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY,
+                    rtp_session_id, ssrc))
+              transport_stream_add_ssrc_map_item (item,
+                  GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, ssrc, i);
           }
           g_strfreev (split);
         }
@@ -6624,13 +6748,12 @@ on_rtpbin_pad_added (GstElement * rtpbin, GstPad * new_pad,
   GST_TRACE_OBJECT (webrtc, "new rtpbin pad %s", new_pad_name);
   if (g_str_has_prefix (new_pad_name, "recv_rtp_src_")) {
     guint32 session_id = 0, ssrc = 0, pt = 0;
-    GstWebRTCRTPTransceiver *rtp_trans;
+    SsrcMapItem *mid_entry;
+    GstWebRTCRTPTransceiver *rtp_trans = NULL;
     WebRTCTransceiver *trans;
     TransportStream *stream;
     GstWebRTCBinPad *pad;
-    guint media_idx = 0;
-    gboolean found_ssrc = FALSE;
-    guint i;
+    guint media_idx;
 
     if (sscanf (new_pad_name, "recv_rtp_src_%u_%u_%u", &session_id, &ssrc,
             &pt) != 3) {
@@ -6638,33 +6761,43 @@ on_rtpbin_pad_added (GstElement * rtpbin, GstPad * new_pad,
       return;
     }
 
+    media_idx = session_id;
+
+    PC_LOCK (webrtc);
     stream = _find_transport_for_session (webrtc, session_id);
     if (!stream)
       g_warn_if_reached ();
 
-    media_idx = session_id;
+    mid_entry =
+        find_mid_ssrc_for_ssrc (webrtc,
+        GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, session_id, ssrc);
 
-    for (i = 0; i < stream->remote_ssrcmap->len; i++) {
-      SsrcMapItem *item = g_ptr_array_index (stream->remote_ssrcmap, i);
-      if (item->ssrc == ssrc) {
-        media_idx = item->media_idx;
-        found_ssrc = TRUE;
-        break;
+    if (mid_entry) {
+      if (mid_entry->mid) {
+        /* Can't use the mid_entry if the mid doesn't exist */
+        rtp_trans = _find_transceiver_for_mid (webrtc, mid_entry->mid);
+        if (rtp_trans) {
+          g_assert_cmpint (rtp_trans->mline, ==, mid_entry->media_idx);
+        }
       }
-    }
 
-    if (!found_ssrc) {
+      if (mid_entry->media_idx != -1)
+        media_idx = mid_entry->media_idx;
+    } else {
       GST_WARNING_OBJECT (webrtc, "Could not find ssrc %u", ssrc);
+      /* TODO: connect up to fakesink and reconnect later when this information
+       * is known from RTCP SDES or RTP Header extension
+       */
     }
 
-    rtp_trans = _find_transceiver_for_mline (webrtc, media_idx);
+    if (!rtp_trans)
+      rtp_trans = _find_transceiver_for_mline (webrtc, media_idx);
     if (!rtp_trans)
       g_warn_if_reached ();
     trans = WEBRTC_TRANSCEIVER (rtp_trans);
     g_assert (trans->stream == stream);
 
     pad = _find_pad_for_transceiver (webrtc, GST_PAD_SRC, rtp_trans);
-
     GST_TRACE_OBJECT (webrtc, "found pad %" GST_PTR_FORMAT
         " for rtpbin pad name %s", pad, new_pad_name);
     if (!_remove_pending_pad (webrtc, pad)) {
@@ -6708,6 +6841,7 @@ on_rtpbin_request_pt_map (GstElement * rtpbin, guint session_id, guint pt,
   GST_DEBUG_OBJECT (webrtc, "getting pt map for pt %d in session %d", pt,
       session_id);
 
+  PC_LOCK (webrtc);
   stream = _find_transport_for_session (webrtc, session_id);
   if (!stream)
     goto unknown_session;
@@ -6718,10 +6852,12 @@ on_rtpbin_request_pt_map (GstElement * rtpbin, guint session_id, guint pt,
   GST_DEBUG_OBJECT (webrtc, "Found caps %" GST_PTR_FORMAT " for pt %d in "
       "session %d", ret, pt, session_id);
 
+  PC_UNLOCK (webrtc);
   return ret;
 
 unknown_session:
   {
+    PC_UNLOCK (webrtc);
     GST_DEBUG_OBJECT (webrtc, "unknown session %d", session_id);
     return NULL;
   }
@@ -6897,6 +7033,10 @@ on_rtpbin_bye_ssrc (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u received bye", session_id, ssrc);
+
+  PC_LOCK (webrtc);
+  remove_ssrc_entry_by_ssrc (webrtc, session_id, ssrc);
+  PC_UNLOCK (webrtc);
 }
 
 static void
@@ -6904,6 +7044,10 @@ on_rtpbin_bye_timeout (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u bye timeout", session_id, ssrc);
+
+  PC_LOCK (webrtc);
+  remove_ssrc_entry_by_ssrc (webrtc, session_id, ssrc);
+  PC_UNLOCK (webrtc);
 }
 
 static void
@@ -6912,6 +7056,10 @@ on_rtpbin_sender_timeout (GstElement * rtpbin, guint session_id, guint ssrc,
 {
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u sender timeout", session_id,
       ssrc);
+
+  PC_LOCK (webrtc);
+  remove_ssrc_entry_by_ssrc (webrtc, session_id, ssrc);
+  PC_UNLOCK (webrtc);
 }
 
 static void
@@ -6919,6 +7067,14 @@ on_rtpbin_new_ssrc (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u new ssrc", session_id, ssrc);
+
+  if (ssrc == 0)
+    return;
+
+  PC_LOCK (webrtc);
+  find_or_add_ssrc_map_item (webrtc,
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, session_id, ssrc, -1);
+  PC_UNLOCK (webrtc);
 }
 
 static void
@@ -6939,7 +7095,30 @@ static void
 on_rtpbin_ssrc_sdes (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
+  GObject *session;
+
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u sdes", session_id, ssrc);
+
+  g_signal_emit_by_name (rtpbin, "get-internal-session", session_id, &session);
+  if (session) {
+    GObject *source;
+
+    g_signal_emit_by_name (session, "get-source-by-ssrc", ssrc, &source);
+    if (source) {
+      GstStructure *sdes;
+
+      g_object_get (source, "sdes", &sdes, NULL);
+
+      /* TODO: when the sdes contains the mid, use that to correlate streams
+       * as necessary */
+      GST_DEBUG_OBJECT (webrtc, "session %u ssrc %u sdes %" GST_PTR_FORMAT,
+          session_id, ssrc, sdes);
+
+      gst_clear_structure (&sdes);
+      gst_clear_object (&source);
+    }
+    g_clear_object (&session);
+  }
 }
 
 static void
@@ -6954,14 +7133,72 @@ on_rtpbin_timeout (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u timeout", session_id, ssrc);
+
+  PC_LOCK (webrtc);
+  remove_ssrc_entry_by_ssrc (webrtc, session_id, ssrc);
+  PC_UNLOCK (webrtc);
 }
 
 static void
 on_rtpbin_new_sender_ssrc (GstElement * rtpbin, guint session_id, guint ssrc,
     GstWebRTCBin * webrtc)
 {
+  SsrcMapItem *mid;
+
   GST_INFO_OBJECT (webrtc, "session %u ssrc %u new sender ssrc", session_id,
       ssrc);
+
+  PC_LOCK (webrtc);
+  mid = find_mid_ssrc_for_ssrc (webrtc,
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, session_id, ssrc);
+  if (!mid) {
+    TransportStream *stream = _find_transport_for_session (webrtc, session_id);
+    transport_stream_add_ssrc_map_item (stream,
+        GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, ssrc, -1);
+  } else if (mid->mid) {
+    /* XXX: when peers support the sdes rtcp item, use this to send the mid rtcp
+     * sdes item.  Requires being able to set the sdes on the rtpsource. */
+#if 0
+    GObject *session;
+
+    g_signal_emit_by_name (rtpbin, "get-internal-session", session_id,
+        &session, NULL);
+    if (session) {
+      GObject *source;
+
+      g_signal_emit_by_name (session, "get-source-by-ssrc", ssrc, &source);
+      if (source) {
+        GstStructure *sdes;
+        const char *sdes_field_name;
+
+        g_object_get (source, "sdes", &sdes, NULL);
+        GST_WARNING_OBJECT (webrtc, "session %u ssrc %u retrieve sdes %"
+            GST_PTR_FORMAT, session_id, ssrc, sdes);
+        sdes_field_name = gst_rtcp_sdes_type_to_name (GST_RTCP_SDES_MID);
+        g_assert (sdes_field_name);
+        gst_structure_set (sdes, sdes_field_name, G_TYPE_STRING, mid->mid,
+            NULL);
+        if (mid->rid) {
+          sdes_field_name =
+              gst_rtcp_sdes_type_to_name (GST_RTCP_SDES_RTP_STREAM_ID);
+          g_assert (sdes_field_name);
+          gst_structure_set (sdes, sdes_field_name, mid->rid, NULL);
+          // TODO: repaired-rtp-stream-id
+        }
+        // TODO: writable sdes?
+        g_object_set (source, "sdes", sdes, NULL);
+        GST_INFO_OBJECT (webrtc,
+            "session %u ssrc %u set sdes %" GST_PTR_FORMAT, session_id, ssrc,
+            sdes);
+
+        gst_clear_structure (&sdes);
+        gst_clear_object (&source);
+      }
+      g_clear_object (&session);
+    }
+#endif
+  }
+  PC_UNLOCK (webrtc);
 }
 
 static void
@@ -6972,12 +7209,49 @@ on_rtpbin_sender_ssrc_active (GstElement * rtpbin, guint session_id, guint ssrc,
       ssrc);
 }
 
+struct new_jb_args
+{
+  GstWebRTCBin *webrtc;
+  GstElement *jitterbuffer;
+  TransportStream *stream;
+  guint ssrc;
+};
+
+static gboolean
+jitter_buffer_set_retransmission (SsrcMapItem * item,
+    const struct new_jb_args *data)
+{
+  GstWebRTCRTPTransceiver *trans;
+  gboolean do_nack;
+
+  if (item->media_idx == -1)
+    return TRUE;
+
+  trans = _find_transceiver_for_mline (data->webrtc, item->media_idx);
+  if (!trans) {
+    g_warn_if_reached ();
+    return TRUE;
+  }
+
+  do_nack = WEBRTC_TRANSCEIVER (trans)->do_nack;
+  /* We don't set do-retransmission on rtpbin as we want per-session control */
+  GST_LOG_OBJECT (data->webrtc, "setting do-nack=%s for transceiver %"
+      GST_PTR_FORMAT " with transport %" GST_PTR_FORMAT
+      " rtp session %u ssrc %u", do_nack ? "true" : "false", trans,
+      data->stream, data->stream->session_id, data->ssrc);
+  g_object_set (data->jitterbuffer, "do-retransmission", do_nack, NULL);
+
+  g_weak_ref_set (&item->rtpjitterbuffer, data->jitterbuffer);
+
+  return TRUE;
+}
+
 static void
 on_rtpbin_new_jitterbuffer (GstElement * rtpbin, GstElement * jitterbuffer,
     guint session_id, guint ssrc, GstWebRTCBin * webrtc)
 {
   TransportStream *stream;
-  guint i;
+  struct new_jb_args d = { 0, };
 
   PC_LOCK (webrtc);
   GST_INFO_OBJECT (webrtc, "new jitterbuffer %" GST_PTR_FORMAT " for "
@@ -6988,33 +7262,13 @@ on_rtpbin_new_jitterbuffer (GstElement * rtpbin, GstElement * jitterbuffer,
     goto out;
   }
 
-  /* XXX: this will fail with no ssrc in the remote sdp as used with e.g. simulcast
-   * newer SDP versions from chrome/firefox */
-  for (i = 0; i < stream->remote_ssrcmap->len; i++) {
-    SsrcMapItem *item = g_ptr_array_index (stream->remote_ssrcmap, i);
+  d.webrtc = webrtc;
+  d.jitterbuffer = jitterbuffer;
+  d.stream = stream;
+  d.ssrc = ssrc;
+  transport_stream_filter_ssrc_map_item (stream, &d,
+      (FindSsrcMapFunc) jitter_buffer_set_retransmission);
 
-    if (item->ssrc == ssrc) {
-      GstWebRTCRTPTransceiver *trans;
-      gboolean do_nack;
-
-      trans = _find_transceiver_for_mline (webrtc, item->media_idx);
-      if (!trans) {
-        g_warn_if_reached ();
-        break;
-      }
-
-      do_nack = WEBRTC_TRANSCEIVER (trans)->do_nack;
-      /* We don't set do-retransmission on rtpbin as we want per-session control */
-      GST_LOG_OBJECT (webrtc, "setting do-nack=%s for transceiver %"
-          GST_PTR_FORMAT " with transport %" GST_PTR_FORMAT
-          " rtp session %u ssrc %u", do_nack ? "true" : "false", trans, stream,
-          session_id, ssrc);
-      g_object_set (jitterbuffer, "do-retransmission", do_nack, NULL);
-
-      g_weak_ref_set (&item->rtpjitterbuffer, jitterbuffer);
-      break;
-    }
-  }
 out:
   PC_UNLOCK (webrtc);
 }
@@ -7145,6 +7399,91 @@ sink_pad_block (GstPad * pad, GstPadProbeInfo * info, gpointer unused)
   return GST_PAD_PROBE_OK;
 }
 
+static void
+peek_sink_buffer (GstWebRTCBin * webrtc, guint rtp_session_id,
+    guint media_idx, WebRTCTransceiver * trans, GstBuffer * buffer)
+{
+  GstRTPBuffer rtp = GST_RTP_BUFFER_INIT;
+  SsrcMapItem *item;
+  guint ssrc;
+
+  if (!gst_rtp_buffer_map (buffer, GST_MAP_READ, &rtp))
+    return;
+  ssrc = gst_rtp_buffer_get_ssrc (&rtp);
+  gst_rtp_buffer_unmap (&rtp);
+
+  if (!ssrc) {
+    GST_WARNING_OBJECT (webrtc,
+        "incoming buffer does not contain a valid ssrc");
+    return;
+  }
+
+  PC_LOCK (webrtc);
+  item =
+      find_or_add_ssrc_map_item (webrtc,
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, rtp_session_id, ssrc,
+      media_idx);
+  if (item->media_idx == -1) {
+    char *str;
+
+    GST_DEBUG_OBJECT (webrtc, "updating media idx of ssrc item %p to %u", item,
+        media_idx);
+    item->media_idx = media_idx;
+
+    /* ensure that the rtx mapping contains a valid ssrc to use for rtx when
+     * used even when there are no ssrc's in the input/codec preferences caps */
+    str = g_strdup_printf ("%u", ssrc);
+    if (!gst_structure_has_field_typed (trans->local_rtx_ssrc_map, str,
+            G_TYPE_UINT)) {
+      /* TODO: ssrc-collision? */
+      gst_structure_set (trans->local_rtx_ssrc_map, str, G_TYPE_UINT,
+          g_random_int (), NULL);
+      _set_internal_rtpbin_element_props_from_stream (webrtc, trans->stream);
+    }
+    g_free (str);
+  }
+  PC_UNLOCK (webrtc);
+}
+
+static GstPadProbeReturn
+sink_pad_buffer_peek (GstPad * pad, GstPadProbeInfo * info,
+    GstWebRTCBin * webrtc)
+{
+  GstWebRTCBinPad *webrtc_pad = GST_WEBRTC_BIN_PAD (pad);
+  WebRTCTransceiver *trans;
+  guint rtp_session_id, media_idx;
+
+  if (!webrtc_pad->trans)
+    return GST_PAD_PROBE_OK;
+
+  trans = (WebRTCTransceiver *) webrtc_pad->trans;
+  if (!trans->stream)
+    return GST_PAD_PROBE_OK;
+
+  rtp_session_id = trans->stream->session_id;
+  media_idx = webrtc_pad->trans->mline;
+
+  if (media_idx != G_MAXUINT)
+    return GST_PAD_PROBE_OK;
+
+  if (info->type & GST_PAD_PROBE_TYPE_BUFFER) {
+    GstBuffer *buffer = GST_PAD_PROBE_INFO_BUFFER (info);
+    peek_sink_buffer (webrtc, rtp_session_id, media_idx, trans, buffer);
+  } else if (info->type & GST_PAD_PROBE_TYPE_BUFFER_LIST) {
+    GstBufferList *list = GST_PAD_PROBE_INFO_BUFFER_LIST (info);
+    guint i, n;
+
+    n = gst_buffer_list_length (list);
+    for (i = 0; i < n; i++) {
+      GstBuffer *buffer = gst_buffer_list_get (list, i);
+      peek_sink_buffer (webrtc, rtp_session_id, media_idx, trans, buffer);
+    }
+  } else {
+    g_assert_not_reached ();
+  }
+
+  return GST_PAD_PROBE_OK;
+}
 
 static GstPad *
 gst_webrtc_bin_request_new_pad (GstElement * element, GstPadTemplate * templ,
@@ -7310,6 +7649,10 @@ gst_webrtc_bin_request_new_pad (GstElement * element, GstPadTemplate * templ,
       g_list_append (webrtc->priv->pending_sink_transceivers,
       gst_object_ref (pad));
 
+  gst_pad_add_probe (GST_PAD (pad),
+      GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_BUFFER_LIST,
+      (GstPadProbeCallback) sink_pad_buffer_peek, webrtc, NULL);
+
   if (lock_mline) {
     WebRTCTransceiver *wtrans = WEBRTC_TRANSCEIVER (trans);
     wtrans->mline_locked = TRUE;
index 3d31b4a..1d0dfb1 100644 (file)
@@ -321,10 +321,11 @@ _get_stats_from_rtp_source_stats (GstWebRTCBin * webrtc,
 
     gst_structure_get (source_stats, "have-sr", G_TYPE_BOOLEAN, &have_sr, NULL);
 
-    for (i = 0; i < stream->remote_ssrcmap->len; i++) {
-      SsrcMapItem *item = g_ptr_array_index (stream->remote_ssrcmap, i);
+    for (i = 0; i < stream->ssrcmap->len; i++) {
+      SsrcMapItem *item = g_ptr_array_index (stream->ssrcmap, i);
 
-      if (item->ssrc == ssrc) {
+      if (item->direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY
+          && item->ssrc == ssrc) {
         GObject *jb = g_weak_ref_get (&item->rtpjitterbuffer);
 
         if (jb) {
index ec23751..e3c5eb0 100644 (file)
@@ -190,7 +190,7 @@ transport_stream_finalize (GObject * object)
   TransportStream *stream = TRANSPORT_STREAM (object);
 
   g_array_free (stream->ptmap, TRUE);
-  g_ptr_array_free (stream->remote_ssrcmap, TRUE);
+  g_ptr_array_free (stream->ssrcmap, TRUE);
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
@@ -277,11 +277,13 @@ clear_ptmap_item (PtMapItem * item)
     gst_caps_unref (item->caps);
 }
 
-SsrcMapItem *
-ssrcmap_item_new (guint32 ssrc, guint media_idx)
+static SsrcMapItem *
+ssrcmap_item_new (GstWebRTCRTPTransceiverDirection direction, guint32 ssrc,
+    guint media_idx)
 {
-  SsrcMapItem *ssrc_item = g_slice_new (SsrcMapItem);
+  SsrcMapItem *ssrc_item = g_new0 (SsrcMapItem, 1);
 
+  ssrc_item->direction = direction;
   ssrc_item->media_idx = media_idx;
   ssrc_item->ssrc = ssrc;
   g_weak_ref_init (&ssrc_item->rtpjitterbuffer, NULL);
@@ -293,7 +295,67 @@ static void
 ssrcmap_item_free (SsrcMapItem * item)
 {
   g_weak_ref_clear (&item->rtpjitterbuffer);
-  g_slice_free (SsrcMapItem, item);
+  g_clear_pointer (&item->mid, g_free);
+  g_clear_pointer (&item->rid, g_free);
+  g_free (item);
+}
+
+SsrcMapItem *
+transport_stream_find_ssrc_map_item (TransportStream * stream,
+    gconstpointer data, FindSsrcMapFunc func)
+{
+  int i;
+
+  for (i = 0; i < stream->ssrcmap->len; i++) {
+    SsrcMapItem *item = g_ptr_array_index (stream->ssrcmap, i);
+
+    if (func (item, data))
+      return item;
+  }
+
+  return NULL;
+}
+
+void
+transport_stream_filter_ssrc_map_item (TransportStream * stream,
+    gconstpointer data, FindSsrcMapFunc func)
+{
+  int i;
+
+  for (i = 0; i < stream->ssrcmap->len;) {
+    SsrcMapItem *item = g_ptr_array_index (stream->ssrcmap, i);
+
+    if (!func (item, data)) {
+      GST_TRACE_OBJECT (stream, "removing ssrc %u", item->ssrc);
+      g_ptr_array_remove_index_fast (stream->ssrcmap, i);
+    } else {
+      i++;
+    }
+  }
+}
+
+SsrcMapItem *
+transport_stream_add_ssrc_map_item (TransportStream * stream,
+    GstWebRTCRTPTransceiverDirection direction, guint32 ssrc, guint media_idx)
+{
+  SsrcMapItem *ret = NULL;
+  char *dir_str = gst_webrtc_rtp_transceiver_direction_to_string (direction);
+
+  g_return_val_if_fail (direction ==
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY
+      || direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, NULL);
+  g_return_val_if_fail (ssrc != 0, NULL);
+
+  GST_INFO_OBJECT (stream, "Adding mapping for rtp session %u media_idx %u "
+      "direction %s ssrc %u", stream->session_id, media_idx, dir_str, ssrc);
+
+  /* XXX: duplicates? */
+  ret = ssrcmap_item_new (direction, ssrc, media_idx);
+
+  g_ptr_array_add (stream->ssrcmap, ret);
+  g_free (dir_str);
+
+  return ret;
 }
 
 static void
@@ -301,7 +363,7 @@ transport_stream_init (TransportStream * stream)
 {
   stream->ptmap = g_array_new (FALSE, TRUE, sizeof (PtMapItem));
   g_array_set_clear_func (stream->ptmap, (GDestroyNotify) clear_ptmap_item);
-  stream->remote_ssrcmap = g_ptr_array_new_with_free_func (
+  stream->ssrcmap = g_ptr_array_new_with_free_func (
       (GDestroyNotify) ssrcmap_item_free);
 }
 
index 39794b6..af6ff76 100644 (file)
@@ -40,14 +40,14 @@ typedef struct
 
 typedef struct
 {
+  GstWebRTCRTPTransceiverDirection direction;
   guint32 ssrc;
   guint media_idx;
+  char *mid;
+  char *rid;
   GWeakRef rtpjitterbuffer; /* for stats */
 } SsrcMapItem;
 
-SsrcMapItem *           ssrcmap_item_new            (guint32 ssrc,
-                                                     guint media_idx);
-
 struct _TransportStream
 {
   GstObject                 parent;
@@ -62,7 +62,7 @@ struct _TransportStream
   GstWebRTCDTLSTransport   *transport;
 
   GArray                   *ptmap;                  /* array of PtMapItem's */
-  GPtrArray                *remote_ssrcmap;         /* array of SsrcMapItem's */
+  GPtrArray                *ssrcmap;                /* array of SsrcMapItem's */
   gboolean                  output_connected;       /* whether receive bin is connected to rtpbin */
 
   GstElement               *rtxsend;
@@ -88,6 +88,21 @@ int *                   transport_stream_get_all_pt (TransportStream * stream,
 GstCaps *               transport_stream_get_caps_for_pt    (TransportStream * stream,
                                                              guint pt);
 
+typedef gboolean (*FindSsrcMapFunc) (SsrcMapItem * e1, gconstpointer data);
+
+SsrcMapItem *           transport_stream_find_ssrc_map_item (TransportStream * stream,
+                                                      gconstpointer data,
+                                                      FindSsrcMapFunc func);
+
+void                    transport_stream_filter_ssrc_map_item (TransportStream * stream,
+                                                      gconstpointer data,
+                                                      FindSsrcMapFunc func);
+
+SsrcMapItem *           transport_stream_add_ssrc_map_item (TransportStream * stream,
+                                                      GstWebRTCRTPTransceiverDirection direction,
+                                                      guint32 ssrc,
+                                                      guint media_idx);
+
 G_END_DECLS
 
 #endif /* __TRANSPORT_STREAM_H__ */
index ed4f906..279db59 100644 (file)
@@ -157,9 +157,10 @@ webrtc_transceiver_finalize (GObject * object)
     gst_structure_free (trans->local_rtx_ssrc_map);
   trans->local_rtx_ssrc_map = NULL;
 
-  gst_caps_replace (&trans->last_configured_caps, NULL);
+  gst_caps_replace (&trans->last_retrieved_caps, NULL);
+  gst_caps_replace (&trans->last_send_configured_caps, NULL);
 
-  gst_event_replace (&trans->ssrc_event, NULL);
+  gst_event_replace (&trans->tos_event, NULL);
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
index a3c80c5..989873c 100644 (file)
@@ -40,15 +40,19 @@ struct _WebRTCTransceiver
 
   TransportStream          *stream;
   GstStructure             *local_rtx_ssrc_map;
-  guint                     current_ssrc;
-  GstEvent                 *ssrc_event;
+  GstEvent                 *tos_event;
 
   /* Properties */
   GstWebRTCFECType         fec_type;
   guint                    fec_percentage;
   gboolean                 do_nack;
 
-  GstCaps                  *last_configured_caps;
+  /* The last caps that we put into to a SDP media section */
+  GstCaps                  *last_retrieved_caps;
+  /* The last caps that we successfully configured from a valid
+   * set_local/remote description call.
+   */
+  GstCaps                  *last_send_configured_caps;
 
   gboolean                 mline_locked;
 
index 279496e..72e4ee7 100644 (file)
@@ -4727,6 +4727,383 @@ GST_START_TEST (test_max_bundle_fec)
 
 GST_END_TEST;
 
+#define RTPHDREXT_MID GST_RTP_HDREXT_BASE "sdes:mid"
+#define RTPHDREXT_STREAM_ID GST_RTP_HDREXT_BASE "sdes:rtp-stream-id"
+#define RTPHDREXT_REPAIRED_STREAM_ID GST_RTP_HDREXT_BASE "sdes:repaired-rtp-stream-id"
+
+#define L16_CAPS "application/x-rtp, payload=11, media=audio," \
+      " encoding-name=L16, clock-rate=44100"
+
+static GstCaps *
+create_simulcast_audio_caps (GstWebRTCRTPTransceiverDirection direction,
+    guint n_rid, guint ssrc[], const char *mid, guint mid_ext_id,
+    const char *const *rids, guint stream_ext_id, guint repaired_ext_id)
+{
+  GstStructure *s;
+  GstCaps *caps;
+  const char *dir_str;
+
+  if (direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY)
+    dir_str = "recv";
+  else if (direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY)
+    dir_str = "send";
+  else
+    g_assert_not_reached ();
+
+  caps = gst_caps_from_string (L16_CAPS);
+  s = gst_caps_get_structure (caps, 0);
+  if (mid && mid_ext_id != G_MAXUINT) {
+    char *extmap_key = g_strdup_printf ("extmap-%u", mid_ext_id);
+    gst_structure_set (s, "a-mid", G_TYPE_STRING, mid, extmap_key,
+        G_TYPE_STRING, RTPHDREXT_MID, NULL);
+    g_free (extmap_key);
+  }
+  if (rids && n_rid > 0 && stream_ext_id != G_MAXUINT) {
+    GString *simulcast_value = g_string_new (dir_str);
+    char *extmap_key, *value;
+    int i;
+
+    g_string_append_c (simulcast_value, ' ');
+
+    for (i = 0; i < n_rid; i++) {
+      char *rid_key = g_strdup_printf ("rid-%s", rids[i]);
+      gst_structure_set (s, rid_key, G_TYPE_STRING, dir_str, NULL);
+      if (i > 0)
+        g_string_append_c (simulcast_value, ';');
+      g_string_append (simulcast_value, rids[i]);
+      g_free (rid_key);
+    }
+    value = g_string_free (simulcast_value, FALSE);
+    simulcast_value = NULL;
+    extmap_key = g_strdup_printf ("extmap-%u", stream_ext_id);
+    gst_structure_set (s, extmap_key, G_TYPE_STRING, RTPHDREXT_STREAM_ID,
+        "a-simulcast", G_TYPE_STRING, value, NULL);
+    g_clear_pointer (&extmap_key, g_free);
+    g_clear_pointer (&value, g_free);
+
+    if (repaired_ext_id != G_MAXUINT) {
+      extmap_key = g_strdup_printf ("extmap-%u", repaired_ext_id);
+      gst_structure_set (s, extmap_key, G_TYPE_STRING,
+          RTPHDREXT_REPAIRED_STREAM_ID, NULL);
+      g_clear_pointer (&extmap_key, g_free);
+    }
+  }
+
+  return caps;
+}
+
+static void
+add_simulcast_audio_test_src_harness (GstHarness * h, guint n_rid,
+    guint ssrc[], const char *mid, guint mid_ext_id,
+    const char *const *rids, guint stream_ext_id, guint repaired_ext_id)
+{
+  GstRTPHeaderExtension *ext;
+  GstElement *capsfilter;
+  char *launch_str;
+  GstCaps *caps;
+  int i;
+
+  caps =
+      create_simulcast_audio_caps
+      (GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, n_rid, ssrc, mid,
+      mid_ext_id, rids, stream_ext_id, repaired_ext_id);
+
+  gst_harness_set_src_caps (h, gst_caps_ref (caps));
+
+  if (n_rid == 0) {
+    launch_str =
+        g_strdup ("audiotestsrc is-live=true ! " L16_CAPS
+        ",ssrc=(uint)3384078954 ! rtpL16pay name=payloader0");
+  } else {
+    GString *launch = g_string_new (NULL);
+
+    for (i = 0; i < n_rid; i++) {
+      const char *rtpfunnel = "funnel.";
+      if (i == 0)
+        rtpfunnel = "rtpfunnel name=funnel ! capsfilter name=capsfilter";
+
+      g_string_append_printf (launch, "audiotestsrc is-live=true ! "
+          "rtpL16pay name=payloader%u ! " L16_CAPS ", ssrc=(uint)%u ! %s ", i,
+          ssrc[i], rtpfunnel);
+    }
+
+    launch_str = g_string_free (launch, FALSE);
+  }
+  GST_INFO ("generated launch string %s", launch_str);
+  gst_harness_add_src_parse (h, launch_str, TRUE);
+  g_clear_pointer (&launch_str, g_free);
+  capsfilter =
+      gst_bin_get_by_name (GST_BIN (h->src_harness->element), "capsfilter");
+  g_object_set (capsfilter, "caps", caps, NULL);
+  gst_clear_object (&capsfilter);
+  gst_clear_caps (&caps);
+
+  for (i = 0; i == 0 || i < n_rid; i++) {
+    const char *rid = n_rid > 0 ? rids[i] : NULL;
+    char *pay_name = g_strdup_printf ("payloader%u", i);
+    GstElement *payloader =
+        gst_bin_get_by_name (GST_BIN (h->src_harness->element), pay_name);
+    fail_unless (payloader);
+    g_clear_pointer (&pay_name, g_free);
+
+    if (mid_ext_id != G_MAXUINT) {
+      ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_MID);
+      fail_unless (ext);
+      gst_rtp_header_extension_set_id (ext, mid_ext_id);
+      g_object_set (ext, "mid", mid, NULL);
+      g_signal_emit_by_name (payloader, "add-extension", ext);
+      gst_clear_object (&ext);
+    }
+    if (n_rid > 0 && stream_ext_id != G_MAXUINT) {
+      ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_STREAM_ID);
+      fail_unless (ext);
+      gst_rtp_header_extension_set_id (ext, stream_ext_id);
+      g_object_set (ext, "rid", rid, NULL);
+      g_signal_emit_by_name (payloader, "add-extension", ext);
+      gst_clear_object (&ext);
+    }
+    if (n_rid > 0 && stream_ext_id != G_MAXUINT) {
+      ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_STREAM_ID);
+      fail_unless (ext);
+      gst_rtp_header_extension_set_id (ext, stream_ext_id);
+      g_object_set (ext, "rid", rid, NULL);
+      g_signal_emit_by_name (payloader, "add-extension", ext);
+      gst_clear_object (&ext);
+    }
+    gst_clear_object (&payloader);
+  }
+}
+
+#undef L16_CAPS
+
+static gboolean
+gst_g_ptr_array_find_str (GPtrArray * ptr, const char *needle, guint * index)
+{
+  guint i;
+
+  for (i = 0; i < ptr->len; i++) {
+    const char *test = g_ptr_array_index (ptr, i);
+    if (g_strcmp0 (test, needle) == 0) {
+      if (index)
+        *index = i;
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+struct ExpectedRid
+{
+  guint n_rid;
+  const char *const *rid;
+};
+
+static void
+on_sdp_media_rid (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  struct ExpectedRid *expected_rids = user_data;
+  int i;
+
+  for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) {
+    const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i);
+    struct ExpectedRid *expected_rid = &expected_rids[i];
+    GPtrArray *seen_rid = g_ptr_array_new_with_free_func (g_free);
+    int j;
+
+    for (j = 0; j < gst_sdp_media_attributes_len (media); j++) {
+      const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j);
+
+      if (g_strcmp0 (attr->key, "rid") == 0) {
+        const char *p;
+        char *v;
+        guint k;
+
+        p = attr->value;
+        /* take up to either space or nul-terminator */
+        while (p && *p && *p == ' ')
+          p++;
+        v = (char *) p;
+        /* take up to either space or nul-terminator */
+        while (p && *p && *p != ' ')
+          p++;
+        g_assert (v != p);
+        v = g_strndup (v, p - v);
+        GST_INFO ("rid = %s", v);
+
+        fail_unless (FALSE == gst_g_ptr_array_find_str (seen_rid, v, NULL),
+            "duplicate/multiple rid for media %u", i);
+        for (k = 0; k < expected_rid->n_rid; k++) {
+          GST_LOG ("expected %u = %s", k, expected_rid->rid[k]);
+          if (g_strcmp0 (v, expected_rid->rid[k]) == 0)
+            break;
+        }
+        fail_unless (k < expected_rid->n_rid, "rid %s not found in media %u",
+            v, i);
+        g_ptr_array_add (seen_rid, v);
+      }
+    }
+    fail_unless (seen_rid->len == expected_rid->n_rid,
+        "mismatch in number of rid's in media %u, seen %u, expected %u", i,
+        seen_rid->len, expected_rid->n_rid);
+    g_ptr_array_unref (seen_rid);
+  }
+}
+
+GST_START_TEST (test_simulcast)
+{
+  struct test_webrtc *t = test_webrtc_new ();
+  guint media_format_count[] = { 1, };
+  VAL_SDP_INIT (media_formats, on_sdp_media_count_formats,
+      media_format_count, NULL);
+  VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL,
+      &media_formats);
+  const char *expected_rids0[] = { "a", "z" };
+  struct ExpectedRid expected_rids = { G_N_ELEMENTS (expected_rids0),
+    expected_rids0
+  };
+  VAL_SDP_INIT (rids, on_sdp_media_rid, &expected_rids, &payloads);
+  VAL_SDP_INIT (non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (1), &rids);
+  VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &non_reject);
+  const gchar *expected_offer_setup[] = { "actpass", };
+  VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count);
+  const gchar *expected_answer_setup[] = { "active", };
+  VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup,
+      &count);
+  const gchar *expected_offer_direction[] = { "sendrecv", };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction,
+      &offer_setup);
+  const gchar *expected_answer_direction[] = { "recvonly", };
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction,
+      &answer_setup);
+  GstHarness *h;
+  GList *sink_harnesses = NULL;
+  GObject *trans;
+  guint i;
+  GstElement *rtpbin2;
+  GstBuffer *buf;
+  guint mid_ext_id = 1;
+  guint stream_ext_id = 2;
+  guint repaired_ext_id = 3;
+  const char *mid = "5";
+  guint ssrcs[] = { 123456789, 987654321 };
+  GArray *ssrcs_received;
+  GstCaps *caps;
+
+  t->on_negotiation_needed = NULL;
+  t->on_ice_candidate = NULL;
+  t->on_pad_added = _pad_added_harness;
+  t->pad_added_data = &sink_harnesses;
+
+  gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy",
+      "max-bundle");
+  gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy",
+      "max-bundle");
+
+  rtpbin2 = gst_bin_get_by_name (GST_BIN (t->webrtc2), "rtpbin");
+  fail_unless (rtpbin2 != NULL);
+  g_signal_connect (rtpbin2, "new-jitterbuffer",
+      G_CALLBACK (new_jitterbuffer_set_fast_start), NULL);
+  g_object_unref (rtpbin2);
+
+  h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL);
+  add_simulcast_audio_test_src_harness (h, expected_rids.n_rid, ssrcs, mid,
+      mid_ext_id, expected_rids.rid, stream_ext_id, repaired_ext_id);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  /* setup recvonly transceiver as answer */
+  caps =
+      create_simulcast_audio_caps
+      (GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, expected_rids.n_rid,
+      ssrcs, mid, mid_ext_id, expected_rids.rid, stream_ext_id,
+      repaired_ext_id);
+  g_signal_emit_by_name (t->webrtc2, "add-transceiver",
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, caps, &trans);
+  gst_clear_caps (&caps);
+  fail_unless (trans != NULL);
+  g_clear_object (&trans);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  fail_if (gst_element_set_state (t->webrtc1,
+          GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE);
+  fail_if (gst_element_set_state (t->webrtc2,
+          GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE);
+
+  for (i = 0; i < 10; i++) {
+    gst_harness_push_from_src (h);
+  }
+
+  ssrcs_received = g_array_new (FALSE, TRUE, sizeof (guint32));
+
+  /* Get one buffer out for each ssrc sent.
+   */
+  g_mutex_lock (&t->lock);
+  while (ssrcs_received->len < G_N_ELEMENTS (ssrcs)) {
+    GList *l;
+    guint i;
+
+    gst_harness_push_from_src (h);
+    if (g_list_length (sink_harnesses) < 2) {
+      g_cond_wait_until (&t->cond, &t->lock, g_get_monotonic_time () + 5000);
+      if (g_list_length (sink_harnesses) < 2)
+        continue;
+    }
+
+    for (l = sink_harnesses; l; l = l->next) {
+      GstHarness *sink_harness = (GstHarness *) l->data;
+      GstRTPBuffer rtp = GST_RTP_BUFFER_INIT;
+      GstWebRTCRTPTransceiver *rtp_trans;
+      char *trans_mid;
+      GstPad *srcpad;
+      guint ssrc;
+
+      fail_unless (sink_harness->element == t->webrtc2);
+
+      buf = gst_harness_try_pull (sink_harness);
+      if (!buf)
+        continue;
+
+      /* ensure that the resulting pad has the correct mid set */
+      srcpad = gst_pad_get_peer (sink_harness->sinkpad);
+      fail_unless (srcpad != NULL);
+      g_object_get (srcpad, "transceiver", &rtp_trans, NULL);
+      gst_clear_object (&srcpad);
+      fail_unless (rtp_trans);
+      g_object_get (rtp_trans, "mid", &trans_mid, NULL);
+      gst_clear_object (&rtp_trans);
+      fail_unless (trans_mid != NULL);
+      fail_unless_equals_string (trans_mid, mid);
+      g_clear_pointer (&trans_mid, g_free);
+
+      fail_unless (gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp));
+
+      ssrc = gst_rtp_buffer_get_ssrc (&rtp);
+      for (i = 0; i < ssrcs_received->len; i++) {
+        if (g_array_index (ssrcs_received, guint, i) == ssrc)
+          break;
+      }
+      if (i == ssrcs_received->len) {
+        g_array_append_val (ssrcs_received, ssrc);
+      }
+
+      gst_rtp_buffer_unmap (&rtp);
+
+      gst_buffer_unref (buf);
+    }
+  }
+  g_mutex_unlock (&t->lock);
+
+  test_webrtc_free (t);
+  g_list_free (sink_harnesses);
+
+  g_array_unref (ssrcs_received);
+}
+
+GST_END_TEST;
+
 static Suite *
 webrtcbin_suite (void)
 {
@@ -4784,6 +5161,7 @@ webrtcbin_suite (void)
     tcase_add_test (tc, test_renego_rtx);
     tcase_add_test (tc, test_bundle_mid_header_extension);
     tcase_add_test (tc, test_max_bundle_fec);
+    tcase_add_test (tc, test_simulcast);
     if (sctpenc && sctpdec) {
       tcase_add_test (tc, test_data_channel_create);
       tcase_add_test (tc, test_data_channel_remote_notify);