Bug 906986 - Ice restart and tests. draft
authorMichael Froman <mfroman@mozilla.com>
Fri, 18 Mar 2016 09:42:46 -0500
changeset 342203 b04c6b235610c76d810ebcd24d053befdcdea3f8
parent 342202 8c0860e60060e6df2651937f4e8256e266af81ce
child 342231 b769e2acf25ef57df94110a1b7b2dba7bea808ea
push id13359
push usermfroman@nostrum.com
push dateFri, 18 Mar 2016 15:14:33 +0000
bugs906986
milestone47.0a1
Bug 906986 - Ice restart and tests. MozReview-Commit-ID: 8hE3PjMpdBr
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_peerConnection_restartIce.html
dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
dom/media/tests/mochitest/test_peerConnection_restartIceLocalRollback.html
dom/media/tests/mochitest/test_peerConnection_restartIceNoBundle.html
dom/webidl/RTCPeerConnection.webidl
media/mtransport/nricectx.cpp
media/mtransport/nricectx.h
media/mtransport/nricectxhandler.cpp
media/mtransport/nricectxhandler.h
media/mtransport/test/ice_unittest.cpp
media/mtransport/third_party/nICEr/src/ice/ice_ctx.c
media/mtransport/third_party/nICEr/src/ice/ice_ctx.h
media/mtransport/transportlayerice.cpp
media/mtransport/transportlayerice.h
media/webrtc/signaling/src/jsep/JsepSession.h
media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
media/webrtc/signaling/src/jsep/JsepSessionImpl.h
media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
media/webrtc/signaling/test/signaling_unittests.cpp
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -228,8 +228,12 @@ skip-if = toolkit == 'gonk' || buildapp 
 skip-if = toolkit == 'gonk' || (android_version == '18' && debug) # b2g (Bug 1059867), android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_remoteReofferRollback.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_selftest.html]
 # Bug 1227781: Crash with bogus TURN server.
 [test_peerConnection_bug1227781.html]
 # Bug 950317: Hack for making a cleanup hook after finishing all WebRTC cases
 [test_zmedia_cleanup.html]
