Implement legacy offer_to_receive options for Unified Plan

This implements the WebRTC specification for handling
the legacy offer options offer_to_receive_audio and
offer_to_receive_video. They are not implemented for CreateAnswer.

With Unified Plan semantics, clients should switch to the
RtpTransceiver API for ensuring the correct media sections are
offered.

Bug: webrtc:7600
Change-Id: I6ced00b86b165a352bd0ca3d64b48fadcfd12235
Reviewed-on: https://webrtc-review.googlesource.com/41341
Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org>
Commit-Queue: Steve Anton <steveanton@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21784}
diff --git a/pc/peerconnection.cc b/pc/peerconnection.cc
index 6ffe561..3578095 100644
--- a/pc/peerconnection.cc
+++ b/pc/peerconnection.cc
@@ -1259,7 +1259,8 @@
 PeerConnection::AddTransceiver(
     cricket::MediaType media_type,
     rtc::scoped_refptr<MediaStreamTrackInterface> track,
-    const RtpTransceiverInit& init) {
+    const RtpTransceiverInit& init,
+    bool fire_callback) {
   RTC_DCHECK((media_type == cricket::MEDIA_TYPE_AUDIO ||
               media_type == cricket::MEDIA_TYPE_VIDEO));
   if (track) {
@@ -1285,7 +1286,9 @@
   auto transceiver = CreateAndAddTransceiver(sender, receiver);
   transceiver->internal()->set_direction(init.direction);
 
-  observer_->OnRenegotiationNeeded();
+  if (fire_callback) {
+    observer_->OnRenegotiationNeeded();
+  }
 
   return rtc::scoped_refptr<RtpTransceiverInterface>(transceiver);
 }
@@ -1565,11 +1568,80 @@
     return;
   }
 
+  // Legacy handling for offer_to_receive_audio and offer_to_receive_video.
+  // Specified in WebRTC section 4.4.3.2 "Legacy configuration extensions".
+  if (IsUnifiedPlan()) {
+    RTCError error = HandleLegacyOfferOptions(options);
+    if (!error.ok()) {
+      PostCreateSessionDescriptionFailure(observer, error.message());
+      return;
+    }
+  }
+
   cricket::MediaSessionOptions session_options;
   GetOptionsForOffer(options, &session_options);
   webrtc_session_desc_factory_->CreateOffer(observer, options, session_options);
 }
 
+RTCError PeerConnection::HandleLegacyOfferOptions(
+    const RTCOfferAnswerOptions& options) {
+  RTC_DCHECK(IsUnifiedPlan());
+
+  if (options.offer_to_receive_audio == 0) {
+    RemoveRecvDirectionFromReceivingTransceiversOfType(
+        cricket::MEDIA_TYPE_AUDIO);
+  } else if (options.offer_to_receive_audio == 1) {
+    AddUpToOneReceivingTransceiverOfType(cricket::MEDIA_TYPE_AUDIO);
+  } else if (options.offer_to_receive_audio > 1) {
+    LOG_AND_RETURN_ERROR(RTCErrorType::UNSUPPORTED_PARAMETER,
+                         "offer_to_receive_audio > 1 is not supported.");
+  }
+
+  if (options.offer_to_receive_video == 0) {
+    RemoveRecvDirectionFromReceivingTransceiversOfType(
+        cricket::MEDIA_TYPE_VIDEO);
+  } else if (options.offer_to_receive_video == 1) {
+    AddUpToOneReceivingTransceiverOfType(cricket::MEDIA_TYPE_VIDEO);
+  } else if (options.offer_to_receive_video > 1) {
+    LOG_AND_RETURN_ERROR(RTCErrorType::UNSUPPORTED_PARAMETER,
+                         "offer_to_receive_video > 1 is not supported.");
+  }
+
+  return RTCError::OK();
+}
+
+void PeerConnection::RemoveRecvDirectionFromReceivingTransceiversOfType(
+    cricket::MediaType media_type) {
+  for (auto transceiver : GetReceivingTransceiversOfType(media_type)) {
+    transceiver->internal()->set_direction(
+        RtpTransceiverDirectionWithRecvSet(transceiver->direction(), false));
+  }
+}
+
+void PeerConnection::AddUpToOneReceivingTransceiverOfType(
+    cricket::MediaType media_type) {
+  if (GetReceivingTransceiversOfType(media_type).empty()) {
+    RtpTransceiverInit init;
+    init.direction = RtpTransceiverDirection::kRecvOnly;
+    AddTransceiver(media_type, nullptr, init, /*fire_callback=*/false);
+  }
+}
+
+std::vector<rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+PeerConnection::GetReceivingTransceiversOfType(cricket::MediaType media_type) {
+  std::vector<
+      rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+      receiving_transceivers;
+  for (auto transceiver : transceivers_) {
+    if (!transceiver->stopped() &&
+        transceiver->internal()->media_type() == media_type &&
+        RtpTransceiverDirectionHasRecv(transceiver->direction())) {
+      receiving_transceivers.push_back(transceiver);
+    }
+  }
+  return receiving_transceivers;
+}
+
 void PeerConnection::CreateAnswer(
     CreateSessionDescriptionObserver* observer,
     const MediaConstraintsInterface* constraints) {
@@ -1615,6 +1687,19 @@
     return;
   }
 
