diff --git a/api/rtptransceiverinterface.h b/api/rtptransceiverinterface.h index 1d2988d4cb..5d74ae1023 100644 --- a/api/rtptransceiverinterface.h +++ b/api/rtptransceiverinterface.h @@ -29,6 +29,9 @@ enum class RtpTransceiverDirection { 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 a4292906a1..a0f94af104 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -400,6 +400,7 @@ if (rtc_include_tests) { "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 24d0d342d0..692bcc2ac9 100644 --- a/pc/mediasession.cc +++ b/pc/mediasession.cc @@ -1309,16 +1309,16 @@ SessionDescription* MediaSessionDescriptionFactory::CreateOffer( // 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(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 @@ SessionDescription* MediaSessionDescriptionFactory::CreateAnswer( 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 @@ SessionDescription* MediaSessionDescriptionFactory::CreateAnswer( RTC_DCHECK(media_description_options.mid == offer_content->name); const ContentInfo* current_content = nullptr; if (current_description && - msection_index < - static_cast(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 @@ bool MediaSessionDescriptionFactory::AddAudioContentForOffer( 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 @@ bool MediaSessionDescriptionFactory::AddVideoContentForOffer( &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 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer( 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 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer( } 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 5a21146490..b99df99014 100644 --- a/pc/peerconnection.cc +++ b/pc/peerconnection.cc @@ -11,6 +11,7 @@ #include "pc/peerconnection.h" #include +#include #include #include #include @@ -330,6 +331,11 @@ bool MediaSectionsInSameOrder(const SessionDescription* existing_desc, } 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 @@ 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 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 @@ RTCError PeerConnection::ApplyLocalDescription( 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 @@ RTCError PeerConnection::ApplyRemoteDescription( // 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 replaced_remote_description; SdpType type = desc->GetType(); if (type == SdpType::kAnswer) { @@ -1812,17 +1856,25 @@ RTCError PeerConnection::ApplyRemoteDescription( 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 @@ RTCError PeerConnection::ApplyRemoteDescription( 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 @@ RTCError PeerConnection::ApplyRemoteDescription( // since only at that point will new streams have all their tracks. rtc::scoped_refptr 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(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(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> + 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>> +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> +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> +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> +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> transceiver, @@ -2655,10 +2936,30 @@ void PeerConnection::PostCreateSessionDescriptionFailure( } 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 @@ void PeerConnection::GetOptionsForOffer( 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 audio_index; @@ -2733,13 +3038,6 @@ void PeerConnection::GetOptionsForOffer( !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 @@ void PeerConnection::GetOptionsForOffer( 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* used_mids) { + RTC_DCHECK(used_mids); + // We're boring: just generate MIDs 0, 1, 2, ... + size_t i = 0; + std::set::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> + 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 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 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 @@ void PeerConnection::GetOptionsForAnswer( 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 audio_index; @@ -2805,12 +3251,6 @@ void PeerConnection::GetOptionsForAnswer( !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 @@ void PeerConnection::GetOptionsForAnswer( 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 @@ void PeerConnection::StopRtcEventLog_w() { 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 @@ RTCError PeerConnection::PushdownTransportDescription( 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 @@ std::string PeerConnection::GetTransportNameForMediaSection( return *first_content_name; } -RTCError PeerConnection::CreateChannels(const SessionDescription* desc) { - RTC_DCHECK(desc); - +RTCErrorOr 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 @@ RTCError PeerConnection::CreateChannels(const SessionDescription* desc) { 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 @@ RTCError PeerConnection::CreateChannels(const SessionDescription* desc) { 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 b1a1d9eec2..b9a86d8653 100644 --- a/pc/peerconnection.h +++ b/pc/peerconnection.h @@ -412,6 +412,43 @@ class PeerConnection : public PeerConnectionInterface, RTCError ApplyRemoteDescription( std::unique_ptr 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> + transceiver, + const cricket::ContentInfo& content, + const cricket::ContentGroup* bundle_group); + + // Associate the given transceiver according to the JSEP rules. + RTCErrorOr< + rtc::scoped_refptr>> + 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> + GetAssociatedTransceiver(const std::string& mid) const; + + // Returns the RtpTransceiver, if found, that was assigned to the given mline + // index in CreateOffer. + rtc::scoped_refptr> + 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> + 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 @@ class PeerConnection : public PeerConnectionInterface, // 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 @@ class PeerConnection : public PeerConnectionInterface, // 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 GetEarlyBundleGroup( + const cricket::SessionDescription& desc) const; // Helper methods to create media channels. cricket::VoiceChannel* CreateVoiceChannel(const std::string& mid, @@ -859,6 +920,9 @@ class PeerConnection : public PeerConnectionInterface, std::vector< rtc::scoped_refptr>> transceivers_; + // MIDs that have been seen either by SetLocalDescription or + // SetRemoteDescription over the life of the PeerConnection. + std::set 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 0000000000..119b1351a0 --- /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 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(); + auto pc = pc_factory_->CreatePeerConnection(config, nullptr, nullptr, + observer.get()); + if (!pc) { + return nullptr; + } + + return rtc::MakeUnique(pc_factory_, pc, + std::move(observer)); + } + + std::unique_ptr vss_; + rtc::AutoSocketServerThread main_; + rtc::scoped_refptr 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> { + 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 121cc64144..d297054696 100644 --- a/pc/peerconnectionwrapper.cc +++ b/pc/peerconnectionwrapper.cc @@ -179,6 +179,45 @@ bool PeerConnectionWrapper::SetSdp( 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 PeerConnectionWrapper::AddTransceiver(cricket::MediaType media_type) { RTCErrorOr> result = diff --git a/pc/peerconnectionwrapper.h b/pc/peerconnectionwrapper.h index e7d19eaa9b..9208207df6 100644 --- a/pc/peerconnectionwrapper.h +++ b/pc/peerconnectionwrapper.h @@ -93,6 +93,21 @@ class PeerConnectionWrapper { bool SetRemoteDescription(std::unique_ptr 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 2919ee779d..61ebdc7f2c 100644 --- a/pc/rtptransceiver.cc +++ b/pc/rtptransceiver.cc @@ -12,8 +12,14 @@ #include +#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 @@ rtc::scoped_refptr RtpTransceiver::receiver() const { 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 @@ RtpTransceiverDirection RtpTransceiver::direction() const { void RtpTransceiver::SetDirection(RtpTransceiverDirection new_direction) { // TODO(steveanton): This should fire OnNegotiationNeeded. - direction_ = new_direction; + set_direction(new_direction); } rtc::Optional RtpTransceiver::current_direction() diff --git a/pc/rtptransceiver.h b/pc/rtptransceiver.h index 19f393f3fe..9e8565bbfd 100644 --- a/pc/rtptransceiver.h +++ b/pc/rtptransceiver.h @@ -115,6 +115,33 @@ class RtpTransceiver final // Returns the backing object for the transceiver's Unified Plan receiver. rtc::scoped_refptr 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 mline_index() const { return mline_index_; } + void set_mline_index(rtc::Optional 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& 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 @@ class RtpTransceiver final RtpTransceiverDirection direction_ = RtpTransceiverDirection::kInactive; rtc::Optional current_direction_; rtc::Optional mid_; + rtc::Optional 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;