+[test_peerConnection_restartIceNoBundle.html]
+[test_peerConnection_restartIce.html]
+[test_peerConnection_restartIceLocalRollback.html]
+[test_peerConnection_restartIceLocalAndRemoteRollback.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIce.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "906986",
+    title: "Renegotiation: restart ice"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+
+    addRenegotiation(test.chain,
+                      [
+                        function PC_SET_OFFER_OPTION(test) {
+                          test.setOfferOptions({ iceRestart: true });
+                        },
+                        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+                          test.pcLocal.iceCheckingRestartExpected = true;
+                        },
+                        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+                          test.pcRemote.iceCheckingRestartExpected = true;
+                        }
+                      ]
+                    );
+
+    test.setMediaConstraints([{audio: true}, {video: true}],
+                             [{audio: true}, {video: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "906986",
+    title: "Renegotiation: restart ice, local and remote rollback"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    var firstNegotiationSize = test.chain.commands.length;
+
+    addRenegotiation(test.chain,
+                      [
+                        // causes a full, normal ice restart
+                        function PC_SET_OFFER_OPTION(test) {
+                          test.setOfferOptions({ iceRestart: true });
+                        },
+                        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+                          test.pcLocal.iceCheckingRestartExpected = true;
+                        },
+                        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+                          test.pcRemote.iceCheckingRestartExpected = true;
+                        },
+                      ]
+                    );
+
+    test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+      [
+        function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+          test.pcLocal.setupIceCandidateHandler(test);
+          if (test.testOptions.steeplechase) {
+            test.pcLocal.endOfTrickleIce.then(() => {
+              send_message({"type": "end_of_trickle_ice"});
+            });
+          }
+        },
+
+        function PC_REMOTE_ROLLBACK(test) {
+          return test.setRemoteDescription(
+              test.pcRemote,
+              new RTCSessionDescription({ type: "rollback" }),
+              STABLE);
+        },
+
+        function PC_LOCAL_ROLLBACK(test) {
+          // We haven't negotiated the new stream yet.
+          test.pcLocal.expectNegotiationNeeded();
+          return test.setLocalDescription(
+              test.pcLocal,
+              new RTCSessionDescription({ type: "rollback", sdp: ""}),
+              STABLE);
+        },
+
+        // Rolling back should shut down gathering
+        function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+          return test.pcLocal.endOfTrickleIce;
+        },
+      ],
+      firstNegotiationSize // Second PC_REMOTE_SET_REMOTE_DESCRIPTION
+    );
+    test.chain.append(commandsPeerConnectionOfferAnswer);
+
+    // for now, only use one stream, because rollback doesn't seem to
+    // like multiple streams
+    test.setMediaConstraints([{audio: true}],
+                             [{audio: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalRollback.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "906986",
+    title: "Renegotiation: restart ice, local rollback"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+
+    addRenegotiation(test.chain,
+                      [
+                        // causes a full, normal ice restart
+                        function PC_SET_OFFER_OPTION(test) {
+                          test.setOfferOptions({ iceRestart: true });
+                        },
+                        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+                          test.pcLocal.iceCheckingRestartExpected = true;
+                        },
+                        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+                          test.pcRemote.iceCheckingRestartExpected = true;
+                        },
+
+                        // causes an ice restart and then rolls it back
+                        // (does not result in sending and offer)
+                        function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+                          test.pcLocal.setupIceCandidateHandler(test);
+                          if (test.testOptions.steeplechase) {
+                            test.pcLocal.endOfTrickleIce.then(() => {
+                              send_message({"type": "end_of_trickle_ice"});
+                            });
+                          }
+                        },
+                        function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+                          return test.createOffer(test.pcLocal).then(offer => {
+                            return test.setLocalDescription(test.pcLocal, 
+                                                            offer, 
+                                                            HAVE_LOCAL_OFFER);
+                          });
+                        },
+                        function PC_LOCAL_ROLLBACK(test) {
+                          return test.setLocalDescription(
+                              test.pcLocal,
+                              new RTCSessionDescription({ type: "rollback", 
+                                                          sdp: ""}),
+                              STABLE);
+                        },
+                        // Rolling back should shut down gathering
+                        function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+                          return test.pcLocal.endOfTrickleIce;
+                        }
+                      ]
+                    );
+
+    // for now, only use one stream, because rollback doesn't seem to
+    // like multiple streams
+    test.setMediaConstraints([{audio: true}],
+                             [{audio: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceNoBundle.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "906986",
+    title: "Renegotiation: restart ice, no bundle"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    options = options || { };
+    options.bundle = false;
+    test = new PeerConnectionTest(options);
+
+    addRenegotiation(test.chain,
+                      [
+                        function PC_SET_OFFER_OPTION(test) {
+                          test.setOfferOptions({ iceRestart: true });
+                        },
+                        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+                          test.pcLocal.iceCheckingRestartExpected = true;
+                        },
+                        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+                          test.pcRemote.iceCheckingRestartExpected = true;
+                        }
+                      ]
+                    );
+
+    test.setMediaConstraints([{audio: true}, {video: true}],
+                             [{audio: true}, {video: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -57,17 +57,17 @@ dictionary RTCOfferAnswerOptions {
 };
 
 dictionary RTCAnswerOptions : RTCOfferAnswerOptions {
 };
 
 dictionary RTCOfferOptions : RTCOfferAnswerOptions {
   long    offerToReceiveVideo;
   long    offerToReceiveAudio;
-  // boolean iceRestart = false; // Not implemented (Bug 906986)
+  boolean iceRestart;
 
   // Mozilla proprietary options (at risk: Bug 1196974)
   boolean mozDontOfferDataChannel;
   boolean mozBundleOnly;
 
   // TODO: Remove old constraint-like RTCOptions support soon (Bug 1064223).
   DeprecatedRTCOfferOptionsSet mandatory;
   sequence<DeprecatedRTCOfferOptionsSet> _optional;
--- a/media/mtransport/nricectx.cpp
+++ b/media/mtransport/nricectx.cpp
@@ -451,36 +451,86 @@ NrIceCtx::InitializeCryptoAndLogging(boo
     if (force_net_interface.Length() > 0) {
       // Stupid cast.... but needed
       const nsCString& flat = PromiseFlatCString(static_cast<nsACString&>(force_net_interface));
       NR_reg_set_string((char *)NR_ICE_REG_PREF_FORCE_INTERFACE_NAME, const_cast<char*>(flat.get()));
     }
   }
 }
 
+std::string
+NrIceCtx::GetNewUfrag()
+{
+  char* ufrag;
+  int r;
+
+  if ((r=nr_ice_get_new_ice_ufrag(&ufrag)))
+    return "";
+
+  std::string ufragStr = ufrag;
+  RFREE(ufrag);
+
+  return ufragStr;
+}
+
+std::string
+NrIceCtx::GetNewPwd()
+{
+  char* pwd;
+  int r;
+
+  if ((r=nr_ice_get_new_ice_pwd(&pwd)))
+    return "";
+
+  std::string pwdStr = pwd;
+  RFREE(pwd);
+
+  return pwdStr;
+}
+
 bool
 NrIceCtx::Initialize(NrIceCtx* ctx,
                      bool hide_non_default)
 {
+  std::string ufrag = GetNewUfrag();
+  std::string pwd = GetNewPwd();
+
+  return Initialize(ctx,
+                    hide_non_default,
+                    ufrag,
+                    pwd);
+}
+
+bool
+NrIceCtx::Initialize(NrIceCtx* ctx,
+                     bool hide_non_default,
+                     const std::string& ufrag,
+                     const std::string& pwd)
+{
   // Create the ICE context
   int r;
 
   UINT4 flags = ctx->offerer_ ? NR_ICE_CTX_FLAGS_OFFERER:
       NR_ICE_CTX_FLAGS_ANSWERER;
   flags |= NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION;
   if (ctx->policy_ == ICE_POLICY_RELAY) {
     flags |= NR_ICE_CTX_FLAGS_RELAY_ONLY;
   }
 
   if (hide_non_default)
     flags |= NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS;
 
-  r = nr_ice_ctx_create(const_cast<char *>(ctx->name_.c_str()),
+  r = nr_ice_ctx_create_with_credentials(const_cast<char *>(ctx->name_.c_str()),
                         flags,
+                        const_cast<char *>(ufrag.c_str()),
+                        const_cast<char *>(pwd.c_str()),
                         &ctx->ctx_);
+  MOZ_ASSERT(ufrag == ctx->ctx_->ufrag);
+  MOZ_ASSERT(pwd == ctx->ctx_->pwd);
+
   if (r) {
     MOZ_MTLOG(ML_ERROR, "Couldn't create ICE ctx for '" << ctx->name_ << "'");
     return false;
   }
 
   nr_interface_prioritizer *prioritizer = CreateInterfacePrioritizer();
   if (!prioritizer) {
     MOZ_MTLOG(LogLevel::Error, "Couldn't create interface prioritizer.");
--- a/media/mtransport/nricectx.h
+++ b/media/mtransport/nricectx.h
@@ -215,16 +215,22 @@ class NrIceCtx {
   };
 
   static void InitializeCryptoAndLogging(bool allow_loopback,
                                          bool tcp_enabled,
                                          bool allow_link_local);
 
   static bool Initialize(NrIceCtx* ice_ctx,
                          bool hide_non_default);
+  static std::string GetNewUfrag();
+  static std::string GetNewPwd();
+  static bool Initialize(NrIceCtx* ice_ctx,
+                         bool hide_non_default,
+                         const std::string& ufrag,
+                         const std::string& pwd);
 
   // Deinitialize all ICE global state. Used only for testing.
   static void internal_DeinitializeGlobal();
 
 
   nr_ice_ctx *ctx() { return ctx_; }
   nr_ice_peer_ctx *peer() { return peer_; }
 
--- a/media/mtransport/nricectxhandler.cpp
+++ b/media/mtransport/nricectxhandler.cpp
@@ -40,13 +40,107 @@ NrIceCtxHandler::Create(const std::strin
 
   return ctx;
 }
 
 
 RefPtr<NrIceMediaStream>
 NrIceCtxHandler::CreateStream(const std::string& name, int components)
 {
-  return NrIceMediaStream::Create(this, name, components);
+  // To make tracking NrIceMediaStreams easier during ICE restart
+  // prepend an int to the name that increments with each ICE restart
+  std::ostringstream os;
+  os << restart_count << "-" << name;
+  return NrIceMediaStream::Create(this, os.str(), components);
+}
+
+
+void
+NrIceCtxHandler::SwapContextData(NrIceCtxHandler& ctx1, NrIceCtxHandler& ctx2)
+{
+  ctx1.streams_.swap(ctx2.streams_);
+
+  ConnectionState tmp_connection_state      = ctx1.connection_state_;
+  GatheringState tmp_gathering_state        = ctx1.gathering_state_;
+  nr_ice_ctx *tmp_ctx                       = ctx1.ctx_;
+  nr_ice_peer_ctx *tmp_peer_ctx             = ctx1.peer_;
+  nr_ice_handler_vtbl *tmp_ice_handler_vtbl = ctx1.ice_handler_vtbl_;
+  nr_ice_handler *tmp_ice_handler           = ctx1.ice_handler_;
+  bool tmp_trickle                          = ctx1.trickle_;
+  nsCOMPtr<nsIEventTarget> tmp_sts_target   = ctx1.sts_target_;
+
+  ctx1.connection_state_ = ctx2.connection_state_;
+  ctx1.gathering_state_  = ctx2.gathering_state_;
+  ctx1.ctx_              = ctx2.ctx_;
+  ctx1.peer_             = ctx2.peer_;
+  ctx1.ice_handler_vtbl_ = ctx2.ice_handler_vtbl_;
+  ctx1.ice_handler_      = ctx2.ice_handler_;
+  if (ctx1.ice_handler_) {
+    ctx1.ice_handler_->obj = &ctx1;
+  }
+  ctx1.trickle_          = ctx2.trickle_;
+  ctx1.sts_target_       = ctx2.sts_target_;
+
+  ctx2.connection_state_ = tmp_connection_state;
+  ctx2.gathering_state_ = tmp_gathering_state;
+  ctx2.ctx_ = tmp_ctx;
+  ctx2.peer_ = tmp_peer_ctx;
+  ctx2.ice_handler_vtbl_ = tmp_ice_handler_vtbl;
+  ctx2.ice_handler_ = tmp_ice_handler;
+  if (ctx2.ice_handler_) {
+    ctx2.ice_handler_->obj = &ctx2;
+  }
+  ctx2.trickle_ = tmp_trickle;
+  ctx2.sts_target_ = tmp_sts_target;
+}
+
+
+void
+NrIceCtxHandler::BeginIceRestart()
+{
+  std::string ufrag = GetNewUfrag();
+  std::string pwd = GetNewPwd();
+  BeginIceRestart(ufrag, pwd);
+}
+
+void
+NrIceCtxHandler::BeginIceRestart(const std::string& ufrag,
+                                 const std::string& pwd)
+{
+  MOZ_ASSERT(!restart_ctx, "existing ice restart in progress");
+  RefPtr<NrIceCtxHandler> new_ctx = new NrIceCtxHandler(name_, offerer_, policy_);
+
+  // reconstitute the hide_non_default bool based on the flags stored in the
+  // original ctx flags
+  bool hide_non_default = ctx_->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS;
+
+  SwapContextData(*new_ctx, *this);
+
+  MOZ_ASSERT(NrIceCtx::Initialize(this,
+                                  hide_non_default,
+                                  ufrag,
+                                  pwd));
+
+  ++restart_count;
+  restart_ctx = new_ctx;
+}
+
+
+void
+NrIceCtxHandler::FinalizeIceRestart()
+{
+  // no harm calling this even if we're not in the middle of restarting
+  restart_ctx = nullptr;
+}
+
+
+void
+NrIceCtxHandler::RollbackIceRestart()
+{
+  if (restart_ctx == nullptr) {
+    return;
+  }
+  SwapContextData(*restart_ctx, *this);
+  restart_ctx = nullptr;
 }
 
 
 } // close namespace
--- a/media/mtransport/nricectxhandler.h
+++ b/media/mtransport/nricectxhandler.h
@@ -15,22 +15,35 @@ public:
                                  bool allow_link_local = false,
                                  bool hide_non_default = false,
                                  Policy policy = ICE_POLICY_ALL);
 
   // Create a media stream
   RefPtr<NrIceMediaStream> CreateStream(const std::string& name,
                                         int components);
 
+  void BeginIceRestart(); // used in testing
+  void BeginIceRestart(const std::string& ufrag,
+                       const std::string& pwd);
+  bool IsRestarting() { return (restart_ctx != nullptr); }
+  void FinalizeIceRestart();
+  void RollbackIceRestart();
+
 protected:
   NrIceCtxHandler(const std::string& name,
                   bool offerer,
                   Policy policy)
-  : NrIceCtx(name, offerer, policy)
-    {}
+  : NrIceCtx(name, offerer, policy),
+    restart_ctx(nullptr), restart_count(0) {}
 
   NrIceCtxHandler(); // disable
   virtual ~NrIceCtxHandler();
+
+  void SwapContextData(NrIceCtxHandler& ctx1, NrIceCtxHandler& ctx2);
+
+private:
+  RefPtr<NrIceCtxHandler> restart_ctx; // for while restart is in progress
+  int restart_count; // used to differentiate streams between restarted ctx
 };
 
 } // close namespace
 
 #endif // nricectxhandler_h__
--- a/media/mtransport/test/ice_unittest.cpp
+++ b/media/mtransport/test/ice_unittest.cpp
@@ -564,16 +564,60 @@ class IceTestPeer : public sigslot::has_
         WrapRunnableRet(&result, this, &IceTestPeer::is_ready_s, stream),
         NS_DISPATCH_SYNC);
     return result;
   }
   bool ice_complete() { return ice_complete_; }
   bool ice_reached_checking() { return ice_reached_checking_; }
   size_t received() { return received_; }
   size_t sent() { return sent_; }
+  void resetStats() { received_ = sent_ = 0; }
+
+
+  void RestartIce() {
+    test_utils->sts_target()->Dispatch(
+        WrapRunnable(this, &IceTestPeer::RestartIce_s),
+        NS_DISPATCH_SYNC);
+  }
+
+
+  void RestartIce_s() {
+    ice_ctx_->BeginIceRestart();
+    // take care of some local bookkeeping
+    ready_ct_ = 0;
+    gathering_complete_ = false;
+    ice_complete_ = false;
+    ice_reached_checking_ = false;
+    remote_ = nullptr;
+  }
+
+
+  void FinalizeIceRestart() {
+    test_utils->sts_target()->Dispatch(
+        WrapRunnable(this, &IceTestPeer::FinalizeIceRestart_s),
+        NS_DISPATCH_SYNC);
+  }
+
+
+  void FinalizeIceRestart_s() {
+    ice_ctx_->FinalizeIceRestart();
+  }
+
+
+  void RollbackIceRestart() {
+    test_utils->sts_target()->Dispatch(
+        WrapRunnable(this, &IceTestPeer::RollbackIceRestart_s),
+        NS_DISPATCH_SYNC);
+  }
+
+
+  void RollbackIceRestart_s() {
+    ice_ctx_->RollbackIceRestart();
+  }
+
 
   // Start connecting to another peer
   void Connect_s(IceTestPeer *remote, TrickleMode trickle_mode,
                  bool start = true) {
     nsresult res;
 
     remote_ = remote;
 
@@ -2200,16 +2244,103 @@ TEST_F(IceConnectTest, TestGatherAutoPri
 
 
 TEST_F(IceConnectTest, TestConnect) {
   AddStream("first", 1);
   ASSERT_TRUE(Gather());
   Connect();
 }
 
+
+TEST_F(IceConnectTest, TestConnectRestartIce) {
+  AddStream("first", 1);
+  ASSERT_TRUE(Gather());
+  Connect();
+  SendReceive(p1_, p2_);
+  p1_->resetStats();
+  p2_->resetStats();
+
+  p2_->RestartIce();
+  ASSERT_FALSE(p2_->gathering_complete());
+
+  // verify p1 and p2 streams are still connected after restarting ice on p2
+  SendReceive(p1_, p2_);
+  p1_->resetStats();
+  p2_->resetStats();
+
+  mozilla::ScopedDeletePtr<IceTestPeer> p3_;
+  p3_ = new IceTestPeer("P3", true, false, false, false, false);
+  InitPeer(p3_);
+  p3_->AddStream(1);
+
+  p2_->AddStream(1);
+  ASSERT_TRUE(GatherCallerAndCallee(p2_, p3_));
+  std::cout << "-------------------------------------------------" << std::endl;
+  ConnectCallerAndCallee(p2_, p3_);
+  SendReceive(p1_, p2_); // p1 and p2 still connected
+  p2_->resetStats();
+  SendReceive(p3_, p2_); // p3 and p2 are now connected
+
+  p2_->FinalizeIceRestart();
+  p1_->resetStats();
+  p2_->resetStats();
+  p3_->resetStats();
+  SendReceive(p3_, p2_); // p3 and p2 are still connected
+
+  p1_->resetStats();
+  p2_->resetStats();
+  SendReceive(p1_, p2_, false, true); // p1 and p2 not connected
+
+  p3_ = nullptr;
+}
+
+
+TEST_F(IceConnectTest, TestConnectRestartIceThenAbort) {
+  AddStream("first", 1);
+  ASSERT_TRUE(Gather());
+  Connect();
+  SendReceive(p1_, p2_);
+  p1_->resetStats();
+  p2_->resetStats();
+
+  p2_->RestartIce();
+  ASSERT_FALSE(p2_->gathering_complete());
+
+  // verify p1 and p2 streams are still connected after restarting ice on p2
+  SendReceive(p1_, p2_);
+  p1_->resetStats();
+  p2_->resetStats();
+
+  mozilla::ScopedDeletePtr<IceTestPeer> p3_;
+  p3_ = new IceTestPeer("P3", true, false, false, false, false);
+  InitPeer(p3_);
+  p3_->AddStream(1);
+
+  p2_->AddStream(1);
+  ASSERT_TRUE(GatherCallerAndCallee(p2_, p3_));
+  std::cout << "-------------------------------------------------" << std::endl;
+  ConnectCallerAndCallee(p2_, p3_);
+  SendReceive(p1_, p2_); // p1 and p2 still connected
+  p2_->resetStats();
+  SendReceive(p3_, p2_); // p3 and p2 are now connected
+
+  p2_->RollbackIceRestart();
+  p1_->resetStats();
+  p2_->resetStats();
+  p3_->resetStats();
+  SendReceive(p1_, p2_); // p1 and p2 are still connected
+
+  p3_->resetStats();
+  p2_->resetStats();
+  SendReceive(p3_, p2_, false, true); // p3 and p2 not connected
+
+  p3_ = nullptr;
+}
+
+
 TEST_F(IceConnectTest, TestConnectTcp) {
   Init(false, true);
   AddStream("first", 1);
   ASSERT_TRUE(Gather());
   SetCandidateFilter(IsTcpCandidate);
   SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
     NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
   Connect();
--- a/media/mtransport/third_party/nICEr/src/ice/ice_ctx.c
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_ctx.c
@@ -314,38 +314,55 @@ int nr_ice_fetch_turn_servers(int ct, nr
     if (_status) RFREE(servers);
     return(_status);
   }
 #endif /* USE_TURN */
 
 #define MAXADDRS 100 /* Ridiculously high */
 int nr_ice_ctx_create(char *label, UINT4 flags, nr_ice_ctx **ctxp)
   {
+    int r,_status;
+    char *ufrag = 0;
+    char *pwd = 0;
+
+    if (r=nr_ice_get_new_ice_ufrag(&ufrag))
+      ABORT(r);
+    if (r=nr_ice_get_new_ice_pwd(&pwd))
+      ABORT(r);
+
+    if (r=nr_ice_ctx_create_with_credentials(label, flags, ufrag, pwd, ctxp))
+      ABORT(r);
+
+    _status=0;
+  abort:
+    RFREE(ufrag);
+    RFREE(pwd);
+
+    return(_status);
+  }
+
+int nr_ice_ctx_create_with_credentials(char *label, UINT4 flags, char *ufrag, char *pwd, nr_ice_ctx **ctxp)
+  {
     nr_ice_ctx *ctx=0;
     int r,_status;
-    char buf[100];
 
     if(r=r_log_register("ice", &LOG_ICE))
       ABORT(r);
 
     if(!(ctx=RCALLOC(sizeof(nr_ice_ctx))))
       ABORT(R_NO_MEMORY);
 
     ctx->flags=flags;
 
     if(!(ctx->label=r_strdup(label)))
       ABORT(R_NO_MEMORY);
 
-    if(r=nr_ice_random_string(buf,8))
-      ABORT(r);
-    if(!(ctx->ufrag=r_strdup(buf)))
+    if(!(ctx->ufrag=r_strdup(ufrag)))
       ABORT(r);
-    if(r=nr_ice_random_string(buf,32))
-      ABORT(r);
-    if(!(ctx->pwd=r_strdup(buf)))
+    if(!(ctx->pwd=r_strdup(pwd)))
       ABORT(r);
 
     /* Get the STUN servers */
     if(r=NR_reg_get_child_count(NR_ICE_REG_STUN_SRV_PRFX,
       (unsigned int *)&ctx->stun_server_ct)||ctx->stun_server_ct==0) {
       r_log(LOG_ICE,LOG_WARNING,"ICE(%s): No STUN servers specified", ctx->label);
       ctx->stun_server_ct=0;
     }
@@ -950,8 +967,45 @@ int nr_ice_ctx_hide_candidate(nr_ice_ctx
 
     if (ctx->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS) {
       if (cand->type == HOST)
         return 1;
     }
 
     return 0;
   }
+
+int nr_ice_get_new_ice_ufrag(char** ufrag)
+  {
+    int r,_status;
+    char buf[10];
+
+    if(r=nr_ice_random_string(buf,8))
+      ABORT(r);
+    if(!(*ufrag=r_strdup(buf)))
+      ABORT(r);
+
+    _status=0;
+  abort:
+    if(_status) {
+      RFREE(*ufrag);
+    }
+    return(_status);
+  }
+
+int nr_ice_get_new_ice_pwd(char** pwd)
+  {
+    int r,_status;
+    char buf[40];
+
+    if(r=nr_ice_random_string(buf,32))
+      ABORT(r);
+    if(!(*pwd=r_strdup(buf)))
+      ABORT(r);
+
+    _status=0;
+  abort:
+    if(_status) {
+      RFREE(*pwd);
+    }
+    return(_status);
+  }
+
--- a/media/mtransport/third_party/nICEr/src/ice/ice_ctx.h
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_ctx.h
@@ -150,16 +150,17 @@ struct nr_ice_ctx_ {
 
   nr_ice_trickle_candidate_cb trickle_cb;
   void *trickle_cb_arg;
 
   char force_net_interface[MAXIFNAME];
 };
 
 int nr_ice_ctx_create(char *label, UINT4 flags, nr_ice_ctx **ctxp);
+int nr_ice_ctx_create_with_credentials(char *label, UINT4 flags, char* ufrag, char* pwd, nr_ice_ctx **ctxp);
 #define NR_ICE_CTX_FLAGS_OFFERER                           1
 #define NR_ICE_CTX_FLAGS_ANSWERER                          (1<<1)
 #define NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION             (1<<2)
 #define NR_ICE_CTX_FLAGS_LITE                              (1<<3)
 #define NR_ICE_CTX_FLAGS_RELAY_ONLY                        (1<<4)
 #define NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS                (1<<5)
 
 int nr_ice_ctx_destroy(nr_ice_ctx **ctxp);
@@ -176,16 +177,18 @@ int nr_ice_ctx_finalize(nr_ice_ctx *ctx,
 int nr_ice_ctx_set_stun_servers(nr_ice_ctx *ctx,nr_ice_stun_server *servers, int ct);
 int nr_ice_ctx_set_turn_servers(nr_ice_ctx *ctx,nr_ice_turn_server *servers, int ct);
 int nr_ice_ctx_set_resolver(nr_ice_ctx *ctx, nr_resolver *resolver);
 int nr_ice_ctx_set_interface_prioritizer(nr_ice_ctx *ctx, nr_interface_prioritizer *prioritizer);
 int nr_ice_ctx_set_turn_tcp_socket_wrapper(nr_ice_ctx *ctx, nr_socket_wrapper_factory *wrapper);
 void nr_ice_ctx_set_socket_factory(nr_ice_ctx *ctx, nr_socket_factory *factory);
 int nr_ice_ctx_set_trickle_cb(nr_ice_ctx *ctx, nr_ice_trickle_candidate_cb cb, void *cb_arg);
 int nr_ice_ctx_hide_candidate(nr_ice_ctx *ctx, nr_ice_candidate *cand);
+int nr_ice_get_new_ice_ufrag(char** ufrag);
+int nr_ice_get_new_ice_pwd(char** pwd);
 
 #define NR_ICE_MAX_ATTRIBUTE_SIZE 256
 
 extern int LOG_ICE;
 
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
--- a/media/mtransport/transportlayerice.cpp
+++ b/media/mtransport/transportlayerice.cpp
@@ -80,44 +80,103 @@ namespace mozilla {
 
 #ifdef ERROR
 #undef ERROR
 #endif
 
 MOZ_MTLOG_MODULE("mtransport")
 
 TransportLayerIce::TransportLayerIce(const std::string& name)
-    : name_(name), ctx_(nullptr), stream_(nullptr), component_(0) {}
+    : name_(name),
+      ctx_(nullptr), stream_(nullptr), component_(0),
+      old_ctx_(nullptr), old_stream_(nullptr), old_component_(0)
+{
+  // setup happens later 
+}
 
 TransportLayerIce::~TransportLayerIce() {
   // No need to do anything here, since we use smart pointers
 }
 
 void TransportLayerIce::SetParameters(RefPtr<NrIceCtx> ctx,
                                       RefPtr<NrIceMediaStream> stream,
                                       int component) {
+  // If SetParameters is called and we already have a stream_, this means
+  // we're handling an ICE restart.  We need to hold the old stream until
+  // we know the new stream is working.
+  if (stream_ && !old_stream_) {
+    MOZ_ASSERT(stream_ != stream); // make sure we're getting a different stream
+    MOZ_ASSERT(old_ctx_ == nullptr);
+    MOZ_ASSERT(old_stream_ == nullptr);
+    MOZ_ASSERT(old_component_ == 0);
+    // Here we leave the old stream's signals connected until we don't need
+    // it anymore.  They will be disconnected if ice restart is successful.
+    old_ctx_ = ctx_;
+    old_stream_ = stream_;
+    old_component_ = component_;
+    MOZ_MTLOG(ML_INFO, LAYER_INFO << "SetParameters save old stream("
+      << old_stream_->name() << ","
+      << old_component_ << ")");
+  }
+
   ctx_ = ctx;
   stream_ = stream;
   component_ = component;
 
   PostSetup();
 }
 
 void TransportLayerIce::PostSetup() {
   target_ = ctx_->thread();
 
   stream_->SignalReady.connect(this, &TransportLayerIce::IceReady);
   stream_->SignalFailed.connect(this, &TransportLayerIce::IceFailed);
   stream_->SignalPacketReceived.connect(this,
                                         &TransportLayerIce::IcePacketReceived);
   if (stream_->state() == NrIceMediaStream::ICE_OPEN) {
     TL_SET_STATE(TS_OPEN);
+    // Reset old ice stream if new stream is good
+    ResetOldStream();
   }
 }
 
+void
+TransportLayerIce::ResetOldStream()
+{
+  if (old_stream_ == nullptr) {
+    return; // no work to do
+  }
+  // ICE Ready on the new stream, we can forget the old stream now
+  MOZ_MTLOG(ML_INFO, LAYER_INFO << "ResetOldStream(" << old_stream_->name() << ","
+    << old_component_ << ")");
+  old_stream_->SignalReady.disconnect(this);
+  old_stream_->SignalFailed.disconnect(this);
+  old_stream_->SignalPacketReceived.disconnect(this);
+  old_ctx_ = nullptr;
+  old_stream_ = nullptr;
+  old_component_ = 0;
+}
+
+void 
+TransportLayerIce::RestoreOldStream()
+{
+  if (old_stream_ == nullptr) {
+    return; // no work to do
+  }
+  // ICE Failed on the new stream, we need to restore the old stream now
+  MOZ_MTLOG(ML_INFO, LAYER_INFO << "RestoreOldStream(" << old_stream_->name() << ","
+    << old_component_ << ")");
+  stream_->SignalReady.disconnect(this);
+  stream_->SignalFailed.disconnect(this);
+  stream_->SignalPacketReceived.disconnect(this);
+  ctx_ = old_ctx_;
+  stream_ = old_stream_;
+  component_ = old_component_;
+}
+
 TransportResult TransportLayerIce::SendPacket(const unsigned char *data,
                                               size_t len) {
   CheckThread();
   nsresult res = stream_->SendPacket(component_, data, len);
 
   if (!NS_SUCCEEDED(res)) {
     return (res == NS_BASE_STREAM_WOULD_BLOCK) ?
         TE_WOULDBLOCK : TE_ERROR;
@@ -131,22 +190,32 @@ TransportResult TransportLayerIce::SendP
 
 void TransportLayerIce::IceCandidate(NrIceMediaStream *stream,
                                      const std::string&) {
   // NO-OP for now
 }
 
 void TransportLayerIce::IceReady(NrIceMediaStream *stream) {
   CheckThread();
+  MOZ_MTLOG(ML_INFO, LAYER_INFO << "ICE Ready(" << stream->name() << ","
+    << component_ << ")");
   TL_SET_STATE(TS_OPEN);
+  // Reset old ice stream if new stream is good after ice restart
+  // Possible to get IceReady call for old stream after restart? ?mjf?
+  ResetOldStream();
 }
 
 void TransportLayerIce::IceFailed(NrIceMediaStream *stream) {
   CheckThread();
+  MOZ_MTLOG(ML_INFO, LAYER_INFO << "ICE Failed(" << stream->name() << ","
+    << component_ << ")");
   TL_SET_STATE(TS_ERROR);
+  // Restore old ice stream if new stream fails during ice restart
+  // Possible to get IceFailed call for old stream after restart? ?mjf?
+  RestoreOldStream();
 }
 
 void TransportLayerIce::IcePacketReceived(NrIceMediaStream *stream, int component,
                        const unsigned char *data, int len) {
   CheckThread();
   // We get packets for both components, so ignore the ones that aren't
   // for us.
   if (component_ != component)
--- a/media/mtransport/transportlayerice.h
+++ b/media/mtransport/transportlayerice.h
@@ -49,17 +49,25 @@ class TransportLayerIce : public Transpo
   void IcePacketReceived(NrIceMediaStream *stream, int component,
                          const unsigned char *data, int len);
 
   TRANSPORT_LAYER_ID("ice")
 
  private:
   DISALLOW_COPY_ASSIGN(TransportLayerIce);
   void PostSetup();
+  void ResetOldStream(); // called after successful ice restart
+  void RestoreOldStream(); // called after unsuccessful ice restart
 
   const std::string name_;
   RefPtr<NrIceCtx> ctx_;
   RefPtr<NrIceMediaStream> stream_;
   int component_;
+
+  // used to hold the old ice ctx, stream, and component
+  // note: holding the old ctx and component are probably overkill (mjf)
+  RefPtr<NrIceCtx> old_ctx_;
+  RefPtr<NrIceMediaStream> old_stream_;
+  int old_component_;
 };
 
 }  // close namespace
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepSession.h
+++ b/media/webrtc/signaling/src/jsep/JsepSession.h
@@ -39,16 +39,17 @@ enum JsepSdpType {
   kJsepSdpRollback
 };
 
 struct JsepOAOptions {};
 struct JsepOfferOptions : public JsepOAOptions {
   Maybe<size_t> mOfferToReceiveAudio;
   Maybe<size_t> mOfferToReceiveVideo;
   Maybe<bool> mDontOfferDataChannel;
+  Maybe<bool> mIceRestart; // currently ignored by JsepSession
 };
 struct JsepAnswerOptions : public JsepOAOptions {};
 
 enum JsepBundlePolicy {
   kBundleBalanced,
   kBundleMaxCompat,
   kBundleMaxBundle
 };
@@ -81,16 +82,17 @@ public:
     return mNegotiations;
   }
 
   // Set up the ICE And DTLS data.
   virtual nsresult SetIceCredentials(const std::string& ufrag,
                                      const std::string& pwd) = 0;
   virtual nsresult SetBundlePolicy(JsepBundlePolicy policy) = 0;
   virtual bool RemoteIsIceLite() const = 0;
+  virtual bool RemoteIsIceRestarting() const = 0;
   virtual std::vector<std::string> GetIceOptions() const = 0;
 
   virtual nsresult AddDtlsFingerprint(const std::string& algorithm,
                                       const std::vector<uint8_t>& value) = 0;
 
   virtual nsresult AddAudioRtpExtension(const std::string& extensionName) = 0;
   virtual nsresult AddVideoRtpExtension(const std::string& extensionName) = 0;
 
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -148,16 +148,20 @@ JsepSessionImpl::RemoveTrack(const std::
 nsresult
 JsepSessionImpl::SetIceCredentials(const std::string& ufrag,
                                    const std::string& pwd)
 {
   mLastError.clear();
   mIceUfrag = ufrag;
   mIcePwd = pwd;
 
+  if (mCurrentLocalDescription.get()) {
+    mLocalIsIceRestarting = true;
+  }
+
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetBundlePolicy(JsepBundlePolicy policy)
 {
   mLastError.clear();
   if (mCurrentLocalDescription) {
@@ -635,28 +639,28 @@ JsepSessionImpl::CreateOffer(const JsepO
     JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
     return NS_ERROR_UNEXPECTED;
   }
 
   // Undo track assignments from a previous call to CreateOffer
   // (ie; if the track has not been negotiated yet, it doesn't necessarily need
   // to stay in the same m-section that it was in)
   for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    if (!trackWrapper.mTrack->GetNegotiatedDetails()) {
+    if (!trackWrapper.mTrack->GetNegotiatedDetails() || mLocalIsIceRestarting) {
       trackWrapper.mAssignedMLine.reset();
     }
   }
 
   UniquePtr<Sdp> sdp;
 
   // Make the basic SDP that is common to offer/answer.
   nsresult rv = CreateGenericSDP(&sdp);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mCurrentLocalDescription) {
+  if (mCurrentLocalDescription && !mLocalIsIceRestarting) {
     rv = AddReofferMsections(*mCurrentLocalDescription,
                              *GetAnswer(),
                              sdp.get());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Ensure that we have all the m-sections we need, and disable extras
   rv = SetupOfferMSections(options, sdp.get());
@@ -1203,16 +1207,40 @@ JsepSessionImpl::SetRemoteDescription(Js
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = ValidateRemoteDescription(*parsed);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool iceLite =
       parsed->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
 
+  // check for mismatch ufrag/pwd indicating ice restart
+  bool iceRestarting = false;
+  if (mCurrentRemoteDescription.get()) {
+    for (size_t i = 0;
+         !iceRestarting && i < mCurrentRemoteDescription->GetMediaSectionCount();
+         ++i) {
+      if (mSdpHelper.MsectionIsDisabled(parsed->GetMediaSection(i)) ||
+          mSdpHelper.MsectionIsDisabled(mCurrentRemoteDescription->GetMediaSection(i))) {
+        continue;
+      }
+
+      const SdpAttributeList& newAttrs(
+          parsed->GetMediaSection(i).GetAttributeList());
+      const SdpAttributeList& oldAttrs(
+          mCurrentRemoteDescription->GetMediaSection(i).GetAttributeList());
+
+      if ((newAttrs.GetIceUfrag() != oldAttrs.GetIceUfrag()) ||
+          (newAttrs.GetIcePwd() != oldAttrs.GetIcePwd())) {
+        MOZ_MTLOG(ML_DEBUG, "Mismatched ice creds indicate ICE restart");
+        iceRestarting = true;
+      }
+    }
+  }
+
   std::vector<std::string> iceOptions;
   if (parsed->GetAttributeList().HasAttribute(
           SdpAttribute::kIceOptionsAttribute)) {
     iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
   }
 
   switch (type) {
     case kJsepSdpOffer:
@@ -1224,16 +1252,17 @@ JsepSessionImpl::SetRemoteDescription(Js
       break;
     case kJsepSdpRollback:
       MOZ_CRASH(); // Handled above
   }
 
   if (NS_SUCCEEDED(rv)) {
     mRemoteIsIceLite = iceLite;
     mIceOptions = iceOptions;
+    mRemoteIsIceRestarting = iceRestarting;
   }
 
   return rv;
 }
 
 nsresult
 JsepSessionImpl::HandleNegotiatedSession(const UniquePtr<Sdp>& local,
                                          const UniquePtr<Sdp>& remote)
@@ -1520,17 +1549,19 @@ JsepSessionImpl::AddTransportAttributes(
 
 nsresult
 JsepSessionImpl::CopyPreviousTransportParams(const Sdp& oldAnswer,
                                              const Sdp& newOffer,
                                              Sdp* newLocal)
 {
   for (size_t i = 0; i < oldAnswer.GetMediaSectionCount(); ++i) {
     if (!mSdpHelper.MsectionIsDisabled(newLocal->GetMediaSection(i)) &&
-        mSdpHelper.AreOldTransportParamsValid(oldAnswer, newOffer, i)) {
+        mSdpHelper.AreOldTransportParamsValid(oldAnswer, newOffer, i) &&
+        !(mLocalIsIceRestarting || mRemoteIsIceRestarting)
+       ) {
       // If newLocal is an offer, this will be the number of components we used
       // last time, and if it is an answer, this will be the number of
       // components we've decided we're using now.
       size_t numComponents = mTransports[i]->mComponents;
       nsresult rv = mSdpHelper.CopyTransportParams(
           numComponents,
           mCurrentLocalDescription->GetMediaSection(i),
           &newLocal->GetMediaSection(i));
@@ -1836,42 +1867,16 @@ JsepSessionImpl::ValidateRemoteDescripti
   SdpHelper::BundledMids bundledMids;
   nsresult rv = GetNegotiatedBundledMids(&bundledMids);
   NS_ENSURE_SUCCESS(rv, rv);
 
   SdpHelper::BundledMids newBundledMids;
   rv = mSdpHelper.GetBundledMids(description, &newBundledMids);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  for (size_t i = 0;
-       i < mCurrentRemoteDescription->GetMediaSectionCount();
-       ++i) {
-    if (mSdpHelper.MsectionIsDisabled(description.GetMediaSection(i)) ||
-        mSdpHelper.MsectionIsDisabled(mCurrentRemoteDescription->GetMediaSection(i))) {
-      continue;
-    }
-
-    const SdpAttributeList& newAttrs(
-        description.GetMediaSection(i).GetAttributeList());
-    const SdpAttributeList& oldAttrs(
-        mCurrentRemoteDescription->GetMediaSection(i).GetAttributeList());
-
-    if ((newAttrs.GetIceUfrag() != oldAttrs.GetIceUfrag()) ||
-        (newAttrs.GetIcePwd() != oldAttrs.GetIcePwd())) {
-      JSEP_SET_ERROR("ICE restart is unsupported at this time "
-                     "(new remote description changes either the ice-ufrag "
-                     "or ice-pwd)" <<
-                     "ice-ufrag (old): " << oldAttrs.GetIceUfrag() <<
-                     "ice-ufrag (new): " << newAttrs.GetIceUfrag() <<
-                     "ice-pwd (old): " << oldAttrs.GetIcePwd() <<
-                     "ice-pwd (new): " << newAttrs.GetIcePwd());
-      return NS_ERROR_INVALID_ARG;
-    }
-  }
-
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::ValidateAnswer(const Sdp& offer, const Sdp& answer)
 {
   if (offer.GetMediaSectionCount() != answer.GetMediaSectionCount()) {
     JSEP_SET_ERROR("Offer and answer have different number of m-lines "
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
@@ -30,16 +30,18 @@ class JsepSessionImpl : public JsepSessi
 {
 public:
   JsepSessionImpl(const std::string& name, UniquePtr<JsepUuidGenerator> uuidgen)
       : JsepSession(name),
         mIsOfferer(false),
         mWasOffererLastTime(false),
         mIceControlling(false),
         mRemoteIsIceLite(false),
+        mLocalIsIceRestarting(false),
+        mRemoteIsIceRestarting(false),
         mBundlePolicy(kBundleBalanced),
         mSessionId(0),
         mSessionVersion(0),
         mUuidGen(Move(uuidgen)),
         mSdpHelper(&mLastError)
   {
   }
 
@@ -55,16 +57,22 @@ public:
                                      const std::string& pwd) override;
   nsresult SetBundlePolicy(JsepBundlePolicy policy) override;
 
   virtual bool
   RemoteIsIceLite() const override
   {
     return mRemoteIsIceLite;
   }
+  
+  virtual bool
+  RemoteIsIceRestarting() const override
+  {
+    return mRemoteIsIceRestarting;
+  }
 
   virtual std::vector<std::string>
   GetIceOptions() const override
   {
     return mIceOptions;
   }
 
   virtual nsresult AddDtlsFingerprint(const std::string& algorithm,
@@ -297,16 +305,18 @@ private:
   std::vector<JsepTrackPair> mNegotiatedTrackPairs;
 
   bool mIsOfferer;
   bool mWasOffererLastTime;
   bool mIceControlling;
   std::string mIceUfrag;
   std::string mIcePwd;
   bool mRemoteIsIceLite;
+  bool mLocalIsIceRestarting;
+  bool mRemoteIsIceRestarting;
   std::vector<std::string> mIceOptions;
   JsepBundlePolicy mBundlePolicy;
   std::vector<JsepDtlsFingerprint> mDtlsFingerprints;
   uint64_t mSessionId;
   uint64_t mSessionVersion;
   std::vector<SdpExtmapAttributeList::Extmap> mAudioRtpExtensions;
   std::vector<SdpExtmapAttributeList::Extmap> mVideoRtpExtensions;
   UniquePtr<JsepUuidGenerator> mUuidGen;
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
+++ b/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
@@ -194,28 +194,56 @@ FinalizeTransportFlow_s(RefPtr<PeerConne
   for (auto i = aLayerList->values.begin(); i != aLayerList->values.end();
        ++i) {
     layerQueue->push(*i);
   }
   aLayerList->values.clear();
   (void)aFlow->PushLayers(layerQueue); // TODO(bug 854518): Process errors.
 }
 
+static void
+AddNewIceStreamForRestart_s(RefPtr<PeerConnectionMedia> aPCMedia,
+                            RefPtr<TransportFlow> aFlow,
+                            size_t aLevel,
+                            bool aIsRtcp)
+{
+  TransportLayerIce* ice =
+      static_cast<TransportLayerIce*>(aFlow->GetLayer("ice"));
+  ice->SetParameters(aPCMedia->ice_ctx(),
+                     aPCMedia->ice_media_stream(aLevel),
+                     aIsRtcp ? 2 : 1);
+}
+
 nsresult
 MediaPipelineFactory::CreateOrGetTransportFlow(
     size_t aLevel,
     bool aIsRtcp,
     const JsepTransport& aTransport,
     RefPtr<TransportFlow>* aFlowOutparam)
 {
   nsresult rv;
   RefPtr<TransportFlow> flow;
 
   flow = mPCMedia->GetTransportFlow(aLevel, aIsRtcp);
   if (flow) {
+    if (mPCMedia->ice_ctx()->IsRestarting()) {
+      MOZ_MTLOG(ML_INFO, "Flow[" << flow->id() << "]: "
+                                 << "detected ICE restart - level: "
+                                 << aLevel << " rtcp: " << aIsRtcp);
+
+      rv = mPCMedia->GetSTSThread()->Dispatch(
+          WrapRunnableNM(AddNewIceStreamForRestart_s,
+                         mPCMedia, flow, aLevel, aIsRtcp),
+          NS_DISPATCH_NORMAL);
+      if (NS_FAILED(rv)) {
+        MOZ_MTLOG(ML_ERROR, "Failed to dispatch AddNewIceStreamForRestart_s");
+        return rv;
+      }
+    }
+
     *aFlowOutparam = flow;
     return NS_OK;
   }
 
   std::ostringstream osId;
   osId << mPC->GetHandle() << ":" << aLevel << ","
        << (aIsRtcp ? "rtcp" : "rtp");
   flow = new TransportFlow(osId.str());
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -1481,26 +1481,32 @@ PeerConnectionImpl::NotifyDataChannel(al
 #endif
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::CreateOffer(const RTCOfferOptions& aOptions)
 {
   JsepOfferOptions options;
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  // convert the RTCOfferOptions to JsepOfferOptions
   if (aOptions.mOfferToReceiveAudio.WasPassed()) {
     options.mOfferToReceiveAudio =
       mozilla::Some(size_t(aOptions.mOfferToReceiveAudio.Value()));
   }
 
   if (aOptions.mOfferToReceiveVideo.WasPassed()) {
     options.mOfferToReceiveVideo =
         mozilla::Some(size_t(aOptions.mOfferToReceiveVideo.Value()));
   }
 
+  if (aOptions.mIceRestart.WasPassed()) {
+    options.mIceRestart =
+      mozilla::Some(aOptions.mIceRestart.Value());
+  }
+
   if (aOptions.mMozDontOfferDataChannel.WasPassed()) {
     options.mDontOfferDataChannel =
       mozilla::Some(aOptions.mMozDontOfferDataChannel.Value());
   }
 #endif
   return CreateOffer(options);
 }
 
@@ -1533,17 +1539,34 @@ PeerConnectionImpl::CreateOffer(const Js
     PeerConnectionCtx::GetInstance()->queueJSEPOperation(
         WrapRunnableNM(DeferredCreateOffer, mHandle, aOptions));
     STAMP_TIMECARD(mTimeCard, "Deferring CreateOffer (not ready)");
     return NS_OK;
   }
 
   CSFLogDebug(logTag, "CreateOffer()");
 
-  nsresult nrv = ConfigureJsepSessionCodecs();
+  nsresult nrv;
+  if (aOptions.mIceRestart.isSome() && *(aOptions.mIceRestart)) {
+    CSFLogInfo(logTag, "Offerer restarting ice");
+    std::string ufrag = mMedia->ice_ctx()->GetNewUfrag();
+    std::string pwd = mMedia->ice_ctx()->GetNewPwd();
+
+    mMedia->BeginIceRestart(ufrag, pwd);
+
+    nrv = mJsepSession->SetIceCredentials(ufrag, pwd);
+    if (NS_FAILED(nrv)) {
+      CSFLogError(logTag, "%s: Couldn't set ICE credentials, res=%u",
+                           __FUNCTION__,
+                           static_cast<unsigned>(nrv));
+      return nrv;
+    }
+  }
+
+  nrv = ConfigureJsepSessionCodecs();
   if (NS_FAILED(nrv)) {
     CSFLogError(logTag, "Failed to configure codecs");
     return nrv;
   }
 
   STAMP_TIMECARD(mTimeCard, "Create Offer");
 
   std::string offer;
@@ -1578,23 +1601,41 @@ PeerConnectionImpl::CreateAnswer()
   PC_AUTO_ENTER_API_CALL(true);
 
   RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
   if (!pco) {
     return NS_OK;
   }
 
   CSFLogDebug(logTag, "CreateAnswer()");
+
+  nsresult nrv;
+  if (mJsepSession->RemoteIsIceRestarting()) {
+    CSFLogInfo(logTag, "Answerer restarting ice");
+    std::string ufrag = mMedia->ice_ctx()->GetNewUfrag();
+    std::string pwd = mMedia->ice_ctx()->GetNewPwd();
+
+    mMedia->BeginIceRestart(ufrag, pwd);
+
+    nrv = mJsepSession->SetIceCredentials(ufrag, pwd);
+    if (NS_FAILED(nrv)) {
+      CSFLogError(logTag, "%s: Couldn't set ICE credentials, res=%u",
+                           __FUNCTION__,
+                           static_cast<unsigned>(nrv));
+      return nrv;
+    }
+  }
+
   STAMP_TIMECARD(mTimeCard, "Create Answer");
   // TODO(bug 1098015): Once RTCAnswerOptions is standardized, we'll need to
   // add it as a param to CreateAnswer, and convert it here.
   JsepAnswerOptions options;
   std::string answer;
 
-  nsresult nrv = mJsepSession->CreateAnswer(options, &answer);
+  nrv = mJsepSession->CreateAnswer(options, &answer);
   JSErrorResult rv;
   if (NS_FAILED(nrv)) {
     Error error;
     switch (nrv) {
       case NS_ERROR_UNEXPECTED:
         error = kInvalidState;
         break;
       default:
@@ -2734,16 +2775,20 @@ PeerConnectionImpl::SetSignalingState_m(
        !rollback)) {
     mMedia->EnsureTransports(*mJsepSession);
   }
 
   mSignalingState = aSignalingState;
 
   bool fireNegotiationNeeded = false;
   if (mSignalingState == PCImplSignalingState::SignalingStable) {
+    if (rollback && mMedia->ice_ctx()->IsRestarting()) {
+      mMedia->RollbackIceRestart();
+    }
+
     // Either negotiation is done, or we've rolled back. In either case, we
     // need to re-evaluate whether further negotiation is required.
     mNegotiationNeeded = false;
     // If we're rolling back a local offer, we might need to remove some
     // transports, but nothing further needs to be done.
     mMedia->ActivateOrRemoveTransports(*mJsepSession);
     if (!rollback) {
       mMedia->UpdateMediaPipelines(*mJsepSession);
@@ -3025,16 +3070,23 @@ void PeerConnectionImpl::IceConnectionSt
           Telemetry::WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE,
           mAddCandidateErrorCount);
     }
   }
 #endif
 
   mIceConnectionState = domState;
 
+  if (mIceConnectionState == PCImplIceConnectionState::Connected ||
+      mIceConnectionState == PCImplIceConnectionState::Completed) {
+    if (mMedia->ice_ctx()->IsRestarting()) {
+      mMedia->FinalizeIceRestart();
+    }
+  }
+
   // Would be nice if we had a means of converting one of these dom enums
   // to a string that wasn't almost as much text as this switch statement...
   switch (mIceConnectionState) {
     case PCImplIceConnectionState::New:
       STAMP_TIMECARD(mTimeCard, "Ice state: new");
       break;
     case PCImplIceConnectionState::Checking:
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
@@ -593,16 +593,80 @@ PeerConnectionMedia::StartIceChecks_s(
   mIceCtx->SetControlling(aIsControlling ?
                           NrIceCtx::ICE_CONTROLLING :
                           NrIceCtx::ICE_CONTROLLED);
 
   mIceCtx->StartChecks();
 }
 
 void
+PeerConnectionMedia::BeginIceRestart(const std::string& ufrag,
+                                     const std::string& pwd)
+{
+  ASSERT_ON_THREAD(mMainThread);
+
+  RUN_ON_THREAD(GetSTSThread(),
+                WrapRunnable(
+                    RefPtr<PeerConnectionMedia>(this),
+                    &PeerConnectionMedia::BeginIceRestart_s,
+                    std::string(ufrag), // Make copies.
+                    std::string(pwd)),
+                NS_DISPATCH_NORMAL);
+}
+
+void
+PeerConnectionMedia::BeginIceRestart_s(const std::string& ufrag,
+                                       const std::string& pwd)
+{
+  ASSERT_ON_THREAD(mSTSThread);
+
+  ice_ctx()->BeginIceRestart(ufrag, pwd);
+}
+
+void
+PeerConnectionMedia::FinalizeIceRestart()
+{
+  ASSERT_ON_THREAD(mMainThread);
+
+  RUN_ON_THREAD(GetSTSThread(),
+                WrapRunnable(
+                    RefPtr<PeerConnectionMedia>(this),
+                    &PeerConnectionMedia::FinalizeIceRestart_s),
+                NS_DISPATCH_NORMAL);
+}
+
+void
+PeerConnectionMedia::FinalizeIceRestart_s()
+{
+  ASSERT_ON_THREAD(mSTSThread);
+
+  ice_ctx()->FinalizeIceRestart();
+}
+
+void
+PeerConnectionMedia::RollbackIceRestart()
+{
+  ASSERT_ON_THREAD(mMainThread);
+
+  RUN_ON_THREAD(GetSTSThread(),
+                WrapRunnable(
+                    RefPtr<PeerConnectionMedia>(this),
+                    &PeerConnectionMedia::RollbackIceRestart_s),
+                NS_DISPATCH_NORMAL);
+}
+
+void
+PeerConnectionMedia::RollbackIceRestart_s()
+{
+  ASSERT_ON_THREAD(mSTSThread);
+
+  ice_ctx()->RollbackIceRestart();
+}
+
+void
 PeerConnectionMedia::AddIceCandidate(const std::string& candidate,
                                      const std::string& mid,
                                      uint32_t aMLine) {
   RUN_ON_THREAD(GetSTSThread(),
                 WrapRunnable(
                     RefPtr<PeerConnectionMedia>(this),
                     &PeerConnectionMedia::AddIceCandidate_s,
                     std::string(candidate), // Make copies.
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
@@ -248,16 +248,24 @@ class PeerConnectionMedia : public sigsl
 
   // Activate or remove ICE transports at the conclusion of offer/answer,
   // or when rollback occurs.
   void ActivateOrRemoveTransports(const JsepSession& aSession);
 
   // Start ICE checks.
   void StartIceChecks(const JsepSession& session);
 
+  // Begin ICE restart
+  void BeginIceRestart(const std::string& ufrag,
+                       const std::string& pwd);
+  // Finalize ICE restart
+  void FinalizeIceRestart();
+  // Abort ICE restart
+  void RollbackIceRestart();
+
   // Process a trickle ICE candidate.
   void AddIceCandidate(const std::string& candidate, const std::string& mid,
                        uint32_t aMLine);
 
   // Handle complete media pipelines.
   nsresult UpdateMediaPipelines(const JsepSession& session);
 
   // Add a track (main thread only)
@@ -437,16 +445,21 @@ class PeerConnectionMedia : public sigsl
   void GatherIfReady();
   void FlushIceCtxOperationQueueIfReady();
   void PerformOrEnqueueIceCtxOperation(nsIRunnable* runnable);
   void EnsureIceGathering_s();
   void StartIceChecks_s(bool aIsControlling,
                         bool aIsIceLite,
                         const std::vector<std::string>& aIceOptionsList);
 
+  void BeginIceRestart_s(const std::string& ufrag,
+                         const std::string& pwd);
+  void FinalizeIceRestart_s();
+  void RollbackIceRestart_s();
+
   // Process a trickle ICE candidate.
   void AddIceCandidate_s(const std::string& aCandidate, const std::string& aMid,
                          uint32_t aMLine);
 
 
   // ICE events
   void IceGatheringStateChange_s(NrIceCtx* ctx,
                                NrIceCtx::GatheringState state);
--- a/media/webrtc/signaling/test/signaling_unittests.cpp
+++ b/media/webrtc/signaling/test/signaling_unittests.cpp
@@ -83,16 +83,21 @@ class OfferOptions : public mozilla::Jse
 public:
   void setInt32Option(const char *namePtr, size_t value) {
     if (!strcmp(namePtr, "OfferToReceiveAudio")) {
       mOfferToReceiveAudio = mozilla::Some(value);
     } else if (!strcmp(namePtr, "OfferToReceiveVideo")) {
       mOfferToReceiveVideo = mozilla::Some(value);
     }
   }
+  void setBoolOption(const char* namePtr, bool value) {
+    if (!strcmp(namePtr, "IceRestart")) {
+      mIceRestart = mozilla::Some(value);
+    }
+  }
 private:
 };
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 // XXX Workaround for bug 998092 to maintain the existing broken semantics
 template<>
@@ -529,17 +534,17 @@ class ParsedSDP {
     }
   }
 
   void DeleteLine(const std::string &objType)
   {
     DeleteLines(objType, 1);
   }
 
-  // Replaces the first instance of objType in the SDP with
+  // Replaces the index-th instance of objType in the SDP with
   // a new string.
   // If content is an empty string then the line will be removed
   void ReplaceLine(const std::string &objType,
                    const std::string &content,
                    size_t index = 0)
   {
     auto it = FindLine(objType, index);
     if(it != sdp_lines_.end()) {
@@ -2529,22 +2534,16 @@ TEST_P(SignalingTest, RenegotiationAnswe
   // ANSWER_AUDIO causes a new audio track to be added
   OfferAnswer(options, ANSWER_AUDIO);
 
   CloseStreams();
 }
 
 TEST_P(SignalingTest, BundleRenegotiation)
 {
-  if (UseBundle()) {
-    // We don't support ICE restart, which is a prereq for renegotiating bundle
-    // off.
-    return;
-  }
-
   OfferOptions options;
   OfferAnswer(options, OFFER_AV | ANSWER_AV);
 
   // If we did bundle before, turn it off, if not, turn it on
   if (a1_->mBundleEnabled && a2_->mBundleEnabled) {
     a1_->SetBundleEnabled(false);
   } else {
     a1_->SetBundleEnabled(true);
@@ -3438,16 +3437,27 @@ TEST_P(SignalingTest, AudioOnlyG722Rejec
   ASSERT_EQ(a2_->getLocalDescription().find("a=rtpmap:9 G722/8000"), std::string::npos);
 
   CheckPipelines();
   CheckStreams();
 
   CloseStreams();
 }
 
+TEST_P(SignalingTest, RestartIce)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV);
+
+  options.setBoolOption("IceRestart", true);
+  OfferAnswer(options, OFFER_NONE);
+
+  CloseStreams();
+}
+
 TEST_P(SignalingTest, FullCallAudioNoMuxVideoMux)
 {
   if (UseBundle()) {
     // This test doesn't make sense for bundle
     return;
   }
 
   EnsureInit();