+  if (IsUnifiedPlan()) {
+    if (options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) {
+      RTC_LOG(LS_WARNING) << "CreateAnswer: offer_to_receive_audio is not "
+                             "supported with Unified Plan semantics. Use the "
+                             "RtpTransceiver API instead.";
+    }
+    if (options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) {
+      RTC_LOG(LS_WARNING) << "CreateAnswer: offer_to_receive_video is not "
+                             "supported with Unified Plan semantics. Use the "
+                             "RtpTransceiver API instead.";
+    }
+  }
+
   cricket::MediaSessionOptions session_options;
   GetOptionsForAnswer(options, &session_options);
 
diff --git a/pc/peerconnection.h b/pc/peerconnection.h
index 3ebe8fd..e88fab8 100644
--- a/pc/peerconnection.h
+++ b/pc/peerconnection.h
@@ -354,10 +354,13 @@
   rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
   FindTransceiverBySender(rtc::scoped_refptr<RtpSenderInterface> sender);
 
+  // Internal implementation for AddTransceiver family of methods. If
+  // |fire_callback| is set, fires OnRenegotiationNeeded callback if successful.
   RTCErrorOr<rtc::scoped_refptr<RtpTransceiverInterface>> AddTransceiver(
       cricket::MediaType media_type,
       rtc::scoped_refptr<MediaStreamTrackInterface> track,
-      const RtpTransceiverInit& init);
+      const RtpTransceiverInit& init,
+      bool fire_callback = true);
 
   rtc::scoped_refptr<RtpSenderProxyWithInternal<RtpSenderInternal>>
   CreateSender(cricket::MediaType media_type,
@@ -482,6 +485,14 @@
           offer_answer_options,
       cricket::MediaSessionOptions* session_options);
 
