webrtc: Initial support for stream addition/removal
authorMatthew Waters <matthew@centricular.com>
Wed, 28 Nov 2018 06:23:31 +0000 (17:23 +1100)
committerMatthew Waters <matthew@centricular.com>
Thu, 30 May 2019 11:33:09 +0000 (21:33 +1000)
Limitations:
- No transport changes at all (ICE, DTLS)
- Codec changes are untested and probably don't work
- Stream removal doesn't remove transports (i.e. non-bundled transports
  will stay around until webrtcbin is shutdown)
- Unified Plan SDP only. No Plan-B support.

16 files changed:
.gitignore
ext/webrtc/gstwebrtcbin.c
ext/webrtc/gstwebrtcbin.h
ext/webrtc/transportstream.c
ext/webrtc/transportstream.h
ext/webrtc/utils.c
ext/webrtc/utils.h
ext/webrtc/webrtcsdp.c
ext/webrtc/webrtcsdp.h
ext/webrtc/webrtctransceiver.c
ext/webrtc/webrtctransceiver.h
gst-libs/gst/webrtc/rtptransceiver.c
tests/check/elements/webrtcbin.c
tests/examples/webrtc/Makefile.am
tests/examples/webrtc/meson.build
tests/examples/webrtc/webrtcrenego.c [new file with mode: 0644]

index 7fa8dbc..539e470 100644 (file)
@@ -75,6 +75,7 @@ gst*orc.h
 /tests/examples/webrtc/webrtc
 /tests/examples/webrtc/webrtcbidirectional
 /tests/examples/webrtc/webrtcswap
+/tests/examples/webrtc/webrtcrenego
 /tests/examples/webrtc/webrtctransceiver
 
 Build
index c6d2ae3..665220d 100644 (file)
@@ -245,18 +245,28 @@ static gboolean
 gst_webrtcbin_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
 {
   GstWebRTCBinPad *wpad = GST_WEBRTC_BIN_PAD (pad);
+  GstWebRTCBin *webrtc = GST_WEBRTC_BIN (parent);
+  gboolean check_negotiation = FALSE;
 
   if (GST_EVENT_TYPE (event) == GST_EVENT_CAPS) {
     GstCaps *caps;
-    gboolean do_update;
 
     gst_event_parse_caps (event, &caps);
-    do_update = (!wpad->received_caps
+    check_negotiation = (!wpad->received_caps
         || gst_caps_is_equal (wpad->received_caps, caps));
     gst_caps_replace (&wpad->received_caps, caps);
 
-    if (do_update)
-      _update_need_negotiation (GST_WEBRTC_BIN (parent));
+    GST_DEBUG_OBJECT (parent,
+        "On %" GST_PTR_FORMAT " checking negotiation? %u, caps %"
+        GST_PTR_FORMAT, pad, check_negotiation, caps);
+  } else if (GST_EVENT_TYPE (event) == GST_EVENT_EOS) {
+    check_negotiation = TRUE;
+  }
+
+  if (check_negotiation) {
+    PC_LOCK (webrtc);
+    _update_need_negotiation (webrtc);
+    PC_UNLOCK (webrtc);
   }
 
   return gst_pad_event_default (pad, parent, event);
@@ -1164,10 +1174,14 @@ _all_sinks_have_caps (GstWebRTCBin * webrtc)
   GST_OBJECT_LOCK (webrtc);
   l = GST_ELEMENT (webrtc)->pads;
   for (; l; l = g_list_next (l)) {
+    GstWebRTCBinPad *wpad;
+
     if (!GST_IS_WEBRTC_BIN_PAD (l->data))
       continue;
-    if (GST_PAD_DIRECTION (l->data) == GST_PAD_SINK
-        && !GST_WEBRTC_BIN_PAD (l->data)->received_caps) {
+
+    wpad = GST_WEBRTC_BIN_PAD (l->data);
+    if (GST_PAD_DIRECTION (l->data) == GST_PAD_SINK && !wpad->received_caps
+        && (!wpad->trans || !wpad->trans->stopped)) {
       goto done;
     }
   }
@@ -1207,10 +1221,6 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
    * FIXME */
   /* FIXME: emit when input caps/format changes? */
 
-  /* If connection has created any RTCDataChannel's, and no m= section has
-   * been negotiated yet for data, return "true".
-   * FIXME */
-
   if (!webrtc->current_local_description) {
     GST_LOG_OBJECT (webrtc, "no local description set");
     return TRUE;
@@ -1221,6 +1231,18 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
     return TRUE;
   }
 
+  /* If connection has created any RTCDataChannel's, and no m= section has
+   * been negotiated yet for data, return "true". */
+  if (webrtc->priv->data_channels->len > 0) {
+    if (_message_get_datachannel_index (webrtc->current_local_description->
+            sdp) >= G_MAXUINT) {
+      GST_LOG_OBJECT (webrtc,
+          "no data channel media section and have %u " "transports",
+          webrtc->priv->data_channels->len);
+      return TRUE;
+    }
+  }
+
   for (i = 0; i < webrtc->priv->transceivers->len; i++) {
     GstWebRTCRTPTransceiver *trans;
 
@@ -1240,8 +1262,8 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
       GstWebRTCRTPTransceiverDirection local_dir, remote_dir;
 
       if (trans->mline == -1 || trans->mid == NULL) {
-        GST_LOG_OBJECT (webrtc, "unassociated transceiver %i %" GST_PTR_FORMAT,
-            i, trans);
+        GST_LOG_OBJECT (webrtc, "unassociated transceiver %i %" GST_PTR_FORMAT
+            " mid %s", i, trans, trans->mid);
         return TRUE;
       }
       /* internal inconsistency */
@@ -1271,8 +1293,26 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
          * nor answer matches t's direction, return "true". */
 
         if (local_dir != trans->direction && remote_dir != trans->direction) {
-          GST_LOG_OBJECT (webrtc,
-              "transceiver direction doesn't match description");
+          gchar *local_str, *remote_str, *dir_str;
+
+          local_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              local_dir);
+          remote_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              remote_dir);
+          dir_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              trans->direction);
+
+          GST_LOG_OBJECT (webrtc, "transceiver direction (%s) doesn't match "
+              "description (local %s remote %s)", dir_str, local_str,
+              remote_str);
+
+          g_free (dir_str);
+          g_free (local_str);
+          g_free (remote_str);
+
           return TRUE;
         }
       } else if (webrtc->current_local_description->type ==
@@ -1288,8 +1328,30 @@ _check_if_negotiation_is_needed (GstWebRTCBin * webrtc)
         intersect_dir = _intersect_answer_directions (remote_dir, local_dir);
 
         if (intersect_dir != trans->direction) {
-          GST_LOG_OBJECT (webrtc,
-              "transceiver direction doesn't match description");
+          gchar *local_str, *remote_str, *inter_str, *dir_str;
+
+          local_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              local_dir);
+          remote_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              remote_dir);
+          dir_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              trans->direction);
+          inter_str =
+              _enum_value_to_string (GST_TYPE_WEBRTC_RTP_TRANSCEIVER_DIRECTION,
+              intersect_dir);
+
+          GST_LOG_OBJECT (webrtc, "transceiver direction (%s) doesn't match "
+              "description intersected direction %s (local %s remote %s)",
+              dir_str, local_str, inter_str, remote_str);
+
+          g_free (dir_str);
+          g_free (local_str);
+          g_free (remote_str);
+          g_free (inter_str);
+
           return TRUE;
         }
       }
@@ -1342,21 +1404,32 @@ _update_need_negotiation (GstWebRTCBin * webrtc)
 }
 
 static GstCaps *
