Bug 1213773: Better handling of answer with direction of inactive in renegotiation. r=mt
authorByron Campen [:bwc] <docfaraday@gmail.com>
Fri, 05 Feb 2016 13:21:11 -0600
changeset 339587 b4ef8b4ff0bcdba936ee3a42e93ed7ad96516cc4
parent 339586 a61e4c04aadb7de253803e9ed44211bcb3a45948
child 339588 d174ac797c08e621f5ed59139ea38ed82927deed
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt
bugs1213773
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1213773: Better handling of answer with direction of inactive in renegotiation. r=mt MozReview-Commit-ID: Fjo2rBtIYvD
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/sdpUtils.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html
dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html
dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html
media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
media/webrtc/signaling/src/jsep/JsepTrack.h
media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
media/webrtc/signaling/test/jsep_session_unittest.cpp
media/webrtc/signaling/test/mediaconduit_unittests.cpp
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -456,16 +456,34 @@ function waitUntil(func, time) {
     }, time || 200);
   });
 }
 
 /** Time out while waiting for a promise to get resolved or rejected. */
 var timeout = (promise, time, msg) =>
   Promise.race([promise, wait(time).then(() => Promise.reject(new Error(msg)))]);
 
+/** Adds a |finally| function to a promise whose argument is invoked whether the
+ * promise is resolved or rejected, and that does not interfere with chaining.*/
+var addFinallyToPromise = promise => {
+  promise.finally = func => {
+    return promise.then(
+      result => {
+        func();
+        return Promise.resolve(result);
+      },
+      error => {
+        func();
+        return Promise.reject(error);
+      }
+    );
+  }
+  return promise;
+}
+
 /** Use event listener to call passed-in function on fire until it returns true */
 var listenUntil = (target, eventName, onFire) => {
   return new Promise(resolve => target.addEventListener(eventName,
                                                         function callback() {
     var result = onFire();
     if (result) {
       target.removeEventListener(eventName, callback, false);
       resolve(result);
@@ -615,113 +633,115 @@ CommandChain.prototype = {
    * Add new commands to the end of the chain
    */
   append: function(commands) {
     this.commands = this.commands.concat(commands);
   },
 
   /**
    * Returns the index of the specified command in the chain.
-   * @param {start} Optional param specifying the index at which the search will
-   * start. If not specified, the search starts at index 0.
+   * @param {occurrence} Optional param specifying which occurrence to match,
+   * with 0 representing the first occurrence.
    */
-  indexOf: function(functionOrName, start) {
-    start = start || 0;
-    if (typeof functionOrName === 'string') {
-      var index = this.commands.slice(start).findIndex(f => f.name === functionOrName);
-      if (index !== -1) {
-        index += start;
+  indexOf: function(functionOrName, occurrence) {
+    occurrence = occurrence || 0;
+    return this.commands.findIndex(func => {
+      if (typeof functionOrName === 'string') {
+        if (func.name !== functionOrName) {
+          return false;
+        }
+      } else if (func !== functionOrName) {
+        return false;
       }
-      return index;
-    }
-    return this.commands.indexOf(functionOrName, start);
+      if (occurrence) {
+        --occurrence;
+        return false;
+      }
+      return true;
+    });
   },
 
-  mustHaveIndexOf: function(functionOrName, start) {
-    var index = this.indexOf(functionOrName, start);
+  mustHaveIndexOf: function(functionOrName, occurrence) {
+    var index = this.indexOf(functionOrName, occurrence);
     if (index == -1) {
       throw new Error("Unknown test: " + functionOrName);
     }
     return index;
   },
 
   /**
    * Inserts the new commands after the specified command.
    */
-  insertAfter: function(functionOrName, commands, all, start) {
-    this._insertHelper(functionOrName, commands, 1, all, start);
+  insertAfter: function(functionOrName, commands, all, occurrence) {
+    this._insertHelper(functionOrName, commands, 1, all, occurrence);
   },
 
   /**
    * Inserts the new commands after every occurrence of the specified command
    */
   insertAfterEach: function(functionOrName, commands) {
     this._insertHelper(functionOrName, commands, 1, true);
   },
 
   /**
    * Inserts the new commands before the specified command.
    */
-  insertBefore: function(functionOrName, commands, all, start) {
-    this._insertHelper(functionOrName, commands, 0, all, start);
+  insertBefore: function(functionOrName, commands, all, occurrence) {
+    this._insertHelper(functionOrName, commands, 0, all, occurrence);
   },
 
-  _insertHelper: function(functionOrName, commands, delta, all, start) {
-    var index = this.mustHaveIndexOf(functionOrName);
-    start = start || 0;
-    for (; index !== -1; index = this.indexOf(functionOrName, index)) {
-      if (!start) {
-        this.commands = [].concat(
-          this.commands.slice(0, index + delta),
-          commands,
-          this.commands.slice(index + delta));
-        if (!all) {
-          break;
-        }
-      } else {
-        start -= 1;
+  _insertHelper: function(functionOrName, commands, delta, all, occurrence) {
+    occurrence = occurrence || 0;
+    for (var index = this.mustHaveIndexOf(functionOrName, occurrence);
+         index !== -1;
+         index = this.indexOf(functionOrName, ++occurrence)) {
+      this.commands = [].concat(
+        this.commands.slice(0, index + delta),
+        commands,
+        this.commands.slice(index + delta));
+      if (!all) {
+        break;
       }
-      index += (commands.length + 1);
     }
   },
 
   /**
    * Removes the specified command, returns what was removed.
    */
-  remove: function(functionOrName) {
-    return this.commands.splice(this.mustHaveIndexOf(functionOrName), 1);
+  remove: function(functionOrName, occurrence) {
+    return this.commands.splice(this.mustHaveIndexOf(functionOrName, occurrence), 1);
   },
 
   /**
    * Removes all commands after the specified one, returns what was removed.
    */
-  removeAfter: function(functionOrName, start) {
-    return this.commands.splice(this.mustHaveIndexOf(functionOrName, start) + 1);
+  removeAfter: function(functionOrName, occurrence) {
+    return this.commands.splice(this.mustHaveIndexOf(functionOrName, occurrence) + 1);
   },
 
   /**
    * Removes all commands before the specified one, returns what was removed.
    */
-  removeBefore: function(functionOrName) {
-    return this.commands.splice(0, this.mustHaveIndexOf(functionOrName));
+  removeBefore: function(functionOrName, occurrence) {
+    return this.commands.splice(0, this.mustHaveIndexOf(functionOrName, occurrence));
   },
 
   /**
    * Replaces a single command, returns what was removed.
    */
   replace: function(functionOrName, commands) {
     this.insertBefore(functionOrName, commands);
     return this.remove(functionOrName);
   },
 
   /**
    * Replaces all commands after the specified one, returns what was removed.
    */
-  replaceAfter: function(functionOrName, commands, start) {
-    var oldCommands = this.removeAfter(functionOrName, start);
+  replaceAfter: function(functionOrName, commands, occurrence) {
+    var oldCommands = this.removeAfter(functionOrName, occurrence);
     this.append(commands);
     return oldCommands;
   },
 
   /**
    * Replaces all commands before the specified one, returns what was removed.
    */
   replaceBefore: function(functionOrName, commands) {
@@ -733,16 +753,91 @@ CommandChain.prototype = {
   /**
    * Remove all commands whose name match the specified regex.
    */
   filterOut: function (id_match) {
     this.commands = this.commands.filter(c => !id_match.test(c.name));
   },
 };
 
+function AudioStreamHelper() {
+  this._context = new AudioContext();
+}
+
+AudioStreamHelper.prototype = {
+  checkAudio: function(stream, analyser, fun) {
+    analyser.enableDebugCanvas();
+    return analyser.waitForAnalysisSuccess(fun)
+      .then(() => analyser.disableDebugCanvas());
+  },
+
+  checkAudioFlowing: function(stream) {
+    var analyser = new AudioStreamAnalyser(this._context, stream);
+    var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+    return this.checkAudio(stream, analyser, array => array[freq] > 200);
+  },
+
+  checkAudioNotFlowing: function(stream) {
+    var analyser = new AudioStreamAnalyser(this._context, stream);
+    var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+    return this.checkAudio(stream, analyser, array => array[freq] < 50);
+  }
+}
+
+function VideoStreamHelper() {
+  this._helper = new CaptureStreamTestHelper2D(50,50);
+  this._canvas = this._helper.createAndAppendElement('canvas', 'source_canvas');
+  // Make sure this is initted
+  this._helper.drawColor(this._canvas, this._helper.green);
+  this._stream = this._canvas.captureStream(10);
+}
+
+VideoStreamHelper.prototype = {
+  stream: function() {
+    return this._stream;
+  },
+
+  startCapturingFrames: function() {
+    var i = 0;
+    var helper = this;
+    return setInterval(function() {
+      try {
+        helper._helper.drawColor(helper._canvas,
+                                 i ? helper._helper.green : helper._helper.red);
+        i = 1 - i;
+        helper._stream.requestFrame();
+      } catch (e) {
+        // ignore; stream might have shut down, and we don't bother clearing
+        // the setInterval.
+      }
+    }, 100);
+  },
+
+  waitForFrames: function(canvas, timeout_value) {
+    var intervalId = this.startCapturingFrames();
+
+    return addFinallyToPromise(timeout(
+      Promise.all([
+        this._helper.waitForPixelColor(canvas, this._helper.green, 128,
+                                       canvas.id + " should become green"),
+        this._helper.waitForPixelColor(canvas, this._helper.red, 128,
+                                       canvas.id + " should become red")
+      ]),
+      2000,
+      "Timed out waiting for frames")).finally(() => clearInterval(intervalId));
+  },
+
+  verifyNoFrames: function(canvas) {
+    return this.waitForFrames(canvas).then(
+      () => ok(false, "Color should not change"),
+      () => ok(true, "Color should not change")
+    );
+  }
+}
+
 
 function IsMacOSX10_6orOlder() {
   if (navigator.platform.indexOf("Mac") !== 0) {
     return false;
   }
 
   var version = Cc["@mozilla.org/system-info;1"]
       .getService(Ci.nsIPropertyBag2)
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -254,16 +254,22 @@ skip-if = toolkit == 'gonk' # B2G emulat
 [test_peerConnection_addDataChannelNoBundle.html]
 # b2g(Bug 960442, video support for WebRTC is disabled on b2g), android(Bug 1189784, timeouts on 4.3 emulator)
 skip-if = toolkit == 'gonk' || (android_version == '18' && debug) # b2g(emulator seems to be so slow that DTLS cannot establish properly), android(bug 1240256, intermittent ICE failures starting w/bug 1232082, possibly from timeout)
 [test_peerConnection_verifyAudioAfterRenegotiation.html]
 skip-if = toolkit == 'gonk' || (android_version == '18' && debug) # B2G emulator is too slow to finish a renegotiation test in under 5 minutes, android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_verifyVideoAfterRenegotiation.html]
 # B2G emulator is too slow to finish a renegotiation test in under 5 minutes, Bug 1180000 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
 skip-if = toolkit == 'gonk' || android_version == '18'
+[test_peerConnection_audioRenegotiationInactiveAnswer.html]
+# B2G emulator is too slow to finish a renegotiation test in under 5 minutes, Bug 1180000 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
+skip-if = toolkit == 'gonk' || android_version == '18'
+[test_peerConnection_videoRenegotiationInactiveAnswer.html]
+# B2G emulator is too slow to finish a renegotiation test in under 5 minutes, Bug 1180000 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
+skip-if = toolkit == 'gonk' || android_version == '18'
 [test_peerConnection_webAudio.html]
 tags = webaudio webrtc
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_localRollback.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_localReofferRollback.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_remoteRollback.html]
--- a/dom/media/tests/mochitest/sdpUtils.js
+++ b/dom/media/tests/mochitest/sdpUtils.js
@@ -44,16 +44,22 @@ removeRtcpMux: function(sdp) {
 removeBundle: function(sdp) {
   return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
 },
 
 reduceAudioMLineToPcmuPcma: function(sdp) {
   return sdp.replace(/m=audio .*\r\n/g, "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n");
 },
 
+setAllMsectionsInactive: function(sdp) {
+  return sdp.replace(/\r\na=sendrecv/g, "\r\na=inactive")
+            .replace(/\r\na=sendonly/g, "\r\na=inactive")
+            .replace(/\r\na=recvonly/g, "\r\na=inactive");
+},
+
 removeAllRtpMaps: function(sdp) {
   return sdp.replace(/a=rtpmap:.*\r\n/g, "");
 },
 
 reduceAudioMLineToDynamicPtAndOpus: function(sdp) {
   return sdp.replace(/m=audio .*\r\n/g, "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n");
 },
 
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -468,20 +468,20 @@ var commandsPeerConnectionOfferAnswer = 
 
   function PC_LOCAL_CHECK_MSID(test) {
     return test.pcLocal.checkMsids();
   },
   function PC_REMOTE_CHECK_MSID(test) {
     return test.pcRemote.checkMsids();
   },
 
-  function PC_LOCAL_CHECK_STATS(test) {
+  function PC_LOCAL_CHECK_TRACK_STATS(test) {
     return checkAllTrackStats(test.pcLocal);
   },
-  function PC_REMOTE_CHECK_STATS(test) {
+  function PC_REMOTE_CHECK_TRACK_STATS(test) {
     return checkAllTrackStats(test.pcRemote);
   },
   function PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
     if (test.pcLocal.endOfTrickleSdp) {
       /* In case the endOfTrickleSdp promise is resolved already it will win the
        * race because it gets evaluated first. But if endOfTrickleSdp is still
        * pending the rejection will win the race. */
       return Promise.race([
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1213773",
+    title: "Renegotiation: answerer uses a=inactive for audio"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    var helper = new AudioStreamHelper();
+
+    test = new PeerConnectionTest(options);
+    test.setMediaConstraints([{audio: true}], []);
+
+    test.chain.append([
+      function PC_REMOTE_CHECK_AUDIO_FLOWING() {
+        return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+      }
+    ]);
+
+    addRenegotiation(test.chain, []);
+
+    test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+        function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+          test._remote_answer.sdp =
+            sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+        }
+    ], false, 1);
+
+    test.chain.append([
+      function PC_REMOTE_CHECK_AUDIO_NOT_FLOWING() {
+        return helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+      }
+    ]);
+
+    test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+    test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+
+    addRenegotiation(test.chain, []);
+
+    test.chain.append([
+      function PC_REMOTE_CHECK_AUDIO_FLOWING_2() {
+        return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+      }
+    ]);
+
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
@@ -9,17 +9,16 @@
   createHTML({
     bug: "952145",
     title: "Rollback remote reoffer"
   });
 
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
-    var firstNegotiationSize = test.chain.commands.length;
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
           return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ]
@@ -52,17 +51,17 @@
               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
+      1 // Second PC_REMOTE_SET_REMOTE_DESCRIPTION
     );
     test.chain.append(commandsPeerConnectionOfferAnswer);
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
 </pre>
 </body>
--- a/dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
@@ -9,17 +9,16 @@
   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_LOCAL_SET_OFFER_OPTION(test) {
           test.setOfferOptions({ iceRestart: true });
         }
       ]
@@ -77,17 +76,17 @@
 
         function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
           test.pcLocal.iceCheckingRestartExpected = true;
         },
         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
           test.pcRemote.iceCheckingRestartExpected = true;
         }
       ],
-      firstNegotiationSize // Replaces after second PC_REMOTE_CREATE_ANSWER
+      1 // Replaces after second PC_REMOTE_CREATE_ANSWER
     );
     test.chain.append(commandsPeerConnectionOfferAnswer);
 
     // for now, only use one stream, because rollback doesn't seem to
     // like multiple streams.  See bug 1259465.
     test.setMediaConstraints([{audio: true}],
                              [{audio: true}]);
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html
@@ -11,70 +11,37 @@
     bug: "1231507",
     title: "Basic video-only peer connection with Simulcast offer",
     visible: true
   });
 
   var test;
   var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
 
-  function alternateRedAndGreen(helper, canvas, stream) {
-    var i = 0;
-    setInterval(function() {
-      try {
-        helper.drawColor(canvas, i ? helper.green : helper.red);
-        i = 1 - i;
-        stream.requestFrame();
-      } catch (e) {
-        // ignore; stream might have shut down, and we don't bother clearing
-        // the setInterval.
-      }
-    }, 500);
-  }
-
   function selectRecvSsrc(pc, index) {
     var receivers = pc._pc.getReceivers();
     is(receivers.length, 1, "We have exactly one RTP receiver");
     var receiver = receivers[0];
 
     SpecialPowers.wrap(pc._pc).mozSelectSsrc(receiver, index);
   }
 
-  var waitForColorChange = (helper, canvas) =>
-    Promise.all([
-        helper.waitForPixelColor(canvas, helper.green, 128,
-                                 canvas.id + " should become green"),
-        helper.waitForPixelColor(canvas, helper.red, 128,
-                                 canvas.id + " should become red")
-    ]);
-
-  var ensureNoColorChange = (helper, canvas) => Promise.race([
-      waitForColorChange(helper, canvas).then(() => ok(false, "Color should not change")),
-      wait(2000).then(() => ok(true, "No color change"))
-  ]);
-
   runNetworkTest(() =>
     pushPrefs(['media.peerconnection.simulcast', true],
               ['media.peerconnection.video.min_bitrate_estimate', 100*1000]).then(() => {
       SimpleTest.requestCompleteLog();
-      var stream;
-      var helper = new CaptureStreamTestHelper2D(50,50);
-      var canvas = helper.createAndAppendElement('canvas', 'source_canvas');
-      helper.drawColor(canvas, helper.green); // Make sure this is initted
+      var helper;
 
       test = new PeerConnectionTest({bundle: false});
       test.setMediaConstraints([{video: true}], []);
 
       test.chain.replace("PC_LOCAL_GUM", [
         function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
-          stream = canvas.captureStream(10);
-          test.pcLocal.attachLocalStream(stream);
-        },
-        function PC_LOCAL_CANVAS_ALTERNATE_COLOR(test) {
-          alternateRedAndGreen(helper, canvas, stream);
+          helper = new VideoStreamHelper();
+          test.pcLocal.attachLocalStream(helper.stream());
         }
       ]);
 
       test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
         function PC_LOCAL_SET_RIDS(test) {
           var senders = test.pcLocal._pc.getSenders();
           is(senders.length, 1, "We have exactly one RTP sender");
           var sender = senders[0];
@@ -101,20 +68,20 @@
         function PC_REMOTE_SET_RTP_FIRST_RID(test) {
           // Cause pcRemote to filter out everything but the first SSRC. This
           // lets only one of the simulcast streams through.
           selectRecvSsrc(test.pcRemote, 0);
         }
       ]);
 
       test.chain.append([
-        function PC_REMOTE_WAIT_FOR_COLOR_CHANGE_1() {
+        function PC_REMOTE_WAIT_FOR_FRAMES() {
           var vremote = test.pcRemote.remoteMediaElements[0];
           ok(vremote, "Should have remote video element for pcRemote");
-          return waitForColorChange(helper, vremote);
+          return helper.waitForFrames(vremote);
         },
         function PC_REMOTE_CHECK_SIZE_1() {
           var vlocal = test.pcLocal.localMediaElements[0];
           var vremote = test.pcRemote.remoteMediaElements[0];
           ok(vlocal, "Should have local video element for pcLocal");
           ok(vremote, "Should have remote video element for pcRemote");
           ok(vlocal.videoWidth > 0, "source width is positive");
           ok(vlocal.videoHeight > 0, "source height is positive");
@@ -124,40 +91,40 @@
         function PC_REMOTE_SET_RTP_SECOND_RID(test) {
           // Now, cause pcRemote to filter out everything but the second SSRC.
           // This lets only the other simulcast stream through.
           selectRecvSsrc(test.pcRemote, 1);
         },
         function PC_REMOTE_WAIT_FOR_SECOND_MEDIA_FLOW(test) {
           return test.pcRemote.waitForMediaFlow();
         },
-        function PC_REMOTE_WAIT_FOR_COLOR_CHANGE_2() {
+        function PC_REMOTE_WAIT_FOR_FRAMES_2() {
           var vremote = test.pcRemote.remoteMediaElements[0];
           ok(vremote, "Should have remote video element for pcRemote");
-          return waitForColorChange(helper, vremote);
+          return helper.waitForFrames(vremote);
         },
         function PC_REMOTE_CHECK_SIZE_2() {
           var vlocal = test.pcLocal.localMediaElements[0];
           var vremote = test.pcRemote.remoteMediaElements[0];
           ok(vlocal, "Should have local video element for pcLocal");
           ok(vremote, "Should have remote video element for pcRemote");
           ok(vlocal.videoWidth > 0, "source width is positive");
           ok(vlocal.videoHeight > 0, "source height is positive");
           is(vremote.videoWidth, vlocal.videoWidth / 2, "sink is 1/2 width of source");
           is(vremote.videoHeight, vlocal.videoHeight / 2,  "sink is 1/2 height of source");
         },
         function PC_REMOTE_SET_RTP_NONEXISTENT_RID(test) {
           // Now, cause pcRemote to filter out everything, just to make sure
           // selectRecvSsrc is working.
           selectRecvSsrc(test.pcRemote, 2);
         },
-        function PC_REMOTE_ENSURE_NO_COLOR_CHANGE() {
+        function PC_REMOTE_ENSURE_NO_FRAMES() {
           var vremote = test.pcRemote.remoteMediaElements[0];
           ok(vremote, "Should have remote video element for pcRemote");
-          return ensureNoColorChange(helper, vremote);
+          return helper.verifyNoFrames(vremote);
         },
       ]);
 
       return test.run();
   })
   .catch(e => ok(false, "unexpected failure: " + e)));
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
@@ -9,60 +9,42 @@
   createHTML({
     bug: "1166832",
     title: "Renegotiation: verify audio after renegotiation"
   });
 
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
-
-    var checkAudio = (analyser, fun) => {
-      analyser.enableDebugCanvas();
-      return analyser.waitForAnalysisSuccess(fun)
-        .then(() => analyser.disableDebugCanvas());
-    };
-    var checkAudioEnabled = (analyser, freq) =>
-      checkAudio(analyser, array => array[freq] > 200);
-    var checkAudioDisabled = (analyser, freq) =>
-      checkAudio(analyser, array => array[freq] < 50);
-
-    var ac = new AudioContext();
-    var local1Analyser;
-    var remote1Analyser;
+    var helper = new AudioStreamHelper();
 
     test.chain.append([
       function CHECK_ASSUMPTIONS() {
         is(test.pcLocal.localMediaElements.length, 1,
            "pcLocal should have one media element");
         is(test.pcRemote.remoteMediaElements.length, 1,
            "pcRemote should have one media element");
         is(test.pcLocal._pc.getLocalStreams().length, 1,
            "pcLocal should have one stream");
         is(test.pcRemote._pc.getRemoteStreams().length, 1,
            "pcRemote should have one stream");
       },
       function CHECK_AUDIO() {
-        local1Analyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]);
-        remote1Analyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]);
-
-        freq = local1Analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
-
         return Promise.resolve()
           .then(() => info("Checking local audio enabled"))
-          .then(() => checkAudioEnabled(local1Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
           .then(() => info("Checking remote audio enabled"))
-          .then(() => checkAudioEnabled(remote1Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
 
           .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false)
 
           .then(() => info("Checking local audio disabled"))
-          .then(() => checkAudioDisabled(local1Analyser, freq))
+          .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
           .then(() => info("Checking remote audio disabled"))
-          .then(() => checkAudioDisabled(remote1Analyser, freq))
+          .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
       }
     ]);
 
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}],
                                    []);
@@ -78,44 +60,39 @@
         is(test.pcRemote.remoteMediaElements.length, 2,
            "pcRemote should have two media elements");
         is(test.pcLocal._pc.getLocalStreams().length, 2,
            "pcLocal should have two streams");
         is(test.pcRemote._pc.getRemoteStreams().length, 2,
            "pcRemote should have two streams");
       },
       function RE_CHECK_AUDIO() {
-        local2Analyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[1]);
-        remote2Analyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[1]);
-
-        freq = local2Analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
-
         return Promise.resolve()
-          .then(() => info("Checking local audio disabled"))
-          .then(() => checkAudioDisabled(local1Analyser, freq))
-          .then(() => info("Checking remote audio disabled"))
-          .then(() => checkAudioDisabled(remote1Analyser, freq))
+          .then(() => info("Checking local audio enabled"))
+          .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+          .then(() => info("Checking remote audio enabled"))
+          .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
 
           .then(() => info("Checking local2 audio enabled"))
-          .then(() => checkAudioEnabled(local2Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[1]))
           .then(() => info("Checking remote2 audio enabled"))
-          .then(() => checkAudioEnabled(remote2Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
 
           .then(() => test.pcLocal._pc.getLocalStreams()[1].getAudioTracks()[0].enabled = false)
           .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = true)
 
           .then(() => info("Checking local2 audio disabled"))
-          .then(() => checkAudioDisabled(local2Analyser, freq))
+          .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[1]))
           .then(() => info("Checking remote2 audio disabled"))
-          .then(() => checkAudioDisabled(remote2Analyser, freq))
+          .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
 
           .then(() => info("Checking local audio enabled"))
-          .then(() => checkAudioEnabled(local1Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
           .then(() => info("Checking remote audio enabled"))
-          .then(() => checkAudioEnabled(remote1Analyser, freq))
+          .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
       }
     ]);
 
     test.setMediaConstraints([{audio: true}], []);
     test.run();
   });
 </script>
 </pre>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="sdpUtils.js"></script>
+  <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1213773",
+    title: "Renegotiation: answerer uses a=inactive for video"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    var helper;
+
+    test = new PeerConnectionTest(options);
+
+    test.chain.replace("PC_LOCAL_GUM", [
+      function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+        helper = new VideoStreamHelper();
+        test.pcLocal.attachLocalStream(helper.stream());
+      }
+    ]);
+
+    test.chain.append([
+      function PC_REMOTE_WAIT_FOR_FRAMES() {
+        var vremote = test.pcRemote.remoteMediaElements[0];
+        ok(vremote, "Should have remote video element for pcRemote");
+        return helper.waitForFrames(vremote);
+      }
+    ]);
+
+    addRenegotiation(test.chain, []);
+
+    test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+        function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+          test._remote_answer.sdp =
+            sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+        }
+    ], false, 1);
+
+    test.chain.append([
+      function PC_REMOTE_ENSURE_NO_FRAMES() {
+        var vremote = test.pcRemote.remoteMediaElements[0];
+        ok(vremote, "Should have remote video element for pcRemote");
+        return helper.verifyNoFrames(vremote);
+      },
+    ]);
+
+    test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+    test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+
+    addRenegotiation(test.chain, []);
+
+    test.chain.append([
+      function PC_REMOTE_WAIT_FOR_FRAMES_2() {
+        var vremote = test.pcRemote.remoteMediaElements[0];
+        ok(vremote, "Should have remote video element for pcRemote");
+        return helper.waitForFrames(vremote);
+      }
+    ]);
+
+    test.setMediaConstraints([{video: true}], []);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -1328,23 +1328,16 @@ JsepSessionImpl::HandleNegotiatedSession
     RefPtr<JsepTransport> transport = mTransports[transportLevel];
 
     rv = FinalizeTransport(
         remote->GetMediaSection(transportLevel).GetAttributeList(),
         answer.GetMediaSection(transportLevel).GetAttributeList(),
         transport);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    if (!answer.GetMediaSection(i).IsSending() &&
-        !answer.GetMediaSection(i).IsReceiving()) {
-      MOZ_MTLOG(ML_DEBUG, "Inactive m-section, skipping creation of negotiated "
-                          "track pair.");
-      continue;
-    }
-
     JsepTrackPair trackPair;
     rv = MakeNegotiatedTrackPair(remote->GetMediaSection(i),
                                  local->GetMediaSection(i),
                                  transport,
                                  usingBundle,
                                  transportLevel,
                                  &trackPair);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -1397,51 +1390,48 @@ JsepSessionImpl::MakeNegotiatedTrackPair
   MOZ_ASSERT(mRecvonlySsrcs.size() > local.GetLevel(),
              "Failed to set the default ssrc for an active m-section");
   trackPairOut->mRecvonlySsrc = mRecvonlySsrcs[local.GetLevel()];
 
   if (usingBundle) {
     trackPairOut->mBundleLevel = Some(transportLevel);
   }
 
-  if (sending) {
-    auto sendTrack = FindTrackByLevel(mLocalTracks, local.GetLevel());
-    if (sendTrack == mLocalTracks.end()) {
-      JSEP_SET_ERROR("Failed to find local track for level " <<
-                     local.GetLevel()
-                     << " in local SDP. This should never happen.");
-      NS_ASSERTION(false, "Failed to find local track for level");
-      return NS_ERROR_FAILURE;
-    }
-
+  auto sendTrack = FindTrackByLevel(mLocalTracks, local.GetLevel());
+  if (sendTrack != mLocalTracks.end()) {
     sendTrack->mTrack->Negotiate(answer, remote);
-
+    sendTrack->mTrack->SetActive(sending);
     trackPairOut->mSending = sendTrack->mTrack;
+  } else if (sending) {
+    JSEP_SET_ERROR("Failed to find local track for level " <<
+                   local.GetLevel()
+                   << " in local SDP. This should never happen.");
+    NS_ASSERTION(false, "Failed to find local track for level");
+    return NS_ERROR_FAILURE;
   }
 
-  if (receiving) {
-    auto recvTrack = FindTrackByLevel(mRemoteTracks, local.GetLevel());
-    if (recvTrack == mRemoteTracks.end()) {
-      JSEP_SET_ERROR("Failed to find remote track for level "
-                     << local.GetLevel()
-                     << " in remote SDP. This should never happen.");
-      NS_ASSERTION(false, "Failed to find remote track for level");
-      return NS_ERROR_FAILURE;
-    }
+  auto recvTrack = FindTrackByLevel(mRemoteTracks, local.GetLevel());
+  if (recvTrack != mRemoteTracks.end()) {
+    recvTrack->mTrack->Negotiate(answer, remote);
+    recvTrack->mTrack->SetActive(receiving);
+    trackPairOut->mReceiving = recvTrack->mTrack;
 
-    recvTrack->mTrack->Negotiate(answer, remote);
-
-    if (trackPairOut->mBundleLevel.isSome() &&
+    if (receiving &&
+        trackPairOut->mBundleLevel.isSome() &&
         recvTrack->mTrack->GetSsrcs().empty() &&
         recvTrack->mTrack->GetMediaType() != SdpMediaSection::kApplication) {
       MOZ_MTLOG(ML_ERROR, "Bundled m-section has no ssrc attributes. "
                           "This may cause media packets to be dropped.");
     }
-
-    trackPairOut->mReceiving = recvTrack->mTrack;
+  } else if (receiving) {
+    JSEP_SET_ERROR("Failed to find remote track for level "
+                   << local.GetLevel()
+                   << " in remote SDP. This should never happen.");
+    NS_ASSERTION(false, "Failed to find remote track for level");
+    return NS_ERROR_FAILURE;
   }
 
   trackPairOut->mRtpTransport = transport;
 
   if (transport->mComponents == 2) {
     // RTCP MUX or not.
     // TODO(bug 1095743): verify that the PTs are consistent with mux.
     MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is off");
--- a/media/webrtc/signaling/src/jsep/JsepTrack.h
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.h
@@ -69,17 +69,18 @@ class JsepTrack
 public:
   JsepTrack(mozilla::SdpMediaSection::MediaType type,
             const std::string& streamid,
             const std::string& trackid,
             sdp::Direction direction = sdp::kSend)
       : mType(type),
         mStreamId(streamid),
         mTrackId(trackid),
-        mDirection(direction)
+        mDirection(direction),
+        mActive(false)
   {}
 
   virtual mozilla::SdpMediaSection::MediaType
   GetMediaType() const
   {
     return mType;
   }
 
@@ -132,16 +133,28 @@ public:
   }
 
   virtual void
   AddSsrc(uint32_t ssrc)
   {
     mSsrcs.push_back(ssrc);
   }
 
+  bool
+  GetActive() const
+  {
+    return mActive;
+  }
+
+  void
+  SetActive(bool active)
+  {
+    mActive = active;
+  }
+
   virtual void PopulateCodecs(
       const std::vector<JsepCodecDescription*>& prototype);
 
   template <class UnaryFunction>
   void ForEachCodec(UnaryFunction func)
   {
     std::for_each(mPrototypeCodecs.values.begin(),
                   mPrototypeCodecs.values.end(), func);
@@ -254,16 +267,17 @@ private:
   const sdp::Direction mDirection;
   PtrVector<JsepCodecDescription> mPrototypeCodecs;
   // Holds encoding params/constraints from JS. Simulcast happens when there are
   // multiple of these. If there are none, we assume unconstrained unicast with
   // no rid.
   std::vector<JsConstraints> mJsEncodeConstraints;
   UniquePtr<JsepTrackNegotiatedDetails> mNegotiatedDetails;
   std::vector<uint32_t> mSsrcs;
+  bool mActive;
 };
 
 // Need a better name for this.
 struct JsepTrackPair {
   size_t mLevel;
   // Is this track pair sharing a transport with another?
   Maybe<size_t> mBundleLevel;
   uint32_t mRecvonlySsrc;
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
@@ -750,21 +750,16 @@ WebrtcVideoConduit::ConfigureSendMediaCo
     if (mPtrRTP->SetNACKStatus(mChannel, true) != 0)
     {
       CSFLogError(logTag,  "%s NACKStatus Failed %d ", __FUNCTION__,
                   mPtrViEBase->LastError());
       return kMediaConduitNACKStatusError;
     }
   }
 
-  condError = StartTransmitting();
-  if (condError != kMediaConduitNoError) {
-    return condError;
-  }
-
   {
     MutexAutoLock lock(mCodecMutex);
 
     //Copy the applied config for future reference.
     mCurSendCodecConfig = new VideoCodecConfig(*codecConfig);
   }
 
   mPtrRTP->SetRembStatus(mChannel, true, false);
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
+++ b/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
@@ -455,16 +455,24 @@ MediaPipelineFactory::CreateOrUpdateMedi
     rv = GetOrCreateVideoConduit(aTrackPair, aTrack, &conduit);
     if (NS_FAILED(rv))
       return rv;
   } else {
     // We've created the TransportFlow, nothing else to do here.
     return NS_OK;
   }
 
+  if (aTrack.GetActive()) {
+    auto error = conduit->StartTransmitting();
+    if (error) {
+      MOZ_MTLOG(ML_ERROR, "StartTransmitting failed: " << error);
+      return NS_ERROR_FAILURE;
+    }
+  }
+
   RefPtr<MediaPipeline> pipeline =
     stream->GetPipelineByTrackId_m(aTrack.GetTrackId());
 
   if (pipeline && pipeline->level() != static_cast<int>(level)) {
     MOZ_MTLOG(ML_WARNING, "Track " << aTrack.GetTrackId() <<
                           " has moved from level " << pipeline->level() <<
                           " to level " << level <<
                           ". This requires re-creating the MediaPipeline.");
@@ -781,16 +789,25 @@ MediaPipelineFactory::GetOrCreateVideoCo
   }
 
   if (configs.values.empty()) {
     MOZ_MTLOG(ML_ERROR, "Can't set up a conduit with 0 codecs");
     return NS_ERROR_FAILURE;
   }
 
   if (receiving) {
+    if (!aTrackPair.mSending) {
+      // No send track, but we still need to configure an SSRC for receiver
+      // reports.
+      if (!conduit->SetLocalSSRC(aTrackPair.mRecvonlySsrc)) {
+        MOZ_MTLOG(ML_ERROR, "SetLocalSSRC failed");
+        return NS_ERROR_FAILURE;
+      }
+    }
+
     // Prune out stuff we cannot actually do. We should work to eliminate the
     // need for this.
     bool configuredH264 = false;
     for (size_t i = 0; i < configs.values.size();) {
       // TODO(bug 1200768): We can only handle configuring one recv H264 codec
       if (configuredH264 && (configs.values[i]->mName == "H264")) {
         delete configs.values[i];
         configs.values.erase(configs.values.begin() + i);
@@ -811,25 +828,16 @@ MediaPipelineFactory::GetOrCreateVideoCo
     }
 
     auto error = conduit->ConfigureRecvMediaCodecs(configs.values);
 
     if (error) {
       MOZ_MTLOG(ML_ERROR, "ConfigureRecvMediaCodecs failed: " << error);
       return NS_ERROR_FAILURE;
     }
-
-    if (!aTrackPair.mSending) {
-      // No send track, but we still need to configure an SSRC for receiver
-      // reports.
-      if (!conduit->SetLocalSSRC(aTrackPair.mRecvonlySsrc)) {
-        MOZ_MTLOG(ML_ERROR, "SetLocalSSRC failed");
-        return NS_ERROR_FAILURE;
-      }
-    }
   } else {
     // For now we only expect to have one ssrc per local track.
     auto ssrcs = aTrack.GetSsrcs();
     if (!ssrcs.empty()) {
       if (!conduit->SetLocalSSRC(ssrcs.front())) {
         MOZ_MTLOG(ML_ERROR, "SetLocalSSRC failed");
         return NS_ERROR_FAILURE;
       }
--- a/media/webrtc/signaling/test/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/test/jsep_session_unittest.cpp
@@ -572,17 +572,17 @@ protected:
         ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
 
         if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
           msidAttr += pairs[i].mReceiving->GetStreamId();
           msidAttr += " ";
           msidAttr += pairs[i].mReceiving->GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
-            << "Did not find " << msidAttr << " in offer";
+            << "Did not find " << msidAttr << " in answer";
         }
       }
     }
     DumpTrackPairs(mSessionAns);
   }
 
   typedef enum {
     RTP = 1,
@@ -2402,16 +2402,83 @@ TEST_P(JsepSessionTest, RenegotiationWit
         false, remoteAnswer->GetMediaSection(i), i,
         "Remote reanswer after trickle should not have a default RTCP "
         "candidate.");
     CheckEndOfCandidates(false, remoteAnswer->GetMediaSection(i),
         "Remote reanswer after trickle should not have an end-of-candidates.");
   }
 }
 
+TEST_P(JsepSessionTest, RenegotiationAnswererSendonly)
+{
+  AddTracks(mSessionOff);
+  AddTracks(mSessionAns);
+  OfferAnswer();
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  UniquePtr<Sdp> parsedAnswer(Parse(answer));
+  for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) {
+    SdpMediaSection& msection = parsedAnswer->GetMediaSection(i);
+    if (msection.GetMediaType() != SdpMediaSection::kApplication) {
+      msection.SetReceiving(false);
+    }
+  }
+
+  answer = parsedAnswer->ToString();
+
+  SetRemoteAnswer(answer);
+
+  for (const RefPtr<JsepTrack>& track : mSessionOff.GetLocalTracks()) {
+    if (track->GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track->GetActive());
+    }
+  }
+
+  ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().size());
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererInactive)
+{
+  AddTracks(mSessionOff);
+  AddTracks(mSessionAns);
+  OfferAnswer();
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  UniquePtr<Sdp> parsedAnswer(Parse(answer));
+  for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) {
+    SdpMediaSection& msection = parsedAnswer->GetMediaSection(i);
+    if (msection.GetMediaType() != SdpMediaSection::kApplication) {
+      msection.SetReceiving(false);
+      msection.SetSending(false);
+    }
+  }
+
+  answer = parsedAnswer->ToString();
+
+  SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks
+
+  for (const RefPtr<JsepTrack>& track : mSessionOff.GetLocalTracks()) {
+    if (track->GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track->GetActive());
+    }
+  }
+
+  ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().size());
+}
+
 
 INSTANTIATE_TEST_CASE_P(
     Variants,
     JsepSessionTest,
     ::testing::Values("audio",
                       "video",
                       "datachannel",
                       "audio,video",
--- a/media/webrtc/signaling/test/mediaconduit_unittests.cpp
+++ b/media/webrtc/signaling/test/mediaconduit_unittests.cpp
@@ -577,21 +577,25 @@ class TransportConduitTest : public ::te
 
 
     std::vector<mozilla::AudioCodecConfig*> rcvCodecList;
     rcvCodecList.push_back(&cinst1);
     rcvCodecList.push_back(&cinst2);
 
     err = mAudioSession->ConfigureSendMediaCodec(&cinst1);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
+    err = mAudioSession->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
     err = mAudioSession->ConfigureRecvMediaCodecs(rcvCodecList);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     err = mAudioSession2->ConfigureSendMediaCodec(&cinst1);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
+    err = mAudioSession2->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
     err = mAudioSession2->ConfigureRecvMediaCodecs(rcvCodecList);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     //start generating samples
     audioTester.Init(mAudioSession,mAudioSession2, iAudiofilename,oAudiofilename);
     cerr << "   ******************************************************** " << endl;
     cerr << "    Generating Audio Samples " << endl;
     cerr << "   ******************************************************** " << endl;
@@ -651,19 +655,23 @@ class TransportConduitTest : public ::te
     std::vector<mozilla::VideoCodecConfig* > rcvCodecList;
     rcvCodecList.push_back(&cinst1);
     rcvCodecList.push_back(&cinst2);
 
     err = mVideoSession->ConfigureSendMediaCodec(
         send_vp8 ? &cinst1 : &cinst2);
 
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
+    err = mVideoSession->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     err = mVideoSession2->ConfigureSendMediaCodec(
         send_vp8 ? &cinst1 : &cinst2);
+    err = mVideoSession2->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
     err = mVideoSession2->ConfigureRecvMediaCodecs(rcvCodecList);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     //start generating samples
     cerr << "   *************************************************" << endl;
     cerr << "    Starting the Video Sample Generation " << endl;
@@ -771,18 +779,22 @@ class TransportConduitTest : public ::te
 
     cerr << "   *************************************************" << endl;
     cerr << "    1. Same Codec (VP8) Repeated Twice " << endl;
     cerr << "   *************************************************" << endl;
 
 
     err = videoSession->ConfigureSendMediaCodec(&cinst1);
     EXPECT_EQ(mozilla::kMediaConduitNoError, err);
+    err = videoSession->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
     err = videoSession->ConfigureSendMediaCodec(&cinst1);
     EXPECT_EQ(mozilla::kMediaConduitCodecInUse, err);
+    err = videoSession->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
 
     cerr << "   *************************************************" << endl;
     cerr << "    2. Codec With Invalid Payload Names " << endl;
     cerr << "   *************************************************" << endl;
     cerr << "   Setting payload with name: I4201234tttttthhhyyyy89087987y76t567r7756765rr6u6676" << endl;
 
     err = videoSession->ConfigureSendMediaCodec(&cinst3);
@@ -825,16 +837,18 @@ class TransportConduitTest : public ::te
 
     mozilla::EncodingConstraints constraints;
     constraints.maxFs = max_fs;
     // Configure send codecs on the conduit.
     mozilla::VideoCodecConfig cinst1(120, "VP8", constraints);
 
     err = mVideoSession->ConfigureSendMediaCodec(&cinst1);
     ASSERT_EQ(mozilla::kMediaConduitNoError, err);
+    err = mVideoSession->StartTransmitting();
+    ASSERT_EQ(mozilla::kMediaConduitNoError, err);
 
     // Send one frame.
     MOZ_ASSERT(!(orig_width & 1));
     MOZ_ASSERT(!(orig_height & 1));
     int len = ((orig_width * orig_height) * 3 / 2);
     uint8_t* frame = (uint8_t*) PR_MALLOC(len);
 
     memset(frame, COLOR, len);