Bug 1249859 - Optimize simulcast if two layers are too close to each other in resolution; r?jesup draft
authorDan Minor <dminor@mozilla.com>
Wed, 02 Nov 2016 11:39:05 -0400
changeset 433982 96c40f022ebf96dd193084bab39a61fd0a96a7a9
parent 433311 3b80868f7a8fe0361918a814fbbbfb9308ae0c0a
child 536001 38b43948711a58aa45c9804279865f8774ffa79c
push id34700
push userdminor@mozilla.com
push dateFri, 04 Nov 2016 17:24:06 +0000
reviewersjesup
bugs1249859
milestone52.0a1
Bug 1249859 - Optimize simulcast if two layers are too close to each other in resolution; r?jesup MozReview-Commit-ID: 6chD9O7Ms36
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_peerConnection_simulcastOfferPruning.html
media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -177,16 +177,17 @@ skip-if = android_version
 [test_peerConnection_restartIceLocalRollback.html]
 skip-if = android_version
 [test_peerConnection_restartIceLocalAndRemoteRollback.html]
 skip-if = android_version
 [test_peerConnection_scaleResolution.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_simulcastOffer.html]
 skip-if = android_version # no simulcast support on android
+[test_peerConnection_simulcastOfferPruning.html]
 #[test_peerConnection_relayOnly.html]
 [test_peerConnection_callbacks.html]
 skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_replaceTrack.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_syncSetDescription.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_simulcastOfferPruning.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.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: "1249859",
+    title: "Test pruning simulcast resolutions that are too close together",
+    visible: true
+  });
+
+  var test;
+  var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+  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);
+  }
+
+  runNetworkTest(() =>
+    pushPrefs(['media.peerconnection.video.min_bitrate_estimate', 100*1000]).then(() => {
+      SimpleTest.requestCompleteLog();
+      var helper;
+
+      test = new PeerConnectionTest({bundle: false});
+      test.setMediaConstraints([{video: true}], []);
+
+      test.chain.replace("PC_LOCAL_GUM", [
+        function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+          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];
+          ok(sender.track, "Sender has a track");
+
+          // We expect the 1.25 and 3.0 scalings to be pruned, so the rest
+          // of this test is functionally equivalent to
+          // test_peerConnection_simulcastOffer.html.
+          return sender.setParameters({
+            encodings: [{ rid: "foo", maxBitrate: 40000 },
+                        { rid: "bar", maxBitrate: 40000, scaleResolutionDownBy: 1.25 },
+                        { rid: "baz", maxBitrate: 40000, scaleResolutionDownBy: 2.0 },
+                        { rid: "bazier", maxBitrate: 40000, scaleResolutionDownBy: 3.0 }]
+          });
+        }
+      ]);
+
+      test.chain.insertAfter('PC_LOCAL_GET_ANSWER', [
+        function PC_LOCAL_ADD_RIDS_TO_ANSWER(test) {
+          test._remote_answer.sdp = sdputils.transferSimulcastProperties(
+            test.originalOffer.sdp, test._remote_answer.sdp);
+          info("Answer with RIDs: " + JSON.stringify(test._remote_answer));
+          ok(test._remote_answer.sdp.match(/a=simulcast:/), "Modified answer has simulcast");
+          ok(test._remote_answer.sdp.match(/a=rid:/), "Modified answer has rid");
+        }
+      ]);
+
+      test.chain.insertAfter('PC_REMOTE_WAIT_FOR_MEDIA_FLOW',[
+        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_FRAMES() {
+          var vremote = test.pcRemote.remoteMediaElements[0];
+          ok(vremote, "Should have remote video element for pcRemote");
+          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");
+          is(vremote.videoWidth, vlocal.videoWidth, "sink is same width as source");
+          is(vremote.videoHeight, vlocal.videoHeight, "sink is same height as source");
+        },
+        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_FRAMES_2() {
+          var vremote = test.pcRemote.remoteMediaElements[0];
+          ok(vremote, "Should have remote video element for pcRemote");
+          return helper.waitForFrames(vremote);
+        },
+        // For some reason, even though we're getting a 25x25 stream, sometimes
+        // the resolution isn't updated on the video element on the first frame.
+        function PC_REMOTE_WAIT_FOR_FRAMES_3() {
+          var vremote = test.pcRemote.remoteMediaElements[0];
+          ok(vremote, "Should have remote video element for pcRemote");
+          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_FRAMES() {
+          var vremote = test.pcRemote.remoteMediaElements[0];
+          ok(vremote, "Should have remote video element for pcRemote");
+          return helper.verifyNoFrames(vremote);
+        },
+      ]);
+
+      return test.run();
+  })
+  .catch(e => ok(false, "unexpected failure: " + e)));
+</script>
+</pre>
+</body>
+</html>
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
@@ -44,16 +44,20 @@
 
 namespace mozilla {
 
 static const char* logTag ="WebrtcVideoSessionConduit";
 
 // 32 bytes is what WebRTC CodecInst expects
 const unsigned int WebrtcVideoConduit::CODEC_PLNAME_SIZE = 32;
 
+// A simulcast stream must be at least this many times smaller in resolution
+// than the previous stream or it will be pruned as an optimization.
+const unsigned int SIMULCAST_PRUNING_FACTOR=4;
+
 /**
  * Factory Method for VideoConduit
  */
 RefPtr<VideoSessionConduit>
 VideoSessionConduit::Create()
 {
   NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
   CSFLogDebug(logTag,  "%s ", __FUNCTION__);
@@ -1406,24 +1410,42 @@ WebrtcVideoConduit::ReconfigureSendCodec
 
   // These are based on lowest-fidelity, because if there is insufficient
   // bandwidth for all streams, only the lowest fidelity one will be sent.
   uint32_t minMinBitrate = 0;
   uint32_t minStartBitrate = 0;
   // Total for all simulcast streams.
   uint32_t totalMaxBitrate = 0;
 
+  uint32_t last_width = 0;
+  uint32_t last_height = 0;
+
   for (size_t i = vie_codec.numberOfSimulcastStreams; i > 0; --i) {
     webrtc::SimulcastStream& stream(vie_codec.simulcastStream[i - 1]);
     stream.width = width;
     stream.height = height;
     MOZ_ASSERT(stream.jsScaleDownBy >= 1.0);
     uint32_t new_width = uint32_t(width / stream.jsScaleDownBy);
     uint32_t new_height = uint32_t(height / stream.jsScaleDownBy);
-    // TODO: If two layers are similar, only alloc bits to one (Bug 1249859)
+
+    // This ensures we preserve the highest resolution stream, which is the
+    // first one we examine.
+    if (last_width != 0 && last_height != 0) {
+      if (SIMULCAST_PRUNING_FACTOR*new_width*new_height > last_width*last_height) {
+        for (size_t j = i - 1; j < vie_codec.numberOfSimulcastStreams - 1; ++j) {
+          vie_codec.simulcastStream[j] = vie_codec.simulcastStream[j + 1];
+        }
+        --vie_codec.numberOfSimulcastStreams;
+        continue;
+      }
+    }
+
+    last_width = new_width;
+    last_height = new_height;
+
     if (new_width != width || new_height != height) {
       if (vie_codec.numberOfSimulcastStreams == 1) {
         // Use less strict scaling in unicast. That way 320x240 / 3 = 106x79.
         ConstrainPreservingAspectRatio(new_width, new_height,
                                        &stream.width, &stream.height);
       } else {
         // webrtc.org supposedly won't tolerate simulcast unless every stream
         // is exactly the same aspect ratio. 320x240 / 3 = 80x60.