-_find_codec_preferences (GstWebRTCBin * webrtc, GstWebRTCRTPTransceiver * trans,
-    GstPadDirection direction, guint media_idx)
+_find_codec_preferences (GstWebRTCBin * webrtc,
+    GstWebRTCRTPTransceiver * rtp_trans, GstPadDirection direction,
+    guint media_idx)
 {
+  WebRTCTransceiver *trans = (WebRTCTransceiver *) rtp_trans;
   GstCaps *ret = NULL;
 
   GST_LOG_OBJECT (webrtc, "retreiving codec preferences from %" GST_PTR_FORMAT,
       trans);
 
-  if (trans && trans->codec_preferences) {
+  if (rtp_trans && rtp_trans->codec_preferences) {
     GST_LOG_OBJECT (webrtc, "Using codec preferences: %" GST_PTR_FORMAT,
-        trans->codec_preferences);
-    ret = gst_caps_ref (trans->codec_preferences);
+        rtp_trans->codec_preferences);
+    ret = gst_caps_ref (rtp_trans->codec_preferences);
   } else {
-    GstWebRTCBinPad *pad = _find_pad_for_mline (webrtc, direction, media_idx);
-    if (pad) {
+    GstWebRTCBinPad *pad = NULL;
+
+    /* try to find a pad */
+    if (!trans
+        || !(pad = _find_pad_for_transceiver (webrtc, direction, rtp_trans)))
+      pad = _find_pad_for_mline (webrtc, direction, media_idx);
+
+    if (!pad) {
+      if (trans && trans->last_configured_caps)
+        ret = gst_caps_ref (trans->last_configured_caps);
+    } else {
       GstCaps *caps = NULL;
 
       if (pad->received_caps) {
@@ -1369,12 +1442,20 @@ _find_codec_preferences (GstWebRTCBin * webrtc, GstWebRTCRTPTransceiver * trans,
           GST_LOG_OBJECT (webrtc, "Using peer query caps: %" GST_PTR_FORMAT,
               caps);
       }
-      if (caps)
+      if (caps) {
+        if (trans)
+          gst_caps_replace (&trans->last_configured_caps, caps);
+
         ret = caps;
+      }
+
       gst_object_unref (pad);
     }
   }
 
+  if (!ret)
+    GST_DEBUG_OBJECT (trans, "Could not find caps for mline %u", media_idx);
+
   return ret;
 }
 
@@ -1966,8 +2047,8 @@ _add_fingerprint_to_media (GstWebRTCDTLSTransport * transport,
 static gboolean
 sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
     GstWebRTCRTPTransceiver * trans, GstWebRTCSDPType type, guint media_idx,
-    GString * bundled_mids, guint bundle_idx, gboolean bundle_only,
-    GArray * reserved_pts)
+    GString * bundled_mids, guint bundle_idx, gchar * bundle_ufrag,
+    gchar * bundle_pwd, GArray * reserved_pts)
 {
   /* TODO:
    * rtp header extensions
@@ -1979,19 +2060,48 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
    * dtls fingerprints
    * multiple dtls fingerprints https://tools.ietf.org/html/draft-ietf-mmusic-4572-update-05
    */
-  gchar *direction, *sdp_mid;
+  GstSDPMessage *last_offer = _get_latest_offer (webrtc);
+  gchar *direction, *sdp_mid, *ufrag, *pwd;
+  gboolean bundle_only;
   GstCaps *caps;
   int i;
 
-  /* "An m= section is generated for each RtpTransceiver that has been added
-   * to the Bin, excluding any stopped RtpTransceivers." */
-  if (trans->stopped)
-    return FALSE;
   if (trans->direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_NONE
       || trans->direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE)
     return FALSE;
 
-  gst_sdp_media_set_port_info (media, bundle_only ? 0 : 9, 0);
+  g_assert (trans->mline == -1 || trans->mline == media_idx);
+
+  bundle_only = bundled_mids && bundle_idx != media_idx
+      && webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE;
+
+  /* mandated by JSEP */
+  gst_sdp_media_add_attribute (media, "setup", "actpass");
+
+  /* FIXME: deal with ICE restarts */
+  if (last_offer && trans->mline != -1 && trans->mid) {
+    ufrag = g_strdup (_media_get_ice_ufrag (last_offer, trans->mline));
+    pwd = g_strdup (_media_get_ice_pwd (last_offer, trans->mline));
+    GST_DEBUG_OBJECT (trans, "%u Using previous ice parameters", media_idx);
+  } else {
+    GST_DEBUG_OBJECT (trans,
+        "%u Generating new ice parameters mline %i, mid %s", media_idx,
+        trans->mline, trans->mid);
+    if (webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_NONE) {
+      _generate_ice_credentials (&ufrag, &pwd);
+    } else {
+      g_assert (bundle_ufrag && bundle_pwd);
+      ufrag = g_strdup (bundle_ufrag);
+      pwd = g_strdup (bundle_pwd);
+    }
+  }
+
+  gst_sdp_media_add_attribute (media, "ice-ufrag", ufrag);
+  gst_sdp_media_add_attribute (media, "ice-pwd", pwd);
+  g_free (ufrag);
+  g_free (pwd);
+
+  gst_sdp_media_set_port_info (media, bundle_only || trans->stopped ? 0 : 9, 0);
   gst_sdp_media_set_proto (media, "UDP/TLS/RTP/SAVPF");
   gst_sdp_media_add_connection (media, "IN", "IP4", "0.0.0.0", 0, 0);
 
@@ -2018,9 +2128,6 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
     caps =
         _add_supported_attributes_to_caps (webrtc, WEBRTC_TRANSCEIVER (trans),
         caps);
-  } else if (type == GST_WEBRTC_SDP_TYPE_ANSWER) {
-    caps = _find_codec_preferences (webrtc, trans, GST_PAD_SRC, media_idx);
-    /* FIXME: add rtcp-fb paramaters */
   } else {
     g_assert_not_reached ();
   }
@@ -2080,10 +2187,18 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
   _media_add_ssrcs (media, caps, webrtc, WEBRTC_TRANSCEIVER (trans));
 
   /* Some identifier; we also add the media name to it so it's identifiable */
-  sdp_mid = g_strdup_printf ("%s%u", gst_sdp_media_get_media (media),
-      webrtc->priv->media_counter++);
-  gst_sdp_media_add_attribute (media, "mid", sdp_mid);
-  g_free (sdp_mid);
+  if (trans->mid) {
+    gst_sdp_media_add_attribute (media, "mid", trans->mid);
+  } else {
+    sdp_mid = g_strdup_printf ("%s%u", gst_sdp_media_get_media (media),
+        webrtc->priv->media_counter++);
+    gst_sdp_media_add_attribute (media, "mid", sdp_mid);
+    g_free (sdp_mid);
+  }
+
+  /* TODO:
+   * - add a=candidate lines for gathered candidates
+   */
 
   if (trans->sender) {
     if (!trans->sender->transport) {
@@ -2099,6 +2214,13 @@ sdp_media_from_transceiver (GstWebRTCBin * webrtc, GstSDPMedia * media,
     _add_fingerprint_to_media (trans->sender->transport, media);
   }
 
+  if (bundled_mids) {
+    const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
+
+    g_assert (mid);
+    g_string_append_printf (bundled_mids, " %s", mid);
+  }
+
   gst_caps_unref (caps);
 
   return TRUE;
@@ -2132,25 +2254,122 @@ gather_reserved_pts (GstWebRTCBin * webrtc)
   return reserved_pts;
 }
 
+static gboolean
+_add_data_channel_offer (GstWebRTCBin * webrtc, GstSDPMessage * msg,
+    GstSDPMedia * media, GString * bundled_mids, guint bundle_idx,
+    gchar * bundle_ufrag, gchar * bundle_pwd)
+{
+  GstSDPMessage *last_offer = _get_latest_offer (webrtc);
+  gchar *ufrag, *pwd, *sdp_mid;
+  gboolean bundle_only = bundled_mids
+      && webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE
+      && gst_sdp_message_medias_len (msg) != bundle_idx;
+  guint last_data_index = G_MAXUINT;
+
+  /* add data channel support */
+  if (webrtc->priv->data_channels->len == 0)
+    return FALSE;
+
+  if (last_offer) {
+    last_data_index = _message_get_datachannel_index (last_offer);
+    if (last_data_index < G_MAXUINT) {
+      g_assert (last_data_index < gst_sdp_message_medias_len (last_offer));
+      /* XXX: is this always true when recycling transceivers?
+       * i.e. do we always put the data channel in the same mline */
+      g_assert (last_data_index == gst_sdp_message_medias_len (msg));
+    }
+  }
+
+  /* mandated by JSEP */
+  gst_sdp_media_add_attribute (media, "setup", "actpass");
+
+  /* FIXME: only needed when restarting ICE */
+  if (last_offer && last_data_index < G_MAXUINT) {
+    ufrag = g_strdup (_media_get_ice_ufrag (last_offer, last_data_index));
+    pwd = g_strdup (_media_get_ice_pwd (last_offer, last_data_index));
+  } else {
+    if (webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_NONE) {
+      _generate_ice_credentials (&ufrag, &pwd);
+    } else {
+      ufrag = g_strdup (bundle_ufrag);
+      pwd = g_strdup (bundle_pwd);
+    }
+  }
+  gst_sdp_media_add_attribute (media, "ice-ufrag", ufrag);
+  gst_sdp_media_add_attribute (media, "ice-pwd", pwd);
+  g_free (ufrag);
+  g_free (pwd);
+
+  gst_sdp_media_set_media (media, "application");
+  gst_sdp_media_set_port_info (media, bundle_only ? 0 : 9, 0);
+  gst_sdp_media_set_proto (media, "UDP/DTLS/SCTP");
+  gst_sdp_media_add_connection (media, "IN", "IP4", "0.0.0.0", 0, 0);
+  gst_sdp_media_add_format (media, "webrtc-datachannel");
+
+  if (bundle_idx != gst_sdp_message_medias_len (msg))
+    gst_sdp_media_add_attribute (media, "bundle-only", NULL);
+
+  if (last_offer && last_data_index < G_MAXUINT) {
+    const GstSDPMedia *last_data_media;
+    const gchar *mid;
+
+    last_data_media = gst_sdp_message_get_media (last_offer, last_data_index);
+    mid = gst_sdp_media_get_attribute_val (last_data_media, "mid");
+
+    gst_sdp_media_add_attribute (media, "mid", mid);
+  } else {
+    sdp_mid = g_strdup_printf ("%s%u", gst_sdp_media_get_media (media),
+        webrtc->priv->media_counter++);
+    gst_sdp_media_add_attribute (media, "mid", sdp_mid);
+    g_free (sdp_mid);
+  }
+
+  if (bundled_mids) {
+    const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
+
+    g_assert (mid);
+    g_string_append_printf (bundled_mids, " %s", mid);
+  }
+
+  /* FIXME: negotiate this properly */
+  gst_sdp_media_add_attribute (media, "sctp-port", "5000");
+
+  _get_or_create_data_channel_transports (webrtc,
+      bundled_mids ? 0 : webrtc->priv->transceivers->len);
+  _add_fingerprint_to_media (webrtc->priv->sctp_transport->transport, media);
+
+  return TRUE;
+}
+
 /* TODO: use the options argument */
 static GstSDPMessage *
 _create_offer_task (GstWebRTCBin * webrtc, const GstStructure * options)
 {
   GstSDPMessage *ret;
-  int i;
   GString *bundled_mids = NULL;
   gchar *bundle_ufrag = NULL;
   gchar *bundle_pwd = NULL;
   GArray *reserved_pts = NULL;
+  GstSDPMessage *last_offer = _get_latest_offer (webrtc);
+  GList *seen_transceivers = NULL;
+  guint media_idx = 0;
+  int i;
 
   gst_sdp_message_new (&ret);
 
   gst_sdp_message_set_version (ret, "0");
   {
-    /* FIXME: session id and version need special handling depending on the state we're in */
-    gchar *sess_id = g_strdup_printf ("%" G_GUINT64_FORMAT, RANDOM_SESSION_ID);
-    gst_sdp_message_set_origin (ret, "-", sess_id, "0", "IN", "IP4", "0.0.0.0");
+    gchar *v, *sess_id;
+    v = g_strdup_printf ("%u", webrtc->priv->offer_count++);
+    if (last_offer) {
+      const GstSDPOrigin *origin = gst_sdp_message_get_origin (last_offer);
+      sess_id = g_strdup (origin->sess_id);
+    } else {
+      sess_id = g_strdup_printf ("%" G_GUINT64_FORMAT, RANDOM_SESSION_ID);
+    }
+    gst_sdp_message_set_origin (ret, "-", sess_id, v, "IN", "IP4", "0.0.0.0");
     g_free (sess_id);
+    g_free (v);
   }
   gst_sdp_message_set_session_name (ret, "-");
   gst_sdp_message_add_time (ret, "0", "0", NULL);
@@ -2163,53 +2382,119 @@ _create_offer_task (GstWebRTCBin * webrtc, const GstStructure * options)
   }
 
   if (webrtc->bundle_policy != GST_WEBRTC_BUNDLE_POLICY_NONE) {
-    _generate_ice_credentials (&bundle_ufrag, &bundle_pwd);
+    GStrv last_bundle = NULL;
+    guint bundle_media_index;
+
     reserved_pts = gather_reserved_pts (webrtc);
+    if (last_offer && _parse_bundle (last_offer, &last_bundle) && last_bundle
+        && last_bundle && last_bundle[0]
+        && _get_bundle_index (last_offer, last_bundle, &bundle_media_index)) {
+      bundle_ufrag =
+          g_strdup (_media_get_ice_ufrag (last_offer, bundle_media_index));
+      bundle_pwd =
+          g_strdup (_media_get_ice_pwd (last_offer, bundle_media_index));
+    } else {
+      _generate_ice_credentials (&bundle_ufrag, &bundle_pwd);
+    }
+
+    g_strfreev (last_bundle);
   }
 
-  /* for each rtp transceiver */
+  /* FIXME: recycle transceivers */
+
+  /* Fill up the renegotiated streams first */
+  if (last_offer) {
+    for (i = 0; i < gst_sdp_message_medias_len (last_offer); i++) {
+      GstWebRTCRTPTransceiver *trans = NULL;
+      const GstSDPMedia *last_media;
+
+      last_media = gst_sdp_message_get_media (last_offer, i);
+
+      if (g_strcmp0 (gst_sdp_media_get_media (last_media), "audio") == 0
+          || g_strcmp0 (gst_sdp_media_get_media (last_media), "video") == 0) {
+        const gchar *last_mid;
+        int j;
+        last_mid = gst_sdp_media_get_attribute_val (last_media, "mid");
+
+        for (j = 0; j < webrtc->priv->transceivers->len; j++) {
+          trans =
+              g_array_index (webrtc->priv->transceivers,
+              GstWebRTCRTPTransceiver *, j);
+
+          if (trans->mid && g_strcmp0 (trans->mid, last_mid) == 0) {
+            GstSDPMedia *media;
+
+            g_assert (!g_list_find (seen_transceivers, trans));
+
+            GST_LOG_OBJECT (webrtc, "using previous negotiatied transceiver %"
+                GST_PTR_FORMAT " with mid %s into media index %u", trans,
+                trans->mid, media_idx);
+
+            /* FIXME: deal with format changes */
+            gst_sdp_media_copy (last_media, &media);
+            _media_replace_direction (media, trans->direction);
+
+            if (bundled_mids) {
+              const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
+
+              g_assert (mid);
+              g_string_append_printf (bundled_mids, " %s", mid);
+            }
+
+            gst_sdp_message_add_media (ret, media);
+            media_idx++;
+
+            gst_sdp_media_free (media);
+            seen_transceivers = g_list_prepend (seen_transceivers, trans);
+            break;
+          }
+        }
+      } else if (g_strcmp0 (gst_sdp_media_get_media (last_media),
+              "application") == 0) {
+        GstSDPMedia media = { 0, };
+        gst_sdp_media_init (&media);
+        if (_add_data_channel_offer (webrtc, ret, &media, bundled_mids, 0,
+                bundle_ufrag, bundle_pwd)) {
+          gst_sdp_message_add_media (ret, &media);
+          media_idx++;
+        } else {
+          gst_sdp_media_uninit (&media);
+        }
+      }
+    }
+  }
+
+  /* add any extra streams */
   for (i = 0; i < webrtc->priv->transceivers->len; i++) {
     GstWebRTCRTPTransceiver *trans;
     GstSDPMedia media = { 0, };
-    gchar *ufrag, *pwd;
-    gboolean bundle_only = bundled_mids
-        && webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE
-        && i != 0;
 
     trans =
         g_array_index (webrtc->priv->transceivers, GstWebRTCRTPTransceiver *,
         i);
 
+    /* don't add transceivers twice */
+    if (g_list_find (seen_transceivers, trans))
+      continue;
+
+    /* don't add stopped transceivers */
+    if (trans->stopped)
+      continue;
+
     gst_sdp_media_init (&media);
-    /* mandated by JSEP */
-    gst_sdp_media_add_attribute (&media, "setup", "actpass");
 
-    /* FIXME: only needed when restarting ICE */
     if (webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_NONE) {
       reserved_pts = g_array_new (FALSE, FALSE, sizeof (guint));
-      _generate_ice_credentials (&ufrag, &pwd);
-    } else {
-      ufrag = g_strdup (bundle_ufrag);
-      pwd = g_strdup (bundle_pwd);
     }
 
-    gst_sdp_media_add_attribute (&media, "ice-ufrag", ufrag);
-    gst_sdp_media_add_attribute (&media, "ice-pwd", pwd);
-    g_free (ufrag);
-    g_free (pwd);
-
-    g_assert (reserved_pts != NULL);
+    GST_LOG_OBJECT (webrtc, "adding transceiver %" GST_PTR_FORMAT " at media "
+        "index %u", trans, media_idx);
 
     if (sdp_media_from_transceiver (webrtc, &media, trans,
-            GST_WEBRTC_SDP_TYPE_OFFER, i, bundled_mids, 0, bundle_only,
-            reserved_pts)) {
-      if (bundled_mids) {
-        const gchar *mid = gst_sdp_media_get_attribute_val (&media, "mid");
-
-        g_assert (mid);
-        g_string_append_printf (bundled_mids, " %s", mid);
-      }
+            GST_WEBRTC_SDP_TYPE_OFFER, media_idx, bundled_mids, 0, bundle_ufrag,
+            bundle_pwd, reserved_pts)) {
       gst_sdp_message_add_media (ret, &media);
+      media_idx++;
     } else {
       gst_sdp_media_uninit (&media);
     }
@@ -2217,62 +2502,28 @@ _create_offer_task (GstWebRTCBin * webrtc, const GstStructure * options)
     if (webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_NONE) {
       g_array_free (reserved_pts, TRUE);
     }
+    seen_transceivers = g_list_prepend (seen_transceivers, trans);
   }
 
   if (webrtc->bundle_policy != GST_WEBRTC_BUNDLE_POLICY_NONE) {
     g_array_free (reserved_pts, TRUE);
   }
 
-  /* add data channel support */
-  if (webrtc->priv->data_channels->len > 0) {
+  /* add a data channel if exists and not renegotiated */
+  if (_message_get_datachannel_index (ret) == G_MAXUINT) {
     GstSDPMedia media = { 0, };
-    gchar *ufrag, *pwd, *sdp_mid;
-    gboolean bundle_only = bundled_mids
-        && webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE
-        && webrtc->priv->transceivers->len != 0;
-
     gst_sdp_media_init (&media);
-    /* mandated by JSEP */
-    gst_sdp_media_add_attribute (&media, "setup", "actpass");
-
-    /* FIXME: only needed when restarting ICE */
-    if (webrtc->bundle_policy == GST_WEBRTC_BUNDLE_POLICY_NONE) {
-      _generate_ice_credentials (&ufrag, &pwd);
+    if (_add_data_channel_offer (webrtc, ret, &media, bundled_mids, 0,
+            bundle_ufrag, bundle_pwd)) {
+      gst_sdp_message_add_media (ret, &media);
+      media_idx++;
     } else {
-      ufrag = g_strdup (bundle_ufrag);
-      pwd = g_strdup (bundle_pwd);
+      gst_sdp_media_uninit (&media);
     }
-    gst_sdp_media_add_attribute (&media, "ice-ufrag", ufrag);
-    gst_sdp_media_add_attribute (&media, "ice-pwd", pwd);
-    g_free (ufrag);
-    g_free (pwd);
-
-    gst_sdp_media_set_media (&media, "application");
-    gst_sdp_media_set_port_info (&media, bundle_only ? 0 : 9, 0);
-    gst_sdp_media_set_proto (&media, "UDP/DTLS/SCTP");
-    gst_sdp_media_add_connection (&media, "IN", "IP4", "0.0.0.0", 0, 0);
-    gst_sdp_media_add_format (&media, "webrtc-datachannel");
-
-    if (bundle_only)
-      gst_sdp_media_add_attribute (&media, "bundle-only", NULL);
-
-    sdp_mid = g_strdup_printf ("%s%u", gst_sdp_media_get_media (&media),
-        webrtc->priv->media_counter++);
-    gst_sdp_media_add_attribute (&media, "mid", sdp_mid);
-    if (bundled_mids)
-      g_string_append_printf (bundled_mids, " %s", sdp_mid);
-    g_free (sdp_mid);
-
-    /* FIXME: negotiate this properly */
-    gst_sdp_media_add_attribute (&media, "sctp-port", "5000");
-
-    _get_or_create_data_channel_transports (webrtc,
-        bundled_mids ? 0 : webrtc->priv->transceivers->len);
-    _add_fingerprint_to_media (webrtc->priv->sctp_transport->transport, &media);
-
-    gst_sdp_message_add_media (ret, &media);
   }
 
+  g_assert (media_idx == gst_sdp_message_medias_len (ret));
+
   if (bundled_mids) {
     gchar *mids = g_string_free (bundled_mids, FALSE);
 
@@ -2291,6 +2542,8 @@ _create_offer_task (GstWebRTCBin * webrtc, const GstStructure * options)
   /* XXX: only true for the initial offerer */
   g_object_set (webrtc->priv->ice, "controller", TRUE, NULL);
 
+  g_list_free (seen_transceivers);
+
   return ret;
 }
 
@@ -2417,6 +2670,7 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
   gchar *bundle_ufrag = NULL;
   gchar *bundle_pwd = NULL;
   GList *seen_transceivers = NULL;
+  GstSDPMessage *last_answer = _get_latest_answer (webrtc);
 
   if (!webrtc->pending_remote_description) {
     GST_ERROR_OBJECT (webrtc,
@@ -2428,6 +2682,9 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
     goto out;
 
   if (bundled) {
+    GStrv last_bundle = NULL;
+    guint bundle_media_index;
+
     if (!_get_bundle_index (pending_remote->sdp, bundled, &bundle_idx)) {
       GST_ERROR_OBJECT (webrtc, "Bundle tag is %s but no media found matching",
           bundled[0]);
@@ -2438,18 +2695,28 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
       bundled_mids = g_string_new ("BUNDLE");
     }
 
-    _generate_ice_credentials (&bundle_ufrag, &bundle_pwd);
+    if (last_answer && _parse_bundle (last_answer, &last_bundle)
+        && last_bundle && last_bundle[0]
+        && _get_bundle_index (last_answer, last_bundle, &bundle_media_index)) {
+      bundle_ufrag =
+          g_strdup (_media_get_ice_ufrag (last_answer, bundle_media_index));
+      bundle_pwd =
+          g_strdup (_media_get_ice_pwd (last_answer, bundle_media_index));
+    } else {
+      _generate_ice_credentials (&bundle_ufrag, &bundle_pwd);
+    }
+
+    g_strfreev (last_bundle);
   }
 
   gst_sdp_message_new (&ret);
 
-  /* FIXME: session id and version need special handling depending on the state we're in */
   gst_sdp_message_set_version (ret, "0");
   {
     const GstSDPOrigin *offer_origin =
         gst_sdp_message_get_origin (pending_remote->sdp);
-    gst_sdp_message_set_origin (ret, "-", offer_origin->sess_id, "0", "IN",
-        "IP4", "0.0.0.0");
+    gst_sdp_message_set_origin (ret, "-", offer_origin->sess_id,
+        offer_origin->sess_version, "IN", "IP4", "0.0.0.0");
   }
   gst_sdp_message_set_session_name (ret, "-");
 
@@ -2465,17 +2732,10 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
   for (i = 0; i < gst_sdp_message_medias_len (pending_remote->sdp); i++) {
     GstSDPMedia *media = NULL;
     GstSDPMedia *offer_media;
-    GstWebRTCRTPTransceiver *rtp_trans = NULL;
-    WebRTCTransceiver *trans = NULL;
-    GstWebRTCRTPTransceiverDirection offer_dir, answer_dir;
     GstWebRTCDTLSSetup offer_setup, answer_setup;
-    GstCaps *offer_caps, *answer_caps = NULL;
-    guint j;
-    guint k;
-    gint target_pt = -1;
-    gint original_target_pt = -1;
-    guint target_ssrc = 0;
+    guint j, k;
     gboolean bundle_only;
+    const gchar *mid;
 
     offer_media =
         (GstSDPMedia *) gst_sdp_message_get_media (pending_remote->sdp, i);
@@ -2489,13 +2749,19 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
     gst_sdp_media_add_connection (media, "IN", "IP4", "0.0.0.0", 0, 0);
 
     {
-      /* FIXME: only needed when restarting ICE */
       gchar *ufrag, *pwd;
-      if (!bundled) {
-        _generate_ice_credentials (&ufrag, &pwd);
+
+      /* FIXME: deal with ICE restarts */
+      if (last_answer && i < gst_sdp_message_medias_len (last_answer)) {
+        ufrag = g_strdup (_media_get_ice_ufrag (last_answer, i));
+        pwd = g_strdup (_media_get_ice_pwd (last_answer, i));
       } else {
-        ufrag = g_strdup (bundle_ufrag);
-        pwd = g_strdup (bundle_pwd);
+        if (!bundled) {
+          _generate_ice_credentials (&ufrag, &pwd);
+        } else {
+          ufrag = g_strdup (bundle_ufrag);
+          pwd = g_strdup (bundle_pwd);
+        }
       }
       gst_sdp_media_add_attribute (media, "ice-ufrag", ufrag);
       gst_sdp_media_add_attribute (media, "ice-pwd", pwd);
@@ -2514,6 +2780,10 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
       }
     }
 
+    mid = gst_sdp_media_get_attribute_val (media, "mid");
+    /* XXX: not strictly required but a lot of functionality requires a mid */
+    g_assert (mid);
+
     /* set the a=setup: attribute */
     offer_setup = _get_dtls_setup_from_media (offer_media);
     answer_setup = _intersect_dtls_setup (offer_setup);
@@ -2560,8 +2830,6 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
           bundled_mids ? bundle_idx : i);
 
       if (bundled_mids) {
-        const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
-
         g_assert (mid);
         g_string_append_printf (bundled_mids, " %s", mid);
       }
@@ -2570,69 +2838,97 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
           media);
     } else if (g_strcmp0 (gst_sdp_media_get_media (offer_media), "audio") == 0
         || g_strcmp0 (gst_sdp_media_get_media (offer_media), "video") == 0) {
-      gst_sdp_media_set_proto (media, "UDP/TLS/RTP/SAVPF");
-
-      offer_caps = gst_caps_new_empty ();
-      for (j = 0; j < gst_sdp_media_formats_len (offer_media); j++) {
-        guint pt = atoi (gst_sdp_media_get_format (offer_media, j));
-        GstCaps *caps;
+      GstCaps *offer_caps, *answer_caps = NULL;
+      GstWebRTCRTPTransceiver *rtp_trans = NULL;
+      WebRTCTransceiver *trans = NULL;
+      GstWebRTCRTPTransceiverDirection offer_dir, answer_dir;
+      gint target_pt = -1;
+      gint original_target_pt = -1;
+      guint target_ssrc = 0;
 
-        caps = gst_sdp_media_get_caps_from_media (offer_media, pt);
-
-        /* gst_sdp_media_get_caps_from_media() produces caps with name
-         * "application/x-unknown" which will fail intersection with
-         * "application/x-rtp" caps so mangle the returns caps to have the
-         * correct name here */
-        for (k = 0; k < gst_caps_get_size (caps); k++) {
-          GstStructure *s = gst_caps_get_structure (caps, k);
-          gst_structure_set_name (s, "application/x-rtp");
-        }
-
-        gst_caps_append (offer_caps, caps);
-      }
-
-      for (j = 0; j < webrtc->priv->transceivers->len; j++) {
-        GstCaps *trans_caps;
+      gst_sdp_media_set_proto (media, "UDP/TLS/RTP/SAVPF");
+      offer_caps = _rtp_caps_from_media (offer_media);
+
+      if (last_answer && i < gst_sdp_message_medias_len (last_answer)
+          && (rtp_trans =
+              _find_transceiver (webrtc, mid,
+                  (FindTransceiverFunc) match_for_mid))) {
+        const GstSDPMedia *last_media =
+            gst_sdp_message_get_media (last_answer, i);
+        const gchar *last_mid =
+            gst_sdp_media_get_attribute_val (last_media, "mid");
+
+        /* FIXME: assumes no shenanigans with recycling transceivers */
+        g_assert (g_strcmp0 (mid, last_mid) == 0);
+
+        if (!answer_caps
+            && (rtp_trans->direction ==
+                GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV
+                || rtp_trans->direction ==
+                GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY))
+          answer_caps =
+              _find_codec_preferences (webrtc, rtp_trans, GST_PAD_SINK, i);
+        if (!answer_caps
+            && (rtp_trans->direction ==
+                GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV
+                || rtp_trans->direction ==
+                GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY))
+          answer_caps =
+              _find_codec_preferences (webrtc, rtp_trans, GST_PAD_SRC, i);
+        if (!answer_caps)
+          answer_caps = _rtp_caps_from_media (last_media);
+
+        /* XXX: In theory we're meant to use the sendrecv formats for the
+         * inactive direction however we don't know what that may be and would
+         * require asking outside what it expects to possibly send later */
+
+        GST_LOG_OBJECT (webrtc, "Found existing previously negotiated "
+            "transceiver %" GST_PTR_FORMAT " from mid %s for mline %u "
+            "using caps %" GST_PTR_FORMAT, rtp_trans, mid, i, answer_caps);
+      } else {
+        for (j = 0; j < webrtc->priv->transceivers->len; j++) {
+          GstCaps *trans_caps;
 
-        rtp_trans =
-            g_array_index (webrtc->priv->transceivers,
-            GstWebRTCRTPTransceiver *, j);
+          rtp_trans =
+              g_array_index (webrtc->priv->transceivers,
+              GstWebRTCRTPTransceiver *, j);
 
-        if (g_list_find (seen_transceivers, rtp_trans)) {
-          /* Don't double allocate a transceiver to multiple mlines */
-          rtp_trans = NULL;
-          continue;
-        }
+          if (g_list_find (seen_transceivers, rtp_trans)) {
+            /* Don't double allocate a transceiver to multiple mlines */
+            rtp_trans = NULL;
+            continue;
+          }
 
-        trans_caps =
-            _find_codec_preferences (webrtc, rtp_trans, GST_PAD_SINK, j);
-
-        GST_TRACE_OBJECT (webrtc, "trying to compare %" GST_PTR_FORMAT
-            " and %" GST_PTR_FORMAT, offer_caps, trans_caps);
-
-        /* FIXME: technically this is a little overreaching as some fields we
-         * we can deal with not having and/or we may have unrecognized fields
-         * that we cannot actually support */
-        if (trans_caps) {
-          answer_caps = gst_caps_intersect (offer_caps, trans_caps);
-          if (answer_caps && !gst_caps_is_empty (answer_caps)) {
-            GST_LOG_OBJECT (webrtc,
-                "found compatible transceiver %" GST_PTR_FORMAT
-                " for offer media %u", rtp_trans, i);
-            if (trans_caps)
-              gst_caps_unref (trans_caps);
-            break;
-          } else {
-            if (answer_caps) {
-              gst_caps_unref (answer_caps);
-              answer_caps = NULL;
+          trans_caps =
+              _find_codec_preferences (webrtc, rtp_trans, GST_PAD_SINK, j);
+
+          GST_TRACE_OBJECT (webrtc, "trying to compare %" GST_PTR_FORMAT
+              " and %" GST_PTR_FORMAT, offer_caps, trans_caps);
+
+          /* FIXME: technically this is a little overreaching as some fields we
+           * we can deal with not having and/or we may have unrecognized fields
+           * that we cannot actually support */
+          if (trans_caps) {
+            answer_caps = gst_caps_intersect (offer_caps, trans_caps);
+            if (answer_caps && !gst_caps_is_empty (answer_caps)) {
+              GST_LOG_OBJECT (webrtc,
+                  "found compatible transceiver %" GST_PTR_FORMAT
+                  " for offer media %u", rtp_trans, i);
+              if (trans_caps)
+                gst_caps_unref (trans_caps);
+              break;
+            } else {
+              if (answer_caps) {
+                gst_caps_unref (answer_caps);
+                answer_caps = NULL;
+              }
+              if (trans_caps)
+                gst_caps_unref (trans_caps);
+              rtp_trans = NULL;
             }
-            if (trans_caps)
-              gst_caps_unref (trans_caps);
+          } else {
             rtp_trans = NULL;
           }
-        } else {
-          rtp_trans = NULL;
         }
       }
 
@@ -2652,6 +2948,9 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
       if (!rtp_trans) {
         trans = _create_webrtc_transceiver (webrtc, answer_dir, i);
         rtp_trans = GST_WEBRTC_RTP_TRANSCEIVER (trans);
+
+        GST_LOG_OBJECT (webrtc, "Created new transceiver %" GST_PTR_FORMAT
+            " for mline %u", trans, i);
       } else {
         trans = WEBRTC_TRANSCEIVER (rtp_trans);
       }
@@ -2699,18 +2998,19 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
       if (!trans->stream) {
         TransportStream *item;
 
-        if (bundled_mids) {
-          const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
-          item = _get_or_create_transport_stream (webrtc, bundle_idx, FALSE);
-
-          g_assert (mid);
-          g_string_append_printf (bundled_mids, " %s", mid);
-        } else {
-          item = _get_or_create_transport_stream (webrtc, i, FALSE);
-        }
+        item =
+            _get_or_create_transport_stream (webrtc,
+            bundled_mids ? bundle_idx : i, FALSE);
         webrtc_transceiver_set_transport (trans, item);
       }
 
+      if (bundled_mids) {
+        const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid");
+
+        g_assert (mid);
+        g_string_append_printf (bundled_mids, " %s", mid);
+      }
+
       /* set the a=fingerprint: for this transport */
       _add_fingerprint_to_media (trans->stream->transport, media);
 
@@ -2750,8 +3050,7 @@ _create_answer_task (GstWebRTCBin * webrtc, const GstStructure * options)
   g_object_set (webrtc->priv->ice, "controller", FALSE, NULL);
 
 out:
-  if (bundled)
-    g_strfreev (bundled);
+  g_strfreev (bundled);
 
   g_list_free (seen_transceivers);
 
@@ -2982,7 +3281,14 @@ _connect_output_stream (GstWebRTCBin * webrtc,
  */
   gchar *pad_name;
 
-  GST_INFO_OBJECT (webrtc, "linking output stream %u", session_id);
+  if (stream->output_connected) {
+    GST_DEBUG_OBJECT (webrtc, "stream %" GST_PTR_FORMAT " is already "
+        "connected to rtpbin.  Not connecting", stream);
+    return;
+  }
+
+  GST_INFO_OBJECT (webrtc, "linking output stream %u %" GST_PTR_FORMAT,
+      session_id, stream);
 
   pad_name = g_strdup_printf ("recv_rtp_sink_%u", session_id);
   if (!gst_element_link_pads (GST_ELEMENT (stream->receive_bin),
@@ -2994,6 +3300,8 @@ _connect_output_stream (GstWebRTCBin * webrtc,
 
   /* The webrtcbin src_%u output pads will be created when rtpbin receives
    * data on that stream in on_rtpbin_pad_added() */
+
+  stream->output_connected = TRUE;
 }
 
 typedef struct
@@ -3037,6 +3345,38 @@ _filter_sdp_fields (GQuark field_id, const GValue * value,
 }
 
 static void
+_set_rtx_ptmap_from_stream (GstWebRTCBin * webrtc, TransportStream * stream)
+{
+  gint *rtx_pt;
+  gsize rtx_count;
+
+  rtx_pt = transport_stream_get_all_pt (stream, "RTX", &rtx_count);
+  GST_LOG_OBJECT (stream, "have %" G_GSIZE_FORMAT " rtx payloads", rtx_count);
+  if (rtx_pt) {
+    GstStructure *pt_map = gst_structure_new_empty ("application/x-rtp-pt-map");
+    gsize i;
+
+    for (i = 0; i < rtx_count; i++) {
+      GstCaps *rtx_caps = transport_stream_get_caps_for_pt (stream, rtx_pt[i]);
+      const GstStructure *s = gst_caps_get_structure (rtx_caps, 0);
+      const gchar *apt = gst_structure_get_string (s, "apt");
+
+      GST_LOG_OBJECT (stream, "setting rtx mapping: %s -> %u", apt, rtx_pt[i]);
+      gst_structure_set (pt_map, apt, G_TYPE_UINT, rtx_pt[i], NULL);
+    }
+
+    GST_DEBUG_OBJECT (stream, "setting payload map on %" GST_PTR_FORMAT " : %"
+        GST_PTR_FORMAT " and %" GST_PTR_FORMAT, stream->rtxreceive,
+        stream->rtxsend, pt_map);
+
+    if (stream->rtxreceive)
+      g_object_set (stream->rtxreceive, "payload-type-map", pt_map, NULL);
+    if (stream->rtxsend)
+      g_object_set (stream->rtxsend, "payload-type-map", pt_map, NULL);
+  }
+}
+
+static void
 _update_transport_ptmap_from_media (GstWebRTCBin * webrtc,
     TransportStream * stream, const GstSDPMessage * sdp, guint media_idx)
 {
@@ -3109,7 +3449,7 @@ static void
 _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
     const GstSDPMessage * sdp, guint media_idx,
     TransportStream * stream, GstWebRTCRTPTransceiver * rtp_trans,
-    GStrv bundled, guint bundle_idx, gboolean * should_connect_bundle_stream)
+    GStrv bundled, guint bundle_idx)
 {
   WebRTCTransceiver *trans = WEBRTC_TRANSCEIVER (rtp_trans);
   GstWebRTCRTPTransceiverDirection prev_dir = rtp_trans->current_direction;
@@ -3117,6 +3457,7 @@ _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
   const GstSDPMedia *media = gst_sdp_message_get_media (sdp, media_idx);
   GstWebRTCDTLSSetup new_setup;
   gboolean new_rtcp_mux, new_rtcp_rsize;
+  ReceiveState receive_state = 0;
   int i;
 
   rtp_trans->mline = media_idx;
@@ -3156,6 +3497,7 @@ _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
       return;
 
     if (prev_dir != GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_NONE
+        && new_dir != GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE
         && prev_dir != new_dir) {
       GST_FIXME_OBJECT (webrtc, "implement transceiver direction changes");
       return;
@@ -3182,10 +3524,28 @@ _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
   }
 
   if (new_dir != prev_dir) {
-    ReceiveState receive_state = 0;
-
     GST_TRACE_OBJECT (webrtc, "transceiver direction change");
 
+    if (new_dir == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE) {
+      GstWebRTCBinPad *pad;
+
+      pad = _find_pad_for_mline (webrtc, GST_PAD_SRC, media_idx);
+      if (pad) {
+        GstPad *target = gst_ghost_pad_get_target (GST_GHOST_PAD (pad));
+        if (target) {
+          GstPad *peer = gst_pad_get_peer (target);
+          if (peer) {
+            gst_pad_send_event (peer, gst_event_new_eos ());
+            gst_object_unref (peer);
+          }
+          gst_object_unref (target);
+        }
+        gst_object_unref (pad);
+      }
+
+      /* XXX: send eos event up the sink pad as well? */
+    }
+
     if (new_dir == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY ||
         new_dir == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV) {
       GstWebRTCBinPad *pad =
@@ -3230,10 +3590,8 @@ _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
           webrtc_transceiver_set_transport (trans, item);
         }
 
-        if (!bundled)
-          _connect_output_stream (webrtc, trans->stream, media_idx);
-        else
-          *should_connect_bundle_stream = TRUE;
+        _connect_output_stream (webrtc, trans->stream,
+            bundled ? bundle_idx : media_idx);
         /* delay adding the pad until rtpbin creates the recv output pad
          * to ghost to so queries/events travel through the pipeline correctly
          * as soon as the pad is added */
@@ -3245,20 +3603,25 @@ _update_transceiver_from_sdp_media (GstWebRTCBin * webrtc,
       receive_state = RECEIVE_STATE_DROP;
     }
 
-    if (!bundled || bundle_idx == media_idx)
-      g_object_set (stream, "dtls-client",
-          new_setup == GST_WEBRTC_DTLS_SETUP_ACTIVE, NULL);
-
-    /* Must be after setting the "dtls-client" so that data is not pushed into
-     * the dtlssrtp elements before the ssl direction has been set which will
-     * throw SSL errors */
-    if (receive_state > 0)
-      transport_receive_bin_set_receive_state (stream->receive_bin,
-          receive_state);
-
     rtp_trans->mline = media_idx;
     rtp_trans->current_direction = new_dir;
   }
+
+  if (!bundled || bundle_idx == media_idx) {
+    if (stream->rtxsend || stream->rtxreceive) {
+      _set_rtx_ptmap_from_stream (webrtc, stream);
+    }
+
+    g_object_set (stream, "dtls-client",
+        new_setup == GST_WEBRTC_DTLS_SETUP_ACTIVE, NULL);
+  }
+
+  /* Must be after setting the "dtls-client" so that data is not pushed into
+   * the dtlssrtp elements before the ssl direction has been set which will
+   * throw SSL errors */
+  if (receive_state > 0)
+    transport_receive_bin_set_receive_state (stream->receive_bin,
+        receive_state);
 }
 
 /* must be called with the pc lock held */
@@ -3441,7 +3804,6 @@ _update_transceivers_from_sdp (GstWebRTCBin * webrtc, SDPSource source,
   gboolean ret = FALSE;
   GStrv bundled = NULL;
   guint bundle_idx = 0;
-  gboolean should_connect_bundle_stream = FALSE;
   TransportStream *bundle_stream = NULL;
 
   if (!_parse_bundle (sdp->sdp, &bundled))
@@ -3501,7 +3863,7 @@ _update_transceivers_from_sdp (GstWebRTCBin * webrtc, SDPSource source,
           g_strcmp0 (gst_sdp_media_get_media (media), "video") == 0) {
         if (trans) {
           _update_transceiver_from_sdp_media (webrtc, sdp->sdp, i, stream,
-              trans, bundled, bundle_idx, &should_connect_bundle_stream);
+              trans, bundled, bundle_idx);
         } else {
           trans = _find_transceiver (webrtc, NULL,
               (FindTransceiverFunc) _find_compatible_unassociated_transceiver);
@@ -3515,7 +3877,7 @@ _update_transceivers_from_sdp (GstWebRTCBin * webrtc, SDPSource source,
                     _get_direction_from_media (media), i));
           }
           _update_transceiver_from_sdp_media (webrtc, sdp->sdp, i, stream,
-              trans, bundled, bundle_idx, &should_connect_bundle_stream);
+              trans, bundled, bundle_idx);
         }
       } else if (_message_media_is_datachannel (sdp->sdp, i)) {
         _update_data_channel_from_sdp_media (webrtc, sdp->sdp, i, stream);
@@ -3525,16 +3887,11 @@ _update_transceivers_from_sdp (GstWebRTCBin * webrtc, SDPSource source,
     }
   }
 
-  if (should_connect_bundle_stream) {
-    g_assert (bundle_stream);
-    _connect_output_stream (webrtc, bundle_stream, bundle_idx);
-  }
-
   ret = TRUE;
 
 done:
-  if (bundled)
-    g_strfreev (bundled);
+  g_strfreev (bundled);
+
   return ret;
 }
 
@@ -3826,8 +4183,7 @@ _set_description_task (GstWebRTCBin * webrtc, struct set_description *sd)
   }
 
 out:
-  if (bundled)
-    g_strfreev (bundled);
+  g_strfreev (bundled);
 
   PC_UNLOCK (webrtc);
   gst_promise_reply (sd->promise, NULL);
@@ -4243,8 +4599,11 @@ gst_webrtc_bin_create_data_channel (GstWebRTCBin * webrtc, const gchar * label,
     _link_data_channel_to_sctp (webrtc, ret);
     if (webrtc->priv->sctp_transport &&
         webrtc->priv->sctp_transport->association_established
-        && !ret->negotiated)
+        && !ret->negotiated) {
       gst_webrtc_data_channel_start_negotiation (ret);
+    } else {
+      _update_need_negotiation (webrtc);
+    }
   }
 
   PC_UNLOCK (webrtc);
@@ -4268,6 +4627,7 @@ on_rtpbin_pad_added (GstElement * rtpbin, GstPad * new_pad,
     TransportStream *stream;
     GstWebRTCBinPad *pad;
     guint media_idx = 0;
+    gboolean found_ssrc = FALSE;
     guint i;
 
     if (sscanf (new_pad_name, "recv_rtp_src_%u_%u_%u", &session_id, &ssrc,
@@ -4287,10 +4647,15 @@ on_rtpbin_pad_added (GstElement * rtpbin, GstPad * new_pad,
           &g_array_index (stream->remote_ssrcmap, SsrcMapItem, i);
       if (item->ssrc == ssrc) {
         media_idx = item->media_idx;
+        found_ssrc = TRUE;
         break;
       }
     }
 
+    if (!found_ssrc) {
+      GST_WARNING_OBJECT (webrtc, "Could not find ssrc %u", ssrc);
+    }
+
     rtp_trans = _find_transceiver_for_mline (webrtc, media_idx);
     if (!rtp_trans)
       g_warn_if_reached ();
@@ -4351,7 +4716,8 @@ on_rtpbin_request_aux_sender (GstElement * rtpbin, guint session_id,
     GstWebRTCBin * webrtc)
 {
   TransportStream *stream;
-  GstStructure *pt_map = gst_structure_new_empty ("application/x-rtp-pt-map");
+  gboolean have_rtx = FALSE;
+  GstStructure *pt_map = NULL;
   GstElement *ret = NULL;
   GstWebRTCRTPTransceiver *trans;
 
@@ -4359,41 +4725,28 @@ on_rtpbin_request_aux_sender (GstElement * rtpbin, guint session_id,
   trans = _find_transceiver (webrtc, &session_id,
       (FindTransceiverFunc) transceiver_match_for_mline);
 
-  if (stream) {
-    guint i;
-
-    for (i = 0; i < stream->ptmap->len; i++) {
-      PtMapItem *item = &g_array_index (stream->ptmap, PtMapItem, i);
-      if (!gst_caps_is_empty (item->caps)) {
-        GstStructure *s = gst_caps_get_structure (item->caps, 0);
-        gint pt;
-        const gchar *apt_str = gst_structure_get_string (s, "apt");
-
-        if (!apt_str)
-          continue;
-
-        if (!g_strcmp0 (gst_structure_get_string (s, "encoding-name"), "RTX") &&
-            gst_structure_get_int (s, "payload", &pt)) {
-          gst_structure_set (pt_map, apt_str, G_TYPE_UINT, pt, NULL);
-        }
-      }
-    }
-  }
+  if (stream)
+    have_rtx = transport_stream_get_pt (stream, "RTX") != 0;
 
   GST_LOG_OBJECT (webrtc, "requesting aux sender for stream %" GST_PTR_FORMAT
       " with transport %" GST_PTR_FORMAT " and pt map %" GST_PTR_FORMAT, stream,
       trans, pt_map);
 
-  if (gst_structure_n_fields (pt_map)) {
+  if (have_rtx) {
     GstElement *rtx;
     GstPad *pad;
     gchar *name;
 
+    if (stream->rtxsend) {
+      GST_WARNING_OBJECT (webrtc, "rtprtxsend already created! rtpbin bug?!");
+      goto out;
+    }
+
     GST_INFO ("creating AUX sender");
     ret = gst_bin_new (NULL);
     rtx = gst_element_factory_make ("rtprtxsend", NULL);
-    g_object_set (rtx, "payload-type-map", pt_map, "max-size-packets", 500,
-        NULL);
+    g_object_set (rtx, "max-size-packets", 500, NULL);
+    _set_rtx_ptmap_from_stream (webrtc, stream);
 
     if (WEBRTC_TRANSCEIVER (trans)->local_rtx_ssrc_map)
       g_object_set (rtx, "ssrc-map",
@@ -4412,9 +4765,13 @@ on_rtpbin_request_aux_sender (GstElement * rtpbin, guint session_id,
     gst_element_add_pad (ret, gst_ghost_pad_new (name, pad));
     g_free (name);
     gst_object_unref (pad);
+
+    stream->rtxsend = gst_object_ref (rtx);
   }
 
-  gst_structure_free (pt_map);
+out:
+  if (pt_map)
+    gst_structure_free (pt_map);
 
   return ret;
 }
@@ -4437,28 +4794,27 @@ on_rtpbin_request_aux_receiver (GstElement * rtpbin, guint session_id,
     rtx_pt = transport_stream_get_pt (stream, "RTX");
   }
 
-  GST_LOG_OBJECT (webrtc, "requesting aux receiver for stream %" GST_PTR_FORMAT
-      " with pt red:%u rtx:%u", stream, red_pt, rtx_pt);
+  GST_LOG_OBJECT (webrtc, "requesting aux receiver for stream %" GST_PTR_FORMAT,
+      stream);
 
   if (red_pt || rtx_pt)
     ret = gst_bin_new (NULL);
 
   if (rtx_pt) {
-    GstCaps *rtx_caps = transport_stream_get_caps_for_pt (stream, rtx_pt);
-    GstElement *rtx = gst_element_factory_make ("rtprtxreceive", NULL);
-    GstStructure *pt_map;
-    const GstStructure *s = gst_caps_get_structure (rtx_caps, 0);
+    if (stream->rtxreceive) {
+      GST_WARNING_OBJECT (webrtc,
+          "rtprtxreceive already created! rtpbin bug?!");
+      goto error;
+    }
 
-    gst_bin_add (GST_BIN (ret), rtx);
+    stream->rtxreceive = gst_element_factory_make ("rtprtxreceive", NULL);
+    _set_rtx_ptmap_from_stream (webrtc, stream);
 
-    pt_map = gst_structure_new_empty ("application/x-rtp-pt-map");
-    gst_structure_set (pt_map, gst_structure_get_string (s, "apt"), G_TYPE_UINT,
-        rtx_pt, NULL);
-    g_object_set (rtx, "payload-type-map", pt_map, NULL);
+    gst_bin_add (GST_BIN (ret), stream->rtxreceive);
 
-    sinkpad = gst_element_get_static_pad (rtx, "sink");
+    sinkpad = gst_element_get_static_pad (stream->rtxreceive, "sink");
 
-    prev = rtx;
+    prev = gst_object_ref (stream->rtxreceive);
   }
 
   if (red_pt) {
@@ -4496,7 +4852,13 @@ on_rtpbin_request_aux_receiver (GstElement * rtpbin, guint session_id,
     gst_element_add_pad (ret, ghost);
   }
 
+out:
   return ret;
+
+error:
+  if (ret)
+    gst_object_unref (ret);
+  goto out;
 }
 
 static GstElement *
@@ -4795,7 +5157,9 @@ gst_webrtc_bin_change_state (GstElement * element, GstStateChange transition)
       if (!_have_nice_elements (webrtc) || !_have_dtls_elements (webrtc))
         return GST_STATE_CHANGE_FAILURE;
       _start_thread (webrtc);
+      PC_LOCK (webrtc);
       _update_need_negotiation (webrtc);
+      PC_UNLOCK (webrtc);
       break;
     }
     case GST_STATE_CHANGE_READY_TO_PAUSED:
@@ -4896,6 +5260,9 @@ gst_webrtc_bin_release_pad (GstElement * element, GstPad * pad)
   webrtc_pad->trans = NULL;
 
   _remove_pad (webrtc, webrtc_pad);
+  PC_LOCK (webrtc);
+  _update_need_negotiation (webrtc);
+  PC_UNLOCK (webrtc);
 }
 
 static void
index 84158e8..546ae6d 100644 (file)
@@ -131,6 +131,8 @@ struct _GstWebRTCBinPrivate
   /* count of the number of media streams we've offered for uniqueness */
   /* FIXME: overflow? */
   guint media_counter;
+  /* the number of times create_offer has been called for the version field */
+  guint offer_count;
 
   GstStructure *stats;
 };
index 01fa2dc..01261ae 100644 (file)
@@ -75,6 +75,36 @@ transport_stream_get_pt (TransportStream * stream, const gchar * encoding_name)
   return ret;
 }
 
+int *
+transport_stream_get_all_pt (TransportStream * stream,
+    const gchar * encoding_name, gsize * pt_len)
+{
+  guint i;
+  gsize ret_i = 0;
+  gsize ret_size = 8;
+  int *ret = NULL;
+
+  for (i = 0; i < stream->ptmap->len; i++) {
+    PtMapItem *item = &g_array_index (stream->ptmap, PtMapItem, i);
+    if (!gst_caps_is_empty (item->caps)) {
+      GstStructure *s = gst_caps_get_structure (item->caps, 0);
+      if (!g_strcmp0 (gst_structure_get_string (s, "encoding-name"),
+              encoding_name)) {
+        if (!ret)
+          ret = g_new0 (int, ret_size);
+        if (ret_i >= ret_size) {
+          ret_size *= 2;
+          ret = g_realloc_n (ret, ret_size, sizeof (int));
+        }
+        ret[ret_i++] = item->pt;
+      }
+    }
+  }
+
+  *pt_len = ret_i;
+  return ret;
+}
+
 static void
 transport_stream_set_property (GObject * object, guint prop_id,
     const GValue * value, GParamSpec * pspec)
@@ -152,6 +182,14 @@ transport_stream_dispose (GObject * object)
     gst_object_unref (stream->rtcp_transport);
   stream->rtcp_transport = NULL;
 
+  if (stream->rtxsend)
+    gst_object_unref (stream->rtxsend);
+  stream->rtxsend = NULL;
+
+  if (stream->rtxreceive)
+    gst_object_unref (stream->rtxreceive);
+  stream->rtxreceive = NULL;
+
   GST_OBJECT_PARENT (object) = NULL;
 
   G_OBJECT_CLASS (parent_class)->dispose (object);
index 8b90a94..97d4f21 100644 (file)
@@ -61,6 +61,10 @@ struct _TransportStream
 
   GArray                   *ptmap;                  /* array of PtMapItem's */
   GArray                   *remote_ssrcmap;         /* array of SsrcMapItem's */
+  gboolean                  output_connected;       /* whether receive bin is connected to rtpbin */
+
+  GstElement               *rtxsend;
+  GstElement               *rtxreceive;
 };
 
 struct _TransportStreamClass
@@ -72,6 +76,9 @@ TransportStream *       transport_stream_new        (GstWebRTCBin * webrtc,
                                                      guint session_id);
 int                     transport_stream_get_pt     (TransportStream * stream,
                                                      const gchar * encoding_name);
+int *                   transport_stream_get_all_pt (TransportStream * stream,
+                                                     const gchar * encoding_name,
+                                                     gsize * pt_len);
 GstCaps *               transport_stream_get_caps_for_pt    (TransportStream * stream,
                                                              guint pt);
 
index ac050bf..f2225ef 100644 (file)
@@ -21,6 +21,8 @@
 # include "config.h"
 #endif
 
+#include <stdlib.h>
+
 #include "utils.h"
 #include "gstwebrtcbin.h"
 
@@ -53,28 +55,48 @@ _find_pad_template (GstElement * element, GstPadDirection direction,
 }
 
 GstSDPMessage *
-_get_latest_sdp (GstWebRTCBin * webrtc)
+_get_latest_offer (GstWebRTCBin * webrtc)
 {
   if (webrtc->current_local_description &&
-      webrtc->current_local_description->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
+      webrtc->current_local_description->type == GST_WEBRTC_SDP_TYPE_OFFER) {
     return webrtc->current_local_description->sdp;
   }
   if (webrtc->current_remote_description &&
-      webrtc->current_remote_description->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
+      webrtc->current_remote_description->type == GST_WEBRTC_SDP_TYPE_OFFER) {
     return webrtc->current_remote_description->sdp;
   }
+
+  return NULL;
+}
+
+GstSDPMessage *
+_get_latest_answer (GstWebRTCBin * webrtc)
+{
   if (webrtc->current_local_description &&
-      webrtc->current_local_description->type == GST_WEBRTC_SDP_TYPE_OFFER) {
+      webrtc->current_local_description->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
     return webrtc->current_local_description->sdp;
   }
   if (webrtc->current_remote_description &&
-      webrtc->current_remote_description->type == GST_WEBRTC_SDP_TYPE_OFFER) {
+      webrtc->current_remote_description->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
     return webrtc->current_remote_description->sdp;
   }
 
   return NULL;
 }
 
+GstSDPMessage *
+_get_latest_sdp (GstWebRTCBin * webrtc)
+{
+  GstSDPMessage *ret = NULL;
+
+  if ((ret = _get_latest_answer (webrtc)))
+    return ret;
+  if ((ret = _get_latest_offer (webrtc)))
+    return ret;
+
+  return NULL;
+}
+
 struct pad_block *
 _create_pad_block (GstElement * element, GstPad * pad, gulong block_id,
     gpointer user_data, GDestroyNotify notify)
@@ -142,3 +164,31 @@ _g_checksum_to_webrtc_string (GChecksumType type)
       return NULL;
   }
 }
+
+GstCaps *
+_rtp_caps_from_media (const GstSDPMedia * media)
+{
+  GstCaps *ret;
+  int i, j;
+
+  ret = gst_caps_new_empty ();
+  for (i = 0; i < gst_sdp_media_formats_len (media); i++) {
+    guint pt = atoi (gst_sdp_media_get_format (media, i));
+    GstCaps *caps;
+
+    caps = gst_sdp_media_get_caps_from_media (media, pt);
+
+    /* gst_sdp_media_get_caps_from_media() produces caps with name
+     * "application/x-unknown" which will fail intersection with
+     * "application/x-rtp" caps so mangle the returns caps to have the
+     * correct name here */
+    for (j = 0; j < gst_caps_get_size (caps); j++) {
+      GstStructure *s = gst_caps_get_structure (caps, j);
+      gst_structure_set_name (s, "application/x-rtp");
+    }
+
+    gst_caps_append (ret, caps);
+  }
+
+  return ret;
+}
index 8472647..ee56d3e 100644 (file)
@@ -47,6 +47,8 @@ GstPadTemplate *        _find_pad_template          (GstElement * element,
                                                      const gchar * name);
 
 GstSDPMessage *         _get_latest_sdp             (GstWebRTCBin * webrtc);
+GstSDPMessage *         _get_latest_offer           (GstWebRTCBin * webrtc);
+GstSDPMessage *         _get_latest_answer          (GstWebRTCBin * webrtc);
 
 GstWebRTCICEStream *    _find_ice_stream_for_session            (GstWebRTCBin * webrtc,
                                                                  guint session_id);
@@ -74,6 +76,8 @@ G_GNUC_INTERNAL
 gchar *                 _enum_value_to_string       (GType type, guint value);
 G_GNUC_INTERNAL
 const gchar *           _g_checksum_to_webrtc_string (GChecksumType type);
+G_GNUC_INTERNAL
+GstCaps *               _rtp_caps_from_media        (const GstSDPMedia * media);
 
 G_END_DECLS
 
index cad0540..56ecc79 100644 (file)
@@ -212,7 +212,7 @@ _media_has_mid (const GstSDPMedia * media, guint media_idx, GError ** error)
   return TRUE;
 }
 
-static const gchar *
+const gchar *
 _media_get_ice_ufrag (const GstSDPMessage * msg, guint media_idx)
 {
   const gchar *ice_ufrag;
@@ -227,7 +227,7 @@ _media_get_ice_ufrag (const GstSDPMessage * msg, guint media_idx)
   return ice_ufrag;
 }
 
-static const gchar *
+const gchar *
 _media_get_ice_pwd (const GstSDPMessage * msg, guint media_idx)
 {
   const gchar *ice_pwd;
@@ -437,11 +437,13 @@ _media_replace_direction (GstSDPMedia * media,
 
     if (g_strcmp0 (attr->key, "sendonly") == 0
         || g_strcmp0 (attr->key, "sendrecv") == 0
-        || g_strcmp0 (attr->key, "recvonly") == 0) {
+        || g_strcmp0 (attr->key, "recvonly") == 0
+        || g_strcmp0 (attr->key, "inactive") == 0) {
       GstSDPAttribute new_attr = { 0, };
       GST_TRACE ("replace %s with %s", attr->key, dir_str);
       gst_sdp_attribute_set (&new_attr, dir_str, "");
       gst_sdp_media_replace_attribute (media, i, &new_attr);
+      g_free (dir_str);
       return;
     }
   }
@@ -768,6 +770,21 @@ _message_media_is_datachannel (const GstSDPMessage * msg, guint media_id)
   return TRUE;
 }
 
+guint
+_message_get_datachannel_index (const GstSDPMessage * msg)
+{
+  guint i;
+
+  for (i = 0; i < gst_sdp_message_medias_len (msg); i++) {
+    if (_message_media_is_datachannel (msg, i)) {
+      g_assert (i < G_MAXUINT);
+      return i;
+    }
+  }
+
+  return G_MAXUINT;
+}
+
 void
 _get_ice_credentials_from_sdp_media (const GstSDPMessage * sdp, guint media_idx,
     gchar ** ufrag, gchar ** pwd)
@@ -833,6 +850,8 @@ _parse_bundle (GstSDPMessage * sdp, GStrv * bundled)
     if (!(*bundled)[0]) {
       GST_ERROR ("Invalid format for BUNDLE group, expected at least "
           "one mid (%s)", group);
+      g_strfreev (*bundled);
+      *bundled = NULL;
       goto done;
     }
   } else {
index a9a86fc..7620091 100644 (file)
@@ -89,6 +89,8 @@ void                                _get_ice_credentials_from_sdp_media     (con
 G_GNUC_INTERNAL
 gboolean                            _message_media_is_datachannel           (const GstSDPMessage * msg,
                                                                              guint media_id);
+G_GNUC_INTERNAL
+guint                               _message_get_datachannel_index          (const GstSDPMessage * msg);
 
 G_GNUC_INTERNAL
 gboolean                            _get_bundle_index                       (GstSDPMessage * sdp,
@@ -98,4 +100,11 @@ G_GNUC_INTERNAL
 gboolean                            _parse_bundle                           (GstSDPMessage * sdp,
                                                                              GStrv * bundled);
 
+G_GNUC_INTERNAL
+const gchar *                       _media_get_ice_pwd                  (const GstSDPMessage * msg,
+                                                                             guint media_idx);
+G_GNUC_INTERNAL
+const gchar *                       _media_get_ice_ufrag                (const GstSDPMessage * msg,
+                                                                             guint media_idx);
+
 #endif /* __WEBRTC_UTILS_H__ */
index 64030ac..b373931 100644 (file)
 #include "utils.h"
 #include "webrtctransceiver.h"
 
+#define GST_CAT_DEFAULT webrtc_transceiver_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
 #define webrtc_transceiver_parent_class parent_class
-G_DEFINE_TYPE (WebRTCTransceiver, webrtc_transceiver,
-    GST_TYPE_WEBRTC_RTP_TRANSCEIVER);
+G_DEFINE_TYPE_WITH_CODE (WebRTCTransceiver, webrtc_transceiver,
+    GST_TYPE_WEBRTC_RTP_TRANSCEIVER,
+    GST_DEBUG_CATEGORY_INIT (webrtc_transceiver_debug,
+        "webrtctransceiver", 0, "webrtctransceiver"););
 
 #define DEFAULT_FEC_TYPE GST_WEBRTC_FEC_TYPE_NONE
 #define DEFAULT_DO_NACK FALSE
@@ -172,6 +177,8 @@ 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);
+
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
 
index f1e338f..c037304 100644 (file)
@@ -44,6 +44,8 @@ struct _WebRTCTransceiver
   GstWebRTCFECType         fec_type;
   guint                    fec_percentage;
   gboolean                 do_nack;
+
+  GstCaps                  *last_configured_caps;
 };
 
 struct _WebRTCTransceiverClass
index 332c1bd..9fa884e 100644 (file)
@@ -39,7 +39,7 @@ GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
 G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GstWebRTCRTPTransceiver,
     gst_webrtc_rtp_transceiver, GST_TYPE_OBJECT,
     GST_DEBUG_CATEGORY_INIT (gst_webrtc_rtp_transceiver_debug,
-        "webrtctransceiver", 0, "webrtctransceiver");
+        "webrtcrtptransceiver", 0, "webrtcrtptransceiver");
     );
 
 enum
index a9f52d6..15636ae 100644 (file)
@@ -41,6 +41,8 @@
 #define TEST_GET_OFFEROR(t) (TEST_IS_OFFER_ELEMENT(t, t->webrtc1) ? (t)->webrtc1 : t->webrtc2)
 #define TEST_GET_ANSWERER(t) (TEST_IS_OFFER_ELEMENT(t, t->webrtc1) ? (t)->webrtc2 : t->webrtc1)
 
+#define TEST_SDP_IS_LOCAL(t, e, d) ((TEST_IS_OFFER_ELEMENT (t, e) ^ ((d)->type == GST_WEBRTC_SDP_TYPE_OFFER)) == 0)
+
 typedef enum
 {
   STATE_NEW,
@@ -626,12 +628,12 @@ _pad_added_fakesink (struct test_webrtc *t, GstElement * element,
 }
 
 static void
-_count_num_sdp_media (struct test_webrtc *t, GstElement * element,
-    GstWebRTCSessionDescription * desc, gpointer user_data)
+on_negotiation_needed_hit (struct test_webrtc *t, GstElement * element,
+    gpointer user_data)
 {
-  guint expected = GPOINTER_TO_UINT (user_data);
+  guint *flag = (guint *) user_data;
 
-  fail_unless_equals_int (gst_sdp_message_medias_len (desc->sdp), expected);
+  *flag = 1;
 }
 
 typedef void (*ValidateSDPFunc) (struct test_webrtc * t, GstElement * element,
@@ -645,6 +647,9 @@ struct validate_sdp
   struct validate_sdp *next;
 };
 
+#define VAL_SDP_INIT(name,func,data,next) \
+    struct validate_sdp name = { func, data, next }
+
 static GstWebRTCSessionDescription *
 _check_validate_sdp (struct test_webrtc *t, GstElement * element,
     GstPromise * promise, gpointer user_data)
@@ -698,13 +703,20 @@ test_validate_sdp (struct test_webrtc *t, struct validate_sdp *offer,
   fail_unless (t->state == STATE_ANSWER_CREATED);
 }
 
+static void
+_count_num_sdp_media (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  guint expected = GPOINTER_TO_UINT (user_data);
+
+  fail_unless_equals_int (gst_sdp_message_medias_len (desc->sdp), expected);
+}
+
 GST_START_TEST (test_sdp_no_media)
 {
   struct test_webrtc *t = test_webrtc_new ();
-  struct validate_sdp offer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (0), NULL };
-  struct validate_sdp answer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (0), NULL };
+  VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (0), NULL);
+  VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (0), NULL);
 
   /* check that a no stream connection creates 0 media sections */
 