+  RTCError HandleLegacyOfferOptions(const RTCOfferAnswerOptions& options);
+  void RemoveRecvDirectionFromReceivingTransceiversOfType(
+      cricket::MediaType media_type);
+  void AddUpToOneReceivingTransceiverOfType(cricket::MediaType media_type);
+  std::vector<
+      rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+  GetReceivingTransceiversOfType(cricket::MediaType media_type);
+
   // Returns a MediaSessionOptions struct with options decided by
   // |constraints|, the local MediaStreams and DataChannels.
   void GetOptionsForAnswer(const RTCOfferAnswerOptions& offer_answer_options,
diff --git a/pc/peerconnection_media_unittest.cc b/pc/peerconnection_media_unittest.cc
index 40fc1dd..cfd4803 100644
--- a/pc/peerconnection_media_unittest.cc
+++ b/pc/peerconnection_media_unittest.cc
@@ -361,39 +361,38 @@
 // Test that a new stream in a subsequent answer causes a new send stream to be
 // created on the callee when added locally.
 TEST_P(PeerConnectionMediaTest, NewStreamInLocalAnswerAddsSendStreams) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
-  if (IsUnifiedPlan()) {
-    return;
-  }
-
   auto caller = CreatePeerConnection();
   auto callee = CreatePeerConnectionWithAudioVideo();
 
-  RTCOfferAnswerOptions options;
-  options.offer_to_receive_audio =
+  RTCOfferAnswerOptions offer_options;
+  offer_options.offer_to_receive_audio =
       RTCOfferAnswerOptions::kOfferToReceiveMediaTrue;
-  options.offer_to_receive_video =
+  offer_options.offer_to_receive_video =
       RTCOfferAnswerOptions::kOfferToReceiveMediaTrue;
+  RTCOfferAnswerOptions answer_options;
 
-  ASSERT_TRUE(
-      callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal(options)));
-  ASSERT_TRUE(
-      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get(), offer_options,
+                                              answer_options));
 
   // Add second set of tracks to the callee.
   callee->AddAudioTrack("a2");
   callee->AddVideoTrack("v2");
 
-  ASSERT_TRUE(
-      callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal(options)));
-  ASSERT_TRUE(
-      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get(), offer_options,
+                                              answer_options));
 
   auto callee_voice = callee->media_engine()->GetVoiceChannel(0);
-  EXPECT_EQ(2u, callee_voice->send_streams().size());
+  ASSERT_TRUE(callee_voice);
   auto callee_video = callee->media_engine()->GetVideoChannel(0);
-  EXPECT_EQ(2u, callee_video->send_streams().size());
+  ASSERT_TRUE(callee_video);
+
+  if (IsUnifiedPlan()) {
+    EXPECT_EQ(1u, callee_voice->send_streams().size());
+    EXPECT_EQ(1u, callee_video->send_streams().size());
+  } else {
+    EXPECT_EQ(2u, callee_voice->send_streams().size());
+    EXPECT_EQ(2u, callee_video->send_streams().size());
+  }
 }
 
 // A PeerConnection with no local streams and no explicit answer constraints
@@ -438,11 +437,6 @@
 // Tests that the correct direction is set on the media description according
 // to the presence of a local media track and the offer_to_receive setting.
 TEST_P(PeerConnectionMediaOfferDirectionTest, VerifyDirection) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
-  if (IsUnifiedPlan()) {
-    return;
-  }
   auto caller = CreatePeerConnection();
   if (send_media_) {
     caller->AddAudioTrack("a");
@@ -496,11 +490,13 @@
 // in the offer, the presence of a local media track on the receive side and the
 // offer_to_receive setting.
 TEST_P(PeerConnectionMediaAnswerDirectionTest, VerifyDirection) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
-  if (IsUnifiedPlan()) {
+  if (IsUnifiedPlan() &&
+      offer_to_receive_ != RTCOfferAnswerOptions::kUndefined) {
+    // offer_to_receive_ is not implemented when creating answers with Unified
+    // Plan semantics specified.
     return;
   }
+
   auto caller = CreatePeerConnection();
   caller->AddAudioTrack("a");
 
@@ -544,11 +540,13 @@
 // local media track and has set offer_to_receive to 0, no matter which
 // direction the caller indicated in the offer.
 TEST_P(PeerConnectionMediaAnswerDirectionTest, VerifyRejected) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
-  if (IsUnifiedPlan()) {
+  if (IsUnifiedPlan() &&
+      offer_to_receive_ != RTCOfferAnswerOptions::kUndefined) {
+    // offer_to_receive_ is not implemented when creating answers with Unified
+    // Plan semantics specified.
     return;
   }
+
   auto caller = CreatePeerConnection();
   caller->AddAudioTrack("a");
 
@@ -587,12 +585,6 @@
                                 Values(-1, 0, 1)));
 
 TEST_P(PeerConnectionMediaTest, OfferHasDifferentDirectionForAudioVideo) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
-  if (IsUnifiedPlan()) {
-    return;
-  }
-
   auto caller = CreatePeerConnection();
   caller->AddVideoTrack("v");
 
@@ -608,9 +600,9 @@
 }
 
 TEST_P(PeerConnectionMediaTest, AnswerHasDifferentDirectionsForAudioVideo) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
   if (IsUnifiedPlan()) {
+    // offer_to_receive_ is not implemented when creating answers with Unified
+    // Plan semantics specified.
     return;
   }
 
