Reland "Make negotiationneeded processing in PeerConnection spec compliant."
The new processing applies only in Unified Plan mode.
Plan B retains the old-style processing.
This is a reland of 1fa06041bcd8a0119e557d16e7b54a9110c5ad03
Original change's description:
> Make negotiationneeded processing in PeerConnection spec compliant.
>
> This CL fixes the problem of misfired negotiationneeded notifications due
> to the lack of a NegotiationNeeded slot and the proper procedure to
> update it.
>
>
> Change-Id: Ie273c691f11316c9846606446f6cf838226b5d5c
> Bug: chromium:740501
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/131283
> Commit-Queue: Guido Urdaneta <guidou@webrtc.org>
> Reviewed-by: Steve Anton <steveanton@webrtc.org>
> Reviewed-by: Henrik Boström <hbos@webrtc.org>
> Reviewed-by: Magnus Jedvert <magjed@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#27594}
Bug: chromium:740501
Change-Id: I048ae81b2b00086f6d669e94eecf426f0db0ec08
TBR: steveanton@webrtc.org
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/133162
Commit-Queue: Guido Urdaneta <guidou@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#27640}
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index e7a1a11..8ac1e83 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -698,6 +698,15 @@
}
}
+const ContentInfo* FindTransceiverMSection(
+ RtpTransceiverProxyWithInternal<RtpTransceiver>* transceiver,
+ const SessionDescriptionInterface* session_description) {
+ return transceiver->mid()
+ ? session_description->description()->GetContentByName(
+ *transceiver->mid())
+ : nullptr;
+}
+
} // namespace
// Upon completion, posts a task to execute the callback of the
@@ -1254,7 +1263,7 @@
}
stats_->AddStream(local_stream);
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
return true;
}
@@ -1284,7 +1293,7 @@
if (IsClosed()) {
return;
}
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>> PeerConnection::AddTrack(
@@ -1313,7 +1322,7 @@
(IsUnifiedPlan() ? AddTrackUnifiedPlan(track, stream_ids)
: AddTrackPlanB(track, stream_ids));
if (sender_or_error.ok()) {
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
stats_->AddTrack(track);
}
return sender_or_error;
@@ -1460,7 +1469,7 @@
"Couldn't find sender " + sender->id() + " to remove.");
}
}
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
return RTCError::OK();
}
@@ -1527,7 +1536,7 @@
cricket::MediaType media_type,
rtc::scoped_refptr<MediaStreamTrackInterface> track,
const RtpTransceiverInit& init,
- bool fire_callback) {
+ bool update_negotiation_needed) {
RTC_DCHECK((media_type == cricket::MEDIA_TYPE_AUDIO ||
media_type == cricket::MEDIA_TYPE_VIDEO));
if (track) {
@@ -1616,8 +1625,8 @@
auto transceiver = CreateAndAddTransceiver(sender, receiver);
transceiver->internal()->set_direction(init.direction);
- if (fire_callback) {
- Observer()->OnRenegotiationNeeded();
+ if (update_negotiation_needed) {
+ UpdateNegotiationNeeded();
}
return rtc::scoped_refptr<RtpTransceiverInterface>(transceiver);
@@ -1695,7 +1704,7 @@
void PeerConnection::OnNegotiationNeeded() {
RTC_DCHECK_RUN_ON(signaling_thread());
RTC_DCHECK(!IsClosed());
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
rtc::scoped_refptr<RtpSenderInterface> PeerConnection::CreateSender(
@@ -1943,7 +1952,7 @@
// Trigger the onRenegotiationNeeded event for every new RTP DataChannel, or
// the first SCTP DataChannel.
if (data_channel_type() == cricket::DCT_RTP || first_datachannel) {
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
NoteUsageEvent(UsageEvent::DATA_ADDED);
return DataChannelProxy::Create(signaling_thread(), channel.get());
@@ -2045,7 +2054,8 @@
<< " transceiver since CreateOffer specified offer_to_receive=1";
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kRecvOnly;
- AddTransceiver(media_type, nullptr, init, /*fire_callback=*/false);
+ AddTransceiver(media_type, nullptr, init,
+ /*update_negotiation_needed=*/false);
}
}
@@ -2189,6 +2199,16 @@
// Make UMA notes about what was agreed to.
ReportNegotiatedSdpSemantics(*local_description());
}
+
+ if (IsUnifiedPlan()) {
+ bool was_negotiation_needed = is_negotiation_needed_;
+ UpdateNegotiationNeeded();
+ if (signaling_state() == kStable && was_negotiation_needed &&
+ is_negotiation_needed_) {
+ Observer()->OnRenegotiationNeeded();
+ }
+ }
+
NoteUsageEvent(UsageEvent::SET_LOCAL_DESCRIPTION_CALLED);
}
@@ -2546,6 +2566,15 @@
ReportNegotiatedSdpSemantics(*remote_description());
}
+ if (IsUnifiedPlan()) {
+ bool was_negotiation_needed = is_negotiation_needed_;
+ UpdateNegotiationNeeded();
+ if (signaling_state() == kStable && was_negotiation_needed &&
+ is_negotiation_needed_) {
+ Observer()->OnRenegotiationNeeded();
+ }
+ }
+
observer->OnSetRemoteDescriptionComplete(RTCError::OK());
NoteUsageEvent(UsageEvent::SET_REMOTE_DESCRIPTION_CALLED);
}
@@ -4158,7 +4187,7 @@
return;
}
AddAudioTrack(track, stream);
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
void PeerConnection::OnAudioTrackRemoved(AudioTrackInterface* track,
@@ -4167,7 +4196,7 @@
return;
}
RemoveAudioTrack(track, stream);
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
void PeerConnection::OnVideoTrackAdded(VideoTrackInterface* track,
@@ -4176,7 +4205,7 @@
return;
}
AddVideoTrack(track, stream);
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
void PeerConnection::OnVideoTrackRemoved(VideoTrackInterface* track,
@@ -4185,7 +4214,7 @@
return;
}
RemoveVideoTrack(track, stream);
- Observer()->OnRenegotiationNeeded();
+ UpdateNegotiationNeeded();
}
void PeerConnection::PostSetSessionDescriptionSuccess(
@@ -7125,4 +7154,174 @@
nullptr);
}
+void PeerConnection::UpdateNegotiationNeeded() {
+ RTC_DCHECK_RUN_ON(signaling_thread());
+ if (!IsUnifiedPlan()) {
+ Observer()->OnRenegotiationNeeded();
+ return;
+ }
+
+ // If connection's [[IsClosed]] slot is true, abort these steps.
+ if (IsClosed())
+ return;
+
+ // If connection's signaling state is not "stable", abort these steps.
+ if (signaling_state() != kStable)
+ return;
+
+ // NOTE
+ // The negotiation-needed flag will be updated once the state transitions to
+ // "stable", as part of the steps for setting an RTCSessionDescription.
+
+ // If the result of checking if negotiation is needed is false, clear the
+ // negotiation-needed flag by setting connection's [[NegotiationNeeded]] slot
+ // to false, and abort these steps.
+ bool is_negotiation_needed = CheckIfNegotiationIsNeeded();
+ if (!is_negotiation_needed) {
+ is_negotiation_needed_ = false;
+ return;
+ }
+
+ // If connection's [[NegotiationNeeded]] slot is already true, abort these
+ // steps.
+ if (is_negotiation_needed_)
+ return;
+
+ // Set connection's [[NegotiationNeeded]] slot to true.
+ is_negotiation_needed_ = true;
+
+ // Queue a task that runs the following steps:
+ // If connection's [[IsClosed]] slot is true, abort these steps.
+ // If connection's [[NegotiationNeeded]] slot is false, abort these steps.
+ // Fire an event named negotiationneeded at connection.
+ Observer()->OnRenegotiationNeeded();
+}
+
+bool PeerConnection::CheckIfNegotiationIsNeeded() {
+ RTC_DCHECK_RUN_ON(signaling_thread());
+ // 1. If any implementation-specific negotiation is required, as described at
+ // the start of this section, return true.
+
+ // 2. Let description be connection.[[CurrentLocalDescription]].
+ const SessionDescriptionInterface* description = current_local_description();
+ if (!description)
+ return true;
+
+ // 3. If connection has created any RTCDataChannels, and no m= section in
+ // description has been negotiated yet for data, return true.
+ if (!sctp_data_channels_.empty()) {
+ if (!cricket::GetFirstDataContent(description->description()->contents()))
+ return true;
+ }
+
+ // 4. For each transceiver in connection's set of transceivers, perform the
+ // following checks:
+ for (const auto& transceiver : transceivers_) {
+ const ContentInfo* current_local_msection =
+ FindTransceiverMSection(transceiver.get(), description);
+
+ const ContentInfo* current_remote_msection = FindTransceiverMSection(
+ transceiver.get(), current_remote_description());
+
+ // 4.3 If transceiver is stopped and is associated with an m= section,
+ // but the associated m= section is not yet rejected in
+ // connection.[[CurrentLocalDescription]] or
+ // connection.[[CurrentRemoteDescription]], return true.
+ if (transceiver->stopped()) {
+ if (current_local_msection && !current_local_msection->rejected &&
+ ((current_remote_msection && !current_remote_msection->rejected) ||
+ !current_remote_msection)) {
+ return true;
+ }
+ continue;
+ }
+
+ // 4.1 If transceiver isn't stopped and isn't yet associated with an m=
+ // section in description, return true.
+ if (!current_local_msection)
+ return true;
+
+ const MediaContentDescription* current_local_media_description =
+ current_local_msection->media_description();
+ // 4.2 If transceiver isn't stopped and is associated with an m= section
+ // in description then perform the following checks:
+
+ // 4.2.1 If transceiver.[[Direction]] is "sendrecv" or "sendonly", and the
+ // associated m= section in description either doesn't contain a single
+ // "a=msid" line, or the number of MSIDs from the "a=msid" lines in this
+ // m= section, or the MSID values themselves, differ from what is in
+ // transceiver.sender.[[AssociatedMediaStreamIds]], return true.
+ if (RtpTransceiverDirectionHasSend(transceiver->direction())) {
+ if (current_local_media_description->streams().size() == 0)
+ return true;
+
+ std::vector<std::string> msection_msids;
+ for (const auto& stream : current_local_media_description->streams()) {
+ for (const std::string& msid : stream.stream_ids())
+ msection_msids.push_back(msid);
+ }
+
+ std::vector<std::string> transceiver_msids =
+ transceiver->sender()->stream_ids();
+ if (msection_msids.size() != transceiver_msids.size())
+ return true;
+
+ absl::c_sort(transceiver_msids);
+ absl::c_sort(msection_msids);
+ if (transceiver_msids != msection_msids)
+ return true;
+ }
+
+ // 4.2.2 If description is of type "offer", and the direction of the
+ // associated m= section in neither connection.[[CurrentLocalDescription]]
+ // nor connection.[[CurrentRemoteDescription]] matches
+ // transceiver.[[Direction]], return true.
+ if (description->GetType() == SdpType::kOffer) {
+ if (!current_remote_description())
+ return true;
+
+ if (!current_remote_msection)
+ return true;
+
+ RtpTransceiverDirection current_local_direction =
+ current_local_media_description->direction();
+ RtpTransceiverDirection current_remote_direction =
+ current_remote_msection->media_description()->direction();
+ if (transceiver->direction() != current_local_direction &&
+ transceiver->direction() !=
+ RtpTransceiverDirectionReversed(current_remote_direction)) {
+ return true;
+ }
+ }
+
+ // 4.2.3 If description is of type "answer", and the direction of the
+ // associated m= section in the description does not match
+ // transceiver.[[Direction]] intersected with the offered direction (as
+ // described in [JSEP] (section 5.3.1.)), return true.
+ if (description->GetType() == SdpType::kAnswer) {
+ if (!remote_description())
+ return true;
+
+ const ContentInfo* offered_remote_msection =
+ FindTransceiverMSection(transceiver.get(), remote_description());
+
+ RtpTransceiverDirection offered_direction =
+ offered_remote_msection
+ ? offered_remote_msection->media_description()->direction()
+ : RtpTransceiverDirection::kInactive;
+
+ if (current_local_media_description->direction() !=
+ (RtpTransceiverDirectionIntersection(
+ transceiver->direction(),
+ RtpTransceiverDirectionReversed(offered_direction)))) {
+ return true;
+ }
+ }
+ }
+
+ // If all the preceding checks were performed and true was not returned,
+ // nothing remains to be negotiated; return false.
+ return false;
+}
+
} // namespace webrtc
diff --git a/pc/peer_connection.h b/pc/peer_connection.h
index c639a58..2cf3662 100644
--- a/pc/peer_connection.h
+++ b/pc/peer_connection.h
@@ -1086,6 +1086,9 @@
return media_transport;
}
+ void UpdateNegotiationNeeded();
+ bool CheckIfNegotiationIsNeeded();
+
sigslot::signal1<DataChannel*> SignalDataChannelCreated_
RTC_GUARDED_BY(signaling_thread());
@@ -1329,6 +1332,7 @@
// channel manager and the session description factory.
rtc::UniqueRandomIdGenerator ssrc_generator_
RTC_GUARDED_BY(signaling_thread());
+ bool is_negotiation_needed_ RTC_GUARDED_BY(signaling_thread()) = false;
};
} // namespace webrtc
diff --git a/pc/peer_connection_interface_unittest.cc b/pc/peer_connection_interface_unittest.cc
index e69fc96..d73e45a 100644
--- a/pc/peer_connection_interface_unittest.cc
+++ b/pc/peer_connection_interface_unittest.cc
@@ -814,8 +814,6 @@
auto sender_or_error =
pc_->AddTrack(CreateVideoTrack(track_label), stream_ids);
ASSERT_EQ(RTCErrorType::NONE, sender_or_error.error().type());
- EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
- observer_.renegotiation_needed_ = false;
}
void AddVideoStream(const std::string& label) {
@@ -823,8 +821,6 @@
pc_factory_->CreateLocalMediaStream(label));
stream->AddTrack(CreateVideoTrack(label + "v0"));
ASSERT_TRUE(pc_->AddStream(stream));
- EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
- observer_.renegotiation_needed_ = false;
}
rtc::scoped_refptr<AudioTrackInterface> CreateAudioTrack(
@@ -837,8 +833,6 @@
auto sender_or_error =
pc_->AddTrack(CreateAudioTrack(track_label), stream_ids);
ASSERT_EQ(RTCErrorType::NONE, sender_or_error.error().type());
- EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
- observer_.renegotiation_needed_ = false;
}
void AddAudioStream(const std::string& label) {
@@ -846,8 +840,6 @@
pc_factory_->CreateLocalMediaStream(label));
stream->AddTrack(CreateAudioTrack(label + "a0"));
ASSERT_TRUE(pc_->AddStream(stream));
- EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
- observer_.renegotiation_needed_ = false;
}
void AddAudioVideoStream(const std::string& stream_id,
@@ -859,8 +851,6 @@
stream->AddTrack(CreateAudioTrack(audio_track_label));
stream->AddTrack(CreateVideoTrack(video_track_label));
ASSERT_TRUE(pc_->AddStream(stream));
- EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
- observer_.renegotiation_needed_ = false;
}
rtc::scoped_refptr<RtpReceiverInterface> GetFirstReceiverOfType(
@@ -2204,9 +2194,12 @@
EXPECT_TRUE(observer_.renegotiation_needed_);
observer_.renegotiation_needed_ = false;
+ CreateOfferReceiveAnswer();
+
rtc::scoped_refptr<DataChannelInterface> dc2 =
pc_->CreateDataChannel("test2", NULL);
- EXPECT_TRUE(observer_.renegotiation_needed_);
+ EXPECT_EQ(observer_.renegotiation_needed_,
+ GetParam() == SdpSemantics::kPlanB);
}
// This test that a data channel closes when a PeerConnection is deleted/closed.
@@ -3894,14 +3887,17 @@
EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
observer_.renegotiation_needed_ = false;
+ CreateOfferReceiveAnswer();
stream->AddTrack(video_track);
EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
observer_.renegotiation_needed_ = false;
+ CreateOfferReceiveAnswer();
stream->RemoveTrack(audio_track);
EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
observer_.renegotiation_needed_ = false;
+ CreateOfferReceiveAnswer();
stream->RemoveTrack(video_track);
EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout);
observer_.renegotiation_needed_ = false;
diff --git a/pc/peer_connection_rtp_unittest.cc b/pc/peer_connection_rtp_unittest.cc
index c03bab4..6925300 100644
--- a/pc/peer_connection_rtp_unittest.cc
+++ b/pc/peer_connection_rtp_unittest.cc
@@ -1135,11 +1135,14 @@
TEST_F(PeerConnectionRtpTestUnifiedPlan,
AddTrackChangesDirectionFromInactiveToSendOnly) {
auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kInactive;
auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
caller->observer()->clear_negotiation_needed();
ASSERT_TRUE(caller->AddAudioTrack("a"));
EXPECT_TRUE(caller->observer()->negotiation_needed());
@@ -1152,11 +1155,14 @@
TEST_F(PeerConnectionRtpTestUnifiedPlan,
AddTrackChangesDirectionFromRecvOnlyToSendRecv) {
auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kRecvOnly;
auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
caller->observer()->clear_negotiation_needed();
ASSERT_TRUE(caller->AddAudioTrack("a"));
EXPECT_TRUE(caller->observer()->negotiation_needed());
@@ -1219,18 +1225,21 @@
TEST_F(PeerConnectionRtpTestUnifiedPlan,
RemoveTrackChangesDirectionFromSendRecvToRecvOnly) {
auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kSendRecv;
auto transceiver =
caller->AddTransceiver(caller->CreateAudioTrack("a"), init);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
caller->observer()->clear_negotiation_needed();
+
ASSERT_TRUE(caller->pc()->RemoveTrack(transceiver->sender()));
EXPECT_TRUE(caller->observer()->negotiation_needed());
EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceiver->direction());
- EXPECT_TRUE(caller->observer()->renegotiation_needed_);
}
// Test that calling RemoveTrack on a sender where the transceiver is configured
@@ -1238,13 +1247,17 @@
TEST_F(PeerConnectionRtpTestUnifiedPlan,
RemoveTrackChangesDirectionFromSendOnlyToInactive) {
auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kSendOnly;
auto transceiver =
caller->AddTransceiver(caller->CreateAudioTrack("a"), init);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
caller->observer()->clear_negotiation_needed();
+
ASSERT_TRUE(caller->pc()->RemoveTrack(transceiver->sender()));
EXPECT_TRUE(caller->observer()->negotiation_needed());
@@ -1394,10 +1407,15 @@
TEST_F(PeerConnectionRtpTestUnifiedPlan,
RenegotiationNeededAfterTransceiverSetDirection) {
auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+ EXPECT_FALSE(caller->observer()->negotiation_needed());
auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
caller->observer()->clear_negotiation_needed();
+
transceiver->SetDirection(RtpTransceiverDirection::kInactive);
EXPECT_TRUE(caller->observer()->negotiation_needed());
}
@@ -1777,6 +1795,42 @@
}
}
+// This test exercises the code path that fires a NegotiationNeeded
+// notification when the stream IDs of the local description differ from
+// the ones in the transceiver. Since SetStreams() is not yet available
+// on RtpSenderInterface, adding a track is used to trigger the check for
+// the NegotiationNeeded notification.
+// TODO(https://crbug.com/webrtc/10129): Replace this test with a test that
+// checks that calling SetStreams() on a sender fires the notification once
+// the method becomes available in RtpSenderInterface.
+TEST_F(PeerConnectionRtpTestUnifiedPlan,
+ ChangeAssociatedStreamsTriggersRenegotiation) {
+ auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+
+ RtpTransceiverInit init;
+ init.direction = RtpTransceiverDirection::kSendRecv;
+ auto transceiver =
+ caller->AddTransceiver(caller->CreateAudioTrack("a"), init);
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+ caller->observer()->clear_negotiation_needed();
+
+ SessionDescriptionInterface* cld = const_cast<SessionDescriptionInterface*>(
+ caller->pc()->current_local_description());
+ ASSERT_EQ(cld->description()->contents().size(), 1u);
+
+ cricket::SessionDescription* description = cld->description();
+ cricket::ContentInfo& content_info = description->contents()[0];
+ ASSERT_EQ(content_info.media_description()->mutable_streams().size(), 1u);
+ content_info.media_description()->mutable_streams()[0].set_stream_ids(
+ {"stream3", "stream4", "stream5"});
+
+ ASSERT_TRUE(caller->AddTrack(caller->CreateAudioTrack("a2")));
+ EXPECT_TRUE(caller->observer()->negotiation_needed());
+}
+
INSTANTIATE_TEST_SUITE_P(PeerConnectionRtpTest,
PeerConnectionRtpTest,
Values(SdpSemantics::kPlanB,
diff --git a/pc/rtp_media_utils.cc b/pc/rtp_media_utils.cc
index f4a6120..6e8be58 100644
--- a/pc/rtp_media_utils.cc
+++ b/pc/rtp_media_utils.cc
@@ -81,4 +81,14 @@
return "";
}
+RtpTransceiverDirection RtpTransceiverDirectionIntersection(
+ RtpTransceiverDirection lhs,
+ RtpTransceiverDirection rhs) {
+ return RtpTransceiverDirectionFromSendRecv(
+ RtpTransceiverDirectionHasSend(lhs) &&
+ RtpTransceiverDirectionHasSend(rhs),
+ RtpTransceiverDirectionHasRecv(lhs) &&
+ RtpTransceiverDirectionHasRecv(rhs));
+}
+
} // namespace webrtc
diff --git a/pc/rtp_media_utils.h b/pc/rtp_media_utils.h
index 3ff40c9..f556fe39 100644
--- a/pc/rtp_media_utils.h
+++ b/pc/rtp_media_utils.h
@@ -44,6 +44,11 @@
// Returns an unspecified string representation of the given direction.
const char* RtpTransceiverDirectionToString(RtpTransceiverDirection direction);
+// Returns the intersection of the directions of two transceivers.
+RtpTransceiverDirection RtpTransceiverDirectionIntersection(
+ RtpTransceiverDirection lhs,
+ RtpTransceiverDirection rhs);
+
#ifdef UNIT_TEST
inline std::ostream& operator<<( // no-presubmit-check TODO(webrtc:8982)
std::ostream& os, // no-presubmit-check TODO(webrtc:8982)