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}
This commit is contained in:
Steve Anton
2017-12-22 16:02:54 -08:00
committed by Commit Bot
parent 24de1735b7
commit dcc3c02468
10 changed files with 1518 additions and 155 deletions

View File

@ -29,6 +29,9 @@ enum class RtpTransceiverDirection {
kInactive 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 // Structure for initializing an RtpTransceiver in a call to
// PeerConnectionInterface::AddTransceiver. // PeerConnectionInterface::AddTransceiver.
// https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiverinit // https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiverinit

View File

@ -400,6 +400,7 @@ if (rtc_include_tests) {
"peerconnection_datachannel_unittest.cc", "peerconnection_datachannel_unittest.cc",
"peerconnection_ice_unittest.cc", "peerconnection_ice_unittest.cc",
"peerconnection_integrationtest.cc", "peerconnection_integrationtest.cc",
"peerconnection_jsep_unittest.cc",
"peerconnection_media_unittest.cc", "peerconnection_media_unittest.cc",
"peerconnection_rtp_unittest.cc", "peerconnection_rtp_unittest.cc",
"peerconnection_signaling_unittest.cc", "peerconnection_signaling_unittest.cc",

View File

@ -1309,16 +1309,16 @@ SessionDescription* MediaSessionDescriptionFactory::CreateOffer(
// Iterate through the media description options, matching with existing media // Iterate through the media description options, matching with existing media
// descriptions in |current_description|. // descriptions in |current_description|.
int msection_index = 0; size_t msection_index = 0;
for (const MediaDescriptionOptions& media_description_options : for (const MediaDescriptionOptions& media_description_options :
session_options.media_description_options) { session_options.media_description_options) {
const ContentInfo* current_content = nullptr; const ContentInfo* current_content = nullptr;
if (current_description && if (current_description &&
msection_index < msection_index < current_description->contents().size()) {
static_cast<int>(current_description->contents().size())) {
current_content = &current_description->contents()[msection_index]; current_content = &current_description->contents()[msection_index];
// Media type must match. // Media type must match unless this media section is being recycled.
RTC_DCHECK(IsMediaContentOfType(current_content, RTC_DCHECK(current_content->rejected ||
IsMediaContentOfType(current_content,
media_description_options.type)); media_description_options.type));
} }
switch (media_description_options.type) { switch (media_description_options.type) {
@ -1424,7 +1424,7 @@ SessionDescription* MediaSessionDescriptionFactory::CreateAnswer(
session_options.media_description_options.size()); session_options.media_description_options.size());
// Iterate through the media description options, matching with existing // Iterate through the media description options, matching with existing
// media descriptions in |current_description|. // media descriptions in |current_description|.
int msection_index = 0; size_t msection_index = 0;
for (const MediaDescriptionOptions& media_description_options : for (const MediaDescriptionOptions& media_description_options :
session_options.media_description_options) { session_options.media_description_options) {
const ContentInfo* offer_content = &offer->contents()[msection_index]; const ContentInfo* offer_content = &offer->contents()[msection_index];
@ -1435,8 +1435,7 @@ SessionDescription* MediaSessionDescriptionFactory::CreateAnswer(
RTC_DCHECK(media_description_options.mid == offer_content->name); RTC_DCHECK(media_description_options.mid == offer_content->name);
const ContentInfo* current_content = nullptr; const ContentInfo* current_content = nullptr;
if (current_description && if (current_description &&
msection_index < msection_index < current_description->contents().size()) {
static_cast<int>(current_description->contents().size())) {
current_content = &current_description->contents()[msection_index]; current_content = &current_description->contents()[msection_index];
} }
switch (media_description_options.type) { switch (media_description_options.type) {
@ -1802,8 +1801,8 @@ bool MediaSessionDescriptionFactory::AddAudioContentForOffer(
GetAudioCodecsForOffer(media_description_options.direction); GetAudioCodecsForOffer(media_description_options.direction);
AudioCodecs filtered_codecs; AudioCodecs filtered_codecs;
// Add the codecs from current content if exists. // Add the codecs from current content if it exists and is not being recycled.
if (current_content) { if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO)); RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
const AudioContentDescription* acd = const AudioContentDescription* acd =
current_content->media_description()->as_audio(); current_content->media_description()->as_audio();
@ -1877,8 +1876,8 @@ bool MediaSessionDescriptionFactory::AddVideoContentForOffer(
&crypto_suites); &crypto_suites);
VideoCodecs filtered_codecs; VideoCodecs filtered_codecs;
// Add the codecs from current content if exists. // Add the codecs from current content if it exists and is not being recycled.
if (current_content) { if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO)); RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
const VideoContentDescription* vcd = const VideoContentDescription* vcd =
current_content->media_description()->as_video(); current_content->media_description()->as_video();
@ -2038,8 +2037,8 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer(
GetAudioCodecsForAnswer(offer_rtd, answer_rtd); GetAudioCodecsForAnswer(offer_rtd, answer_rtd);
AudioCodecs filtered_codecs; AudioCodecs filtered_codecs;
// Add the codecs from current content if exists. // Add the codecs from current content if it exists and is not being recycled.
if (current_content) { if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO)); RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
const AudioContentDescription* acd = const AudioContentDescription* acd =
current_content->media_description()->as_audio(); current_content->media_description()->as_audio();
@ -2120,8 +2119,8 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer(
} }
VideoCodecs filtered_codecs; VideoCodecs filtered_codecs;
// Add the codecs from current content if exists. // Add the codecs from current content if it exists and is not being recycled.
if (current_content) { if (current_content && !current_content->rejected) {
RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO)); RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
const VideoContentDescription* vcd = const VideoContentDescription* vcd =
current_content->media_description()->as_video(); current_content->media_description()->as_video();

View File

@ -11,6 +11,7 @@
#include "pc/peerconnection.h" #include "pc/peerconnection.h"
#include <algorithm> #include <algorithm>
#include <queue>
#include <set> #include <set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -330,6 +331,11 @@ bool MediaSectionsInSameOrder(const SessionDescription* existing_desc,
} }
for (size_t i = 0; i < existing_desc->contents().size(); ++i) { 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) { if (new_desc->contents()[i].name != existing_desc->contents()[i].name) {
return false; return false;
} }
@ -1637,34 +1643,69 @@ RTCError PeerConnection::ApplyLocalDescription(
} }
} }
// 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) { 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); current_local_description_ = std::move(desc);
pending_local_description_ = nullptr; pending_local_description_ = nullptr;
current_remote_description_ = std::move(pending_remote_description_); current_remote_description_ = std::move(pending_remote_description_);
} else { } else {
replaced_local_description = std::move(pending_local_description_);
pending_local_description_ = std::move(desc); pending_local_description_ = std::move(desc);
} }
// The session description to apply now must be accessed by // The session description to apply now must be accessed by
// |local_description()|. // |local_description()|.
RTC_DCHECK(local_description()); RTC_DCHECK(local_description());
// Transport and Media channels will be created only when offer is set. if (IsUnifiedPlan()) {
if (type == SdpType::kOffer) { RTCError error = UpdateTransceiversAndDataChannels(
// TODO(mallinath) - Handle CreateChannel failure, as new local description cricket::CS_LOCAL, old_local_description, *local_description());
// is applied. Restore back to old description.
RTCError error = CreateChannels(local_description()->description());
if (!error.ok()) { if (!error.ok()) {
return error; 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. // Remove unused channels if MediaContentDescription is rejected.
RemoveUnusedChannels(local_description()->description()); RemoveUnusedChannels(local_description()->description());
}
error = UpdateSessionState(type, cricket::CS_LOCAL); error = UpdateSessionState(type, cricket::CS_LOCAL);
if (!error.ok()) { if (!error.ok()) {
return error; return error;
} }
if (remote_description()) { if (remote_description()) {
// Now that we have a local description, we can push down remote candidates. // Now that we have a local description, we can push down remote candidates.
UseCandidatesInSessionDescription(remote_description()); UseCandidatesInSessionDescription(remote_description());
@ -1682,29 +1723,31 @@ RTCError PeerConnection::ApplyLocalDescription(
AllocateSctpSids(role); AllocateSctpSids(role);
} }
// Update state and SSRC of local MediaStreams and DataChannels based on the if (!IsUnifiedPlan()) {
// local session description. // Update state and SSRC of local MediaStreams and DataChannels based on the
const cricket::ContentInfo* audio_content = // local session description.
GetFirstAudioContent(local_description()->description()); const cricket::ContentInfo* audio_content =
if (audio_content) { GetFirstAudioContent(local_description()->description());
if (audio_content->rejected) { if (audio_content) {
RemoveSenders(cricket::MEDIA_TYPE_AUDIO); if (audio_content->rejected) {
} else { RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
const cricket::AudioContentDescription* audio_desc = } else {
audio_content->media_description()->as_audio(); const cricket::AudioContentDescription* audio_desc =
UpdateLocalSenders(audio_desc->streams(), audio_desc->type()); audio_content->media_description()->as_audio();
UpdateLocalSenders(audio_desc->streams(), audio_desc->type());
}
} }
}
const cricket::ContentInfo* video_content = const cricket::ContentInfo* video_content =
GetFirstVideoContent(local_description()->description()); GetFirstVideoContent(local_description()->description());
if (video_content) { if (video_content) {
if (video_content->rejected) { if (video_content->rejected) {
RemoveSenders(cricket::MEDIA_TYPE_VIDEO); RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
} else { } else {
const cricket::VideoContentDescription* video_desc = const cricket::VideoContentDescription* video_desc =
video_content->media_description()->as_video(); video_content->media_description()->as_video();
UpdateLocalSenders(video_desc->streams(), video_desc->type()); UpdateLocalSenders(video_desc->streams(), video_desc->type());
}
} }
} }
@ -1787,13 +1830,14 @@ RTCError PeerConnection::ApplyRemoteDescription(
// Update stats here so that we have the most recent stats for tracks and // Update stats here so that we have the most recent stats for tracks and
// streams that might be removed by updating the session description. // streams that might be removed by updating the session description.
stats_->UpdateStats(kStatsOutputLevelStandard); 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 = const SessionDescriptionInterface* old_remote_description =
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; std::unique_ptr<SessionDescriptionInterface> replaced_remote_description;
SdpType type = desc->GetType(); SdpType type = desc->GetType();
if (type == SdpType::kAnswer) { if (type == SdpType::kAnswer) {
@ -1812,17 +1856,25 @@ RTCError PeerConnection::ApplyRemoteDescription(
RTC_DCHECK(remote_description()); RTC_DCHECK(remote_description());
// Transport and Media channels will be created only when offer is set. // Transport and Media channels will be created only when offer is set.
if (type == SdpType::kOffer) { if (IsUnifiedPlan()) {
// TODO(mallinath) - Handle CreateChannel failure, as new local description RTCError error = UpdateTransceiversAndDataChannels(
// is applied. Restore back to old description. cricket::CS_REMOTE, old_remote_description, *remote_description());
RTCError error = CreateChannels(remote_description()->description());
if (!error.ok()) { if (!error.ok()) {
return error; 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. // Remove unused channels if MediaContentDescription is rejected.
RemoveUnusedChannels(remote_description()->description()); RemoveUnusedChannels(remote_description()->description());
}
// NOTE: Candidates allocation will be initiated only when SetLocalDescription // NOTE: Candidates allocation will be initiated only when SetLocalDescription
// is called. // is called.
@ -1887,6 +1939,43 @@ RTCError PeerConnection::ApplyRemoteDescription(
AllocateSctpSids(role); 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 = const cricket::ContentInfo* audio_content =
GetFirstAudioContent(remote_description()->description()); GetFirstAudioContent(remote_description()->description());
const cricket::ContentInfo* video_content = const cricket::ContentInfo* video_content =
@ -1910,64 +1999,256 @@ RTCError PeerConnection::ApplyRemoteDescription(
// since only at that point will new streams have all their tracks. // since only at that point will new streams have all their tracks.
rtc::scoped_refptr<StreamCollection> new_streams(StreamCollection::Create()); rtc::scoped_refptr<StreamCollection> new_streams(StreamCollection::Create());
// TODO(steveanton): When removing RTP senders/receivers in response to a if (!IsUnifiedPlan()) {
// rejected media section, there is some cleanup logic that expects the voice/ // TODO(steveanton): When removing RTP senders/receivers in response to a
// video channel to still be set. But in this method the voice/video channel // rejected media section, there is some cleanup logic that expects the
// would have been destroyed by the SetRemoteDescription caller above so the // voice/ video channel to still be set. But in this method the voice/video
// cleanup that relies on them fails to run. The RemoveSenders calls should be // channel would have been destroyed by the SetRemoteDescription caller
// moved to right before the DestroyChannel calls to fix this. // 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 // Find all audio rtp streams and create corresponding remote AudioTracks
// and MediaStreams. // and MediaStreams.
if (audio_content) { if (audio_content) {
if (audio_content->rejected) { if (audio_content->rejected) {
RemoveSenders(cricket::MEDIA_TYPE_AUDIO); RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
} else { } else {
bool default_audio_track_needed = bool default_audio_track_needed =
!remote_peer_supports_msid_ && !remote_peer_supports_msid_ &&
RtpTransceiverDirectionHasSend(audio_desc->direction()); RtpTransceiverDirectionHasSend(audio_desc->direction());
UpdateRemoteSendersList(GetActiveStreams(audio_desc), UpdateRemoteSendersList(GetActiveStreams(audio_desc),
default_audio_track_needed, audio_desc->type(), default_audio_track_needed, audio_desc->type(),
new_streams); new_streams);
}
} }
}
// Find all video rtp streams and create corresponding remote VideoTracks // Find all video rtp streams and create corresponding remote VideoTracks
// and MediaStreams. // and MediaStreams.
if (video_content) { if (video_content) {
if (video_content->rejected) { if (video_content->rejected) {
RemoveSenders(cricket::MEDIA_TYPE_VIDEO); RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
} else { } else {
bool default_video_track_needed = bool default_video_track_needed =
!remote_peer_supports_msid_ && !remote_peer_supports_msid_ &&
RtpTransceiverDirectionHasSend(video_desc->direction()); RtpTransceiverDirectionHasSend(video_desc->direction());
UpdateRemoteSendersList(GetActiveStreams(video_desc), UpdateRemoteSendersList(GetActiveStreams(video_desc),
default_video_track_needed, video_desc->type(), default_video_track_needed, video_desc->type(),
new_streams); new_streams);
}
} }
}
// Update the DataChannels with the information from the remote peer. // Update the DataChannels with the information from the remote peer.
if (data_desc) { if (data_desc) {
if (rtc::starts_with(data_desc->protocol().data(), if (rtc::starts_with(data_desc->protocol().data(),
cricket::kMediaProtocolRtpPrefix)) { cricket::kMediaProtocolRtpPrefix)) {
UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc)); UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc));
}
} }
}
// Iterate new_streams and notify the observer about new MediaStreams. // Iterate new_streams and notify the observer about new MediaStreams.
for (size_t i = 0; i < new_streams->count(); ++i) { for (size_t i = 0; i < new_streams->count(); ++i) {
MediaStreamInterface* new_stream = new_streams->at(i); MediaStreamInterface* new_stream = new_streams->at(i);
stats_->AddStream(new_stream); stats_->AddStream(new_stream);
observer_->OnAddStream( observer_->OnAddStream(
rtc::scoped_refptr<MediaStreamInterface>(new_stream)); rtc::scoped_refptr<MediaStreamInterface>(new_stream));
} }
UpdateEndedRemoteMediaStreams(); UpdateEndedRemoteMediaStreams();
}
return RTCError::OK(); 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( const cricket::ContentInfo* PeerConnection::FindMediaSectionForTransceiver(
rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>> rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
transceiver, transceiver,
@ -2655,10 +2936,30 @@ void PeerConnection::PostCreateSessionDescriptionFailure(
} }
void PeerConnection::GetOptionsForOffer( void PeerConnection::GetOptionsForOffer(
const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options, const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_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. // Figure out transceiver directional preferences.
bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO); bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO); bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@ -2673,15 +2974,19 @@ void PeerConnection::GetOptionsForOffer(
bool offer_new_data_description = HasDataChannels(); bool offer_new_data_description = HasDataChannels();
// The "offer_to_receive_X" options allow those defaults to be overridden. // The "offer_to_receive_X" options allow those defaults to be overridden.
if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) { if (offer_answer_options.offer_to_receive_audio !=
recv_audio = (rtc_options.offer_to_receive_audio > 0); RTCOfferAnswerOptions::kUndefined) {
recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
offer_new_audio_description = 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) { if (offer_answer_options.offer_to_receive_video !=
recv_video = (rtc_options.offer_to_receive_video > 0); RTCOfferAnswerOptions::kUndefined) {
recv_video = (offer_answer_options.offer_to_receive_video > 0);
offer_new_video_description = 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; rtc::Optional<size_t> audio_index;
@ -2733,13 +3038,6 @@ void PeerConnection::GetOptionsForOffer(
!data_index ? nullptr !data_index ? nullptr
: &session_options->media_description_options[*data_index]; : &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, AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
video_media_description_options); video_media_description_options);
AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options); AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@ -2752,16 +3050,162 @@ void PeerConnection::GetOptionsForOffer(
if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) { if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
session_options->data_channel_type = data_channel_type(); 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->rtcp_cname = rtcp_cname_;
session_options->crypto_options = factory_->options().crypto_options; session_options->crypto_options = factory_->options().crypto_options;
} }
void PeerConnection::GetOptionsForAnswer( void PeerConnection::GetOptionsForPlanBAnswer(
const RTCOfferAnswerOptions& rtc_options, const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_options) { cricket::MediaSessionOptions* session_options) {
ExtractSharedMediaSessionOptions(rtc_options, session_options);
// Figure out transceiver directional preferences. // Figure out transceiver directional preferences.
bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO); bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO); bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@ -2772,11 +3216,13 @@ void PeerConnection::GetOptionsForAnswer(
bool recv_video = true; bool recv_video = true;
// The "offer_to_receive_X" options allow those defaults to be overridden. // The "offer_to_receive_X" options allow those defaults to be overridden.
if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) { if (offer_answer_options.offer_to_receive_audio !=
recv_audio = (rtc_options.offer_to_receive_audio > 0); RTCOfferAnswerOptions::kUndefined) {
recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
} }
if (rtc_options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) { if (offer_answer_options.offer_to_receive_video !=
recv_video = (rtc_options.offer_to_receive_video > 0); RTCOfferAnswerOptions::kUndefined) {
recv_video = (offer_answer_options.offer_to_receive_video > 0);
} }
rtc::Optional<size_t> audio_index; rtc::Optional<size_t> audio_index;
@ -2805,12 +3251,6 @@ void PeerConnection::GetOptionsForAnswer(
!data_index ? nullptr !data_index ? nullptr
: &session_options->media_description_options[*data_index]; : &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, AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
video_media_description_options); video_media_description_options);
AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options); AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@ -2822,9 +3262,30 @@ void PeerConnection::GetOptionsForAnswer(
if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) { if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
session_options->data_channel_type = data_channel_type(); session_options->data_channel_type = data_channel_type();
} }
}
session_options->rtcp_cname = rtcp_cname_; void PeerConnection::GetOptionsForUnifiedPlanAnswer(
session_options->crypto_options = factory_->options().crypto_options; 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( void PeerConnection::GenerateMediaDescriptionOptions(
@ -3540,11 +4001,11 @@ void PeerConnection::StopRtcEventLog_w() {
cricket::BaseChannel* PeerConnection::GetChannel( cricket::BaseChannel* PeerConnection::GetChannel(
const std::string& content_name) { const std::string& content_name) {
if (voice_channel() && voice_channel()->content_name() == content_name) { for (auto transceiver : transceivers_) {
return voice_channel(); cricket::BaseChannel* channel = transceiver->internal()->channel();
} if (channel && channel->content_name() == content_name) {
if (video_channel() && video_channel()->content_name() == content_name) { return channel;
return video_channel(); }
} }
if (rtp_data_channel() && if (rtp_data_channel() &&
rtp_data_channel()->content_name() == content_name) { rtp_data_channel()->content_name() == content_name) {
@ -3779,9 +4240,9 @@ RTCError PeerConnection::PushdownTransportDescription(
tinfo.content_name, tinfo.description, type, &error); tinfo.content_name, tinfo.description, type, &error);
} }
if (!success) { if (!success) {
LOG_AND_RETURN_ERROR( LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
RTCErrorType::INVALID_PARAMETER, "Failed to push down transport description for " +
"Failed to push down transport description: " + error); tinfo.content_name + ": " + error);
} }
} }
@ -4308,22 +4769,30 @@ std::string PeerConnection::GetTransportNameForMediaSection(
return *first_content_name; return *first_content_name;
} }
RTCError PeerConnection::CreateChannels(const SessionDescription* desc) { RTCErrorOr<const cricket::ContentGroup*> PeerConnection::GetEarlyBundleGroup(
RTC_DCHECK(desc); const SessionDescription& desc) const {
const cricket::ContentGroup* bundle_group = nullptr; const cricket::ContentGroup* bundle_group = nullptr;
if (configuration_.bundle_policy == if (configuration_.bundle_policy ==
PeerConnectionInterface::kBundlePolicyMaxBundle) { PeerConnectionInterface::kBundlePolicyMaxBundle) {
bundle_group = desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE); bundle_group = desc.GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
if (!bundle_group) { if (!bundle_group) {
LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
"max-bundle configured but session description " "max-bundle configured but session description "
"has no BUNDLE group"); "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. // 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 && if (voice && !voice->rejected &&
!GetAudioTransceiver()->internal()->channel()) { !GetAudioTransceiver()->internal()->channel()) {
cricket::VoiceChannel* voice_channel = CreateVoiceChannel( cricket::VoiceChannel* voice_channel = CreateVoiceChannel(
@ -4336,7 +4805,7 @@ RTCError PeerConnection::CreateChannels(const SessionDescription* desc) {
GetAudioTransceiver()->internal()->SetChannel(voice_channel); GetAudioTransceiver()->internal()->SetChannel(voice_channel);
} }
const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc); const cricket::ContentInfo* video = cricket::GetFirstVideoContent(&desc);
if (video && !video->rejected && if (video && !video->rejected &&
!GetVideoTransceiver()->internal()->channel()) { !GetVideoTransceiver()->internal()->channel()) {
cricket::VideoChannel* video_channel = CreateVideoChannel( cricket::VideoChannel* video_channel = CreateVideoChannel(
@ -4349,7 +4818,7 @@ RTCError PeerConnection::CreateChannels(const SessionDescription* desc) {
GetVideoTransceiver()->internal()->SetChannel(video_channel); 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 && if (data_channel_type_ != cricket::DCT_NONE && data && !data->rejected &&
!rtp_data_channel_ && !sctp_transport_) { !rtp_data_channel_ && !sctp_transport_) {
if (!CreateDataChannel(data->name, GetTransportNameForMediaSection( if (!CreateDataChannel(data->name, GetTransportNameForMediaSection(

View File

@ -412,6 +412,43 @@ class PeerConnection : public PeerConnectionInterface,
RTCError ApplyRemoteDescription( RTCError ApplyRemoteDescription(
std::unique_ptr<SessionDescriptionInterface> desc); 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 // Returns the media section in the given session description that is
// associated with the RtpTransceiver. Returns null if none found or this // associated with the RtpTransceiver. Returns null if none found or this
// RtpTransceiver is not associated. Logic varies depending on the // RtpTransceiver is not associated. Logic varies depending on the
@ -427,14 +464,30 @@ class PeerConnection : public PeerConnectionInterface,
// Returns a MediaSessionOptions struct with options decided by |options|, // Returns a MediaSessionOptions struct with options decided by |options|,
// the local MediaStreams and DataChannels. // the local MediaStreams and DataChannels.
void GetOptionsForOffer( void GetOptionsForOffer(const PeerConnectionInterface::RTCOfferAnswerOptions&
const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options, 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); cricket::MediaSessionOptions* session_options);
// Returns a MediaSessionOptions struct with options decided by // Returns a MediaSessionOptions struct with options decided by
// |constraints|, the local MediaStreams and DataChannels. // |constraints|, the local MediaStreams and DataChannels.
void GetOptionsForAnswer(const RTCOfferAnswerOptions& options, void GetOptionsForAnswer(const RTCOfferAnswerOptions& offer_answer_options,
cricket::MediaSessionOptions* session_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 // Generates MediaDescriptionOptions for the |session_opts| based on existing
// local description or remote description. // local description or remote description.
@ -706,7 +759,15 @@ class PeerConnection : public PeerConnectionInterface,
// Allocates media channels based on the |desc|. If |desc| doesn't have // Allocates media channels based on the |desc|. If |desc| doesn't have
// the BUNDLE option, this method will disable BUNDLE in PortAllocator. // the BUNDLE option, this method will disable BUNDLE in PortAllocator.
// This method will also delete any existing media channels before creating. // 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. // Helper methods to create media channels.
cricket::VoiceChannel* CreateVoiceChannel(const std::string& mid, cricket::VoiceChannel* CreateVoiceChannel(const std::string& mid,
@ -859,6 +920,9 @@ class PeerConnection : public PeerConnectionInterface,
std::vector< std::vector<
rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>> rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
transceivers_; 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; SessionError session_error_ = SessionError::kNone;
std::string session_error_desc_; std::string session_error_desc_;

View File

@ -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

View File

@ -179,6 +179,45 @@ bool PeerConnectionWrapper::SetSdp(
return observer->result(); 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> rtc::scoped_refptr<RtpTransceiverInterface>
PeerConnectionWrapper::AddTransceiver(cricket::MediaType media_type) { PeerConnectionWrapper::AddTransceiver(cricket::MediaType media_type) {
RTCErrorOr<rtc::scoped_refptr<RtpTransceiverInterface>> result = RTCErrorOr<rtc::scoped_refptr<RtpTransceiverInterface>> result =

View File

@ -93,6 +93,21 @@ class PeerConnectionWrapper {
bool SetRemoteDescription(std::unique_ptr<SessionDescriptionInterface> desc, bool SetRemoteDescription(std::unique_ptr<SessionDescriptionInterface> desc,
RTCError* error_out); 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 // The following are wrappers for the underlying PeerConnection's
// AddTransceiver method. They return the result of calling AddTransceiver // AddTransceiver method. They return the result of calling AddTransceiver
// with the given arguments, DCHECKing if there is an error. // with the given arguments, DCHECKing if there is an error.

View File

@ -12,8 +12,14 @@
#include <string> #include <string>
#include "pc/rtpmediautils.h"
namespace webrtc { namespace webrtc {
std::ostream& operator<<(std::ostream& os, RtpTransceiverDirection direction) {
return os << RtpTransceiverDirectionToString(direction);
}
RtpTransceiver::RtpTransceiver(cricket::MediaType media_type) RtpTransceiver::RtpTransceiver(cricket::MediaType media_type)
: unified_plan_(false), media_type_(media_type) { : unified_plan_(false), media_type_(media_type) {
RTC_DCHECK(media_type == cricket::MEDIA_TYPE_AUDIO || RTC_DCHECK(media_type == cricket::MEDIA_TYPE_AUDIO ||
@ -142,6 +148,13 @@ rtc::scoped_refptr<RtpReceiverInterface> RtpTransceiver::receiver() const {
return receivers_[0]; 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 { bool RtpTransceiver::stopped() const {
return stopped_; return stopped_;
} }
@ -152,7 +165,7 @@ RtpTransceiverDirection RtpTransceiver::direction() const {
void RtpTransceiver::SetDirection(RtpTransceiverDirection new_direction) { void RtpTransceiver::SetDirection(RtpTransceiverDirection new_direction) {
// TODO(steveanton): This should fire OnNegotiationNeeded. // TODO(steveanton): This should fire OnNegotiationNeeded.
direction_ = new_direction; set_direction(new_direction);
} }
rtc::Optional<RtpTransceiverDirection> RtpTransceiver::current_direction() rtc::Optional<RtpTransceiverDirection> RtpTransceiver::current_direction()

View File

@ -115,6 +115,33 @@ class RtpTransceiver final
// Returns the backing object for the transceiver's Unified Plan receiver. // Returns the backing object for the transceiver's Unified Plan receiver.
rtc::scoped_refptr<RtpReceiverInternal> receiver_internal() const; 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 // According to JSEP rules for SetRemoteDescription, RtpTransceivers can be
// reused only if they were added by AddTrack. // reused only if they were added by AddTrack.
void set_created_by_addtrack(bool created_by_addtrack) { void set_created_by_addtrack(bool created_by_addtrack) {
@ -152,9 +179,8 @@ class RtpTransceiver final
RtpTransceiverDirection direction_ = RtpTransceiverDirection::kInactive; RtpTransceiverDirection direction_ = RtpTransceiverDirection::kInactive;
rtc::Optional<RtpTransceiverDirection> current_direction_; rtc::Optional<RtpTransceiverDirection> current_direction_;
rtc::Optional<std::string> mid_; rtc::Optional<std::string> mid_;
rtc::Optional<size_t> mline_index_;
bool created_by_addtrack_ = false; 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; bool has_ever_been_used_to_send_ = false;
cricket::BaseChannel* channel_ = nullptr; cricket::BaseChannel* channel_ = nullptr;