diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc')
-rw-r--r-- | third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc | 2408 |
1 files changed, 2408 insertions, 0 deletions
diff --git a/third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc b/third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc new file mode 100644 index 0000000000..851d9257e0 --- /dev/null +++ b/third_party/libwebrtc/pc/peer_connection_jsep_unittest.cc @@ -0,0 +1,2408 @@ +/* + * 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 <stddef.h> + +#include <algorithm> +#include <map> +#include <memory> +#include <string> +#include <tuple> +#include <utility> +#include <vector> + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "api/call/call_factory_interface.h" +#include "api/field_trials_view.h" +#include "api/jsep.h" +#include "api/media_stream_interface.h" +#include "api/media_types.h" +#include "api/peer_connection_interface.h" +#include "api/rtc_error.h" +#include "api/rtp_parameters.h" +#include "api/rtp_receiver_interface.h" +#include "api/rtp_sender_interface.h" +#include "api/rtp_transceiver_direction.h" +#include "api/rtp_transceiver_interface.h" +#include "api/scoped_refptr.h" +#include "api/task_queue/default_task_queue_factory.h" +#include "api/task_queue/task_queue_factory.h" +#include "api/transport/field_trial_based_config.h" +#include "api/transport/sctp_transport_factory_interface.h" +#include "media/base/media_engine.h" +#include "media/base/stream_params.h" +#include "media/engine/webrtc_media_engine.h" +#include "media/engine/webrtc_media_engine_defaults.h" +#include "modules/audio_device/include/audio_device.h" +#include "p2p/base/p2p_constants.h" +#include "p2p/base/port_allocator.h" +#include "p2p/base/transport_info.h" +#include "pc/channel_interface.h" +#include "pc/media_session.h" +#include "pc/peer_connection_wrapper.h" +#include "pc/sdp_utils.h" +#include "pc/session_description.h" +#include "pc/test/mock_peer_connection_observers.h" +#include "rtc_base/rtc_certificate_generator.h" +#include "rtc_base/thread.h" +#include "test/gtest.h" +#ifdef WEBRTC_ANDROID +#include "pc/test/android_test_initializer.h" +#endif +#include "pc/test/fake_audio_capture_module.h" +#include "rtc_base/virtual_socket_server.h" +#include "test/gmock.h" +#include "test/pc/sctp/fake_sctp_transport.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::Combine; +using ::testing::ElementsAre; +using ::testing::UnorderedElementsAre; +using ::testing::Values; + +PeerConnectionFactoryDependencies CreatePeerConnectionFactoryDependencies() { + PeerConnectionFactoryDependencies dependencies; + dependencies.worker_thread = rtc::Thread::Current(); + dependencies.network_thread = rtc::Thread::Current(); + dependencies.signaling_thread = rtc::Thread::Current(); + dependencies.task_queue_factory = CreateDefaultTaskQueueFactory(); + dependencies.trials = std::make_unique<FieldTrialBasedConfig>(); + cricket::MediaEngineDependencies media_deps; + media_deps.task_queue_factory = dependencies.task_queue_factory.get(); + media_deps.adm = FakeAudioCaptureModule::Create(); + media_deps.trials = dependencies.trials.get(); + SetMediaEngineDefaults(&media_deps); + dependencies.media_engine = cricket::CreateMediaEngine(std::move(media_deps)); + dependencies.call_factory = CreateCallFactory(); + dependencies.sctp_factory = std::make_unique<FakeSctpTransportFactory>(); + return dependencies; +} + +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 + } + + WrapperPtr CreatePeerConnection() { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + return CreatePeerConnection(config); + } + + WrapperPtr CreatePeerConnection(const RTCConfiguration& config) { + rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory = + CreateModularPeerConnectionFactory( + CreatePeerConnectionFactoryDependencies()); + auto observer = std::make_unique<MockPeerConnectionObserver>(); + auto result = pc_factory->CreatePeerConnectionOrError( + config, PeerConnectionDependencies(observer.get())); + if (!result.ok()) { + return nullptr; + } + + observer->SetPeerConnectionInterface(result.value().get()); + return std::make_unique<PeerConnectionWrapper>( + pc_factory, result.MoveValue(), std::move(observer)); + } + + std::unique_ptr<rtc::VirtualSocketServer> vss_; + rtc::AutoSocketServerThread main_; +}; + +// 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(); + ASSERT_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 an initial offer with one data channel generates one data media +// section. +TEST_F(PeerConnectionJsepTest, DataOnlyInitialOffer) { + auto caller = CreatePeerConnection(); + caller->CreateDataChannel("dc"); + + auto offer = caller->CreateOffer(); + auto contents = offer->description()->contents(); + ASSERT_EQ(1u, contents.size()); + EXPECT_EQ(cricket::MEDIA_TYPE_DATA, contents[0].media_description()->type()); +} + +// Test that creating multiple data channels only results in one data section +// generated in the offer. +TEST_F(PeerConnectionJsepTest, MultipleDataChannelsCreateOnlyOneDataSection) { + auto caller = CreatePeerConnection(); + caller->CreateDataChannel("first"); + caller->CreateDataChannel("second"); + caller->CreateDataChannel("third"); + + auto offer = caller->CreateOffer(); + ASSERT_EQ(1u, offer->description()->contents().size()); +} + +// 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(); + 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->StopInternal(); + + 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]->media_type()); + EXPECT_EQ(caller_audio->mid(), transceivers[0]->mid()); + EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[0]->direction()); + EXPECT_EQ(0u, transceivers[0]->sender()->stream_ids().size()); + + EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, transceivers[1]->media_type()); + EXPECT_EQ(caller_video->mid(), transceivers[1]->mid()); + EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[1]->direction()); + EXPECT_EQ(0u, transceivers[1]->sender()->stream_ids().size()); +} + +// 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->SetDirectionWithError(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(absl::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(absl::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(absl::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]->StopInternal(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto transceivers = callee->pc()->GetTransceivers(); + ASSERT_EQ(2u, transceivers.size()); + // The stopped transceiver is removed in SetLocalDescription(answer) + ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer())); + transceivers = callee->pc()->GetTransceivers(); + ASSERT_EQ(1u, transceivers.size()); + EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[0]->mid()); + EXPECT_FALSE(transceivers[0]->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); + caller->CreateDataChannel("dc"); + auto callee = CreatePeerConnection(); + + auto offer = caller->CreateOffer(); + const auto* offer_data = cricket::GetFirstDataContent(offer->description()); + ASSERT_TRUE( + caller->SetLocalDescription(CloneSessionDescription(offer.get()))); + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + + auto answer = callee->CreateAnswer(); + auto contents = answer->description()->contents(); + ASSERT_EQ(4u, 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); + EXPECT_EQ(cricket::MEDIA_TYPE_DATA, contents[3].media_description()->type()); + EXPECT_EQ(offer_data->name, contents[3].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]->StopInternal(); + + 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->SetDirectionWithError(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->SetDirectionWithError(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()); +} + +TEST_F(PeerConnectionJsepTest, + ChangeDirectionFromRecvOnlyToSendRecvDoesNotBreakVideoNegotiation) { + auto caller = CreatePeerConnection(); + auto caller_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + auto callee = CreatePeerConnection(); + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kSendRecv); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); +} + +TEST_F(PeerConnectionJsepTest, + ChangeDirectionFromRecvOnlyToSendRecvDoesNotBreakAudioNegotiation) { + auto caller = CreatePeerConnection(); + auto caller_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kSendRecv); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); +} + +// 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]->SetDirectionWithError( + 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->StopInternal(); + + 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->StopInternal(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto transceivers = callee->pc()->GetTransceivers(); + EXPECT_EQ(1u, transceivers.size()); + ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer())); + transceivers = callee->pc()->GetTransceivers(); + EXPECT_EQ(0u, transceivers.size()); +} + +// 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->StopInternal(); + + 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 the transceivers are correctly generated and +// updated when the media section is recycled after the callee stops a +// transceiver and sends an answer with a 0 port. +TEST_F(PeerConnectionJsepTest, + RecycleMediaSectionWhenStoppingTransceiverOnAnswerer) { + auto caller = CreatePeerConnection(); + auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + std::string first_mid = *first_transceiver->mid(); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + callee->pc()->GetTransceivers()[0]->StopInternal(); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + EXPECT_TRUE(first_transceiver->stopped()); + // First transceivers are dissociated on caller side. + ASSERT_EQ(absl::nullopt, first_transceiver->mid()); + // They are disassociated on callee side. + ASSERT_EQ(0u, callee->pc()->GetTransceivers().size()); + + // New offer exchange with new transceivers that recycles the m section + // correctly. + caller->AddAudioTrack("audio2"); + callee->AddAudioTrack("audio2"); + auto offer = caller->CreateOffer(); + auto offer_contents = offer->description()->contents(); + std::string second_mid = offer_contents[0].name; + ASSERT_EQ(1u, offer_contents.size()); + EXPECT_FALSE(offer_contents[0].rejected); + EXPECT_NE(first_mid, second_mid); + + // Setting the offer on each side will dissociate the first transceivers and + // associate the new transceivers. + ASSERT_TRUE( + caller->SetLocalDescription(CloneSessionDescription(offer.get()))); + EXPECT_EQ(absl::nullopt, first_transceiver->mid()); + ASSERT_EQ(1u, caller->pc()->GetTransceivers().size()); + EXPECT_EQ(second_mid, caller->pc()->GetTransceivers()[0]->mid()); + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + EXPECT_EQ(second_mid, callee->pc()->GetTransceivers()[0]->mid()); + + // The new answer should also recycle the m section correctly. + 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); + + // Finishing the negotiation shouldn't add or dissociate any transceivers. + ASSERT_TRUE( + callee->SetLocalDescription(CloneSessionDescription(answer.get()))); + ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer))); + auto caller_transceivers = caller->pc()->GetTransceivers(); + ASSERT_EQ(1u, caller_transceivers.size()); + EXPECT_EQ(second_mid, caller_transceivers[0]->mid()); + auto callee_transceivers = callee->pc()->GetTransceivers(); + ASSERT_EQ(1u, callee_transceivers.size()); + EXPECT_EQ(second_mid, callee_transceivers[0]->mid()); +} + +// Test that creating/setting a local offer that recycles an m= section is +// idempotent. +TEST_F(PeerConnectionJsepTest, CreateOfferRecyclesWhenOfferingTwice) { + // Do a negotiation with a port 0 for the media section. + auto caller = CreatePeerConnection(); + auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + first_transceiver->StopInternal(); + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + caller->AddAudioTrack("audio2"); + + // Create a new offer that recycles the media section and set it as a local + // description. + auto offer = caller->CreateOffer(); + auto offer_contents = offer->description()->contents(); + ASSERT_EQ(1u, offer_contents.size()); + EXPECT_FALSE(offer_contents[0].rejected); + ASSERT_TRUE(caller->SetLocalDescription(std::move(offer))); + ASSERT_EQ(1u, caller->pc()->GetTransceivers().size()); + EXPECT_FALSE(caller->pc()->GetTransceivers()[0]->stopped()); + std::string second_mid = offer_contents[0].name; + + // Create another new offer and set the local description again without the + // rest of any negotation ocurring. + auto second_offer = caller->CreateOffer(); + auto second_offer_contents = second_offer->description()->contents(); + ASSERT_EQ(1u, second_offer_contents.size()); + EXPECT_FALSE(second_offer_contents[0].rejected); + // The mid shouldn't change. + EXPECT_EQ(second_mid, second_offer_contents[0].name); + + ASSERT_TRUE(caller->SetLocalDescription(std::move(second_offer))); + // Make sure that the caller's transceivers are associated correctly. + auto caller_transceivers = caller->pc()->GetTransceivers(); + ASSERT_EQ(1u, caller_transceivers.size()); + EXPECT_EQ(second_mid, caller_transceivers[0]->mid()); + EXPECT_FALSE(caller_transceivers[0]->stopped()); +} + +// 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. +// Correct recycling works as follows: +// - The m= section is re-offered with a new MID value and the new media type. +// - The previously-associated transceiver is dissociated when the new offer is +// set as a local description on the offerer or as a remote description on +// the answerer. +// - The new transceiver is associated with the new MID value. +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 that recycling works properly when a new transceiver recycles an m= +// section that was rejected in both the current local and remote descriptions. +TEST_P(RecycleMediaSectionTest, CurrentLocalAndCurrentRemoteRejected) { + 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->StopInternal(); + + 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(absl::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(1u, callee_transceivers.size()); + EXPECT_EQ(second_mid, callee_transceivers[0]->mid()); + EXPECT_EQ(second_type_, callee_transceivers[0]->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 and not create any new + // transceivers. + ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer))); + ASSERT_EQ(1u, caller->pc()->GetTransceivers().size()); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); +} + +// Test that recycling works properly when a new transceiver recycles an m= +// section that was rejected in only the current remote description. +TEST_P(RecycleMediaSectionTest, CurrentRemoteOnlyRejected) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + std::string first_mid = *caller_first_transceiver->mid(); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + auto callee_first_transceiver = callee->pc()->GetTransceivers()[0]; + callee_first_transceiver->StopInternal(); + + // The answer will have a rejected m= section. + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + + // The offer should reuse the previous media section but allocate a new MID + // and change the media type. + auto caller_second_transceiver = caller->AddTransceiver(second_type_); + auto offer = caller->CreateOffer(); + const 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(absl::nullopt, caller_first_transceiver->mid()); + EXPECT_EQ(second_mid, caller_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(1u, callee_transceivers.size()); + EXPECT_EQ(second_mid, callee_transceivers[0]->mid()); + EXPECT_EQ(second_type_, callee_transceivers[0]->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 and not create any new + // transceivers. + ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer))); + ASSERT_EQ(1u, caller->pc()->GetTransceivers().size()); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); +} + +// Test that recycling works properly when a new transceiver recycles an m= +// section that was rejected only in the current local description. +TEST_P(RecycleMediaSectionTest, CurrentLocalOnlyRejected) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + std::string first_mid = *caller_first_transceiver->mid(); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + auto callee_first_transceiver = callee->pc()->GetTransceivers()[0]; + callee_first_transceiver->StopInternal(); + + // The answer will have a rejected m= section. + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + + // The offer should reuse the previous media section but allocate a new MID + // and change the media type. + auto callee_second_transceiver = callee->AddTransceiver(second_type_); + auto offer = callee->CreateOffer(); + const 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( + callee->SetLocalDescription(CloneSessionDescription(offer.get()))); + EXPECT_EQ(absl::nullopt, callee_first_transceiver->mid()); + EXPECT_EQ(second_mid, callee_second_transceiver->mid()); + + // Setting the remote offer will dissociate the previous transceiver and + // create a new transceiver for the media section. + ASSERT_TRUE(caller->SetRemoteDescription(std::move(offer))); + auto caller_transceivers = caller->pc()->GetTransceivers(); + ASSERT_EQ(1u, caller_transceivers.size()); + EXPECT_EQ(second_mid, caller_transceivers[0]->mid()); + EXPECT_EQ(second_type_, caller_transceivers[0]->media_type()); + + // The answer should have only one media section for the new transceiver. + auto answer = caller->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( + caller->SetLocalDescription(CloneSessionDescription(answer.get()))); + + // Setting the remote answer should succeed and not create any new + // transceivers. + ASSERT_TRUE(callee->SetRemoteDescription(std::move(answer))); + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + ASSERT_EQ(1u, caller->pc()->GetTransceivers().size()); +} + +// Test that a m= section is *not* recycled if the media section is only +// rejected in the pending local description and there is no current remote +// description. +TEST_P(RecycleMediaSectionTest, PendingLocalRejectedAndNoRemote) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + + ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer())); + + std::string first_mid = *caller_first_transceiver->mid(); + caller_first_transceiver->StopInternal(); + + // The reoffer will have a rejected m= section. + ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer())); + + auto caller_second_transceiver = caller->AddTransceiver(second_type_); + + // The reoffer should not recycle the existing m= section since it is not + // rejected in either the *current* local or *current* remote description. + auto reoffer = caller->CreateOffer(); + auto reoffer_contents = reoffer->description()->contents(); + ASSERT_EQ(2u, reoffer_contents.size()); + EXPECT_TRUE(reoffer_contents[0].rejected); + EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type()); + EXPECT_EQ(first_mid, reoffer_contents[0].name); + EXPECT_FALSE(reoffer_contents[1].rejected); + EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type()); + std::string second_mid = reoffer_contents[1].name; + EXPECT_NE(first_mid, second_mid); + + ASSERT_TRUE(caller->SetLocalDescription(std::move(reoffer))); + + // Both RtpTransceivers are associated. + EXPECT_EQ(first_mid, caller_first_transceiver->mid()); + EXPECT_EQ(second_mid, caller_second_transceiver->mid()); +} + +// Test that a m= section is *not* recycled if the media section is only +// rejected in the pending local description and not rejected in the current +// remote description. +TEST_P(RecycleMediaSectionTest, PendingLocalRejectedAndNotRejectedRemote) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + std::string first_mid = *caller_first_transceiver->mid(); + caller_first_transceiver->StopInternal(); + + // The reoffer will have a rejected m= section. + ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer())); + + auto caller_second_transceiver = caller->AddTransceiver(second_type_); + + // The reoffer should not recycle the existing m= section since it is not + // rejected in either the *current* local or *current* remote description. + auto reoffer = caller->CreateOffer(); + auto reoffer_contents = reoffer->description()->contents(); + ASSERT_EQ(2u, reoffer_contents.size()); + EXPECT_TRUE(reoffer_contents[0].rejected); + EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type()); + EXPECT_EQ(first_mid, reoffer_contents[0].name); + EXPECT_FALSE(reoffer_contents[1].rejected); + EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type()); + std::string second_mid = reoffer_contents[1].name; + EXPECT_NE(first_mid, second_mid); + + ASSERT_TRUE(caller->SetLocalDescription(std::move(reoffer))); + + // Both RtpTransceivers are associated. + EXPECT_EQ(first_mid, caller_first_transceiver->mid()); + EXPECT_EQ(second_mid, caller_second_transceiver->mid()); +} + +// Test that an m= section is *not* recycled if the media section is only +// rejected in the pending remote description and there is no current local +// description. +TEST_P(RecycleMediaSectionTest, PendingRemoteRejectedAndNoLocal) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + auto callee_first_transceiver = callee->pc()->GetTransceivers()[0]; + std::string first_mid = *callee_first_transceiver->mid(); + caller_first_transceiver->StopInternal(); + + // The reoffer will have a rejected m= section. + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto callee_second_transceiver = callee->AddTransceiver(second_type_); + + // The reoffer should not recycle the existing m= section since it is not + // rejected in either the *current* local or *current* remote description. + auto reoffer = callee->CreateOffer(); + auto reoffer_contents = reoffer->description()->contents(); + ASSERT_EQ(2u, reoffer_contents.size()); + EXPECT_TRUE(reoffer_contents[0].rejected); + EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type()); + EXPECT_EQ(first_mid, reoffer_contents[0].name); + EXPECT_FALSE(reoffer_contents[1].rejected); + EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type()); + std::string second_mid = reoffer_contents[1].name; + EXPECT_NE(first_mid, second_mid); + + // Note: Cannot actually set the reoffer since the callee is in the signaling + // state 'have-remote-offer'. +} + +// Test that an m= section is *not* recycled if the media section is only +// rejected in the pending remote description and not rejected in the current +// local description. +TEST_P(RecycleMediaSectionTest, PendingRemoteRejectedAndNotRejectedLocal) { + auto caller = CreatePeerConnection(); + auto caller_first_transceiver = caller->AddTransceiver(first_type_); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + ASSERT_EQ(1u, callee->pc()->GetTransceivers().size()); + auto callee_first_transceiver = callee->pc()->GetTransceivers()[0]; + std::string first_mid = *callee_first_transceiver->mid(); + caller_first_transceiver->StopInternal(); + + // The reoffer will have a rejected m= section. + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto callee_second_transceiver = callee->AddTransceiver(second_type_); + + // The reoffer should not recycle the existing m= section since it is not + // rejected in either the *current* local or *current* remote description. + auto reoffer = callee->CreateOffer(); + auto reoffer_contents = reoffer->description()->contents(); + ASSERT_EQ(2u, reoffer_contents.size()); + EXPECT_TRUE(reoffer_contents[0].rejected); + EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type()); + EXPECT_EQ(first_mid, reoffer_contents[0].name); + EXPECT_FALSE(reoffer_contents[1].rejected); + EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type()); + std::string second_mid = reoffer_contents[1].name; + EXPECT_NE(first_mid, second_mid); + + // Note: Cannot actually set the reoffer since the callee is in the signaling + // state 'have-remote-offer'. +} + +// 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_SUITE_P( + PeerConnectionJsepTest, + RecycleMediaSectionTest, + Combine(Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO), + Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO))); + +// Test that a new data channel section will not reuse a recycleable audio or +// video media section. Additionally, tests that the new section is added to the +// end of the session description. +TEST_F(PeerConnectionJsepTest, DataChannelDoesNotRecycleMediaSection) { + auto caller = CreatePeerConnection(); + auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + transceiver->StopInternal(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + caller->CreateDataChannel("dc"); + + auto offer = caller->CreateOffer(); + auto offer_contents = offer->description()->contents(); + ASSERT_EQ(2u, offer_contents.size()); + EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, + offer_contents[0].media_description()->type()); + EXPECT_EQ(cricket::MEDIA_TYPE_DATA, + offer_contents[1].media_description()->type()); + + ASSERT_TRUE( + caller->SetLocalDescription(CloneSessionDescription(offer.get()))); + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + + auto answer = callee->CreateAnswer(); + auto answer_contents = answer->description()->contents(); + ASSERT_EQ(2u, answer_contents.size()); + EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, + answer_contents[0].media_description()->type()); + EXPECT_EQ(cricket::MEDIA_TYPE_DATA, + answer_contents[1].media_description()->type()); +} + +// Test that if a new track is added to an existing session that has a data, +// the new section comes at the end of the new offer, after the existing data +// section. +TEST_F(PeerConnectionJsepTest, AudioTrackAddedAfterDataSectionInReoffer) { + auto caller = CreatePeerConnection(); + caller->CreateDataChannel("dc"); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + caller->AddAudioTrack("a"); + + auto offer = caller->CreateOffer(); + auto contents = offer->description()->contents(); + ASSERT_EQ(2u, contents.size()); + EXPECT_EQ(cricket::MEDIA_TYPE_DATA, contents[0].media_description()->type()); + EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, contents[1].media_description()->type()); +} + +// 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 if an audio or video section has the default data section MID, then +// CreateOffer will generate a unique MID for the newly added data section. +TEST_F(PeerConnectionJsepTest, + CreateOfferGeneratesUniqueMidForDataSectionIfAlreadyTaken) { + // First, find what the default MID is for the data channel. + auto pc = CreatePeerConnection(); + pc->CreateDataChannel("dc"); + auto default_offer = pc->CreateOffer(); + std::string default_data_mid = + default_offer->description()->contents()[0].name; + + // Now do an offer/answer with one audio track which has a MID set to the + // default data MID. + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("a"); + auto callee = CreatePeerConnection(); + + auto offer = caller->CreateOffer(); + RenameSection(0, default_data_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 data channel and ensure that the MID is different. + caller->CreateDataChannel("dc"); + + auto reoffer = caller->CreateOffer(); + auto reoffer_contents = reoffer->description()->contents(); + EXPECT_EQ(default_data_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()); +} + +// Tests for MSID properties. + +// Test that adding a track with AddTrack results in an offer that signals the +// track's ID. +TEST_F(PeerConnectionJsepTest, AddingTrackWithAddTrackSpecifiesTrackId) { + const std::string kTrackId = "audio_track"; + + auto caller = CreatePeerConnection(); + caller->AddAudioTrack(kTrackId); + + auto offer = caller->CreateOffer(); + auto contents = offer->description()->contents(); + ASSERT_EQ(1u, contents.size()); + auto streams = contents[0].media_description()->streams(); + ASSERT_EQ(1u, streams.size()); + EXPECT_EQ(kTrackId, streams[0].id); +} + +// Test that adding a track by calling AddTransceiver then SetTrack results in +// an offer that does not signal the track's ID and signals a random ID. +TEST_F(PeerConnectionJsepTest, + AddingTrackWithAddTransceiverSpecifiesRandomTrackId) { + const std::string kTrackId = "audio_track"; + + auto caller = CreatePeerConnection(); + auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + transceiver->sender()->SetTrack(caller->CreateAudioTrack(kTrackId).get()); + + auto offer = caller->CreateOffer(); + auto contents = offer->description()->contents(); + ASSERT_EQ(1u, contents.size()); + auto streams = contents[0].media_description()->streams(); + ASSERT_EQ(1u, streams.size()); + EXPECT_NE(kTrackId, streams[0].id); +} + +// Test that if the transceiver is recvonly or inactive, then no MSID +// information is included in the offer. +TEST_F(PeerConnectionJsepTest, NoMsidInOfferIfTransceiverDirectionHasNoSend) { + auto caller = CreatePeerConnection(); + + RtpTransceiverInit init_recvonly; + init_recvonly.direction = RtpTransceiverDirection::kRecvOnly; + ASSERT_TRUE(caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init_recvonly)); + + RtpTransceiverInit init_inactive; + init_inactive.direction = RtpTransceiverDirection::kInactive; + ASSERT_TRUE(caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init_inactive)); + + auto offer = caller->CreateOffer(); + auto contents = offer->description()->contents(); + ASSERT_EQ(2u, contents.size()); + // MSID is specified in the first stream, so no streams means no MSID. + EXPECT_EQ(0u, contents[0].media_description()->streams().size()); + EXPECT_EQ(0u, contents[1].media_description()->streams().size()); +} + +// Test that if an answer negotiates transceiver directions of recvonly or +// inactive, then no MSID information is included in the answer. +TEST_F(PeerConnectionJsepTest, NoMsidInAnswerIfNoRespondingTracks) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + + // recvonly transceiver will get negotiated to inactive since the callee has + // no tracks to send in response. + RtpTransceiverInit init_recvonly; + init_recvonly.direction = RtpTransceiverDirection::kRecvOnly; + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init_recvonly); + + // sendrecv transceiver will get negotiated to recvonly since the callee has + // no tracks to send in response. + RtpTransceiverInit init_sendrecv; + init_sendrecv.direction = RtpTransceiverDirection::kSendRecv; + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init_sendrecv); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto answer = callee->CreateAnswer(); + auto contents = answer->description()->contents(); + ASSERT_EQ(2u, contents.size()); + // MSID is specified in the first stream, so no streams means no MSID. + EXPECT_EQ(0u, contents[0].media_description()->streams().size()); + EXPECT_EQ(0u, contents[1].media_description()->streams().size()); +} + +// Test that the MSID is included even if the transceiver direction has changed +// to inactive if the transceiver had previously sent media. +TEST_F(PeerConnectionJsepTest, IncludeMsidEvenIfDirectionHasChanged) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + caller->pc()->GetTransceivers()[0]->SetDirectionWithError( + RtpTransceiverDirection::kInactive); + + // The transceiver direction on both sides will turn to inactive. + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + auto* offer = callee->pc()->remote_description(); + auto offer_contents = offer->description()->contents(); + ASSERT_EQ(1u, offer_contents.size()); + // MSID is specified in the first stream. If it is present, assume that MSID + // is there. + EXPECT_EQ(1u, offer_contents[0].media_description()->streams().size()); + + auto* answer = caller->pc()->remote_description(); + auto answer_contents = answer->description()->contents(); + ASSERT_EQ(1u, answer_contents.size()); + EXPECT_EQ(1u, answer_contents[0].media_description()->streams().size()); +} + +// Test that stopping a RtpTransceiver will cause future offers to not include +// any MSID information for that section. +TEST_F(PeerConnectionJsepTest, RemoveMsidIfTransceiverStopped) { + auto caller = CreatePeerConnection(); + auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + transceiver->StopInternal(); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + auto* offer = callee->pc()->remote_description(); + auto offer_contents = offer->description()->contents(); + ASSERT_EQ(1u, offer_contents.size()); + // MSID is specified in the first stream, so no streams means no MSID. + EXPECT_EQ(0u, offer_contents[0].media_description()->streams().size()); +} + +// Test that the callee RtpReceiver created by a call to SetRemoteDescription +// has its ID set to the signaled track ID. +TEST_F(PeerConnectionJsepTest, + RtpReceiverCreatedBySetRemoteDescriptionHasSignaledTrackId) { + const std::string kTrackId = "audio_track"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->AddAudioTrack(kTrackId); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + ASSERT_EQ(1u, callee->pc()->GetReceivers().size()); + auto receiver = callee->pc()->GetReceivers()[0]; + EXPECT_EQ(kTrackId, receiver->id()); +} + +// Test that if the callee RtpReceiver is reused by a call to +// SetRemoteDescription, its ID does not change. +TEST_F(PeerConnectionJsepTest, + RtpReceiverCreatedBeforeSetRemoteDescriptionKeepsId) { + const std::string kTrackId = "audio_track"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->AddAudioTrack(kTrackId); + callee->AddAudioTrack("dummy_track"); + + ASSERT_EQ(1u, callee->pc()->GetReceivers().size()); + auto receiver = callee->pc()->GetReceivers()[0]; + std::string receiver_id = receiver->id(); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + EXPECT_EQ(receiver_id, receiver->id()); +} + +// Test that setting a remote offer with one track that has no streams fires off +// the correct OnAddTrack event. +TEST_F(PeerConnectionJsepTest, + SetRemoteOfferWithOneTrackNoStreamFiresOnAddTrack) { + const std::string kTrackLabel = "audio_track"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + ASSERT_TRUE(caller->AddAudioTrack(kTrackLabel)); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + const auto& track_events = callee->observer()->add_track_events_; + ASSERT_EQ(1u, track_events.size()); + const auto& event = track_events[0]; + EXPECT_EQ(kTrackLabel, event.receiver->track()->id()); + EXPECT_EQ(0u, event.streams.size()); +} + +// Test that setting a remote offer with one track that has one stream fires off +// the correct OnAddTrack event. +TEST_F(PeerConnectionJsepTest, + SetRemoteOfferWithOneTrackOneStreamFiresOnAddTrack) { + const std::string kTrackLabel = "audio_track"; + const std::string kStreamId = "audio_stream"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + ASSERT_TRUE(caller->AddAudioTrack(kTrackLabel, {kStreamId})); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + const auto& track_events = callee->observer()->add_track_events_; + ASSERT_EQ(1u, track_events.size()); + const auto& event = track_events[0]; + ASSERT_EQ(1u, event.streams.size()); + auto stream = event.streams[0]; + EXPECT_EQ(kStreamId, stream->id()); + EXPECT_THAT(track_events[0].snapshotted_stream_tracks.at(stream), + ElementsAre(event.receiver->track())); + EXPECT_EQ(event.receiver->streams(), track_events[0].streams); +} + +// Test that setting a remote offer with two tracks that share the same stream +// fires off two OnAddTrack events, both with the same stream that has both +// tracks present at the time of firing. This is to ensure that track events are +// not fired until SetRemoteDescription has finished processing all the media +// sections. +TEST_F(PeerConnectionJsepTest, + SetRemoteOfferWithTwoTracksSameStreamFiresOnAddTrack) { + const std::string kTrack1Label = "audio_track1"; + const std::string kTrack2Label = "audio_track2"; + const std::string kSharedStreamId = "stream"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + ASSERT_TRUE(caller->AddAudioTrack(kTrack1Label, {kSharedStreamId})); + ASSERT_TRUE(caller->AddAudioTrack(kTrack2Label, {kSharedStreamId})); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + const auto& track_events = callee->observer()->add_track_events_; + ASSERT_EQ(2u, track_events.size()); + const auto& event1 = track_events[0]; + const auto& event2 = track_events[1]; + ASSERT_EQ(1u, event1.streams.size()); + auto stream = event1.streams[0]; + ASSERT_THAT(event2.streams, ElementsAre(stream)); + auto track1 = event1.receiver->track(); + auto track2 = event2.receiver->track(); + EXPECT_THAT(event1.snapshotted_stream_tracks.at(stream), + UnorderedElementsAre(track1, track2)); + EXPECT_THAT(event2.snapshotted_stream_tracks.at(stream), + UnorderedElementsAre(track1, track2)); +} + +// Test that setting a remote offer with one track that has two streams fires +// off the correct OnAddTrack event. +TEST_F(PeerConnectionJsepTest, + SetRemoteOfferWithOneTrackTwoStreamFiresOnAddTrack) { + const std::string kTrackLabel = "audio_track"; + const std::string kStreamId1 = "audio_stream1"; + const std::string kStreamId2 = "audio_stream2"; + + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + ASSERT_TRUE(caller->AddAudioTrack(kTrackLabel, {kStreamId1, kStreamId2})); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + const auto& track_events = callee->observer()->add_track_events_; + ASSERT_EQ(1u, track_events.size()); + const auto& event = track_events[0]; + ASSERT_EQ(2u, event.streams.size()); + EXPECT_EQ(kStreamId1, event.streams[0]->id()); + EXPECT_EQ(kStreamId2, event.streams[1]->id()); +} + +// Test that if an RtpTransceiver with a current_direction set is stopped, then +// current_direction is changed to null. +TEST_F(PeerConnectionJsepTest, CurrentDirectionResetWhenRtpTransceiverStopped) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + + auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + + ASSERT_TRUE(transceiver->current_direction()); + transceiver->StopInternal(); + EXPECT_EQ(transceiver->current_direction(), + RtpTransceiverDirection::kStopped); +} + +// Test that you can't set an answer on a PeerConnection before setting the +// offer. +TEST_F(PeerConnectionJsepTest, AnswerBeforeOfferFails) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + + RTCError error; + ASSERT_FALSE(caller->SetRemoteDescription(callee->CreateAnswer(), &error)); + EXPECT_EQ(RTCErrorType::INVALID_STATE, error.type()); +} + +// Test that a Unified Plan PeerConnection fails to set a Plan B offer if it has +// two video tracks. +TEST_F(PeerConnectionJsepTest, TwoVideoPlanBToUnifiedPlanFails) { + RTCConfiguration config_planb; + config_planb.sdp_semantics = SdpSemantics::kPlanB_DEPRECATED; + auto caller = CreatePeerConnection(config_planb); + auto callee = CreatePeerConnection(); + caller->AddVideoTrack("video1"); + caller->AddVideoTrack("video2"); + + RTCError error; + ASSERT_FALSE(callee->SetRemoteDescription(caller->CreateOffer(), &error)); + EXPECT_EQ(RTCErrorType::INVALID_PARAMETER, error.type()); +} + +// Test that a Unified Plan PeerConnection fails to set a Plan B answer if it +// has two video tracks. +TEST_F(PeerConnectionJsepTest, OneVideoUnifiedPlanToTwoVideoPlanBFails) { + auto caller = CreatePeerConnection(); + RTCConfiguration config_planb; + config_planb.sdp_semantics = SdpSemantics::kPlanB_DEPRECATED; + auto callee = CreatePeerConnection(config_planb); + caller->AddVideoTrack("video"); + callee->AddVideoTrack("video1"); + callee->AddVideoTrack("video2"); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + RTCError error; + ASSERT_FALSE(caller->SetRemoteDescription(caller->CreateAnswer(), &error)); + EXPECT_EQ(RTCErrorType::INVALID_PARAMETER, error.type()); +} + +// Removes the RTP header extension associated with the given URI from the media +// description. +static void RemoveRtpHeaderExtensionByUri( + MediaContentDescription* media_description, + absl::string_view uri) { + std::vector<RtpExtension> header_extensions = + media_description->rtp_header_extensions(); + header_extensions.erase(std::remove_if( + header_extensions.begin(), header_extensions.end(), + [uri](const RtpExtension& extension) { return extension.uri == uri; })); + media_description->set_rtp_header_extensions(header_extensions); +} + +// Transforms a session description to emulate a legacy endpoint which does not +// support a=mid, BUNDLE, and the MID header extension. +static void ClearMids(SessionDescriptionInterface* sdesc) { + cricket::SessionDescription* desc = sdesc->description(); + desc->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + cricket::ContentInfo* audio_content = cricket::GetFirstAudioContent(desc); + if (audio_content) { + desc->GetTransportInfoByName(audio_content->name)->content_name = ""; + audio_content->name = ""; + RemoveRtpHeaderExtensionByUri(audio_content->media_description(), + RtpExtension::kMidUri); + } + cricket::ContentInfo* video_content = cricket::GetFirstVideoContent(desc); + if (video_content) { + desc->GetTransportInfoByName(video_content->name)->content_name = ""; + video_content->name = ""; + RemoveRtpHeaderExtensionByUri(video_content->media_description(), + RtpExtension::kMidUri); + } +} + +// Test that negotiation works with legacy endpoints which do not support a=mid. +TEST_F(PeerConnectionJsepTest, LegacyNoMidAudioOnlyOffer) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + + auto offer = caller->CreateOffer(); + ClearMids(offer.get()); + + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + EXPECT_TRUE(callee->SetLocalDescription(callee->CreateAnswer())); +} +TEST_F(PeerConnectionJsepTest, LegacyNoMidAudioVideoOffer) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + caller->AddVideoTrack("video"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + callee->AddVideoTrack("video"); + + auto offer = caller->CreateOffer(); + ClearMids(offer.get()); + + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + EXPECT_TRUE(callee->SetLocalDescription(callee->CreateAnswer())); +} +TEST_F(PeerConnectionJsepTest, LegacyNoMidAudioOnlyAnswer) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto answer = callee->CreateAnswer(); + ClearMids(answer.get()); + + EXPECT_TRUE(caller->SetRemoteDescription(std::move(answer))); +} +TEST_F(PeerConnectionJsepTest, LegacyNoMidAudioVideoAnswer) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + caller->AddVideoTrack("video"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + callee->AddVideoTrack("video"); + + ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + auto answer = callee->CreateAnswer(); + ClearMids(answer.get()); + + ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer))); +} + +// Test that negotiation works with legacy endpoints which do not support a=mid +// when setting two remote descriptions without setting a local description in +// between. +TEST_F(PeerConnectionJsepTest, LegacyNoMidTwoRemoteOffers) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("audio"); + + auto offer = caller->CreateOffer(); + ClearMids(offer.get()); + + ASSERT_TRUE( + callee->SetRemoteDescription(CloneSessionDescription(offer.get()))); + ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer))); + EXPECT_TRUE(callee->SetLocalDescription(callee->CreateAnswer())); +} + +// Test that SetLocalDescription fails if a=mid lines are missing. +TEST_F(PeerConnectionJsepTest, SetLocalDescriptionFailsMissingMid) { + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("audio"); + + auto offer = caller->CreateOffer(); + ClearMids(offer.get()); + + std::string error; + ASSERT_FALSE(caller->SetLocalDescription(std::move(offer), &error)); + EXPECT_EQ( + "Failed to set local offer sdp: A media section is missing a MID " + "attribute.", + error); +} + +TEST_F(PeerConnectionJsepTest, RollbackSupportedInUnifiedPlan) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->SetRemoteDescription(caller->CreateRollback())); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->SetRemoteDescription(callee->CreateOffer())); +} + +TEST_F(PeerConnectionJsepTest, RollbackNotSupportedInPlanB) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kPlanB_DEPRECATED; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_FALSE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_FALSE(caller->SetRemoteDescription(caller->CreateRollback())); + EXPECT_FALSE(caller->SetRemoteDescription(callee->CreateOffer())); +} + +TEST_F(PeerConnectionJsepTest, RollbackFailsInStableState) { + auto caller = CreatePeerConnection(); + EXPECT_FALSE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_FALSE(caller->SetRemoteDescription(caller->CreateRollback())); +} + +TEST_F(PeerConnectionJsepTest, RollbackToStableStateAndClearLocalOffer) { + auto caller = CreatePeerConnection(); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_EQ(caller->signaling_state(), PeerConnectionInterface::kStable); + EXPECT_EQ(caller->pc()->pending_local_description(), nullptr); + + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(caller->signaling_state(), PeerConnectionInterface::kStable); + EXPECT_EQ(caller->pc()->pending_local_description(), nullptr); +} + +TEST_F(PeerConnectionJsepTest, RollbackToStableStateAndClearRemoteOffer) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->signaling_state(), PeerConnectionInterface::kStable); + EXPECT_EQ(callee->pc()->pending_remote_description(), nullptr); + + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetLocalDescription(caller->CreateRollback())); + EXPECT_EQ(callee->signaling_state(), PeerConnectionInterface::kStable); + EXPECT_EQ(callee->pc()->pending_remote_description(), nullptr); +} + +TEST_F(PeerConnectionJsepTest, RollbackImplicitly) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->signaling_state(), + PeerConnectionInterface::kHaveRemoteOffer); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); + EXPECT_FALSE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_FALSE(callee->observer()->has_negotiation_needed_event()); +} + +TEST_F(PeerConnectionJsepTest, RollbackImplicitlyNegotatiationNotNeeded) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + caller->AddAudioTrack("a"); + callee->AddAudioTrack("b"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + callee->observer()->clear_legacy_renegotiation_needed(); + callee->observer()->clear_latest_negotiation_needed_event(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->signaling_state(), + PeerConnectionInterface::kHaveRemoteOffer); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); + // No negotiation needed as track got attached in the answer. + EXPECT_FALSE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_FALSE(callee->observer()->has_negotiation_needed_event()); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 0u); +} + +TEST_F(PeerConnectionJsepTest, RollbackImplicitlyAndNegotiationNeeded) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + callee->AddAudioTrack("a"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + callee->observer()->clear_legacy_renegotiation_needed(); + callee->observer()->clear_latest_negotiation_needed_event(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->signaling_state(), + PeerConnectionInterface::kHaveRemoteOffer); + EXPECT_FALSE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_FALSE(callee->observer()->has_negotiation_needed_event()); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); + EXPECT_TRUE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_TRUE(callee->observer()->has_negotiation_needed_event()); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 0u); +} + +TEST_F(PeerConnectionJsepTest, AttemptToRollbackImplicitly) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + auto callee = CreatePeerConnection(config); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_FALSE(callee->SetRemoteDescription( + CreateSessionDescription(SdpType::kOffer, "invalid sdp"))); + EXPECT_EQ(callee->signaling_state(), + PeerConnectionInterface::kHaveLocalOffer); +} + +TEST_F(PeerConnectionJsepTest, RollbackRemovesTransceiver) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + ASSERT_EQ(callee->pc()->GetTransceivers().size(), 1u); + auto transceiver = callee->pc()->GetTransceivers()[0]; + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 0u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); + // The removed transceiver should be stopped and its receiver track should be + // ended. + EXPECT_TRUE(transceiver->stopping()); + EXPECT_TRUE(transceiver->stopping()); + EXPECT_EQ(transceiver->receiver()->track()->state(), + MediaStreamTrackInterface::kEnded); +} + +TEST_F(PeerConnectionJsepTest, RollbackKeepsTransceiverAndClearsMid) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + callee->AddAudioTrack("a"); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + // Transceiver can't be removed as track was added to it. + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + // Mid got cleared to make it reusable. + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + // Transceiver should be counted as addTrack-created after rollback. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); + // Because the transceiver is reusable, it must not be stopped and its + // receiver track must still be live. + auto transceiver = callee->pc()->GetTransceivers()[0]; + EXPECT_FALSE(transceiver->stopping()); + EXPECT_FALSE(transceiver->stopping()); + EXPECT_EQ(transceiver->receiver()->track()->state(), + MediaStreamTrackInterface::kLive); +} + +TEST_F(PeerConnectionJsepTest, + RollbackKeepsTransceiverAfterAddTrackEvenWhenTrackIsNulled) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + callee->AddAudioTrack("a"); + callee->pc()->GetTransceivers()[0]->sender()->SetTrack(nullptr); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->track(), nullptr); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + // Transceiver can't be removed as track was added to it. + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + // Mid got cleared to make it reusable. + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + // Transceiver should be counted as addTrack-created after rollback. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); +} + +TEST_F(PeerConnectionJsepTest, RollbackRestoresMid) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("a"); + auto offer = callee->CreateOffer(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_NE(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + EXPECT_TRUE(callee->SetLocalDescription(std::move(offer))); +} + +TEST_F(PeerConnectionJsepTest, RollbackRestoresInitSendEncodings) { + auto caller = CreatePeerConnection(); + RtpTransceiverInit init; + init.direction = RtpTransceiverDirection::kSendRecv; + RtpEncodingParameters encoding; + encoding.rid = "hi"; + init.send_encodings.push_back(encoding); + encoding.rid = "mid"; + init.send_encodings.push_back(encoding); + encoding.rid = "lo"; + init.send_encodings.push_back(encoding); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init); + auto encodings = + caller->pc()->GetTransceivers()[0]->sender()->init_send_encodings(); + EXPECT_TRUE(caller->SetLocalDescription(caller->CreateOffer())); + EXPECT_NE(caller->pc()->GetTransceivers()[0]->sender()->init_send_encodings(), + encodings); + EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_EQ(caller->pc()->GetTransceivers()[0]->sender()->init_send_encodings(), + encodings); +} + +TEST_F(PeerConnectionJsepTest, RollbackDoesNotAffectSendEncodings) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + RtpTransceiverInit init; + init.direction = RtpTransceiverDirection::kSendOnly; + RtpEncodingParameters encoding; + encoding.rid = "hi"; + init.send_encodings.push_back(encoding); + encoding.rid = "mid"; + init.send_encodings.push_back(encoding); + encoding.rid = "lo"; + init.send_encodings.push_back(encoding); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()); + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()); + auto params = caller->pc()->GetTransceivers()[0]->sender()->GetParameters(); + EXPECT_TRUE(params.encodings[0].active); + params.encodings[0].active = false; + caller->pc()->GetTransceivers()[0]->sender()->SetParameters(params); + auto offer = caller->CreateOffer(); + std::string offer_string; + EXPECT_TRUE(offer.get()->ToString(&offer_string)); + std::string simulcast_line = + offer_string.substr(offer_string.find("a=simulcast")); + EXPECT_FALSE(simulcast_line.empty()); + EXPECT_TRUE(caller->SetLocalDescription(std::move(offer))); + EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback())); + EXPECT_FALSE(caller->pc() + ->GetTransceivers()[0] + ->sender() + ->GetParameters() + .encodings[0] + .active); + offer = caller->CreateOffer(); + EXPECT_TRUE(offer.get()->ToString(&offer_string)); + EXPECT_EQ(offer_string.substr(offer_string.find("a=simulcast")), + simulcast_line); +} + +TEST_F(PeerConnectionJsepTest, RollbackRestoresMidAndRemovesTransceiver) { + auto callee = CreatePeerConnection(); + callee->AddVideoTrack("a"); + auto offer = callee->CreateOffer(); + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("b"); + caller->AddVideoTrack("c"); + auto mid = callee->pc()->GetTransceivers()[0]->mid(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 2u); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), mid); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->media_type(), + cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->SetLocalDescription(std::move(offer))); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), + callee->observer()->add_track_events_.size()); +} + +TEST_F(PeerConnectionJsepTest, RollbackHasNoEffectOnStableTransceivers) { + auto callee = CreatePeerConnection(); + callee->AddVideoTrack("a"); + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("b"); + caller->AddVideoTrack("c"); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + // In stable don't add or remove anything. + callee->observer()->clear_legacy_renegotiation_needed(); + callee->observer()->clear_latest_negotiation_needed_event(); + size_t transceiver_count = callee->pc()->GetTransceivers().size(); + auto mid_0 = callee->pc()->GetTransceivers()[0]->mid(); + auto mid_1 = callee->pc()->GetTransceivers()[1]->mid(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), transceiver_count); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), mid_0); + EXPECT_EQ(callee->pc()->GetTransceivers()[1]->mid(), mid_1); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 0u); + EXPECT_FALSE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_FALSE(callee->observer()->has_negotiation_needed_event()); +} + +TEST_F(PeerConnectionJsepTest, ImplicitlyRollbackTransceiversWithSameMids) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + auto callee = CreatePeerConnection(config); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + auto initial_mid = callee->pc()->GetTransceivers()[0]->mid(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 2u); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + EXPECT_EQ(callee->pc()->GetTransceivers()[1]->mid(), + caller->pc()->GetTransceivers()[0]->mid()); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); // Go to stable. + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_NE(callee->pc()->GetTransceivers()[0]->mid(), initial_mid); +} + +TEST_F(PeerConnectionJsepTest, RollbackToNegotiatedStableState) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxBundle; + auto caller = CreatePeerConnection(config); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(config); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); + caller->AddVideoTrack("a"); + callee->AddVideoTrack("b"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 2u); + auto audio_transport = + callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + callee->pc()->GetTransceivers()[1]->sender()->dtls_transport()); + EXPECT_NE(callee->pc()->GetTransceivers()[1]->sender()->dtls_transport(), + nullptr); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + audio_transport); // Audio must remain working after rollback. + EXPECT_EQ(callee->pc()->GetTransceivers()[1]->sender()->dtls_transport(), + nullptr); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + audio_transport); // Audio transport is still the same. +} + +TEST_F(PeerConnectionJsepTest, RollbackHasToDestroyTransport) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxBundle; + auto pc = CreatePeerConnection(config); + pc->AddAudioTrack("a"); + pc->AddVideoTrack("b"); + EXPECT_TRUE(pc->CreateOfferAndSetAsLocal()); + auto offer = pc->CreateOffer(); + EXPECT_EQ(pc->pc()->GetTransceivers().size(), 2u); + auto audio_transport = + pc->pc()->GetTransceivers()[0]->sender()->dtls_transport(); + EXPECT_EQ(pc->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + pc->pc()->GetTransceivers()[1]->sender()->dtls_transport()); + EXPECT_NE(pc->pc()->GetTransceivers()[1]->sender()->dtls_transport(), + nullptr); + EXPECT_TRUE(pc->SetRemoteDescription(pc->CreateRollback())); + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer))); + EXPECT_NE(pc->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + nullptr); + EXPECT_NE(pc->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + audio_transport); +} + +TEST_F(PeerConnectionJsepTest, RollbackLocalDirectionChange) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + callee->AddAudioTrack("a"); + callee->pc()->GetTransceivers()[0]->SetDirectionWithError( + RtpTransceiverDirection::kSendOnly); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + auto audio_transport = + callee->pc()->GetTransceivers()[0]->receiver()->dtls_transport(); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->direction(), + RtpTransceiverDirection::kSendOnly); + // One way audio must remain working after rollback as local direction change + // comes in effect after completing full negotiation round. + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->receiver()->dtls_transport(), + audio_transport); +} + +TEST_F(PeerConnectionJsepTest, RollbackRemoteDirectionChange) { + auto caller = CreatePeerConnection(); + auto caller_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("a"); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + // In stable make remote audio receive only. + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + // The direction attribute is not modified by the offer. + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->direction(), + RtpTransceiverDirection::kSendRecv); + auto audio_transport = + callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 1u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->direction(), + RtpTransceiverDirection::kSendRecv); + // One way audio must remain working after rollback. + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(), + audio_transport); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); +} + +TEST_F(PeerConnectionJsepTest, + RollbackRestoresFiredDirectionAndOnTrackCanFireAgain) { + auto caller = CreatePeerConnection(); + auto caller_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + callee->AddAudioTrack("a"); + ASSERT_EQ(callee->pc()->GetTransceivers().size(), 1u); + auto callee_transceiver = callee->pc()->GetTransceivers()[0]; + EXPECT_FALSE(callee_transceiver->fired_direction().has_value()); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE(callee_transceiver->fired_direction().has_value()); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 1u); + // The existing transceiver becomes associated. Because it already exists, + // rolling it back does not remove the transceiver, so if ontrack fires again + // later it will be because the transceiver's internal states were restored + // rather than due to the creation of a new transceiver. + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + + // Rollback: the transceiver is no longer receiving. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_FALSE(callee_transceiver->fired_direction().has_value()); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); + + // Set the remote offer again: ontrack should fire on the same transceiver. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE(callee_transceiver->fired_direction().has_value()); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 2u); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 1u); +} + +TEST_F(PeerConnectionJsepTest, + RollbackFromInactiveToReceivingMakesOnTrackFire) { + auto caller = CreatePeerConnection(); + auto caller_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + // Perform full O/A so that transceiver is associated. Ontrack fires. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 1u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 0u); + ASSERT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + + // Start negotiating to make the transceiver inactive. Onremovetrack fires. + caller_transceiver->SetDirectionWithError(RtpTransceiverDirection::kInactive); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 1u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); + + // Rollback the inactivation. Ontrack should fire again. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback())); + EXPECT_EQ(callee->observer()->add_track_events_.size(), 2u); + EXPECT_EQ(callee->observer()->remove_track_events_.size(), 1u); +} + +TEST_F(PeerConnectionJsepTest, RollbackAfterMultipleSLD) { + auto callee = CreatePeerConnection(); + callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + callee->observer()->clear_legacy_renegotiation_needed(); + callee->observer()->clear_latest_negotiation_needed_event(); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + EXPECT_TRUE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_TRUE(callee->observer()->has_negotiation_needed_event()); + EXPECT_EQ(callee->pc()->GetTransceivers().size(), 2u); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt); + EXPECT_EQ(callee->pc()->GetTransceivers()[1]->mid(), absl::nullopt); +} + +TEST_F(PeerConnectionJsepTest, NoRollbackNeeded) { + auto caller = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + auto callee = CreatePeerConnection(); + callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(caller->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); +} + +TEST_F(PeerConnectionJsepTest, RollbackMultipleStreamChanges) { + auto callee = CreatePeerConnection(); + auto caller = CreatePeerConnection(); + caller->AddAudioTrack("a_1", {"id_1"}); + caller->AddVideoTrack("v_0", {"id_0"}); // Provide an extra stream id. + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_TRUE( + caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); + caller->pc()->GetTransceivers()[0]->sender()->SetStreams({"id_2"}); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + caller->pc()->GetTransceivers()[0]->sender()->SetStreams({"id_3"}); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal())); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->receiver()->stream_ids()[0], + "id_3"); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->receiver()->stream_ids().size(), + 1u); + EXPECT_EQ(callee->pc()->GetTransceivers()[0]->receiver()->stream_ids()[0], + "id_1"); +} + +TEST_F(PeerConnectionJsepTest, DataChannelImplicitRollback) { + RTCConfiguration config; + config.sdp_semantics = SdpSemantics::kUnifiedPlan; + config.enable_implicit_rollback = true; + auto caller = CreatePeerConnection(config); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + auto callee = CreatePeerConnection(config); + callee->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal()); + EXPECT_TRUE(callee->observer()->legacy_renegotiation_needed()); + EXPECT_TRUE(callee->observer()->has_negotiation_needed_event()); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +TEST_F(PeerConnectionJsepTest, RollbackRemoteDataChannelThenAddTransceiver) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +TEST_F(PeerConnectionJsepTest, + RollbackRemoteDataChannelThenAddTransceiverAndDataChannel) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + callee->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +TEST_F(PeerConnectionJsepTest, RollbackRemoteDataChannelThenAddDataChannel) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + callee->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +TEST_F(PeerConnectionJsepTest, RollbackRemoteTransceiverThenAddDataChannel) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + callee->CreateDataChannel("dummy"); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +TEST_F(PeerConnectionJsepTest, + RollbackRemoteTransceiverThenAddDataChannelAndTransceiver) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer())); + EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback())); + callee->CreateDataChannel("dummy"); + callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + EXPECT_TRUE(callee->CreateOfferAndSetAsLocal()); +} + +} // namespace webrtc |