@@ -780,9 +772,9 @@
 // a series of offer/answers where audio/video are both sent, then audio is
 // rejected, then both audio/video sent again.
 TEST_P(PeerConnectionMediaTest, TestAVOfferWithAudioOnlyAnswer) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
   if (IsUnifiedPlan()) {
+    // offer_to_receive_ is not implemented when creating answers with Unified
+    // Plan semantics specified.
     return;
   }
 
@@ -844,9 +836,9 @@
 // a series of offer/answers where audio/video are both sent, then video is
 // rejected, then both audio/video sent again.
 TEST_P(PeerConnectionMediaTest, TestAVOfferWithVideoOnlyAnswer) {
-  // TODO(bugs.webrtc.org/8765): Enable this test under Unified Plan once
-  // offer_to_receive_audio is implemented.
   if (IsUnifiedPlan()) {
+    // offer_to_receive_ is not implemented when creating answers with Unified
+    // Plan semantics specified.
     return;
   }
 
diff --git a/pc/peerconnectionwrapper.cc b/pc/peerconnectionwrapper.cc
index 21d1542..c09258d 100644
--- a/pc/peerconnectionwrapper.cc
+++ b/pc/peerconnectionwrapper.cc
@@ -24,6 +24,8 @@
 
 namespace webrtc {
 
+using RTCOfferAnswerOptions = PeerConnectionInterface::RTCOfferAnswerOptions;
+
 namespace {
 const uint32_t kDefaultTimeout = 10000U;
 }
@@ -57,7 +59,7 @@
 
 std::unique_ptr<SessionDescriptionInterface>
 PeerConnectionWrapper::CreateOffer() {
-  return CreateOffer(PeerConnectionInterface::RTCOfferAnswerOptions());
+  return CreateOffer(RTCOfferAnswerOptions());
 }
 
 std::unique_ptr<SessionDescriptionInterface> PeerConnectionWrapper::CreateOffer(
@@ -72,8 +74,7 @@
 
 std::unique_ptr<SessionDescriptionInterface>
 PeerConnectionWrapper::CreateOfferAndSetAsLocal() {
-  return CreateOfferAndSetAsLocal(
-      PeerConnectionInterface::RTCOfferAnswerOptions());
+  return CreateOfferAndSetAsLocal(RTCOfferAnswerOptions());
 }
 
 std::unique_ptr<SessionDescriptionInterface>
@@ -89,7 +90,7 @@
 
 std::unique_ptr<SessionDescriptionInterface>
 PeerConnectionWrapper::CreateAnswer() {
-  return CreateAnswer(PeerConnectionInterface::RTCOfferAnswerOptions());
+  return CreateAnswer(RTCOfferAnswerOptions());
 }
 
 std::unique_ptr<SessionDescriptionInterface>
@@ -105,8 +106,7 @@
 
 std::unique_ptr<SessionDescriptionInterface>
 PeerConnectionWrapper::CreateAnswerAndSetAsLocal() {
-  return CreateAnswerAndSetAsLocal(
-      PeerConnectionInterface::RTCOfferAnswerOptions());
+  return CreateAnswerAndSetAsLocal(RTCOfferAnswerOptions());
 }
 
 std::unique_ptr<SessionDescriptionInterface>
@@ -181,12 +181,20 @@
 
 bool PeerConnectionWrapper::ExchangeOfferAnswerWith(
     PeerConnectionWrapper* answerer) {
+  return ExchangeOfferAnswerWith(answerer, RTCOfferAnswerOptions(),
+                                 RTCOfferAnswerOptions());
+}
+
+bool PeerConnectionWrapper::ExchangeOfferAnswerWith(
+    PeerConnectionWrapper* answerer,
+    const PeerConnectionInterface::RTCOfferAnswerOptions& offer_options,
+    const PeerConnectionInterface::RTCOfferAnswerOptions& answer_options) {
   RTC_DCHECK(answerer);
   if (answerer == this) {
     RTC_LOG(LS_ERROR) << "Cannot exchange offer/answer with ourself!";
     return false;
   }
-  auto offer = CreateOffer();
+  auto offer = CreateOffer(offer_options);
   EXPECT_TRUE(offer);
   if (!offer) {
     return false;
@@ -202,7 +210,7 @@
   if (!set_remote_offer) {
     return false;
   }
-  auto answer = answerer->CreateAnswer();
+  auto answer = answerer->CreateAnswer(answer_options);
   EXPECT_TRUE(answer);
   if (!answer) {
     return false;
diff --git a/pc/peerconnectionwrapper.h b/pc/peerconnectionwrapper.h
index b5aa163..b775e85 100644
--- a/pc/peerconnectionwrapper.h
+++ b/pc/peerconnectionwrapper.h
@@ -97,16 +97,20 @@
   // generating the offer and the given PeerConnectionWrapper generating the
   // answer.
   // Equivalent to:
-  // 1. this->CreateOffer()
+  // 1. this->CreateOffer(offer_options)
   // 2. this->SetLocalDescription(offer)
   // 3. answerer->SetRemoteDescription(offer)
-  // 4. answerer->CreateAnswer()
+  // 4. answerer->CreateAnswer(answer_options)
   // 5. answerer->SetLocalDescription(answer)
   // 6. this->SetRemoteDescription(answer)
   // Returns true if all steps succeed, false otherwise.
   // Suggested usage:
   //   ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
   bool ExchangeOfferAnswerWith(PeerConnectionWrapper* answerer);
+  bool ExchangeOfferAnswerWith(
+      PeerConnectionWrapper* answerer,
+      const PeerConnectionInterface::RTCOfferAnswerOptions& offer_options,
+      const PeerConnectionInterface::RTCOfferAnswerOptions& answer_options);
 
   // The following are wrappers for the underlying PeerConnection's
   // AddTransceiver method. They return the result of calling AddTransceiver
diff --git a/pc/rtpmediautils.cc b/pc/rtpmediautils.cc
index f88861c..337aa2d 100644
--- a/pc/rtpmediautils.cc
+++ b/pc/rtpmediautils.cc
@@ -50,6 +50,20 @@
   return direction;
 }
 
+RtpTransceiverDirection RtpTransceiverDirectionWithSendSet(
+    RtpTransceiverDirection direction,
+    bool send) {
+  return RtpTransceiverDirectionFromSendRecv(
+      send, RtpTransceiverDirectionHasRecv(direction));
+}
+
+RtpTransceiverDirection RtpTransceiverDirectionWithRecvSet(
+    RtpTransceiverDirection direction,
+    bool recv) {
+  return RtpTransceiverDirectionFromSendRecv(
+      RtpTransceiverDirectionHasSend(direction), recv);
+}
+
 const char* RtpTransceiverDirectionToString(RtpTransceiverDirection direction) {
   switch (direction) {
     case RtpTransceiverDirection::kSendRecv:
diff --git a/pc/rtpmediautils.h b/pc/rtpmediautils.h
index 6f547bd..6de6f8f 100644
--- a/pc/rtpmediautils.h
+++ b/pc/rtpmediautils.h
@@ -31,6 +31,16 @@
 RtpTransceiverDirection RtpTransceiverDirectionReversed(
     RtpTransceiverDirection direction);
 
+// Returns the RtpTransceiverDirection with its send component set to |send|.
+RtpTransceiverDirection RtpTransceiverDirectionWithSendSet(
+    RtpTransceiverDirection direction,
+    bool send = true);
+
+// Returns the RtpTransceiverDirection with its recv component set to |recv|.
+RtpTransceiverDirection RtpTransceiverDirectionWithRecvSet(
+    RtpTransceiverDirection direction,
+    bool recv = true);
+
 // Returns an unspecified string representation of the given direction.
 const char* RtpTransceiverDirectionToString(RtpTransceiverDirection direction);
 
diff --git a/pc/rtpmediautils_unittest.cc b/pc/rtpmediautils_unittest.cc
index d72f35b..af7e8f6 100644
--- a/pc/rtpmediautils_unittest.cc
+++ b/pc/rtpmediautils_unittest.cc
@@ -8,16 +8,24 @@
  *  be found in the AUTHORS file in the root of the source tree.
  */
 
+#include <tuple>
+
 #include "pc/rtpmediautils.h"
 #include "test/gtest.h"
 
 namespace webrtc {
 
+using ::testing::Bool;
+using ::testing::Combine;
 using ::testing::Values;
+using ::testing::ValuesIn;
+
+RtpTransceiverDirection kAllDirections[] = {
+    RtpTransceiverDirection::kSendRecv, RtpTransceiverDirection::kSendOnly,
+    RtpTransceiverDirection::kRecvOnly, RtpTransceiverDirection::kInactive};
 
 class EnumerateAllDirectionsTest
-    : public ::testing::Test,
-      public ::testing::WithParamInterface<RtpTransceiverDirection> {};
+    : public ::testing::TestWithParam<RtpTransceiverDirection> {};
 
 // Test that converting the direction to send/recv and back again results in the
 // same direction.
@@ -51,9 +59,38 @@
 
 INSTANTIATE_TEST_CASE_P(RtpTransceiverDirectionTest,
                         EnumerateAllDirectionsTest,
-                        Values(RtpTransceiverDirection::kSendRecv,
-                               RtpTransceiverDirection::kSendOnly,
-                               RtpTransceiverDirection::kRecvOnly,
-                               RtpTransceiverDirection::kInactive));
+                        ValuesIn(kAllDirections));
+
+class EnumerateAllDirectionsAndBool
+    : public ::testing::TestWithParam<
+          std::tuple<RtpTransceiverDirection, bool>> {};
+
+TEST_P(EnumerateAllDirectionsAndBool, TestWithSendSet) {
+  RtpTransceiverDirection direction = std::get<0>(GetParam());
+  bool send = std::get<1>(GetParam());
+
+  RtpTransceiverDirection result =
+      RtpTransceiverDirectionWithSendSet(direction, send);
+
+  EXPECT_EQ(send, RtpTransceiverDirectionHasSend(result));
+  EXPECT_EQ(RtpTransceiverDirectionHasRecv(direction),
+            RtpTransceiverDirectionHasRecv(result));
+}
+
+TEST_P(EnumerateAllDirectionsAndBool, TestWithRecvSet) {
+  RtpTransceiverDirection direction = std::get<0>(GetParam());
+  bool recv = std::get<1>(GetParam());
+
+  RtpTransceiverDirection result =
+      RtpTransceiverDirectionWithRecvSet(direction, recv);
+
+  EXPECT_EQ(RtpTransceiverDirectionHasSend(direction),
+            RtpTransceiverDirectionHasSend(result));
+  EXPECT_EQ(recv, RtpTransceiverDirectionHasRecv(result));
+}
+
+INSTANTIATE_TEST_CASE_P(RtpTransceiverDirectionTest,
+                        EnumerateAllDirectionsAndBool,
+                        Combine(ValuesIn(kAllDirections), Bool()));
 
 }  // namespace webrtc