@@ -756,10 +768,8 @@ create_audio_test (void)
 GST_START_TEST (test_audio)
 {
   struct test_webrtc *t = create_audio_test ();
-  struct validate_sdp offer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (1), NULL };
-  struct validate_sdp answer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (1), NULL };
+  VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL);
+  VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL);
 
   /* check that a single stream connection creates the associated number
    * of media sections */
@@ -786,10 +796,8 @@ create_audio_video_test (void)
 GST_START_TEST (test_audio_video)
 {
   struct test_webrtc *t = create_audio_video_test ();
-  struct validate_sdp offer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), NULL };
-  struct validate_sdp answer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), NULL };
+  VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL);
+  VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL);
 
   /* check that a dual stream connection creates the associated number
    * of media sections */
@@ -850,15 +858,14 @@ GST_START_TEST (test_media_direction)
   struct test_webrtc *t = create_audio_video_test ();
   const gchar *expected_offer[] = { "sendrecv", "sendrecv" };
   const gchar *expected_answer[] = { "sendrecv", "recvonly" };
-  struct validate_sdp offer_direction =
-      { on_sdp_media_direction, expected_offer, NULL };
-  struct validate_sdp offer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), &offer_direction };
-  struct validate_sdp answer_direction =
-      { on_sdp_media_direction, expected_answer, NULL };
-  struct validate_sdp answer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), &answer_direction };
   GstHarness *h;
