Add support for JSEP offer/answer with transceivers
This change adds support to PeerConnection's CreateOffer/
CreateAnswer/SetLocalDescription/SetRemoteDescription for
Unified Plan SDP mapping to/from RtpTransceivers. This behavior
is enabled using the kUnifiedPlan SDP semantics in the
PeerConnection configuration.
Bug: webrtc:7600
Change-Id: I4b44f5d3690887d387bf9c47eac00db8ec974571
Reviewed-on: https://webrtc-review.googlesource.com/28341
Commit-Queue: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21442}
diff --git a/api/rtptransceiverinterface.h b/api/rtptransceiverinterface.h
index 1d2988d..5d74ae1 100644
--- a/api/rtptransceiverinterface.h
+++ b/api/rtptransceiverinterface.h
@@ -29,6 +29,9 @@
kInactive
};
+// This is provided as a debugging aid. The format of the output is unspecified.
+std::ostream& operator<<(std::ostream& os, RtpTransceiverDirection direction);
+
// Structure for initializing an RtpTransceiver in a call to
// PeerConnectionInterface::AddTransceiver.
// https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiverinit
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index a429290..a0f94af 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -400,6 +400,7 @@
"peerconnection_datachannel_unittest.cc",
"peerconnection_ice_unittest.cc",
"peerconnection_integrationtest.cc",
+ "peerconnection_jsep_unittest.cc",
"peerconnection_media_unittest.cc",
"peerconnection_rtp_unittest.cc",
"peerconnection_signaling_unittest.cc",
diff --git a/pc/mediasession.cc b/pc/mediasession.cc
index 24d0d34..692bcc2 100644
--- a/pc/mediasession.cc
+++ b/pc/mediasession.cc
@@ -1309,16 +1309,16 @@
// Iterate through the media description options, matching with existing media
// descriptions in |current_description|.
- int msection_index = 0;
+ size_t msection_index = 0;
for (const MediaDescriptionOptions& media_description_options :
session_options.media_description_options) {
const ContentInfo* current_content = nullptr;
if (current_description &&
- msection_index <
- static_cast<int>(current_description->contents().size())) {
+ msection_index < current_description->contents().size()) {
current_content = ¤t_description->contents()[msection_index];
- // Media type must match.
- RTC_DCHECK(IsMediaContentOfType(current_content,
+ // Media type must match unless this media section is being recycled.
+ RTC_DCHECK(current_content->rejected ||
+ IsMediaContentOfType(current_content,
media_description_options.type));
}
switch (media_description_options.type) {
@@ -1424,7 +1424,7 @@
session_options.media_description_options.size());
// Iterate through the media description options, matching with existing
// media descriptions in |current_description|.
- int msection_index = 0;
+ size_t msection_index = 0;
for (const MediaDescriptionOptions& media_description_options :
session_options.media_description_options) {
const ContentInfo* offer_content = &offer->contents()[msection_index];
@@ -1435,8 +1435,7 @@
RTC_DCHECK(media_description_options.mid == offer_content->name);
const ContentInfo* current_content = nullptr;
if (current_description &&
- msection_index <
- static_cast<int>(current_description->contents().size())) {
+ msection_index < current_description->contents().size()) {
current_content = ¤t_description->contents()[msection_index];
}
switch (media_description_options.type) {
@@ -1802,8 +1801,8 @@
GetAudioCodecsForOffer(media_description_options.direction);
AudioCodecs filtered_codecs;
- // Add the codecs from current content if exists.
- if (current_content) {
+ // Add the codecs from current content if it exists and is not being recycled.
+ if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
const AudioContentDescription* acd =
current_content->media_description()->as_audio();
@@ -1877,8 +1876,8 @@
&crypto_suites);
VideoCodecs filtered_codecs;
- // Add the codecs from current content if exists.
- if (current_content) {
+ // Add the codecs from current content if it exists and is not being recycled.
+ if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
const VideoContentDescription* vcd =
current_content->media_description()->as_video();
@@ -2038,8 +2037,8 @@
GetAudioCodecsForAnswer(offer_rtd, answer_rtd);
AudioCodecs filtered_codecs;
- // Add the codecs from current content if exists.
- if (current_content) {
+ // Add the codecs from current content if it exists and is not being recycled.
+ if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
const AudioContentDescription* acd =
current_content->media_description()->as_audio();
@@ -2120,8 +2119,8 @@
}
VideoCodecs filtered_codecs;
- // Add the codecs from current content if exists.
- if (current_content) {
+ // Add the codecs from current content if it exists and is not being recycled.
+ if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
const VideoContentDescription* vcd =
current_content->media_description()->as_video();
diff --git a/pc/peerconnection.cc b/pc/peerconnection.cc
index 5a21146..b99df99 100644
--- a/pc/peerconnection.cc
+++ b/pc/peerconnection.cc
@@ -11,6 +11,7 @@
#include "pc/peerconnection.h"
#include <algorithm>
+#include <queue>
#include <set>
#include <utility>
#include <vector>
@@ -330,6 +331,11 @@
}
for (size_t i = 0; i < existing_desc->contents().size(); ++i) {
+ if (existing_desc->contents()[i].rejected) {
+ // If the media section can be recycled, it's valid for the MID and media
+ // type to change.
+ continue;
+ }
if (new_desc->contents()[i].name != existing_desc->contents()[i].name) {
return false;
}
@@ -1637,34 +1643,69 @@
}
}
+ // Take a reference to the old local description since it's used below to
+ // compare against the new local description. When setting the new local
+ // description, grab ownership of the replaced session description in case it
+ // is the same as |old_local_description|, to keep it alive for the duration
+ // of the method.
+ const SessionDescriptionInterface* old_local_description =
+ local_description();
+ std::unique_ptr<SessionDescriptionInterface> replaced_local_description;
if (type == SdpType::kAnswer) {
+ replaced_local_description = pending_local_description_
+ ? std::move(pending_local_description_)
+ : std::move(current_local_description_);
current_local_description_ = std::move(desc);
pending_local_description_ = nullptr;
current_remote_description_ = std::move(pending_remote_description_);
} else {
+ replaced_local_description = std::move(pending_local_description_);
pending_local_description_ = std::move(desc);
}
// The session description to apply now must be accessed by
// |local_description()|.
RTC_DCHECK(local_description());
- // Transport and Media channels will be created only when offer is set.
- if (type == SdpType::kOffer) {
- // TODO(mallinath) - Handle CreateChannel failure, as new local description
- // is applied. Restore back to old description.
- RTCError error = CreateChannels(local_description()->description());
+ if (IsUnifiedPlan()) {
+ RTCError error = UpdateTransceiversAndDataChannels(
+ cricket::CS_LOCAL, old_local_description, *local_description());
if (!error.ok()) {
return error;
}
- }
+ for (auto transceiver : transceivers_) {
+ const ContentInfo* content =
+ FindMediaSectionForTransceiver(transceiver, local_description());
+ if (!content) {
+ continue;
+ }
+ const MediaContentDescription* media_desc = content->media_description();
+ if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+ transceiver->internal()->set_current_direction(media_desc->direction());
+ }
+ if (content->rejected && !transceiver->stopped()) {
+ transceiver->Stop();
+ }
+ }
+ } else {
+ // Transport and Media channels will be created only when offer is set.
+ if (type == SdpType::kOffer) {
+ // TODO(bugs.webrtc.org/4676) - Handle CreateChannel failure, as new local
+ // description is applied. Restore back to old description.
+ RTCError error = CreateChannels(*local_description()->description());
+ if (!error.ok()) {
+ return error;
+ }
+ }
- // Remove unused channels if MediaContentDescription is rejected.
- RemoveUnusedChannels(local_description()->description());
+ // Remove unused channels if MediaContentDescription is rejected.
+ RemoveUnusedChannels(local_description()->description());
+ }
error = UpdateSessionState(type, cricket::CS_LOCAL);
if (!error.ok()) {
return error;
}
+
if (remote_description()) {
// Now that we have a local description, we can push down remote candidates.
UseCandidatesInSessionDescription(remote_description());
@@ -1682,29 +1723,31 @@
AllocateSctpSids(role);
}
- // Update state and SSRC of local MediaStreams and DataChannels based on the
- // local session description.
- const cricket::ContentInfo* audio_content =
- GetFirstAudioContent(local_description()->description());
- if (audio_content) {
- if (audio_content->rejected) {
- RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
- } else {
- const cricket::AudioContentDescription* audio_desc =
- audio_content->media_description()->as_audio();
- UpdateLocalSenders(audio_desc->streams(), audio_desc->type());
+ if (!IsUnifiedPlan()) {
+ // Update state and SSRC of local MediaStreams and DataChannels based on the
+ // local session description.
+ const cricket::ContentInfo* audio_content =
+ GetFirstAudioContent(local_description()->description());
+ if (audio_content) {
+ if (audio_content->rejected) {
+ RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
+ } else {
+ const cricket::AudioContentDescription* audio_desc =
+ audio_content->media_description()->as_audio();
+ UpdateLocalSenders(audio_desc->streams(), audio_desc->type());
+ }
}
- }
- const cricket::ContentInfo* video_content =
- GetFirstVideoContent(local_description()->description());
- if (video_content) {
- if (video_content->rejected) {
- RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
- } else {
- const cricket::VideoContentDescription* video_desc =
- video_content->media_description()->as_video();
- UpdateLocalSenders(video_desc->streams(), video_desc->type());
+ const cricket::ContentInfo* video_content =
+ GetFirstVideoContent(local_description()->description());
+ if (video_content) {
+ if (video_content->rejected) {
+ RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
+ } else {
+ const cricket::VideoContentDescription* video_desc =
+ video_content->media_description()->as_video();
+ UpdateLocalSenders(video_desc->streams(), video_desc->type());
+ }
}
}
@@ -1787,13 +1830,14 @@
// Update stats here so that we have the most recent stats for tracks and
// streams that might be removed by updating the session description.
stats_->UpdateStats(kStatsOutputLevelStandard);
- // Takes the ownership of |desc|. On success, remote_description() is updated
- // to reflect the description that was passed in.
+ // Take a reference to the old remote description since it's used below to
+ // compare against the new remote description. When setting the new remote
+ // description, grab ownership of the replaced session description in case it
+ // is the same as |old_remote_description|, to keep it alive for the duration
+ // of the method.
const SessionDescriptionInterface* old_remote_description =
remote_description();
- // Grab ownership of the description being replaced for the remainder of this
- // method, since it's used below as |old_remote_description|.
std::unique_ptr<SessionDescriptionInterface> replaced_remote_description;
SdpType type = desc->GetType();
if (type == SdpType::kAnswer) {
@@ -1812,17 +1856,25 @@
RTC_DCHECK(remote_description());
// Transport and Media channels will be created only when offer is set.
- if (type == SdpType::kOffer) {
- // TODO(mallinath) - Handle CreateChannel failure, as new local description
- // is applied. Restore back to old description.
- RTCError error = CreateChannels(remote_description()->description());
+ if (IsUnifiedPlan()) {
+ RTCError error = UpdateTransceiversAndDataChannels(
+ cricket::CS_REMOTE, old_remote_description, *remote_description());
if (!error.ok()) {
return error;
}
- }
+ } else {
+ if (type == SdpType::kOffer) {
+ // TODO(bugs.webrtc.org/4676) - Handle CreateChannel failure, as new local
+ // description is applied. Restore back to old description.
+ RTCError error = CreateChannels(*remote_description()->description());
+ if (!error.ok()) {
+ return error;
+ }
+ }
- // Remove unused channels if MediaContentDescription is rejected.
- RemoveUnusedChannels(remote_description()->description());
+ // Remove unused channels if MediaContentDescription is rejected.
+ RemoveUnusedChannels(remote_description()->description());
+ }
// NOTE: Candidates allocation will be initiated only when SetLocalDescription
// is called.
@@ -1887,6 +1939,43 @@
AllocateSctpSids(role);
}
+ if (IsUnifiedPlan()) {
+ for (auto transceiver : transceivers_) {
+ const ContentInfo* content =
+ FindMediaSectionForTransceiver(transceiver, remote_description());
+ if (!content) {
+ continue;
+ }
+ const MediaContentDescription* media_desc = content->media_description();
+ RtpTransceiverDirection local_direction =
+ RtpTransceiverDirectionReversed(media_desc->direction());
+ // From the WebRTC specification, steps 2.2.8.5/6 of section 4.4.1.6 "Set
+ // the RTCSessionDescription: If direction is sendrecv or recvonly, and
+ // transceiver's current direction is neither sendrecv nor recvonly,
+ // process the addition of a remote track for the media description.
+ if (RtpTransceiverDirectionHasRecv(local_direction) &&
+ (!transceiver->current_direction() ||
+ !RtpTransceiverDirectionHasRecv(
+ *transceiver->current_direction()))) {
+ // TODO(bugs.webrtc.org/7600): Process the addition of a remote track.
+ }
+ // If direction is sendonly or inactive, and transceiver's current
+ // direction is neither sendonly nor inactive, process the removal of a
+ // remote track for the media description.
+ if (!RtpTransceiverDirectionHasRecv(local_direction) &&
+ (!transceiver->current_direction() ||
+ RtpTransceiverDirectionHasRecv(*transceiver->current_direction()))) {
+ // TODO(bugs.webrtc.org/7600): Process the removal of a remote track.
+ }
+ if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+ transceiver->internal()->set_current_direction(local_direction);
+ }
+ if (content->rejected && !transceiver->stopped()) {
+ transceiver->Stop();
+ }
+ }
+ }
+
const cricket::ContentInfo* audio_content =
GetFirstAudioContent(remote_description()->description());
const cricket::ContentInfo* video_content =
@@ -1910,64 +1999,256 @@
// since only at that point will new streams have all their tracks.
rtc::scoped_refptr<StreamCollection> new_streams(StreamCollection::Create());
- // TODO(steveanton): When removing RTP senders/receivers in response to a
- // rejected media section, there is some cleanup logic that expects the voice/
- // video channel to still be set. But in this method the voice/video channel
- // would have been destroyed by the SetRemoteDescription caller above so the
- // cleanup that relies on them fails to run. The RemoveSenders calls should be
- // moved to right before the DestroyChannel calls to fix this.
+ if (!IsUnifiedPlan()) {
+ // TODO(steveanton): When removing RTP senders/receivers in response to a
+ // rejected media section, there is some cleanup logic that expects the
+ // voice/ video channel to still be set. But in this method the voice/video
+ // channel would have been destroyed by the SetRemoteDescription caller
+ // above so the cleanup that relies on them fails to run. The RemoveSenders
+ // calls should be moved to right before the DestroyChannel calls to fix
+ // this.
- // Find all audio rtp streams and create corresponding remote AudioTracks
- // and MediaStreams.
- if (audio_content) {
- if (audio_content->rejected) {
- RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
- } else {
- bool default_audio_track_needed =
- !remote_peer_supports_msid_ &&
- RtpTransceiverDirectionHasSend(audio_desc->direction());
- UpdateRemoteSendersList(GetActiveStreams(audio_desc),
- default_audio_track_needed, audio_desc->type(),
- new_streams);
+ // Find all audio rtp streams and create corresponding remote AudioTracks
+ // and MediaStreams.
+ if (audio_content) {
+ if (audio_content->rejected) {
+ RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
+ } else {
+ bool default_audio_track_needed =
+ !remote_peer_supports_msid_ &&
+ RtpTransceiverDirectionHasSend(audio_desc->direction());
+ UpdateRemoteSendersList(GetActiveStreams(audio_desc),
+ default_audio_track_needed, audio_desc->type(),
+ new_streams);
+ }
}
- }
- // Find all video rtp streams and create corresponding remote VideoTracks
- // and MediaStreams.
- if (video_content) {
- if (video_content->rejected) {
- RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
- } else {
- bool default_video_track_needed =
- !remote_peer_supports_msid_ &&
- RtpTransceiverDirectionHasSend(video_desc->direction());
- UpdateRemoteSendersList(GetActiveStreams(video_desc),
- default_video_track_needed, video_desc->type(),
- new_streams);
+ // Find all video rtp streams and create corresponding remote VideoTracks
+ // and MediaStreams.
+ if (video_content) {
+ if (video_content->rejected) {
+ RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
+ } else {
+ bool default_video_track_needed =
+ !remote_peer_supports_msid_ &&
+ RtpTransceiverDirectionHasSend(video_desc->direction());
+ UpdateRemoteSendersList(GetActiveStreams(video_desc),
+ default_video_track_needed, video_desc->type(),
+ new_streams);
+ }
}
- }
- // Update the DataChannels with the information from the remote peer.
- if (data_desc) {
- if (rtc::starts_with(data_desc->protocol().data(),
- cricket::kMediaProtocolRtpPrefix)) {
- UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc));
+ // Update the DataChannels with the information from the remote peer.
+ if (data_desc) {
+ if (rtc::starts_with(data_desc->protocol().data(),
+ cricket::kMediaProtocolRtpPrefix)) {
+ UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc));
+ }
}
- }
- // Iterate new_streams and notify the observer about new MediaStreams.
- for (size_t i = 0; i < new_streams->count(); ++i) {
- MediaStreamInterface* new_stream = new_streams->at(i);
- stats_->AddStream(new_stream);
- observer_->OnAddStream(
- rtc::scoped_refptr<MediaStreamInterface>(new_stream));
- }
+ // Iterate new_streams and notify the observer about new MediaStreams.
+ for (size_t i = 0; i < new_streams->count(); ++i) {
+ MediaStreamInterface* new_stream = new_streams->at(i);
+ stats_->AddStream(new_stream);
+ observer_->OnAddStream(
+ rtc::scoped_refptr<MediaStreamInterface>(new_stream));
+ }
- UpdateEndedRemoteMediaStreams();
+ UpdateEndedRemoteMediaStreams();
+ }
return RTCError::OK();
}
+RTCError PeerConnection::UpdateTransceiversAndDataChannels(
+ cricket::ContentSource source,
+ const SessionDescriptionInterface* old_session,
+ const SessionDescriptionInterface& new_session) {
+ RTC_DCHECK(IsUnifiedPlan());
+
+ auto bundle_group_or_error = GetEarlyBundleGroup(*new_session.description());
+ if (!bundle_group_or_error.ok()) {
+ return bundle_group_or_error.MoveError();
+ }
+ const cricket::ContentGroup* bundle_group = bundle_group_or_error.MoveValue();
+
+ const ContentInfos& old_contents =
+ (old_session ? old_session->description()->contents() : ContentInfos());
+ const ContentInfos& new_contents = new_session.description()->contents();
+
+ for (size_t i = 0; i < new_contents.size(); ++i) {
+ const cricket::ContentInfo& new_content = new_contents[i];
+ const cricket::ContentInfo* old_content =
+ (i < old_contents.size() ? &old_contents[i] : nullptr);
+ cricket::MediaType media_type = new_content.media_description()->type();
+ seen_mids_.insert(new_content.name);
+ if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+ media_type == cricket::MEDIA_TYPE_VIDEO) {
+ auto transceiver_or_error =
+ AssociateTransceiver(source, i, new_content, old_content);
+ if (!transceiver_or_error.ok()) {
+ return transceiver_or_error.MoveError();
+ }
+ auto transceiver = transceiver_or_error.MoveValue();
+ if (source == cricket::CS_LOCAL && transceiver->stopped()) {
+ continue;
+ }
+ RTCError error =
+ UpdateTransceiverChannel(transceiver, new_content, bundle_group);
+ if (!error.ok()) {
+ return error;
+ }
+ } else if (media_type == cricket::MEDIA_TYPE_DATA) {
+ // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+ // Plan.
+ } else {
+ LOG_AND_RETURN_ERROR(RTCErrorType::INTERNAL_ERROR,
+ "Unknown section type.");
+ }
+ }
+
+ return RTCError::OK();
+}
+
+RTCError PeerConnection::UpdateTransceiverChannel(
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ transceiver,
+ const cricket::ContentInfo& content,
+ const cricket::ContentGroup* bundle_group) {
+ RTC_DCHECK(IsUnifiedPlan());
+ RTC_DCHECK(transceiver);
+ cricket::BaseChannel* channel = transceiver->internal()->channel();
+ if (content.rejected) {
+ if (channel) {
+ transceiver->internal()->SetChannel(nullptr);
+ DestroyBaseChannel(channel);
+ }
+ } else {
+ if (!channel) {
+ if (transceiver->internal()->media_type() == cricket::MEDIA_TYPE_AUDIO) {
+ channel = CreateVoiceChannel(
+ content.name,
+ GetTransportNameForMediaSection(content.name, bundle_group));
+ } else {
+ RTC_DCHECK_EQ(cricket::MEDIA_TYPE_VIDEO,
+ transceiver->internal()->media_type());
+ channel = CreateVideoChannel(
+ content.name,
+ GetTransportNameForMediaSection(content.name, bundle_group));
+ }
+ if (!channel) {
+ LOG_AND_RETURN_ERROR(
+ RTCErrorType::INTERNAL_ERROR,
+ "Failed to create channel for mid=" + content.name);
+ }
+ transceiver->internal()->SetChannel(channel);
+ }
+ }
+ return RTCError::OK();
+}
+
+RTCErrorOr<rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+PeerConnection::AssociateTransceiver(cricket::ContentSource source,
+ size_t mline_index,
+ const ContentInfo& content,
+ const ContentInfo* old_content) {
+ RTC_DCHECK(IsUnifiedPlan());
+ // If the m= section is being recycled (rejected in previous remote
+ // description, not rejected in current description), dissociate the currently
+ // associated RtpTransceiver by setting its mid property to null, and discard
+ // the mapping between the transceiver and its m= section index.
+ if (old_content && old_content->rejected && !content.rejected) {
+ auto old_transceiver = GetAssociatedTransceiver(old_content->name);
+ if (old_transceiver) {
+ old_transceiver->internal()->set_mid(rtc::nullopt);
+ old_transceiver->internal()->set_mline_index(rtc::nullopt);
+ }
+ }
+ const MediaContentDescription* media_desc = content.media_description();
+ auto transceiver = GetAssociatedTransceiver(content.name);
+ if (source == cricket::CS_LOCAL) {
+ // Find the RtpTransceiver that corresponds to this m= section, using the
+ // mapping between transceivers and m= section indices established when
+ // creating the offer.
+ if (!transceiver) {
+ transceiver = GetTransceiverByMLineIndex(mline_index);
+ }
+ if (!transceiver) {
+ LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+ "Unknown transceiver");
+ }
+ } else {
+ RTC_DCHECK_EQ(source, cricket::CS_REMOTE);
+ // If the m= section is sendrecv or recvonly, and there are RtpTransceivers
+ // of the same type...
+ if (!transceiver &&
+ RtpTransceiverDirectionHasRecv(media_desc->direction())) {
+ transceiver = FindAvailableTransceiverToReceive(media_desc->type());
+ }
+ // If no RtpTransceiver was found in the previous step, create one with a
+ // recvonly direction.
+ if (!transceiver) {
+ transceiver = CreateTransceiver(media_desc->type());
+ transceiver->internal()->set_direction(
+ RtpTransceiverDirection::kRecvOnly);
+ }
+ }
+ RTC_DCHECK(transceiver);
+ if (transceiver->internal()->media_type() != media_desc->type()) {
+ LOG_AND_RETURN_ERROR(
+ RTCErrorType::INVALID_PARAMETER,
+ "Transceiver type does not match media description type.");
+ }
+ // Associate the found or created RtpTransceiver with the m= section by
+ // setting the value of the RtpTransceiver's mid property to the MID of the m=
+ // section, and establish a mapping between the transceiver and the index of
+ // the m= section.
+ transceiver->internal()->set_mid(content.name);
+ transceiver->internal()->set_mline_index(mline_index);
+ return std::move(transceiver);
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::GetAssociatedTransceiver(const std::string& mid) const {
+ RTC_DCHECK(IsUnifiedPlan());
+ for (auto transceiver : transceivers_) {
+ if (transceiver->mid() == mid) {
+ return transceiver;
+ }
+ }
+ return nullptr;
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::GetTransceiverByMLineIndex(size_t mline_index) const {
+ RTC_DCHECK(IsUnifiedPlan());
+ for (auto transceiver : transceivers_) {
+ if (transceiver->internal()->mline_index() == mline_index) {
+ return transceiver;
+ }
+ }
+ return nullptr;
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::FindAvailableTransceiverToReceive(
+ cricket::MediaType media_type) const {
+ RTC_DCHECK(IsUnifiedPlan());
+ // From JSEP section 5.10 (Applying a Remote Description):
+ // If the m= section is sendrecv or recvonly, and there are RtpTransceivers of
+ // the same type that were added to the PeerConnection by addTrack and are not
+ // associated with any m= section and are not stopped, find the first such
+ // RtpTransceiver.
+ for (auto transceiver : transceivers_) {
+ if (transceiver->internal()->media_type() == media_type &&
+ transceiver->internal()->created_by_addtrack() && !transceiver->mid() &&
+ !transceiver->stopped()) {
+ return transceiver;
+ }
+ }
+ return nullptr;
+}
+
const cricket::ContentInfo* PeerConnection::FindMediaSectionForTransceiver(
rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
transceiver,
@@ -2655,10 +2936,30 @@
}
void PeerConnection::GetOptionsForOffer(
- const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options,
+ const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_options) {
- ExtractSharedMediaSessionOptions(rtc_options, session_options);
+ ExtractSharedMediaSessionOptions(offer_answer_options, session_options);
+ if (IsUnifiedPlan()) {
+ GetOptionsForUnifiedPlanOffer(offer_answer_options, session_options);
+ } else {
+ GetOptionsForPlanBOffer(offer_answer_options, session_options);
+ }
+
+ // Apply ICE restart flag and renomination flag.
+ for (auto& options : session_options->media_description_options) {
+ options.transport_options.ice_restart = offer_answer_options.ice_restart;
+ options.transport_options.enable_ice_renomination =
+ configuration_.enable_ice_renomination;
+ }
+
+ session_options->rtcp_cname = rtcp_cname_;
+ session_options->crypto_options = factory_->options().crypto_options;
+}
+
+void PeerConnection::GetOptionsForPlanBOffer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
+ cricket::MediaSessionOptions* session_options) {
// Figure out transceiver directional preferences.
bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@@ -2673,15 +2974,19 @@
bool offer_new_data_description = HasDataChannels();
// The "offer_to_receive_X" options allow those defaults to be overridden.
- if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) {
- recv_audio = (rtc_options.offer_to_receive_audio > 0);
+ if (offer_answer_options.offer_to_receive_audio !=
+ RTCOfferAnswerOptions::kUndefined) {
+ recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
offer_new_audio_description =
- offer_new_audio_description || (rtc_options.offer_to_receive_audio > 0);
+ offer_new_audio_description ||
+ (offer_answer_options.offer_to_receive_audio > 0);
}
- if (rtc_options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) {
- recv_video = (rtc_options.offer_to_receive_video > 0);
+ if (offer_answer_options.offer_to_receive_video !=
+ RTCOfferAnswerOptions::kUndefined) {
+ recv_video = (offer_answer_options.offer_to_receive_video > 0);
offer_new_video_description =
- offer_new_video_description || (rtc_options.offer_to_receive_video > 0);
+ offer_new_video_description ||
+ (offer_answer_options.offer_to_receive_video > 0);
}
rtc::Optional<size_t> audio_index;
@@ -2733,13 +3038,6 @@
!data_index ? nullptr
: &session_options->media_description_options[*data_index];
- // Apply ICE restart flag and renomination flag.
- for (auto& options : session_options->media_description_options) {
- options.transport_options.ice_restart = rtc_options.ice_restart;
- options.transport_options.enable_ice_renomination =
- configuration_.enable_ice_renomination;
- }
-
AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
video_media_description_options);
AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@@ -2752,16 +3050,162 @@
if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
session_options->data_channel_type = data_channel_type();
}
+}
+
+// Find a new MID that is not already in |used_mids|, then add it to |used_mids|
+// and return a reference to it.
+// Generated MIDs should be no more than 3 bytes long to take up less space in
+// the RTP packet.
+static const std::string& AllocateMid(std::set<std::string>* used_mids) {
+ RTC_DCHECK(used_mids);
+ // We're boring: just generate MIDs 0, 1, 2, ...
+ size_t i = 0;
+ std::set<std::string>::iterator it;
+ bool inserted;
+ do {
+ std::string mid = rtc::ToString(i++);
+ auto insert_result = used_mids->insert(mid);
+ it = insert_result.first;
+ inserted = insert_result.second;
+ } while (!inserted);
+ return *it;
+}
+
+static cricket::MediaDescriptionOptions
+GetMediaDescriptionOptionsForTransceiver(
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ transceiver,
+ const std::string& mid) {
+ cricket::MediaDescriptionOptions media_description_options(
+ transceiver->internal()->media_type(), mid, transceiver->direction(),
+ transceiver->stopped());
+ cricket::SenderOptions sender_options;
+ sender_options.track_id = transceiver->sender()->id();
+ sender_options.stream_ids = transceiver->sender()->stream_ids();
+ // TODO(bugs.webrtc.org/7600): Set num_sim_layers to the number of encodings
+ // set in the RTP parameters when the transceiver was added.
+ sender_options.num_sim_layers = 1;
+ media_description_options.sender_options.push_back(sender_options);
+ return media_description_options;
+}
+
+void PeerConnection::GetOptionsForUnifiedPlanOffer(
+ const RTCOfferAnswerOptions& offer_answer_options,
+ cricket::MediaSessionOptions* session_options) {
+ // Rules for generating an offer are dictated by JSEP sections 5.2.1 (Initial
+ // Offers) and 5.2.2 (Subsequent Offers).
+ RTC_DCHECK_EQ(session_options->media_description_options.size(), 0);
+ const ContentInfos& local_contents =
+ (local_description() ? local_description()->description()->contents()
+ : ContentInfos());
+ const ContentInfos& remote_contents =
+ (remote_description() ? remote_description()->description()->contents()
+ : ContentInfos());
+ // The mline indices that can be recycled. New transceivers should reuse these
+ // slots first.
+ std::queue<size_t> recycleable_mline_indices;
+ // Track the MIDs used in previous offer/answer exchanges and the current
+ // offer so that new, unique MIDs are generated.
+ std::set<std::string> used_mids = seen_mids_;
+ // First, go through each media section that exists in either the local or
+ // remote description and generate a media section in this offer for the
+ // associated transceiver. If a media section can be recycled, generate a
+ // default, rejected media section here that can be later overwritten.
+ for (size_t i = 0;
+ i < std::max(local_contents.size(), remote_contents.size()); ++i) {
+ // Either |local_content| or |remote_content| is non-null.
+ const ContentInfo* local_content =
+ (i < local_contents.size() ? &local_contents[i] : nullptr);
+ const ContentInfo* remote_content =
+ (i < remote_contents.size() ? &remote_contents[i] : nullptr);
+ bool had_been_rejected = (local_content && local_content->rejected) ||
+ (remote_content && remote_content->rejected);
+ const std::string& mid =
+ (local_content ? local_content->name : remote_content->name);
+ cricket::MediaType media_type =
+ (local_content ? local_content->media_description()->type()
+ : remote_content->media_description()->type());
+ if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+ media_type == cricket::MEDIA_TYPE_VIDEO) {
+ auto transceiver = GetAssociatedTransceiver(mid);
+ RTC_CHECK(transceiver);
+ // A media section is considered eligible for recycling if it is marked as
+ // rejected in either the local or remote description.
+ if (had_been_rejected) {
+ session_options->media_description_options.push_back(
+ cricket::MediaDescriptionOptions(
+ transceiver->internal()->media_type(), mid,
+ RtpTransceiverDirection::kInactive,
+ /*stopped=*/true));
+ recycleable_mline_indices.push(i);
+ } else {
+ session_options->media_description_options.push_back(
+ GetMediaDescriptionOptionsForTransceiver(transceiver, mid));
+ // CreateOffer shouldn't really cause any state changes in
+ // PeerConnection, but we need a way to match new transceivers to new
+ // media sections in SetLocalDescription and JSEP specifies this is done
+ // by recording the index of the media section generated for the
+ // transceiver in the offer.
+ transceiver->internal()->set_mline_index(i);
+ }
+ } else {
+ RTC_CHECK_EQ(cricket::MEDIA_TYPE_DATA, media_type);
+ // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+ // Plan.
+ }
+ }
+ // Next, look for transceivers that are newly added (that is, are not stopped
+ // and not associated). Reuse media sections marked as recyclable first,
+ // otherwise append to the end of the offer. New media sections should be
+ // added in the order they were added to the PeerConnection.
+ for (auto transceiver : transceivers_) {
+ if (transceiver->mid() || transceiver->stopped()) {
+ continue;
+ }
+ size_t mline_index;
+ if (!recycleable_mline_indices.empty()) {
+ mline_index = recycleable_mline_indices.front();
+ recycleable_mline_indices.pop();
+ session_options->media_description_options[mline_index] =
+ GetMediaDescriptionOptionsForTransceiver(transceiver,
+ AllocateMid(&used_mids));
+ } else {
+ mline_index = session_options->media_description_options.size();
+ session_options->media_description_options.push_back(
+ GetMediaDescriptionOptionsForTransceiver(transceiver,
+ AllocateMid(&used_mids)));
+ }
+ // See comment above for why CreateOffer changes the transceiver's state.
+ transceiver->internal()->set_mline_index(mline_index);
+ }
+ // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+ // Plan.
+}
+
+void PeerConnection::GetOptionsForAnswer(
+ const RTCOfferAnswerOptions& offer_answer_options,
+ cricket::MediaSessionOptions* session_options) {
+ ExtractSharedMediaSessionOptions(offer_answer_options, session_options);
+
+ if (IsUnifiedPlan()) {
+ GetOptionsForUnifiedPlanAnswer(offer_answer_options, session_options);
+ } else {
+ GetOptionsForPlanBAnswer(offer_answer_options, session_options);
+ }
+
+ // Apply ICE renomination flag.
+ for (auto& options : session_options->media_description_options) {
+ options.transport_options.enable_ice_renomination =
+ configuration_.enable_ice_renomination;
+ }
session_options->rtcp_cname = rtcp_cname_;
session_options->crypto_options = factory_->options().crypto_options;
}
-void PeerConnection::GetOptionsForAnswer(
- const RTCOfferAnswerOptions& rtc_options,
+void PeerConnection::GetOptionsForPlanBAnswer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_options) {
- ExtractSharedMediaSessionOptions(rtc_options, session_options);
-
// Figure out transceiver directional preferences.
bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@@ -2772,11 +3216,13 @@
bool recv_video = true;
// The "offer_to_receive_X" options allow those defaults to be overridden.
- if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) {
- recv_audio = (rtc_options.offer_to_receive_audio > 0);
+ if (offer_answer_options.offer_to_receive_audio !=
+ RTCOfferAnswerOptions::kUndefined) {
+ recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
}
- if (rtc_options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) {
- recv_video = (rtc_options.offer_to_receive_video > 0);
+ if (offer_answer_options.offer_to_receive_video !=
+ RTCOfferAnswerOptions::kUndefined) {
+ recv_video = (offer_answer_options.offer_to_receive_video > 0);
}
rtc::Optional<size_t> audio_index;
@@ -2805,12 +3251,6 @@
!data_index ? nullptr
: &session_options->media_description_options[*data_index];
- // Apply ICE renomination flag.
- for (auto& options : session_options->media_description_options) {
- options.transport_options.enable_ice_renomination =
- configuration_.enable_ice_renomination;
- }
-
AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
video_media_description_options);
AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@@ -2822,9 +3262,30 @@
if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
session_options->data_channel_type = data_channel_type();
}
+}
- session_options->rtcp_cname = rtcp_cname_;
- session_options->crypto_options = factory_->options().crypto_options;
+void PeerConnection::GetOptionsForUnifiedPlanAnswer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
+ cricket::MediaSessionOptions* session_options) {
+ // Rules for generating an answer are dictated by JSEP sections 5.3.1 (Initial
+ // Answers) and 5.3.2 (Subsequent Answers).
+ RTC_DCHECK(remote_description());
+ RTC_DCHECK(remote_description()->GetType() == SdpType::kOffer);
+ for (const ContentInfo& content :
+ remote_description()->description()->contents()) {
+ cricket::MediaType media_type = content.media_description()->type();
+ if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+ media_type == cricket::MEDIA_TYPE_VIDEO) {
+ auto transceiver = GetAssociatedTransceiver(content.name);
+ RTC_CHECK(transceiver);
+ session_options->media_description_options.push_back(
+ GetMediaDescriptionOptionsForTransceiver(transceiver, content.name));
+ } else {
+ RTC_CHECK_EQ(cricket::MEDIA_TYPE_DATA, media_type);
+ // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+ // Plan.
+ }
+ }
}
void PeerConnection::GenerateMediaDescriptionOptions(
@@ -3540,11 +4001,11 @@
cricket::BaseChannel* PeerConnection::GetChannel(
const std::string& content_name) {
- if (voice_channel() && voice_channel()->content_name() == content_name) {
- return voice_channel();
- }
- if (video_channel() && video_channel()->content_name() == content_name) {
- return video_channel();
+ for (auto transceiver : transceivers_) {
+ cricket::BaseChannel* channel = transceiver->internal()->channel();
+ if (channel && channel->content_name() == content_name) {
+ return channel;
+ }
}
if (rtp_data_channel() &&
rtp_data_channel()->content_name() == content_name) {
@@ -3779,9 +4240,9 @@
tinfo.content_name, tinfo.description, type, &error);
}
if (!success) {
- LOG_AND_RETURN_ERROR(
- RTCErrorType::INVALID_PARAMETER,
- "Failed to push down transport description: " + error);
+ LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+ "Failed to push down transport description for " +
+ tinfo.content_name + ": " + error);
}
}
@@ -4308,22 +4769,30 @@
return *first_content_name;
}
-RTCError PeerConnection::CreateChannels(const SessionDescription* desc) {
- RTC_DCHECK(desc);
-
+RTCErrorOr<const cricket::ContentGroup*> PeerConnection::GetEarlyBundleGroup(
+ const SessionDescription& desc) const {
const cricket::ContentGroup* bundle_group = nullptr;
if (configuration_.bundle_policy ==
PeerConnectionInterface::kBundlePolicyMaxBundle) {
- bundle_group = desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group = desc.GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
if (!bundle_group) {
LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
"max-bundle configured but session description "
"has no BUNDLE group");
}
}
+ return std::move(bundle_group);
+}
+
+RTCError PeerConnection::CreateChannels(const SessionDescription& desc) {
+ auto bundle_group_or_error = GetEarlyBundleGroup(desc);
+ if (!bundle_group_or_error.ok()) {
+ return bundle_group_or_error.MoveError();
+ }
+ const cricket::ContentGroup* bundle_group = bundle_group_or_error.MoveValue();
// Creating the media channels and transport proxies.
- const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(desc);
+ const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(&desc);
if (voice && !voice->rejected &&
!GetAudioTransceiver()->internal()->channel()) {
cricket::VoiceChannel* voice_channel = CreateVoiceChannel(
@@ -4336,7 +4805,7 @@
GetAudioTransceiver()->internal()->SetChannel(voice_channel);
}
- const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc);
+ const cricket::ContentInfo* video = cricket::GetFirstVideoContent(&desc);
if (video && !video->rejected &&
!GetVideoTransceiver()->internal()->channel()) {
cricket::VideoChannel* video_channel = CreateVideoChannel(
@@ -4349,7 +4818,7 @@
GetVideoTransceiver()->internal()->SetChannel(video_channel);
}
- const cricket::ContentInfo* data = cricket::GetFirstDataContent(desc);
+ const cricket::ContentInfo* data = cricket::GetFirstDataContent(&desc);
if (data_channel_type_ != cricket::DCT_NONE && data && !data->rejected &&
!rtp_data_channel_ && !sctp_transport_) {
if (!CreateDataChannel(data->name, GetTransportNameForMediaSection(
diff --git a/pc/peerconnection.h b/pc/peerconnection.h
index b1a1d9e..b9a86d8 100644
--- a/pc/peerconnection.h
+++ b/pc/peerconnection.h
@@ -412,6 +412,43 @@
RTCError ApplyRemoteDescription(
std::unique_ptr<SessionDescriptionInterface> desc);
+ // Updates the local RtpTransceivers according to the JSEP rules. Called as
+ // part of setting the local/remote description.
+ RTCError UpdateTransceiversAndDataChannels(
+ cricket::ContentSource source,
+ const SessionDescriptionInterface* old_session,
+ const SessionDescriptionInterface& new_session);
+
+ // Either creates or destroys the transceiver's BaseChannel according to the
+ // given media section.
+ RTCError UpdateTransceiverChannel(
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ transceiver,
+ const cricket::ContentInfo& content,
+ const cricket::ContentGroup* bundle_group);
+
+ // Associate the given transceiver according to the JSEP rules.
+ RTCErrorOr<
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+ AssociateTransceiver(cricket::ContentSource source,
+ size_t mline_index,
+ const cricket::ContentInfo& content,
+ const cricket::ContentInfo* old_content);
+
+ // Returns the RtpTransceiver, if found, that is associated to the given MID.
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ GetAssociatedTransceiver(const std::string& mid) const;
+
+ // Returns the RtpTransceiver, if found, that was assigned to the given mline
+ // index in CreateOffer.
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ GetTransceiverByMLineIndex(size_t mline_index) const;
+
+ // Returns an RtpTransciever, if available, that can be used to receive the
+ // given media type according to JSEP rules.
+ rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+ FindAvailableTransceiverToReceive(cricket::MediaType media_type) const;
+
// Returns the media section in the given session description that is
// associated with the RtpTransceiver. Returns null if none found or this
// RtpTransceiver is not associated. Logic varies depending on the
@@ -427,14 +464,30 @@
// Returns a MediaSessionOptions struct with options decided by |options|,
// the local MediaStreams and DataChannels.
- void GetOptionsForOffer(
- const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options,
+ void GetOptionsForOffer(const PeerConnectionInterface::RTCOfferAnswerOptions&
+ offer_answer_options,
+ cricket::MediaSessionOptions* session_options);
+ void GetOptionsForPlanBOffer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions&
+ offer_answer_options,
+ cricket::MediaSessionOptions* session_options);
+ void GetOptionsForUnifiedPlanOffer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions&
+ offer_answer_options,
cricket::MediaSessionOptions* session_options);
// Returns a MediaSessionOptions struct with options decided by
// |constraints|, the local MediaStreams and DataChannels.
- void GetOptionsForAnswer(const RTCOfferAnswerOptions& options,
+ void GetOptionsForAnswer(const RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_options);
+ void GetOptionsForPlanBAnswer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions&
+ offer_answer_options,
+ cricket::MediaSessionOptions* session_options);
+ void GetOptionsForUnifiedPlanAnswer(
+ const PeerConnectionInterface::RTCOfferAnswerOptions&
+ offer_answer_options,
+ cricket::MediaSessionOptions* session_options);
// Generates MediaDescriptionOptions for the |session_opts| based on existing
// local description or remote description.
@@ -706,7 +759,15 @@
// Allocates media channels based on the |desc|. If |desc| doesn't have
// the BUNDLE option, this method will disable BUNDLE in PortAllocator.
// This method will also delete any existing media channels before creating.
- RTCError CreateChannels(const cricket::SessionDescription* desc);
+ RTCError CreateChannels(const cricket::SessionDescription& desc);
+
+ // If the BUNDLE policy is max-bundle, then we know for sure that all
+ // transports will be bundled from the start. This method returns the BUNDLE
+ // group if that's the case, or null if BUNDLE will be negotiated later. An
+ // error is returned if max-bundle is specified but the session description
+ // does not have a BUNDLE group.
+ RTCErrorOr<const cricket::ContentGroup*> GetEarlyBundleGroup(
+ const cricket::SessionDescription& desc) const;
// Helper methods to create media channels.
cricket::VoiceChannel* CreateVoiceChannel(const std::string& mid,
@@ -859,6 +920,9 @@
std::vector<
rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
transceivers_;
+ // MIDs that have been seen either by SetLocalDescription or
+ // SetRemoteDescription over the life of the PeerConnection.
+ std::set<std::string> seen_mids_;
SessionError session_error_ = SessionError::kNone;
std::string session_error_desc_;
diff --git a/pc/peerconnection_jsep_unittest.cc b/pc/peerconnection_jsep_unittest.cc
new file mode 100644
index 0000000..119b135
--- /dev/null
+++ b/pc/peerconnection_jsep_unittest.cc
@@ -0,0 +1,734 @@
+/*
+ * Copyright 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "api/audio_codecs/builtin_audio_decoder_factory.h"
+#include "api/audio_codecs/builtin_audio_encoder_factory.h"
+#include "pc/mediasession.h"
+#include "pc/peerconnectionwrapper.h"
+#include "pc/sdputils.h"
+#ifdef WEBRTC_ANDROID
+#include "pc/test/androidtestinitializer.h"
+#endif
+#include "pc/test/fakeaudiocapturemodule.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/ptr_util.h"
+#include "rtc_base/virtualsocketserver.h"
+#include "test/gmock.h"
+
+// This file contains tests that ensure the PeerConnection's implementation of
+// CreateOffer/CreateAnswer/SetLocalDescription/SetRemoteDescription conform
+// to the JavaScript Session Establishment Protocol (JSEP).
+// For now these semantics are only available when configuring the
+// PeerConnection with Unified Plan, but eventually that will be the default.
+
+namespace webrtc {
+
+using cricket::MediaContentDescription;
+using RTCConfiguration = PeerConnectionInterface::RTCConfiguration;
+using ::testing::Values;
+using ::testing::Combine;
+using ::testing::ElementsAre;
+
+class PeerConnectionJsepTest : public ::testing::Test {
+ protected:
+ typedef std::unique_ptr<PeerConnectionWrapper> WrapperPtr;
+
+ PeerConnectionJsepTest()
+ : vss_(new rtc::VirtualSocketServer()), main_(vss_.get()) {
+#ifdef WEBRTC_ANDROID
+ InitializeAndroidObjects();
+#endif
+ pc_factory_ = CreatePeerConnectionFactory(
+ rtc::Thread::Current(), rtc::Thread::Current(), rtc::Thread::Current(),
+ FakeAudioCaptureModule::Create(), CreateBuiltinAudioEncoderFactory(),
+ CreateBuiltinAudioDecoderFactory(), nullptr, nullptr);
+ }
+
+ WrapperPtr CreatePeerConnection() {
+ RTCConfiguration config;
+ config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+ return CreatePeerConnection(config);
+ }
+
+ WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
+ auto observer = rtc::MakeUnique<MockPeerConnectionObserver>();
+ auto pc = pc_factory_->CreatePeerConnection(config, nullptr, nullptr,
+ observer.get());
+ if (!pc) {
+ return nullptr;
+ }
+
+ return rtc::MakeUnique<PeerConnectionWrapper>(pc_factory_, pc,
+ std::move(observer));
+ }
+
+ std::unique_ptr<rtc::VirtualSocketServer> vss_;
+ rtc::AutoSocketServerThread main_;
+ rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory_;
+};
+
+// Tests for JSEP initial offer generation.
+
+// Test that an offer created by a PeerConnection with no transceivers generates
+// no media sections.
+TEST_F(PeerConnectionJsepTest, EmptyInitialOffer) {
+ auto caller = CreatePeerConnection();
+ auto offer = caller->CreateOffer();
+ EXPECT_EQ(0u, offer->description()->contents().size());
+}
+
+// Test that an initial offer with one audio track generates one audio media
+// section.
+TEST_F(PeerConnectionJsepTest, AudioOnlyInitialOffer) {
+ auto caller = CreatePeerConnection();
+ caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto offer = caller->CreateOffer();
+
+ auto contents = offer->description()->contents();
+ ASSERT_EQ(1u, contents.size());
+ EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, contents[0].media_description()->type());
+}
+
+// Test than an initial offer with one video track generates one video media
+// section
+TEST_F(PeerConnectionJsepTest, VideoOnlyInitialOffer) {
+ auto caller = CreatePeerConnection();
+ caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+ auto offer = caller->CreateOffer();
+
+ auto contents = offer->description()->contents();
+ ASSERT_EQ(1u, contents.size());
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[0].media_description()->type());
+}
+
+// Test that multiple media sections in the initial offer are ordered in the
+// order the transceivers were added to the PeerConnection. This is required by
+// JSEP section 5.2.1.
+TEST_F(PeerConnectionJsepTest, MediaSectionsInInitialOfferOrderedCorrectly) {
+ auto caller = CreatePeerConnection();
+ caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+ caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ RtpTransceiverInit init;
+ init.direction = RtpTransceiverDirection::kSendOnly;
+ caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init);
+ auto offer = caller->CreateOffer();
+
+ auto contents = offer->description()->contents();
+ ASSERT_EQ(3u, contents.size());
+
+ const MediaContentDescription* media_description1 =
+ contents[0].media_description();
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, media_description1->type());
+ EXPECT_EQ(RtpTransceiverDirection::kSendRecv,
+ media_description1->direction());
+
+ const MediaContentDescription* media_description2 =
+ contents[1].media_description();
+ EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, media_description2->type());
+ EXPECT_EQ(RtpTransceiverDirection::kSendRecv,
+ media_description2->direction());
+
+ const MediaContentDescription* media_description3 =
+ contents[2].media_description();
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, media_description3->type());
+ EXPECT_EQ(RtpTransceiverDirection::kSendOnly,
+ media_description3->direction());
+}
+
+// Test that media sections in the initial offer have different mids.
+TEST_F(PeerConnectionJsepTest, MediaSectionsInInitialOfferHaveDifferentMids) {
+ auto caller = CreatePeerConnection();
+ caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto offer = caller->CreateOffer();
+
+ std::string sdp;
+ offer->ToString(&sdp);
+ RTC_LOG(LS_INFO) << sdp;
+
+ auto contents = offer->description()->contents();
+ ASSERT_EQ(2u, contents.size());
+ EXPECT_NE(contents[0].name, contents[1].name);
+}
+
+TEST_F(PeerConnectionJsepTest,
+ StoppedTransceiverHasNoMediaSectionInInitialOffer) {
+ auto caller = CreatePeerConnection();
+ auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ transceiver->Stop();
+
+ auto offer = caller->CreateOffer();
+ EXPECT_EQ(0u, offer->description()->contents().size());
+}
+
+// Tests for JSEP SetLocalDescription with a local offer.
+
+TEST_F(PeerConnectionJsepTest, SetLocalEmptyOfferCreatesNoTransceivers) {
+ auto caller = CreatePeerConnection();
+ ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+ EXPECT_THAT(caller->pc()->GetTransceivers(), ElementsAre());
+ EXPECT_THAT(caller->pc()->GetSenders(), ElementsAre());
+ EXPECT_THAT(caller->pc()->GetReceivers(), ElementsAre());
+}
+
+TEST_F(PeerConnectionJsepTest, SetLocalOfferSetsTransceiverMid) {
+ auto caller = CreatePeerConnection();
+ auto audio_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto video_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+
+ auto offer = caller->CreateOffer();
+ std::string audio_mid = offer->description()->contents()[0].name;
+ std::string video_mid = offer->description()->contents()[1].name;
+
+ ASSERT_TRUE(caller->SetLocalDescription(std::move(offer)));
+
+ EXPECT_EQ(audio_mid, audio_transceiver->mid());
+ EXPECT_EQ(video_mid, video_transceiver->mid());
+}
+
+// Tests for JSEP SetRemoteDescription with a remote offer.
+
+// Test that setting a remote offer with sendrecv audio and video creates two
+// transceivers, one for receiving audio and one for receiving video.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferCreatesTransceivers) {
+ auto caller = CreatePeerConnection();
+ auto caller_audio = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto caller_video = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, transceivers.size());
+ EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO,
+ transceivers[0]->receiver()->media_type());
+ EXPECT_EQ(caller_audio->mid(), transceivers[0]->mid());
+ EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[0]->direction());
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO,
+ transceivers[1]->receiver()->media_type());
+ EXPECT_EQ(caller_video->mid(), transceivers[1]->mid());
+ EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[1]->direction());
+}
+
+// Test that setting a remote offer with an audio track will reuse the
+// transceiver created for a local audio track added by AddTrack.
+// This is specified in JSEP section 5.10 (Applying a Remote Description). The
+// intent is to preserve backwards compatibility with clients who only use the
+// AddTrack API.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferReusesTransceiverFromAddTrack) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto caller_audio = caller->pc()->GetTransceivers()[0];
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(1u, transceivers.size());
+ EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+ transceivers[0]->receiver()->track()->kind());
+ EXPECT_EQ(caller_audio->mid(), transceivers[0]->mid());
+}
+
+// Test that setting a remote offer with an audio track marked sendonly will not
+// reuse a transceiver created by AddTrack. JSEP only allows the transceiver to
+// be reused if the offer direction is sendrecv or recvonly.
+TEST_F(PeerConnectionJsepTest,
+ SetRemoteOfferDoesNotReuseTransceiverIfDirectionSendOnly) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto caller_audio = caller->pc()->GetTransceivers()[0];
+ caller_audio->SetDirection(RtpTransceiverDirection::kSendOnly);
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, transceivers.size());
+ EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+ EXPECT_EQ(caller_audio->mid(), transceivers[1]->mid());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// transceiver added by AddTransceiver. The logic for reusing a transceiver is
+// specific to those added by AddTrack and is tested above.
+TEST_F(PeerConnectionJsepTest,
+ SetRemoteOfferDoesNotReuseTransceiverFromAddTransceiver) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ auto transceiver = callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, transceivers.size());
+ EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+ EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+ EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+ transceivers[1]->receiver()->track()->kind());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// transceiver created for a local video track added by AddTrack.
+TEST_F(PeerConnectionJsepTest,
+ SetRemoteOfferDoesNotReuseTransceiverOfWrongType) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ auto video_sender = callee->AddVideoTrack("v");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, transceivers.size());
+ EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+ EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+ EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+ transceivers[1]->receiver()->track()->kind());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// stopped transceiver.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferDoesNotReuseStoppedTransceiver) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+ callee->pc()->GetTransceivers()[0]->Stop();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, transceivers.size());
+ EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+ EXPECT_TRUE(transceivers[0]->stopped());
+ EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+ EXPECT_FALSE(transceivers[1]->stopped());
+}
+
+// Test that audio and video transceivers created on the remote side with
+// AddTrack will all be reused if there is the same number of audio/video tracks
+// in the remote offer. Additionally, this tests that transceivers are
+// successfully matched even if they are in a different order on the remote
+// side.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferReusesTransceiversOfBothTypes) {
+ auto caller = CreatePeerConnection();
+ caller->AddVideoTrack("v");
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+ callee->AddVideoTrack("v");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto caller_transceivers = caller->pc()->GetTransceivers();
+ auto callee_transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, callee_transceivers.size());
+ EXPECT_EQ(caller_transceivers[0]->mid(), callee_transceivers[1]->mid());
+ EXPECT_EQ(caller_transceivers[1]->mid(), callee_transceivers[0]->mid());
+}
+
+// Tests for JSEP initial CreateAnswer.
+
+// Test that the answer to a remote offer creates media sections for each
+// offered media in the same order and with the same mids.
+TEST_F(PeerConnectionJsepTest, CreateAnswerHasSameMidsAsOffer) {
+ auto caller = CreatePeerConnection();
+ auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+ auto second_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto third_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto answer = callee->CreateAnswer();
+ auto contents = answer->description()->contents();
+ ASSERT_EQ(3u, contents.size());
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[0].media_description()->type());
+ EXPECT_EQ(*first_transceiver->mid(), contents[0].name);
+ EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, contents[1].media_description()->type());
+ EXPECT_EQ(*second_transceiver->mid(), contents[1].name);
+ EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[2].media_description()->type());
+ EXPECT_EQ(*third_transceiver->mid(), contents[2].name);
+}
+
+// Test that an answering media section is marked as rejected if the underlying
+// transceiver has been stopped.
+TEST_F(PeerConnectionJsepTest, CreateAnswerRejectsStoppedTransceiver) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ callee->pc()->GetTransceivers()[0]->Stop();
+
+ auto answer = callee->CreateAnswer();
+ auto contents = answer->description()->contents();
+ ASSERT_EQ(1u, contents.size());
+ EXPECT_TRUE(contents[0].rejected);
+}
+
+// Test that CreateAnswer will generate media sections which will only send or
+// receive if the offer indicates it can do the reciprocating direction.
+// The full matrix is tested more extensively in MediaSession.
+TEST_F(PeerConnectionJsepTest, CreateAnswerNegotiatesDirection) {
+ auto caller = CreatePeerConnection();
+ RtpTransceiverInit init;
+ init.direction = RtpTransceiverDirection::kSendOnly;
+ caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init);
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto answer = callee->CreateAnswer();
+ auto contents = answer->description()->contents();
+ ASSERT_EQ(1u, contents.size());
+ EXPECT_EQ(RtpTransceiverDirection::kRecvOnly,
+ contents[0].media_description()->direction());
+}
+
+// Tests for JSEP SetLocalDescription with a local answer.
+// Note that these test only the additional behaviors not covered by
+// SetLocalDescription with a local offer.
+
+// Test that SetLocalDescription with an answer sets the current_direction
+// property of the transceivers mentioned in the session description.
+TEST_F(PeerConnectionJsepTest, SetLocalAnswerUpdatesCurrentDirection) {
+ auto caller = CreatePeerConnection();
+ auto caller_audio = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ caller_audio->SetDirection(RtpTransceiverDirection::kRecvOnly);
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(1u, transceivers.size());
+ // Since the offer was recvonly and the transceiver direction is sendrecv,
+ // the negotiated direction will be sendonly.
+ EXPECT_EQ(RtpTransceiverDirection::kSendOnly,
+ transceivers[0]->current_direction());
+}
+
+// Tests for JSEP SetRemoteDescription with a remote answer.
+// Note that these test only the additional behaviors not covered by
+// SetRemoteDescription with a remote offer.
+
+TEST_F(PeerConnectionJsepTest, SetRemoteAnswerUpdatesCurrentDirection) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+ auto callee_audio = callee->pc()->GetTransceivers()[0];
+ callee_audio->SetDirection(RtpTransceiverDirection::kSendOnly);
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(
+ caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+ auto transceivers = caller->pc()->GetTransceivers();
+ ASSERT_EQ(1u, transceivers.size());
+ // Since the remote transceiver was set to sendonly, the negotiated direction
+ // in the answer would be sendonly which we apply as recvonly to the local
+ // transceiver.
+ EXPECT_EQ(RtpTransceiverDirection::kRecvOnly,
+ transceivers[0]->current_direction());
+}
+
+// Tests for multiple round trips.
+
+// Test that setting a transceiver with the inactive direction does not stop it
+// on either the caller or the callee.
+TEST_F(PeerConnectionJsepTest, SettingTransceiverInactiveDoesNotStopIt) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+ callee->pc()->GetTransceivers()[0]->SetDirection(
+ RtpTransceiverDirection::kInactive);
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(
+ caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+ EXPECT_FALSE(caller->pc()->GetTransceivers()[0]->stopped());
+ EXPECT_FALSE(callee->pc()->GetTransceivers()[0]->stopped());
+}
+
+// Test that if a transceiver had been associated and later stopped, then a
+// media section is still generated for it and the media section is marked as
+// rejected.
+TEST_F(PeerConnectionJsepTest,
+ ReOfferMediaSectionForAssociatedStoppedTransceiverIsRejected) {
+ auto caller = CreatePeerConnection();
+ auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(
+ caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+ ASSERT_TRUE(transceiver->mid());
+ transceiver->Stop();
+
+ auto reoffer = caller->CreateOffer();
+ auto contents = reoffer->description()->contents();
+ ASSERT_EQ(1u, contents.size());
+ EXPECT_TRUE(contents[0].rejected);
+}
+
+// Test that stopping an associated transceiver on the caller side will stop the
+// corresponding transceiver on the remote side when the remote offer is
+// applied.
+TEST_F(PeerConnectionJsepTest,
+ StoppingTransceiverInOfferStopsTransceiverOnRemoteSide) {
+ auto caller = CreatePeerConnection();
+ auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(
+ caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+ transceiver->Stop();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ auto transceivers = callee->pc()->GetTransceivers();
+ EXPECT_TRUE(transceivers[0]->stopped());
+ EXPECT_TRUE(transceivers[0]->mid());
+}
+
+// Test that CreateOffer will only generate a recycled media section if the
+// transceiver to be recycled has been seen stopped by the other side first.
+TEST_F(PeerConnectionJsepTest,
+ CreateOfferDoesNotRecycleMediaSectionIfFirstStopped) {
+ auto caller = CreatePeerConnection();
+ auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+ ASSERT_TRUE(
+ caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+ auto second_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+ first_transceiver->Stop();
+
+ auto reoffer = caller->CreateOffer();
+ auto contents = reoffer->description()->contents();
+ ASSERT_EQ(2u, contents.size());
+ EXPECT_TRUE(contents[0].rejected);
+ EXPECT_FALSE(contents[1].rejected);
+}
+
+// Test that the offer/answer and transceivers for both the caller and callee
+// side are generated/updated correctly when recycling an audio/video media
+// section as a media section of either the same or opposite type.
+class RecycleMediaSectionTest
+ : public PeerConnectionJsepTest,
+ public testing::WithParamInterface<
+ std::tuple<cricket::MediaType, cricket::MediaType>> {
+ protected:
+ RecycleMediaSectionTest() {
+ first_type_ = std::get<0>(GetParam());
+ second_type_ = std::get<1>(GetParam());
+ }
+
+ cricket::MediaType first_type_;
+ cricket::MediaType second_type_;
+};
+
+TEST_P(RecycleMediaSectionTest, VerifyOfferAnswerAndTransceivers) {
+ auto caller = CreatePeerConnection();
+ auto first_transceiver = caller->AddTransceiver(first_type_);
+ auto callee = CreatePeerConnection();
+
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+ std::string first_mid = *first_transceiver->mid();
+ first_transceiver->Stop();
+
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+ auto second_transceiver = caller->AddTransceiver(second_type_);
+
+ // The offer should reuse the previous media section but allocate a new MID
+ // and change the media type.
+ auto offer = caller->CreateOffer();
+ auto offer_contents = offer->description()->contents();
+ ASSERT_EQ(1u, offer_contents.size());
+ EXPECT_FALSE(offer_contents[0].rejected);
+ EXPECT_EQ(second_type_, offer_contents[0].media_description()->type());
+ std::string second_mid = offer_contents[0].name;
+ EXPECT_NE(first_mid, second_mid);
+
+ // Setting the local offer will dissociate the previous transceiver and set
+ // the MID for the new transceiver.
+ ASSERT_TRUE(
+ caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+ EXPECT_EQ(rtc::nullopt, first_transceiver->mid());
+ EXPECT_EQ(second_mid, second_transceiver->mid());
+
+ // Setting the remote offer will dissociate the previous transceiver and
+ // create a new transceiver for the media section.
+ ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+ auto callee_transceivers = callee->pc()->GetTransceivers();
+ ASSERT_EQ(2u, callee_transceivers.size());
+ EXPECT_EQ(rtc::nullopt, callee_transceivers[0]->mid());
+ EXPECT_EQ(first_type_, callee_transceivers[0]->receiver()->media_type());
+ EXPECT_EQ(second_mid, callee_transceivers[1]->mid());
+ EXPECT_EQ(second_type_, callee_transceivers[1]->receiver()->media_type());
+
+ // The answer should have only one media section for the new transceiver.
+ auto answer = callee->CreateAnswer();
+ auto answer_contents = answer->description()->contents();
+ ASSERT_EQ(1u, answer_contents.size());
+ EXPECT_FALSE(answer_contents[0].rejected);
+ EXPECT_EQ(second_mid, answer_contents[0].name);
+ EXPECT_EQ(second_type_, answer_contents[0].media_description()->type());
+
+ // Setting the local answer should succeed.
+ ASSERT_TRUE(
+ callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+
+ // Setting the remote answer should succeed.
+ ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+}
+
+// Test all combinations of audio and video as the first and second media type
+// for the media section. This is needed for full test coverage because
+// MediaSession has separate functions for processing audio and video media
+// sections.
+INSTANTIATE_TEST_CASE_P(
+ PeerConnectionJsepTest,
+ RecycleMediaSectionTest,
+ Combine(Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO),
+ Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO)));
+
+// Tests for MID properties.
+
+static void RenameSection(size_t mline_index,
+ const std::string& new_mid,
+ SessionDescriptionInterface* sdesc) {
+ cricket::SessionDescription* desc = sdesc->description();
+ std::string old_mid = desc->contents()[mline_index].name;
+ desc->contents()[mline_index].name = new_mid;
+ desc->transport_infos()[mline_index].content_name = new_mid;
+ const cricket::ContentGroup* bundle =
+ desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+ if (bundle) {
+ cricket::ContentGroup new_bundle = *bundle;
+ if (new_bundle.RemoveContentName(old_mid)) {
+ new_bundle.AddContentName(new_mid);
+ }
+ desc->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+ desc->AddGroup(new_bundle);
+ }
+}
+
+// Test that two PeerConnections can have a successful offer/answer exchange if
+// the MIDs are changed from the defaults.
+TEST_F(PeerConnectionJsepTest, OfferAnswerWithChangedMids) {
+ constexpr char kFirstMid[] = "nondefaultmid";
+ constexpr char kSecondMid[] = "randommid";
+
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ caller->AddAudioTrack("b");
+ auto callee = CreatePeerConnection();
+
+ auto offer = caller->CreateOffer();
+ RenameSection(0, kFirstMid, offer.get());
+ RenameSection(1, kSecondMid, offer.get());
+
+ ASSERT_TRUE(
+ caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+ auto caller_transceivers = caller->pc()->GetTransceivers();
+ EXPECT_EQ(kFirstMid, caller_transceivers[0]->mid());
+ EXPECT_EQ(kSecondMid, caller_transceivers[1]->mid());
+
+ ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+ auto callee_transceivers = callee->pc()->GetTransceivers();
+ EXPECT_EQ(kFirstMid, callee_transceivers[0]->mid());
+ EXPECT_EQ(kSecondMid, callee_transceivers[1]->mid());
+
+ auto answer = callee->CreateAnswer();
+ auto answer_contents = answer->description()->contents();
+ EXPECT_EQ(kFirstMid, answer_contents[0].name);
+ EXPECT_EQ(kSecondMid, answer_contents[1].name);
+
+ ASSERT_TRUE(
+ callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+ ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+}
+
+// Test that CreateOffer will generate a MID that is not already used if the
+// default it would have picked is already taken. This is tested by using a
+// third PeerConnection to determine what the default would be for the second
+// media section then setting that as the first media section's MID.
+TEST_F(PeerConnectionJsepTest, CreateOfferGeneratesUniqueMidIfAlreadyTaken) {
+ // First, find what the default MID is for the second media section.
+ auto pc = CreatePeerConnection();
+ pc->AddAudioTrack("a");
+ pc->AddAudioTrack("b");
+ auto default_offer = pc->CreateOffer();
+ std::string default_second_mid =
+ default_offer->description()->contents()[1].name;
+
+ // Now, do an offer/answer with one track which has the MID set to the default
+ // second MID.
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+
+ auto offer = caller->CreateOffer();
+ RenameSection(0, default_second_mid, offer.get());
+
+ ASSERT_TRUE(
+ caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+ ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+ ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+ // Add a second track and ensure that the MID is different.
+ caller->AddAudioTrack("b");
+
+ auto reoffer = caller->CreateOffer();
+ auto reoffer_contents = reoffer->description()->contents();
+ EXPECT_EQ(default_second_mid, reoffer_contents[0].name);
+ EXPECT_NE(reoffer_contents[0].name, reoffer_contents[1].name);
+}
+
+// Test that a reoffer initiated by the callee adds a new track to the caller.
+TEST_F(PeerConnectionJsepTest, CalleeDoesReoffer) {
+ auto caller = CreatePeerConnection();
+ caller->AddAudioTrack("a");
+ auto callee = CreatePeerConnection();
+ callee->AddAudioTrack("a");
+ callee->AddVideoTrack("v");
+
+ ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+ EXPECT_EQ(1u, caller->pc()->GetTransceivers().size());
+ EXPECT_EQ(2u, callee->pc()->GetTransceivers().size());
+
+ ASSERT_TRUE(callee->ExchangeOfferAnswerWith(caller.get()));
+
+ EXPECT_EQ(2u, caller->pc()->GetTransceivers().size());
+ EXPECT_EQ(2u, callee->pc()->GetTransceivers().size());
+}
+
+} // namespace webrtc
diff --git a/pc/peerconnectionwrapper.cc b/pc/peerconnectionwrapper.cc
index 121cc64..d297054 100644
--- a/pc/peerconnectionwrapper.cc
+++ b/pc/peerconnectionwrapper.cc
@@ -179,6 +179,45 @@
return observer->result();
}
+bool PeerConnectionWrapper::ExchangeOfferAnswerWith(
+ PeerConnectionWrapper* answerer) {
+ RTC_DCHECK(answerer);
+ if (answerer == this) {
+ RTC_LOG(LS_ERROR) << "Cannot exchange offer/answer with ourself!";
+ return false;
+ }
+ auto offer = CreateOffer();
+ EXPECT_TRUE(offer);
+ if (!offer) {
+ return false;
+ }
+ bool set_local_offer =
+ SetLocalDescription(CloneSessionDescription(offer.get()));
+ EXPECT_TRUE(set_local_offer);
+ if (!set_local_offer) {
+ return false;
+ }
+ bool set_remote_offer = answerer->SetRemoteDescription(std::move(offer));
+ EXPECT_TRUE(set_remote_offer);
+ if (!set_remote_offer) {
+ return false;
+ }
+ auto answer = answerer->CreateAnswer();
+ EXPECT_TRUE(answer);
+ if (!answer) {
+ return false;
+ }
+ bool set_local_answer =
+ answerer->SetLocalDescription(CloneSessionDescription(answer.get()));
+ EXPECT_TRUE(set_local_answer);
+ if (!set_local_answer) {
+ return false;
+ }
+ bool set_remote_answer = SetRemoteDescription(std::move(answer));
+ EXPECT_TRUE(set_remote_answer);
+ return set_remote_answer;
+}
+
rtc::scoped_refptr<RtpTransceiverInterface>
PeerConnectionWrapper::AddTransceiver(cricket::MediaType media_type) {
RTCErrorOr<rtc::scoped_refptr<RtpTransceiverInterface>> result =
diff --git a/pc/peerconnectionwrapper.h b/pc/peerconnectionwrapper.h
index e7d19ea..9208207 100644
--- a/pc/peerconnectionwrapper.h
+++ b/pc/peerconnectionwrapper.h
@@ -93,6 +93,21 @@
bool SetRemoteDescription(std::unique_ptr<SessionDescriptionInterface> desc,
RTCError* error_out);
+ // Does a round of offer/answer with the local PeerConnectionWrapper
+ // generating the offer and the given PeerConnectionWrapper generating the
+ // answer.
+ // Equivalent to:
+ // 1. this->CreateOffer()
+ // 2. this->SetLocalDescription(offer)
+ // 3. answerer->SetRemoteDescription(offer)
+ // 4. answerer->CreateAnswer()
+ // 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);
+
// The following are wrappers for the underlying PeerConnection's
// AddTransceiver method. They return the result of calling AddTransceiver
// with the given arguments, DCHECKing if there is an error.
diff --git a/pc/rtptransceiver.cc b/pc/rtptransceiver.cc
index 2919ee7..61ebdc7 100644
--- a/pc/rtptransceiver.cc
+++ b/pc/rtptransceiver.cc
@@ -12,8 +12,14 @@
#include <string>
+#include "pc/rtpmediautils.h"
+
namespace webrtc {
+std::ostream& operator<<(std::ostream& os, RtpTransceiverDirection direction) {
+ return os << RtpTransceiverDirectionToString(direction);
+}
+
RtpTransceiver::RtpTransceiver(cricket::MediaType media_type)
: unified_plan_(false), media_type_(media_type) {
RTC_DCHECK(media_type == cricket::MEDIA_TYPE_AUDIO ||
@@ -142,6 +148,13 @@
return receivers_[0];
}
+void RtpTransceiver::set_current_direction(RtpTransceiverDirection direction) {
+ current_direction_ = direction;
+ if (RtpTransceiverDirectionHasSend(*current_direction_)) {
+ has_ever_been_used_to_send_ = true;
+ }
+}
+
bool RtpTransceiver::stopped() const {
return stopped_;
}
@@ -152,7 +165,7 @@
void RtpTransceiver::SetDirection(RtpTransceiverDirection new_direction) {
// TODO(steveanton): This should fire OnNegotiationNeeded.
- direction_ = new_direction;
+ set_direction(new_direction);
}
rtc::Optional<RtpTransceiverDirection> RtpTransceiver::current_direction()
diff --git a/pc/rtptransceiver.h b/pc/rtptransceiver.h
index 19f393f..9e8565b 100644
--- a/pc/rtptransceiver.h
+++ b/pc/rtptransceiver.h
@@ -115,6 +115,33 @@
// Returns the backing object for the transceiver's Unified Plan receiver.
rtc::scoped_refptr<RtpReceiverInternal> receiver_internal() const;
+ // RtpTransceivers are not associated until they have a corresponding media
+ // section set in SetLocalDescription or SetRemoteDescription. Therefore,
+ // when setting a local offer we need a way to remember which transceiver was
+ // used to create which media section in the offer. Storing the mline index
+ // in CreateOffer is specified in JSEP to allow us to do that.
+ rtc::Optional<size_t> mline_index() const { return mline_index_; }
+ void set_mline_index(rtc::Optional<size_t> mline_index) {
+ mline_index_ = mline_index;
+ }
+
+ // Sets the MID for this transceiver. If the MID is not null, then the
+ // transceiver is considered "associated" with the media section that has the
+ // same MID.
+ void set_mid(const rtc::Optional<std::string>& mid) { mid_ = mid; }
+
+ // Sets the intended direction for this transceiver. Intended to be used
+ // internally over SetDirection since this does not trigger a negotiation
+ // needed callback.
+ void set_direction(RtpTransceiverDirection direction) {
+ direction_ = direction;
+ }
+
+ // Sets the current direction for this transceiver as negotiated in an offer/
+ // answer exchange. The current direction is null before an answer with this
+ // transceiver has been set.
+ void set_current_direction(RtpTransceiverDirection direction);
+
// According to JSEP rules for SetRemoteDescription, RtpTransceivers can be
// reused only if they were added by AddTrack.
void set_created_by_addtrack(bool created_by_addtrack) {
@@ -152,9 +179,8 @@
RtpTransceiverDirection direction_ = RtpTransceiverDirection::kInactive;
rtc::Optional<RtpTransceiverDirection> current_direction_;
rtc::Optional<std::string> mid_;
+ rtc::Optional<size_t> mline_index_;
bool created_by_addtrack_ = false;
- // TODO(steveanton): Implement this once there is a mechanism to set the
- // current direction.
bool has_ever_been_used_to_send_ = false;
cricket::BaseChannel* channel_ = nullptr;