+  VAL_SDP_INIT (offer_direction, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (2),
+      &offer_direction);
+  VAL_SDP_INIT (answer_direction, on_sdp_media_direction, expected_answer,
+      NULL);
+  VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (2),
+      &answer_direction);
 
   /* check the default media directions for transceivers */
 
@@ -905,9 +912,8 @@ on_sdp_media_payload_types (struct test_webrtc *t, GstElement * element,
 GST_START_TEST (test_payload_types)
 {
   struct test_webrtc *t = create_audio_video_test ();
-  struct validate_sdp payloads = { on_sdp_media_payload_types, NULL, NULL };
-  struct validate_sdp offer =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), &payloads };
+  VAL_SDP_INIT (payloads, on_sdp_media_payload_types, NULL, NULL);
+  VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (2), &payloads);
   GstWebRTCRTPTransceiver *trans;
   GArray *transceivers;
 
@@ -956,8 +962,8 @@ GST_START_TEST (test_media_setup)
   struct test_webrtc *t = create_audio_test ();
   const gchar *expected_offer[] = { "actpass" };
   const gchar *expected_answer[] = { "active" };
-  struct validate_sdp offer = { on_sdp_media_setup, expected_offer, NULL };
-  struct validate_sdp answer = { on_sdp_media_setup, expected_answer, NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_setup, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_setup, expected_answer, NULL);
 
   /* check the default dtls setup negotiation values */
   test_validate_sdp (t, &offer, &answer);
@@ -1333,9 +1339,8 @@ GST_START_TEST (test_add_recvonly_transceiver)
   GstWebRTCRTPTransceiver *trans;
   const gchar *expected_offer[] = { "recvonly" };
   const gchar *expected_answer[] = { "sendonly" };
-  struct validate_sdp offer = { on_sdp_media_direction, expected_offer, NULL };
-  struct validate_sdp answer =
-      { on_sdp_media_direction, expected_answer, NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
   GstCaps *caps;
   GstHarness *h;
 
@@ -1372,9 +1377,8 @@ GST_START_TEST (test_recvonly_sendonly)
   GstWebRTCRTPTransceiver *trans;
   const gchar *expected_offer[] = { "recvonly", "sendonly" };
   const gchar *expected_answer[] = { "sendonly", "recvonly" };
-  struct validate_sdp offer = { on_sdp_media_direction, expected_offer, NULL };
-  struct validate_sdp answer =
-      { on_sdp_media_direction, expected_answer, NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
   GstCaps *caps;
   GstHarness *h;
   GArray *transceivers;
@@ -1446,8 +1450,8 @@ GST_START_TEST (test_data_channel_create)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
   gchar *label;
 
   t->on_negotiation_needed = NULL;
@@ -1500,8 +1504,8 @@ GST_START_TEST (test_data_channel_remote_notify)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1575,8 +1579,8 @@ GST_START_TEST (test_data_channel_transfer_string)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1657,8 +1661,8 @@ GST_START_TEST (test_data_channel_transfer_data)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1715,8 +1719,8 @@ GST_START_TEST (test_data_channel_create_after_negotiate)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1776,8 +1780,8 @@ GST_START_TEST (test_data_channel_low_threshold)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1849,8 +1853,8 @@ GST_START_TEST (test_data_channel_max_message_size)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
 
   t->on_negotiation_needed = NULL;
   t->offer_data = &offer;
@@ -1904,8 +1908,8 @@ GST_START_TEST (test_data_channel_pre_negotiated)
 {
   struct test_webrtc *t = test_webrtc_new ();
   GObject *channel1 = NULL, *channel2 = NULL;
-  struct validate_sdp offer = { on_sdp_has_datachannel, NULL, NULL };
-  struct validate_sdp answer = { on_sdp_has_datachannel, NULL, NULL };
+  VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, NULL);
+  VAL_SDP_INIT (answer, on_sdp_has_datachannel, NULL, NULL);
   GstStructure *s;
   gint n_ready = 0;
 
@@ -2028,17 +2032,16 @@ GST_START_TEST (test_bundle_audio_video_max_bundle_max_bundle)
   const gchar *offer_bundle_only[] = { "video1", NULL };
   const gchar *answer_bundle_only[] = { NULL };
 
-  struct validate_sdp count =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), NULL };
-  struct validate_sdp bundle_tag = { _check_bundle_tag, bundle, &count };
-  struct validate_sdp offer_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (1), &bundle_tag };
-  struct validate_sdp answer_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (2), &bundle_tag };
-  struct validate_sdp offer =
-      { _check_bundle_only_media, &offer_bundle_only, &offer_non_reject };
-  struct validate_sdp answer =
-      { _check_bundle_only_media, &answer_bundle_only, &answer_non_reject };
+  VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL);
+  VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &count);
+  VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (1), &bundle_tag);
+  VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (2), &bundle_tag);
+  VAL_SDP_INIT (offer, _check_bundle_only_media, &offer_bundle_only,
+      &offer_non_reject);
+  VAL_SDP_INIT (answer, _check_bundle_only_media, &answer_bundle_only,
+      &answer_non_reject);
 
   /* We set a max-bundle policy on the offering webrtcbin,
    * this means that all the offered medias should be part
@@ -2068,13 +2071,12 @@ GST_START_TEST (test_bundle_audio_video_max_compat_max_bundle)
   const gchar *bundle[] = { "audio0", "video1", NULL };
   const gchar *bundle_only[] = { NULL };
 
-  struct validate_sdp count =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), NULL };
-  struct validate_sdp bundle_tag = { _check_bundle_tag, bundle, &count };
-  struct validate_sdp count_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (2), &bundle_tag };
-  struct validate_sdp bundle_sdp =
-      { _check_bundle_only_media, &bundle_only, &count_non_reject };
+  VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL);
+  VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &count);
+  VAL_SDP_INIT (count_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (2), &bundle_tag);
+  VAL_SDP_INIT (bundle_sdp, _check_bundle_only_media, &bundle_only,
+      &count_non_reject);
 
   /* We set a max-compat policy on the offering webrtcbin,
    * this means that all the offered medias should be part
@@ -2106,18 +2108,17 @@ GST_START_TEST (test_bundle_audio_video_max_bundle_none)
   const gchar *answer_bundle[] = { NULL };
   const gchar *answer_bundle_only[] = { NULL };
 
-  struct validate_sdp count =
-      { _count_num_sdp_media, GUINT_TO_POINTER (2), NULL };
-  struct validate_sdp count_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (1), &count };
-  struct validate_sdp offer_bundle_tag =
-      { _check_bundle_tag, offer_bundle, &count_non_reject };
-  struct validate_sdp answer_bundle_tag =
-      { _check_bundle_tag, answer_bundle, &count_non_reject };
-  struct validate_sdp offer =
-      { _check_bundle_only_media, &offer_bundle_only, &offer_bundle_tag };
-  struct validate_sdp answer =
-      { _check_bundle_only_media, &answer_bundle_only, &answer_bundle_tag };
+  VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL);
+  VAL_SDP_INIT (count_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (1), &count);
+  VAL_SDP_INIT (offer_bundle_tag, _check_bundle_tag, offer_bundle,
+      &count_non_reject);
+  VAL_SDP_INIT (answer_bundle_tag, _check_bundle_tag, answer_bundle,
+      &count_non_reject);
+  VAL_SDP_INIT (offer, _check_bundle_only_media, &offer_bundle_only,
+      &offer_bundle_tag);
+  VAL_SDP_INIT (answer, _check_bundle_only_media, &answer_bundle_only,
+      &answer_bundle_tag);
 
   /* We set a max-bundle policy on the offering webrtcbin,
    * this means that all the offered medias should be part
@@ -2148,17 +2149,16 @@ GST_START_TEST (test_bundle_audio_video_data)
   const gchar *answer_bundle_only[] = { NULL };
   GObject *channel = NULL;
 
-  struct validate_sdp count =
-      { _count_num_sdp_media, GUINT_TO_POINTER (3), NULL };
-  struct validate_sdp bundle_tag = { _check_bundle_tag, bundle, &count };
-  struct validate_sdp offer_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (1), &bundle_tag };
-  struct validate_sdp answer_non_reject =
-      { _count_non_rejected_media, GUINT_TO_POINTER (3), &bundle_tag };
-  struct validate_sdp offer =
-      { _check_bundle_only_media, &offer_bundle_only, &offer_non_reject };
-  struct validate_sdp answer =
-      { _check_bundle_only_media, &answer_bundle_only, &answer_non_reject };
+  VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (3), NULL);
+  VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &count);
+  VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (1), &bundle_tag);
+  VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (3), &bundle_tag);
+  VAL_SDP_INIT (offer, _check_bundle_only_media, &offer_bundle_only,
+      &offer_non_reject);
+  VAL_SDP_INIT (answer, _check_bundle_only_media, &answer_bundle_only,
+      &answer_non_reject);
 
   /* We set a max-bundle policy on the offering webrtcbin,
    * this means that all the offered medias should be part
@@ -2196,19 +2196,23 @@ GST_START_TEST (test_duplicate_nego)
   struct test_webrtc *t = create_audio_video_test ();
   const gchar *expected_offer[] = { "sendrecv", "sendrecv" };
   const gchar *expected_answer[] = { "sendrecv", "recvonly" };
-  struct validate_sdp offer = { on_sdp_media_direction, expected_offer, NULL };
-  struct validate_sdp answer =
-      { on_sdp_media_direction, expected_answer, NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
   GstHarness *h;
+  guint negotiation_flag = 0;
 
   /* check that negotiating twice succeeds */
 
+  t->on_negotiation_needed = on_negotiation_needed_hit;
+  t->negotiation_data = &negotiation_flag;
+
   h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
   add_fake_audio_src_harness (h, 96);
   t->harnesses = g_list_prepend (t->harnesses, h);
 
-  t->on_negotiation_needed = NULL;
   test_validate_sdp (t, &offer, &answer);
+  fail_unless_equals_int (negotiation_flag, 1);
+
   test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
   test_validate_sdp (t, &offer, &answer);
 
@@ -2222,9 +2226,8 @@ GST_START_TEST (test_dual_audio)
   struct test_webrtc *t = create_audio_test ();
   const gchar *expected_offer[] = { "sendrecv", "sendrecv", };
   const gchar *expected_answer[] = { "sendrecv", "recvonly" };
-  struct validate_sdp offer = { on_sdp_media_direction, expected_offer, NULL };
-  struct validate_sdp answer =
-      { on_sdp_media_direction, expected_answer, NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
   GstHarness *h;
   GstWebRTCRTPTransceiver *trans;
   GArray *transceivers;
@@ -2258,6 +2261,431 @@ GST_START_TEST (test_dual_audio)
   test_webrtc_free (t);
 }
 
+GST_END_TEST;
+
+static void
+sdp_increasing_session_version (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  GstWebRTCSessionDescription *previous;
+  const GstSDPOrigin *our_origin, *previous_origin;
+  const gchar *prop;
+  guint64 our_v, previous_v;
+
+  prop =
+      TEST_SDP_IS_LOCAL (t, element,
+      desc) ? "current-local-description" : "current-remote-description";
+  g_object_get (element, prop, &previous, NULL);
+
+  our_origin = gst_sdp_message_get_origin (desc->sdp);
+  previous_origin = gst_sdp_message_get_origin (previous->sdp);
+
+  our_v = g_ascii_strtoull (our_origin->sess_version, NULL, 10);
+  previous_v = g_ascii_strtoull (previous_origin->sess_version, NULL, 10);
+
+  ck_assert_int_lt (previous_v, our_v);
+
+  gst_webrtc_session_description_free (previous);
+}
+
+static void
+sdp_equal_session_id (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  GstWebRTCSessionDescription *previous;
+  const GstSDPOrigin *our_origin, *previous_origin;
+  const gchar *prop;
+
+  prop =
+      TEST_SDP_IS_LOCAL (t, element,
+      desc) ? "current-local-description" : "current-remote-description";
+  g_object_get (element, prop, &previous, NULL);
+
+  our_origin = gst_sdp_message_get_origin (desc->sdp);
+  previous_origin = gst_sdp_message_get_origin (previous->sdp);
+
+  fail_unless_equals_string (previous_origin->sess_id, our_origin->sess_id);
+  gst_webrtc_session_description_free (previous);
+}
+
+static void
+sdp_media_equal_attribute (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, GstWebRTCSessionDescription * previous,
+    const gchar * attr)
+{
+  guint i, n;
+
+  n = MIN (gst_sdp_message_medias_len (previous->sdp),
+      gst_sdp_message_medias_len (desc->sdp));
+
+  for (i = 0; i < n; i++) {
+    const GstSDPMedia *our_media, *other_media;
+    const gchar *our_mid, *other_mid;
+
+    our_media = gst_sdp_message_get_media (desc->sdp, i);
+    other_media = gst_sdp_message_get_media (previous->sdp, i);
+
+    our_mid = gst_sdp_media_get_attribute_val (our_media, attr);
+    other_mid = gst_sdp_media_get_attribute_val (other_media, attr);
+
+    fail_unless_equals_string (our_mid, other_mid);
+  }
+}
+
+static void
+sdp_media_equal_mid (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  GstWebRTCSessionDescription *previous;
+  const gchar *prop;
+
+  prop =
+      TEST_SDP_IS_LOCAL (t, element,
+      desc) ? "current-local-description" : "current-remote-description";
+  g_object_get (element, prop, &previous, NULL);
+
+  sdp_media_equal_attribute (t, element, desc, previous, "mid");
+
+  gst_webrtc_session_description_free (previous);
+}
+
+static void
+sdp_media_equal_ice_params (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  GstWebRTCSessionDescription *previous;
+  const gchar *prop;
+
+  prop =
+      TEST_SDP_IS_LOCAL (t, element,
+      desc) ? "current-local-description" : "current-remote-description";
+  g_object_get (element, prop, &previous, NULL);
+
+  sdp_media_equal_attribute (t, element, desc, previous, "ice-ufrag");
+  sdp_media_equal_attribute (t, element, desc, previous, "ice-pwd");
+
+  gst_webrtc_session_description_free (previous);
+}
+
+static void
+sdp_media_equal_fingerprint (struct test_webrtc *t, GstElement * element,
+    GstWebRTCSessionDescription * desc, gpointer user_data)
+{
+  GstWebRTCSessionDescription *previous;
+  const gchar *prop;
+
+  prop =
+      TEST_SDP_IS_LOCAL (t, element,
+      desc) ? "current-local-description" : "current-remote-description";
+  g_object_get (element, prop, &previous, NULL);
+
+  sdp_media_equal_attribute (t, element, desc, previous, "fingerprint");
+
+  gst_webrtc_session_description_free (previous);
+}
+
+GST_START_TEST (test_renego_add_stream)
+{
+  struct test_webrtc *t = create_audio_video_test ();
+  const gchar *expected_offer[] = { "sendrecv", "sendrecv", "sendrecv" };
+  const gchar *expected_answer[] = { "sendrecv", "recvonly", "recvonly" };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+  VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL);
+  VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL,
+      &renego_mid);
+  VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params);
+  VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL,
+      &renego_sess_id);
+  VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL,
+      &renego_sess_ver);
+  GstHarness *h;
+
+  /* negotiate an AV stream and then renegotiate an extra stream */
+  h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
+  add_fake_audio_src_harness (h, 96);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL);
+  add_fake_audio_src_harness (h, 98);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  offer.next = &renego_fingerprint;
+  answer.next = &renego_fingerprint;
+
+  /* renegotiate! */
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
+GST_START_TEST (test_renego_stream_add_data_channel)
+{
+  struct test_webrtc *t = create_audio_video_test ();
+  const gchar *expected_offer[] = { "sendrecv", "sendrecv" };
+  const gchar *expected_answer[] = { "sendrecv", "recvonly" };
+
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+  VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL);
+  VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL,
+      &renego_mid);
+  VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params);
+  VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL,
+      &renego_sess_id);
+  VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL,
+      &renego_sess_ver);
+  GObject *channel;
+  GstHarness *h;
+
+  /* negotiate an AV stream and then renegotiate a data channel */
+  h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
+  add_fake_audio_src_harness (h, 96);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL,
+      &channel);
+
+  offer.next = &renego_fingerprint;
+  answer.next = &renego_fingerprint;
+
+  /* renegotiate! */
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  g_object_unref (channel);
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
+GST_START_TEST (test_renego_data_channel_add_stream)
+{
+  struct test_webrtc *t = test_webrtc_new ();
+  const gchar *expected_offer[] = { NULL, "sendrecv" };
+  const gchar *expected_answer[] = { NULL, "recvonly" };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+  VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL);
+  VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL,
+      &renego_mid);
+  VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params);
+  VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL,
+      &renego_sess_id);
+  VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL,
+      &renego_sess_ver);
+  GObject *channel;
+  GstHarness *h;
+
+  /* negotiate an AV stream and then renegotiate a data channel */
+  t->on_negotiation_needed = NULL;
+  t->on_ice_candidate = NULL;
+  t->on_pad_added = _pad_added_fakesink;
+
+  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);
+
+  g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL,
+      &channel);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL);
+  add_fake_audio_src_harness (h, 97);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  offer.next = &renego_fingerprint;
+  answer.next = &renego_fingerprint;
+
+  /* renegotiate! */
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  g_object_unref (channel);
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
+GST_START_TEST (test_bundle_renego_add_stream)
+{
+  struct test_webrtc *t = create_audio_video_test ();
+  const gchar *expected_offer[] = { "sendrecv", "sendrecv", "sendrecv" };
+  const gchar *expected_answer[] = { "sendrecv", "recvonly", "recvonly" };
+  const gchar *bundle[] = { "audio0", "video1", "audio2", NULL };
+  const gchar *offer_bundle_only[] = { "video1", "audio2", NULL };
+  const gchar *answer_bundle_only[] = { NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+
+  VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL);
+  VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL,
+      &renego_mid);
+  VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params);
+  VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL,
+      &renego_sess_id);
+  VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL,
+      &renego_sess_ver);
+  VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &renego_fingerprint);
+  VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (1), &bundle_tag);
+  VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (3), &bundle_tag);
+  VAL_SDP_INIT (offer_bundle_only_sdp, _check_bundle_only_media,
+      &offer_bundle_only, &offer_non_reject);
+  VAL_SDP_INIT (answer_bundle_only_sdp, _check_bundle_only_media,
+      &answer_bundle_only, &answer_non_reject);
+  GstHarness *h;
+
+  /* We set a max-bundle policy on the offering webrtcbin,
+   * this means that all the offered medias should be part
+   * of the group:BUNDLE attribute, and they should be marked
+   * as bundle-only
+   */
+  gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy",
+      "max-bundle");
+  /* We also set a max-bundle policy on the answering webrtcbin,
+   * this means that all the offered medias should be part
+   * of the group:BUNDLE attribute, but need not be marked
+   * as bundle-only.
+   */
+  gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy",
+      "max-bundle");
+
+  /* negotiate an AV stream and then renegotiate an extra stream */
+  h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
+  add_fake_audio_src_harness (h, 96);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL);
+  add_fake_audio_src_harness (h, 98);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  offer.next = &offer_bundle_only_sdp;
+  answer.next = &answer_bundle_only_sdp;
+
+  /* renegotiate! */
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
+GST_START_TEST (test_bundle_max_compat_max_bundle_renego_add_stream)
+{
+  struct test_webrtc *t = create_audio_video_test ();
+  const gchar *expected_offer[] = { "sendrecv", "sendrecv", "sendrecv" };
+  const gchar *expected_answer[] = { "sendrecv", "recvonly", "recvonly" };
+  const gchar *bundle[] = { "audio0", "video1", "audio2", NULL };
+  const gchar *bundle_only[] = { NULL };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+
+  VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL);
+  VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL,
+      &renego_mid);
+  VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params);
+  VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL,
+      &renego_sess_id);
+  VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL,
+      &renego_sess_ver);
+  VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &renego_fingerprint);
+  VAL_SDP_INIT (count_non_reject, _count_non_rejected_media,
+      GUINT_TO_POINTER (3), &bundle_tag);
+  VAL_SDP_INIT (bundle_sdp, _check_bundle_only_media, &bundle_only,
+      &count_non_reject);
+  GstHarness *h;
+
+  /* We set a max-compat policy on the offering webrtcbin,
+   * this means that all the offered medias should be part
+   * of the group:BUNDLE attribute, and they should *not* be marked
+   * as bundle-only
+   */
+  gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy",
+      "max-compat");
+  /* We set a max-bundle policy on the answering webrtcbin,
+   * this means that all the offered medias should be part
+   * of the group:BUNDLE attribute, but need not be marked
+   * as bundle-only.
+   */
+  gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy",
+      "max-bundle");
+
+  /* negotiate an AV stream and then renegotiate an extra stream */
+  h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
+  add_fake_audio_src_harness (h, 96);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL);
+  add_fake_audio_src_harness (h, 98);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  offer.next = &bundle_sdp;
+  answer.next = &bundle_sdp;
+
+  /* renegotiate! */
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
+GST_START_TEST (test_renego_transceiver_set_direction)
+{
+  struct test_webrtc *t = create_audio_test ();
+  const gchar *expected_offer[] = { "sendrecv" };
+  const gchar *expected_answer[] = { "sendrecv" };
+  VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer, NULL);
+  VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer, NULL);
+  GstWebRTCRTPTransceiver *transceiver;
+  GstHarness *h;
+  GstPad *pad;
+
+  /* negotiate an AV stream and then change the transceiver direction */
+  h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL);
+  add_fake_audio_src_harness (h, 96);
+  t->harnesses = g_list_prepend (t->harnesses, h);
+
+  test_validate_sdp (t, &offer, &answer);
+
+  /* renegotiate an inactive transceiver! */
+  pad = gst_element_get_static_pad (t->webrtc1, "sink_0");
+  g_object_get (pad, "transceiver", &transceiver, NULL);
+  fail_unless (transceiver != NULL);
+  gst_webrtc_rtp_transceiver_set_direction (transceiver,
+      GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE);
+  expected_offer[0] = "inactive";
+  expected_answer[0] = "inactive";
+
+  /* TODO: also validate EOS events from the inactive change */
+
+  test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED);
+  test_validate_sdp (t, &offer, &answer);
+
+  gst_object_unref (pad);
+  gst_object_unref (transceiver);
+  test_webrtc_free (t);
+}
+
+GST_END_TEST;
+
 static Suite *
 webrtcbin_suite (void)
 {
@@ -2294,6 +2722,10 @@ webrtcbin_suite (void)
     tcase_add_test (tc, test_bundle_audio_video_max_compat_max_bundle);
     tcase_add_test (tc, test_dual_audio);
     tcase_add_test (tc, test_duplicate_nego);
+    tcase_add_test (tc, test_renego_add_stream);
+    tcase_add_test (tc, test_bundle_renego_add_stream);
+    tcase_add_test (tc, test_bundle_max_compat_max_bundle_renego_add_stream);
+    tcase_add_test (tc, test_renego_transceiver_set_direction);
     if (sctpenc && sctpdec) {
       tcase_add_test (tc, test_data_channel_create);
       tcase_add_test (tc, test_data_channel_remote_notify);
@@ -2304,6 +2736,8 @@ webrtcbin_suite (void)
       tcase_add_test (tc, test_data_channel_max_message_size);
       tcase_add_test (tc, test_data_channel_pre_negotiated);
       tcase_add_test (tc, test_bundle_audio_video_data);
+      tcase_add_test (tc, test_renego_stream_add_data_channel);
+      tcase_add_test (tc, test_renego_data_channel_add_stream);
     } else {
       GST_WARNING ("Some required elements were not found. "
           "All datachannel tests are disabled. sctpenc %p, sctpdec %p", sctpenc,
index 6323263..7d8aec6 100644 (file)
@@ -1,5 +1,5 @@
 
-noinst_PROGRAMS = webrtc webrtcbidirectional webrtcswap webrtctransceiver
+noinst_PROGRAMS = webrtc webrtcbidirectional webrtcswap webrtctransceiver webrtcrenego
 
 webrtc_SOURCES = webrtc.c
 webrtc_CFLAGS=\
@@ -52,3 +52,16 @@ webrtctransceiver_LDADD=\
        $(GST_LIBS) \
        $(GST_SDP_LIBS) \
        $(top_builddir)/gst-libs/gst/webrtc/libgstwebrtc-@GST_API_VERSION@.la
+
+webrtcrenego_SOURCES = webrtcrenego.c
+webrtcrenego_CFLAGS=\
+       -I$(top_srcdir)/gst-libs \
+       -I$(top_builddir)/gst-libs \
+       $(GST_PLUGINS_BASE_CFLAGS) \
+       $(GST_CFLAGS) \
+       $(GST_SDP_CFLAGS)
+webrtcrenego_LDADD=\
+       $(GST_PLUGINS_BASE_LIBS) \
+       $(GST_LIBS) \
+       $(GST_SDP_LIBS) \
+       $(top_builddir)/gst-libs/gst/webrtc/libgstwebrtc-@GST_API_VERSION@.la
index 716bcf3..3913a46 100644 (file)
@@ -1,4 +1,4 @@
-examples = ['webrtc', 'webrtcbidirectional', 'webrtcswap', 'webrtctransceiver']
+examples = ['webrtc', 'webrtcbidirectional', 'webrtcswap', 'webrtctransceiver', 'webrtcrenego']
 
 foreach example : examples
   exe_name = example
diff --git a/tests/examples/webrtc/webrtcrenego.c b/tests/examples/webrtc/webrtcrenego.c
new file mode 100644 (file)
index 0000000..1305ff8
--- /dev/null
@@ -0,0 +1,289 @@
+#include <gst/gst.h>
+#include <gst/sdp/sdp.h>
+#include <gst/webrtc/webrtc.h>
+
+#include <string.h>
+
+static GMainLoop *loop;
+static GstElement *pipe1, *webrtc1, *webrtc2, *extra_src;
+static GstBus *bus1;
+
+#define SEND_SRC(pattern) "videotestsrc is-live=true pattern=" pattern " ! timeoverlay ! queue ! vp8enc ! rtpvp8pay ! queue ! " \
+    "capsfilter caps=application/x-rtp,media=video,payload=96,encoding-name=VP8"
+
+static void
+_element_message (GstElement * parent, GstMessage * msg)
+{
+  switch (GST_MESSAGE_TYPE (msg)) {
+    case GST_MESSAGE_EOS:{
+      GstElement *receive, *webrtc;
+      GstPad *pad, *peer;
+
+      g_print ("Got element EOS message from %s parent %s\n",
+          GST_OBJECT_NAME (msg->src), GST_OBJECT_NAME (parent));
+
+      receive = GST_ELEMENT (msg->src);
+
+      pad = gst_element_get_static_pad (receive, "sink");
+      peer = gst_pad_get_peer (pad);
+
+      webrtc = GST_ELEMENT (gst_pad_get_parent (peer));
+      gst_bin_remove (GST_BIN (pipe1), receive);
+
+      gst_pad_unlink (peer, pad);
+      gst_element_release_request_pad (webrtc, peer);
+
+      gst_object_unref (pad);
+      gst_object_unref (peer);
+
+      gst_element_set_state (receive, GST_STATE_NULL);
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+static gboolean
+_bus_watch (GstBus * bus, GstMessage * msg, GstElement * pipe)
+{
+  switch (GST_MESSAGE_TYPE (msg)) {
+    case GST_MESSAGE_STATE_CHANGED:
+      if (GST_ELEMENT (msg->src) == pipe) {
+        GstState old, new, pending;
+
+        gst_message_parse_state_changed (msg, &old, &new, &pending);
+
+        {
+          gchar *dump_name = g_strconcat ("state_changed-",
+              gst_element_state_get_name (old), "_",
+              gst_element_state_get_name (new), NULL);
+          GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (msg->src),
+              GST_DEBUG_GRAPH_SHOW_ALL, dump_name);
+          g_free (dump_name);
+        }
+      }
+      break;
+    case GST_MESSAGE_ERROR:{
+      GError *err = NULL;
+      gchar *dbg_info = NULL;
+
+      GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (pipe),
+          GST_DEBUG_GRAPH_SHOW_ALL, "error");
+
+      gst_message_parse_error (msg, &err, &dbg_info);
+      g_printerr ("ERROR from element %s: %s\n",
+          GST_OBJECT_NAME (msg->src), err->message);
+      g_printerr ("Debugging info: %s\n", (dbg_info) ? dbg_info : "none");
+      g_error_free (err);
+      g_free (dbg_info);
+      g_main_loop_quit (loop);
+      break;
+    }
+    case GST_MESSAGE_EOS:{
+      GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (pipe),
+          GST_DEBUG_GRAPH_SHOW_ALL, "eos");
+      g_print ("EOS received\n");
+      g_main_loop_quit (loop);
+      break;
+    }
+    case GST_MESSAGE_ELEMENT:{
+      const GstStructure *s = gst_message_get_structure (msg);
+      if (g_strcmp0 (gst_structure_get_name (s), "GstBinForwarded") == 0) {
+        GstMessage *sub_msg;
+
+        gst_structure_get (s, "message", GST_TYPE_MESSAGE, &sub_msg, NULL);
+        _element_message (GST_ELEMENT (msg->src), sub_msg);
+        gst_message_unref (sub_msg);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+
+  return TRUE;
+}
+
+static void
+_webrtc_pad_added (GstElement * webrtc, GstPad * new_pad, GstElement * pipe)
+{
+  GstElement *out;
+  GstPad *sink;
+
+  if (GST_PAD_DIRECTION (new_pad) != GST_PAD_SRC)
+    return;
+
+  out = gst_parse_bin_from_description ("queue ! rtpvp8depay ! vp8dec ! "
+      "videoconvert ! queue ! xvimagesink", TRUE, NULL);
+  gst_bin_add (GST_BIN (pipe), out);
+  gst_element_sync_state_with_parent (out);
+
+  sink = out->sinkpads->data;
+
+  gst_pad_link (new_pad, sink);
+}
+
+static void
+_on_answer_received (GstPromise * promise, gpointer user_data)
+{
+  GstWebRTCSessionDescription *answer = NULL;
+  const GstStructure *reply;
+  gchar *desc;
+
+  g_assert (gst_promise_wait (promise) == GST_PROMISE_RESULT_REPLIED);
+  reply = gst_promise_get_reply (promise);
+  gst_structure_get (reply, "answer",
+      GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &answer, NULL);
+  gst_promise_unref (promise);
+  desc = gst_sdp_message_as_text (answer->sdp);
+  g_print ("Created answer:\n%s\n", desc);
+  g_free (desc);
+
+  /* this is one way to tell webrtcbin that we don't want to be notified when
+   * this task is complete: set a NULL promise */
+  g_signal_emit_by_name (webrtc1, "set-remote-description", answer, NULL);
+  /* this is another way to tell webrtcbin that we don't want to be notified
+   * when this task is complete: interrupt the promise */
+  promise = gst_promise_new ();
+  g_signal_emit_by_name (webrtc2, "set-local-description", answer, promise);
+  gst_promise_interrupt (promise);
+  gst_promise_unref (promise);
+
+  gst_webrtc_session_description_free (answer);
+}
+
+static void
+_on_offer_received (GstPromise * promise, gpointer user_data)
+{
+  GstWebRTCSessionDescription *offer = NULL;
+  const GstStructure *reply;
+  gchar *desc;
+
+  g_assert (gst_promise_wait (promise) == GST_PROMISE_RESULT_REPLIED);
+  reply = gst_promise_get_reply (promise);
+  gst_structure_get (reply, "offer",
+      GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
+  gst_promise_unref (promise);
+  desc = gst_sdp_message_as_text (offer->sdp);
+  g_print ("Created offer:\n%s\n", desc);
+  g_free (desc);
+
+  g_signal_emit_by_name (webrtc1, "set-local-description", offer, NULL);
+  g_signal_emit_by_name (webrtc2, "set-remote-description", offer, NULL);
+
+  promise = gst_promise_new_with_change_func (_on_answer_received, user_data,
+      NULL);
+  g_signal_emit_by_name (webrtc2, "create-answer", NULL, promise);
+
+  gst_webrtc_session_description_free (offer);
+}
+
+static void
+_on_negotiation_needed (GstElement * element, gpointer user_data)
+{
+  GstPromise *promise;
+
+  promise = gst_promise_new_with_change_func (_on_offer_received, user_data,
+      NULL);
+  g_signal_emit_by_name (webrtc1, "create-offer", NULL, promise);
+}
+
+static void
+_on_ice_candidate (GstElement * webrtc, guint mlineindex, gchar * candidate,
+    GstElement * other)
+{
+  g_signal_emit_by_name (other, "add-ice-candidate", mlineindex, candidate);
+}
+
+static gboolean
+stream_change (gpointer data)
+{
+  if (!extra_src) {
+    g_print ("Adding extra stream\n");
+    extra_src =
+        gst_parse_bin_from_description (SEND_SRC ("circular"), TRUE, NULL);
+
+    gst_element_set_locked_state (extra_src, TRUE);
+    gst_bin_add (GST_BIN (pipe1), extra_src);
+    gst_element_link (extra_src, webrtc1);
+    gst_element_set_locked_state (extra_src, FALSE);
+    gst_element_sync_state_with_parent (extra_src);
+    GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (pipe1),
+        GST_DEBUG_GRAPH_SHOW_ALL, "add");
+  } else {
+    GstPad *pad, *peer;
+    GstWebRTCRTPTransceiver *transceiver;
+
+    g_print ("Removing extra stream\n");
+    pad = gst_element_get_static_pad (extra_src, "src");
+    peer = gst_pad_get_peer (pad);
+    gst_element_send_event (extra_src, gst_event_new_eos ());
+
+    g_object_get (peer, "transceiver", &transceiver, NULL);
+    gst_webrtc_rtp_transceiver_set_direction (transceiver,
+        GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE);
+
+    gst_element_set_locked_state (extra_src, TRUE);
+    gst_element_set_state (extra_src, GST_STATE_NULL);
+    gst_pad_unlink (pad, peer);
+    gst_element_release_request_pad (webrtc1, peer);
+
+    gst_object_unref (peer);
+    gst_object_unref (pad);
+
+    gst_bin_remove (GST_BIN (pipe1), extra_src);
+    extra_src = NULL;
+    GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (pipe1),
+        GST_DEBUG_GRAPH_SHOW_ALL, "remove");
+  }
+
+  return G_SOURCE_CONTINUE;
+}
+
+int
+main (int argc, char *argv[])
+{
+  gst_init (&argc, &argv);
+
+  loop = g_main_loop_new (NULL, FALSE);
+  pipe1 = gst_parse_launch (SEND_SRC ("smpte")
+      " ! webrtcbin name=smpte bundle-policy=max-bundle " SEND_SRC ("ball")
+      " ! webrtcbin name=ball bundle-policy=max-bundle", NULL);
+  g_object_set (pipe1, "message-forward", TRUE, NULL);
+  bus1 = gst_pipeline_get_bus (GST_PIPELINE (pipe1));
+  gst_bus_add_watch (bus1, (GstBusFunc) _bus_watch, pipe1);
+
+  webrtc1 = gst_bin_get_by_name (GST_BIN (pipe1), "smpte");
+  g_signal_connect (webrtc1, "on-negotiation-needed",
+      G_CALLBACK (_on_negotiation_needed), NULL);
+  g_signal_connect (webrtc1, "pad-added", G_CALLBACK (_webrtc_pad_added),
+      pipe1);
+  webrtc2 = gst_bin_get_by_name (GST_BIN (pipe1), "ball");
+  g_signal_connect (webrtc2, "pad-added", G_CALLBACK (_webrtc_pad_added),
+      pipe1);
+  g_signal_connect (webrtc1, "on-ice-candidate",
+      G_CALLBACK (_on_ice_candidate), webrtc2);
+  g_signal_connect (webrtc2, "on-ice-candidate",
+      G_CALLBACK (_on_ice_candidate), webrtc1);
+
+  g_print ("Starting pipeline\n");
+  gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_PLAYING);
+
+  g_timeout_add_seconds (5, stream_change, NULL);
+
+  g_main_loop_run (loop);
+
+  gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_NULL);
+  g_print ("Pipeline stopped\n");
+
+  gst_object_unref (webrtc1);
+  gst_object_unref (webrtc2);
+  gst_bus_remove_watch (bus1);
+  gst_object_unref (bus1);
+  gst_object_unref (pipe1);
+
+  gst_deinit ();
+
+  return 0;
+}