/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include "nss.h" #include "ssl.h" #include "mozilla/Preferences.h" #include "mozilla/RefPtr.h" #define GTEST_HAS_RTTI 0 #include "gtest/gtest.h" #include "PeerConnectionImpl.h" #include "sdp/SdpMediaSection.h" #include "sdp/SipccSdpParser.h" #include "jsep/JsepCodecDescription.h" #include "jsep/JsepTrack.h" #include "jsep/JsepSession.h" #include "jsep/JsepSessionImpl.h" namespace mozilla { static std::string kAEqualsCandidate("a=candidate:"); const static size_t kNumCandidatesPerComponent = 3; class JsepSessionTestBase : public ::testing::Test { public: static void SetUpTestCase() { NSS_NoDB_Init(nullptr); NSS_SetDomesticPolicy(); } }; class FakeUuidGenerator : public mozilla::JsepUuidGenerator { public: bool Generate(std::string* str) { std::ostringstream os; os << "FAKE_UUID_" << ++ctr; *str = os.str(); return true; } mozilla::JsepUuidGenerator* Clone() const { return new FakeUuidGenerator(*this); } private: static uint64_t ctr; }; uint64_t FakeUuidGenerator::ctr = 1000; class JsepSessionTest : public JsepSessionTestBase, public ::testing::WithParamInterface { public: JsepSessionTest() : mSdpHelper(&mLastError) { Preferences::SetCString("media.peerconnection.sdp.parser", "legacy"); Preferences::SetCString("media.peerconnection.sdp.alternate_parse_mode", "never"); Preferences::SetBool("media.peerconnection.video.use_rtx", true); Preferences::SetBool("media.navigator.video.use_transport_cc", true); mSessionOff = MakeUnique("Offerer", MakeUnique()); mSessionAns = MakeUnique("Answerer", MakeUnique()); EXPECT_EQ(NS_OK, mSessionOff->Init()); EXPECT_EQ(NS_OK, mSessionAns->Init()); std::vector> preferredCodecs; PeerConnectionImpl::SetupPreferredCodecs(preferredCodecs); mSessionOff->SetDefaultCodecs(preferredCodecs); mSessionAns->SetDefaultCodecs(preferredCodecs); std::vector preferredHeaders; PeerConnectionImpl::SetupPreferredRtpExtensions(preferredHeaders); for (const auto& header : preferredHeaders) { mSessionOff->AddRtpExtension(header.mMediaType, header.extensionname, header.direction); mSessionAns->AddRtpExtension(header.mMediaType, header.extensionname, header.direction); } mOffererTransport = MakeUnique(); mAnswererTransport = MakeUnique(); AddTransportData(*mSessionOff, *mOffererTransport); AddTransportData(*mSessionAns, *mAnswererTransport); mOffCandidates = MakeUnique(); mAnsCandidates = MakeUnique(); } static std::vector& GetTransceivers(JsepSession& aSession) { return aSession.GetTransceivers(); } static const std::vector& GetTransceivers( const JsepSession& aSession) { return aSession.GetTransceivers(); } protected: struct TransportData { std::map> mFingerprints; }; void AddDtlsFingerprint(const std::string& alg, JsepSessionImpl& session, TransportData& tdata) { std::vector fp; fp.assign((alg == "sha-1") ? 20 : 32, (session.GetName() == "Offerer") ? 0x4f : 0x41); session.AddDtlsFingerprint(alg, fp); tdata.mFingerprints[alg] = fp; } void AddTransportData(JsepSessionImpl& session, TransportData& tdata) { AddDtlsFingerprint("sha-1", session, tdata); AddDtlsFingerprint("sha-256", session, tdata); } void CheckTransceiverInvariants( const std::vector& oldTransceivers, const std::vector& newTransceivers) { ASSERT_LE(oldTransceivers.size(), newTransceivers.size()); std::set levels; for (const auto& newTransceiver : newTransceivers) { if (newTransceiver.HasLevel()) { ASSERT_FALSE(levels.count(newTransceiver.GetLevel())) << "Two new transceivers are mapped to level " << newTransceiver.GetLevel(); levels.insert(newTransceiver.GetLevel()); } } auto last = levels.rbegin(); if (last != levels.rend()) { ASSERT_LE(*last, levels.size()) << "Max level observed in transceivers was " << *last << ", but there are only " << levels.size() << " levels in the " "transceivers."; } for (const auto& oldTransceiver : oldTransceivers) { if (oldTransceiver.HasLevel()) { ASSERT_TRUE(levels.count(oldTransceiver.GetLevel())) << "Level " << oldTransceiver.GetLevel() << " had a transceiver in the old, but not the new (or, " "perhaps this level had more than one transceiver in the " "old)"; levels.erase(oldTransceiver.GetLevel()); } } } std::string CreateOffer(const Maybe& options = Nothing()) { std::vector transceiversBefore = GetTransceivers(*mSessionOff); JsepOfferOptions defaultOptions; const JsepOfferOptions& optionsRef = options ? *options : defaultOptions; std::string offer; JsepSession::Result result = mSessionOff->CreateOffer(optionsRef, &offer); EXPECT_FALSE(result.mError.isSome()) << mSessionOff->GetLastError(); std::cerr << "OFFER: " << offer << std::endl; ValidateTransport(*mOffererTransport, offer, sdp::kOffer); if (transceiversBefore.size() != GetTransceivers(*mSessionOff).size()) { EXPECT_TRUE(false) << "CreateOffer changed number of transceivers!"; return offer; } CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionOff)); for (size_t i = 0; i < transceiversBefore.size(); ++i) { JsepTransceiver oldTransceiver = transceiversBefore[i]; JsepTransceiver newTransceiver = GetTransceivers(*mSessionOff)[i]; EXPECT_EQ(oldTransceiver.IsStopped(), newTransceiver.IsStopped()); if (oldTransceiver.IsStopped()) { if (!newTransceiver.HasLevel()) { // Tolerate unmapping of stopped transceivers by removing this // difference. oldTransceiver.ClearLevel(); } } else if (!oldTransceiver.HasLevel()) { EXPECT_TRUE(newTransceiver.HasLevel()); // Tolerate new mappings. oldTransceiver.SetLevel(newTransceiver.GetLevel()); } EXPECT_TRUE(Equals(oldTransceiver, newTransceiver)); } return offer; } typedef enum { NO_ADDTRACK_MAGIC, ADDTRACK_MAGIC } AddTrackMagic; void AddTracks(JsepSessionImpl& side, AddTrackMagic magic = ADDTRACK_MAGIC) { // Add tracks. if (types.empty()) { types = BuildTypes(GetParam()); } AddTracks(side, types, magic); // Now, we move datachannel to the end auto it = std::find(types.begin(), types.end(), SdpMediaSection::kApplication); if (it != types.end()) { types.erase(it); types.push_back(SdpMediaSection::kApplication); } } void AddTracks(JsepSessionImpl& side, const std::string& mediatypes, AddTrackMagic magic = ADDTRACK_MAGIC) { AddTracks(side, BuildTypes(mediatypes), magic); } JsepTrack RemoveTrack(JsepSession& side, size_t index) { if (GetTransceivers(side).size() <= index) { EXPECT_TRUE(false) << "Index " << index << " out of bounds!"; return JsepTrack(SdpMediaSection::kAudio, sdp::kSend); } JsepTransceiver transceiver(GetTransceivers(side)[index]); JsepTrack& track = transceiver.mSendTrack; EXPECT_FALSE(track.GetStreamIds().empty()) << "No track at index " << index; JsepTrack original(track); track.ClearStreamIds(); transceiver.mJsDirection &= SdpDirectionAttribute::Direction::kRecvonly; side.SetTransceiver(transceiver); return original; } void SetDirection(JsepSession& side, size_t index, SdpDirectionAttribute::Direction direction) { ASSERT_LT(index, GetTransceivers(side).size()) << "Index " << index << " out of bounds!"; auto transceiver = GetTransceivers(side)[index]; transceiver.mJsDirection = direction; side.SetTransceiver(transceiver); } std::vector BuildTypes( const std::string& mediatypes) { std::vector result; size_t ptr = 0; for (;;) { size_t comma = mediatypes.find(',', ptr); std::string chunk = mediatypes.substr(ptr, comma - ptr); if (chunk == "audio") { result.push_back(SdpMediaSection::kAudio); } else if (chunk == "video") { result.push_back(SdpMediaSection::kVideo); } else if (chunk == "datachannel") { result.push_back(SdpMediaSection::kApplication); } else { MOZ_CRASH(); } if (comma == std::string::npos) break; ptr = comma + 1; } return result; } void AddTracks(JsepSessionImpl& side, const std::vector& mediatypes, AddTrackMagic magic = ADDTRACK_MAGIC) { std::string stream_id; std::string track_id; ASSERT_TRUE(mUuidGen.Generate(&stream_id)); AddTracksToStream(side, stream_id, mediatypes, magic); } void AddTracksToStream(JsepSessionImpl& side, const std::string stream_id, const std::string& mediatypes, AddTrackMagic magic = ADDTRACK_MAGIC) { AddTracksToStream(side, stream_id, BuildTypes(mediatypes), magic); } // A bit of a hack. JsepSessionImpl populates the track-id automatically, just // in case, because the w3c spec requires msid to be set even when there's no // send track. bool IsNull(const JsepTrack& track) const { return track.GetStreamIds().empty() && (track.GetMediaType() != SdpMediaSection::MediaType::kApplication); } void AddTracksToStream( JsepSessionImpl& side, const std::string stream_id, const std::vector& mediatypes, AddTrackMagic magic = ADDTRACK_MAGIC) { std::string track_id; for (auto type : mediatypes) { ASSERT_TRUE(mUuidGen.Generate(&track_id)); Maybe suitableTransceiver; size_t i; if (magic == ADDTRACK_MAGIC) { // We're simulating addTrack. for (i = 0; i < GetTransceivers(side).size(); ++i) { auto transceiver = GetTransceivers(side)[i]; if (transceiver.mSendTrack.GetMediaType() != type) { continue; } if (IsNull(transceiver.mSendTrack) || transceiver.GetMediaType() == SdpMediaSection::kApplication) { suitableTransceiver = Some(transceiver); break; } } } if (!suitableTransceiver) { i = GetTransceivers(side).size(); side.AddTransceiver(JsepTransceiver(type, mUuidGen)); suitableTransceiver = Some(GetTransceivers(side).back()); if (magic == ADDTRACK_MAGIC) { suitableTransceiver->SetAddTrackMagic(); } } std::cerr << "Updating send track for transceiver " << i << std::endl; suitableTransceiver->SetOnlyExistsBecauseOfSetRemote(false); suitableTransceiver->mJsDirection |= SdpDirectionAttribute::Direction::kSendonly; suitableTransceiver->mSendTrack.UpdateStreamIds( std::vector(1, stream_id)); side.SetTransceiver(*suitableTransceiver); } } bool HasMediaStream(const std::vector& tracks) const { for (const auto& track : tracks) { if (track.GetMediaType() != SdpMediaSection::kApplication) { return true; } } return false; } const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const { auto tracks = GetLocalTracks(side); return tracks.begin()->GetStreamIds()[0]; } std::vector GetLocalTracks(const JsepSession& session) const { std::vector result; for (const auto& transceiver : GetTransceivers(session)) { if (!IsNull(transceiver.mSendTrack)) { result.push_back(transceiver.mSendTrack); } } return result; } std::vector GetRemoteTracks(const JsepSession& session) const { std::vector result; for (const auto& transceiver : GetTransceivers(session)) { if (!IsNull(transceiver.mRecvTrack)) { result.push_back(transceiver.mRecvTrack); } } return result; } JsepTransceiver* GetDatachannelTransceiver(JsepSession& side) { for (auto& transceiver : GetTransceivers(side)) { if (transceiver.mSendTrack.GetMediaType() == SdpMediaSection::MediaType::kApplication) { return &transceiver; } } return nullptr; } JsepTransceiver* GetNegotiatedTransceiver(JsepSession& side, size_t index) { for (auto& transceiver : GetTransceivers(side)) { if (transceiver.mSendTrack.GetNegotiatedDetails() || transceiver.mRecvTrack.GetNegotiatedDetails()) { if (index) { --index; continue; } return &transceiver; } } return nullptr; } Maybe GetTransceiverByLevel( const std::vector& transceivers, size_t level) { for (auto& transceiver : transceivers) { if (transceiver.HasLevel() && transceiver.GetLevel() == level) { return Some(transceiver); } } return Nothing(); } Maybe GetTransceiverByLevel(JsepSession& side, size_t level) { return GetTransceiverByLevel(GetTransceivers(side), level); } std::vector GetMediaStreamIds( const std::vector& tracks) const { std::vector ids; for (const auto& track : tracks) { // data channels don't have msid's if (track.GetMediaType() == SdpMediaSection::kApplication) { continue; } ids.insert(ids.end(), track.GetStreamIds().begin(), track.GetStreamIds().end()); } return ids; } std::vector GetLocalMediaStreamIds(JsepSessionImpl& side) const { return GetMediaStreamIds(GetLocalTracks(side)); } std::vector GetRemoteMediaStreamIds( JsepSessionImpl& side) const { return GetMediaStreamIds(GetRemoteTracks(side)); } std::vector sortUniqueStrVector( std::vector in) const { std::sort(in.begin(), in.end()); auto it = std::unique(in.begin(), in.end()); in.resize(std::distance(in.begin(), it)); return in; } std::vector GetLocalUniqueStreamIds( JsepSessionImpl& side) const { return sortUniqueStrVector(GetLocalMediaStreamIds(side)); } std::vector GetRemoteUniqueStreamIds( JsepSessionImpl& side) const { return sortUniqueStrVector(GetRemoteMediaStreamIds(side)); } JsepTrack GetTrack(JsepSessionImpl& side, SdpMediaSection::MediaType type, size_t index) const { for (const auto& transceiver : GetTransceivers(side)) { if (IsNull(transceiver.mSendTrack) || transceiver.mSendTrack.GetMediaType() != type) { continue; } if (index != 0) { --index; continue; } return transceiver.mSendTrack; } return JsepTrack(type, sdp::kSend); } JsepTrack GetTrackOff(size_t index, SdpMediaSection::MediaType type) { return GetTrack(*mSessionOff, type, index); } JsepTrack GetTrackAns(size_t index, SdpMediaSection::MediaType type) { return GetTrack(*mSessionAns, type, index); } bool Equals(const SdpFingerprintAttributeList::Fingerprint& f1, const SdpFingerprintAttributeList::Fingerprint& f2) const { if (f1.hashFunc != f2.hashFunc) { return false; } if (f1.fingerprint != f2.fingerprint) { return false; } return true; } bool Equals(const SdpFingerprintAttributeList& f1, const SdpFingerprintAttributeList& f2) const { if (f1.mFingerprints.size() != f2.mFingerprints.size()) { return false; } for (size_t i = 0; i < f1.mFingerprints.size(); ++i) { if (!Equals(f1.mFingerprints[i], f2.mFingerprints[i])) { return false; } } return true; } bool Equals(const UniquePtr& t1, const UniquePtr& t2) const { if (!t1 && !t2) { return true; } if (!t1 || !t2) { return false; } if (!Equals(t1->GetFingerprints(), t2->GetFingerprints())) { return false; } if (t1->GetRole() != t2->GetRole()) { return false; } return true; } bool Equals(const UniquePtr& t1, const UniquePtr& t2) const { if (!t1 && !t2) { return true; } if (!t1 || !t2) { return false; } if (t1->GetUfrag() != t2->GetUfrag()) { return false; } if (t1->GetPassword() != t2->GetPassword()) { return false; } return true; } bool Equals(const JsepTransport& t1, const JsepTransport& t2) const { if (t1.mTransportId != t2.mTransportId) { std::cerr << "Transport id differs: " << t1.mTransportId << " vs " << t2.mTransportId << std::endl; return false; } if (t1.mComponents != t2.mComponents) { std::cerr << "Component count differs" << std::endl; return false; } if (!Equals(t1.mIce, t2.mIce)) { std::cerr << "ICE differs" << std::endl; return false; } return true; } bool Equals(const JsepTrack& t1, const JsepTrack& t2) const { if (t1.GetMediaType() != t2.GetMediaType()) { return false; } if (t1.GetDirection() != t2.GetDirection()) { return false; } if (t1.GetStreamIds() != t2.GetStreamIds()) { return false; } if (t1.GetActive() != t2.GetActive()) { return false; } if (t1.GetCNAME() != t2.GetCNAME()) { return false; } if (t1.GetSsrcs() != t2.GetSsrcs()) { return false; } return true; } bool Equals(const JsepTransceiver& p1, const JsepTransceiver& p2) const { if (p1.HasLevel() != p2.HasLevel()) { std::cerr << "One transceiver has a level, the other doesn't" << std::endl; return false; } if (p1.HasLevel() && (p1.GetLevel() != p2.GetLevel())) { std::cerr << "Level differs: " << p1.GetLevel() << " vs " << p2.GetLevel() << std::endl; return false; } // We don't check things like BundleLevel(), since that can change without // any changes to the transport, which is what we're really interested in. if (p1.IsStopped() != p2.IsStopped()) { std::cerr << "One transceiver is stopped, the other is not" << std::endl; return false; } if (p1.IsAssociated() != p2.IsAssociated()) { std::cerr << "One transceiver has a mid, the other doesn't" << std::endl; return false; } if (p1.IsAssociated() && (p1.GetMid() != p2.GetMid())) { std::cerr << "mid differs: " << p1.GetMid() << " vs " << p2.GetMid() << std::endl; return false; } if (!Equals(p1.mSendTrack, p2.mSendTrack)) { std::cerr << "Send track differs" << std::endl; return false; } if (!Equals(p1.mRecvTrack, p2.mRecvTrack)) { std::cerr << "Receive track differs" << std::endl; return false; } if (!Equals(p1.mTransport, p2.mTransport)) { std::cerr << "Transport differs" << std::endl; return false; } return true; } bool Equals(const std::vector& t1, const std::vector& t2) const { if (t1.size() != t2.size()) { std::cerr << "Size differs: t1.size = " << t1.size() << ", t2.size = " << t2.size() << std::endl; return false; } for (size_t i = 0; i < t1.size(); ++i) { if (!Equals(t1[i], t2[i])) { return false; } } return true; } size_t GetTrackCount(JsepSessionImpl& side, SdpMediaSection::MediaType type) const { size_t result = 0; for (const auto& track : GetLocalTracks(side)) { if (track.GetMediaType() == type) { ++result; } } return result; } UniquePtr GetParsedLocalDescription(const JsepSessionImpl& side) const { return Parse(side.GetLocalDescription(kJsepDescriptionCurrent)); } SdpMediaSection* GetMsection(Sdp& sdp, SdpMediaSection::MediaType type, size_t index) const { for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) { auto& msection = sdp.GetMediaSection(i); if (msection.GetMediaType() != type) { continue; } if (index) { --index; continue; } return &msection; } return nullptr; } void SetPayloadTypeNumber(JsepSession& session, const std::string& codecName, const std::string& payloadType) { for (auto& codec : session.Codecs()) { if (codec->mName == codecName) { codec->mDefaultPt = payloadType; } } } void SetCodecEnabled(JsepSession& session, const std::string& codecName, bool enabled) { for (auto& codec : session.Codecs()) { if (codec->mName == codecName) { codec->mEnabled = enabled; } } } void EnsureNegotiationFailure(SdpMediaSection::MediaType type, const std::string& codecName) { for (auto& codec : mSessionOff->Codecs()) { if (codec->Type() == type && codec->mName != codecName) { codec->mEnabled = false; } } for (auto& codec : mSessionAns->Codecs()) { if (codec->Type() == type && codec->mName == codecName) { codec->mEnabled = false; } } } std::string CreateAnswer() { std::vector transceiversBefore = GetTransceivers(*mSessionAns); JsepAnswerOptions options; std::string answer; JsepSession::Result result = mSessionAns->CreateAnswer(options, &answer); EXPECT_FALSE(result.mError.isSome()); std::cerr << "ANSWER: " << answer << std::endl; ValidateTransport(*mAnswererTransport, answer, sdp::kAnswer); CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionAns)); return answer; } static const uint32_t NO_CHECKS = 0; static const uint32_t CHECK_SUCCESS = 1; static const uint32_t CHECK_TRACKS = 1 << 2; static const uint32_t ALL_CHECKS = CHECK_SUCCESS | CHECK_TRACKS; void OfferAnswer(uint32_t checkFlags = ALL_CHECKS, const Maybe& options = Nothing()) { std::string offer = CreateOffer(options); SetLocalOffer(offer, checkFlags); SetRemoteOffer(offer, checkFlags); std::string answer = CreateAnswer(); SetLocalAnswer(answer, checkFlags); SetRemoteAnswer(answer, checkFlags); } void SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS) { std::vector transceiversBefore = GetTransceivers(*mSessionOff); JsepSession::Result result = mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionOff)); if (checkFlags & CHECK_SUCCESS) { ASSERT_FALSE(result.mError.isSome()); } if (checkFlags & CHECK_TRACKS) { // This assumes no recvonly or inactive transceivers. ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); for (const auto& transceiver : GetTransceivers(*mSessionOff)) { if (!transceiver.HasLevel()) { continue; } const auto& track(transceiver.mSendTrack); size_t level = transceiver.GetLevel(); ASSERT_FALSE(IsNull(track)); ASSERT_EQ(types[level], track.GetMediaType()); if (track.GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += track.GetStreamIds()[0]; ASSERT_NE(std::string::npos, offer.find(msidAttr)) << "Did not find " << msidAttr << " in offer"; } } if (types.size() == 1 && types[0] == SdpMediaSection::kApplication) { ASSERT_EQ(std::string::npos, offer.find("a=ssrc")) << "Data channel should not contain SSRC"; } } } void SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS) { std::vector transceiversBefore = GetTransceivers(*mSessionAns); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionAns)); if (checkFlags & CHECK_SUCCESS) { ASSERT_FALSE(result.mError.isSome()); } if (checkFlags & CHECK_TRACKS) { // This assumes no recvonly or inactive transceivers. ASSERT_EQ(types.size(), GetTransceivers(*mSessionAns).size()); for (const auto& transceiver : GetTransceivers(*mSessionAns)) { if (!transceiver.HasLevel()) { continue; } const auto& track(transceiver.mRecvTrack); size_t level = transceiver.GetLevel(); ASSERT_FALSE(IsNull(track)); ASSERT_EQ(types[level], track.GetMediaType()); if (track.GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += track.GetStreamIds()[0]; // Track id will not match, and will eventually not be present at all ASSERT_NE(std::string::npos, offer.find(msidAttr)) << "Did not find " << msidAttr << " in offer"; } } } } void SetLocalAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS) { std::vector transceiversBefore = GetTransceivers(*mSessionAns); JsepSession::Result result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); if (checkFlags & CHECK_SUCCESS) { ASSERT_FALSE(result.mError.isSome()); } CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionAns)); if (checkFlags & CHECK_TRACKS) { // Verify that the right stuff is in the tracks. ASSERT_EQ(types.size(), GetTransceivers(*mSessionAns).size()); for (const auto& transceiver : GetTransceivers(*mSessionAns)) { if (!transceiver.HasLevel()) { continue; } const auto& sendTrack(transceiver.mSendTrack); const auto& recvTrack(transceiver.mRecvTrack); size_t level = transceiver.GetLevel(); ASSERT_FALSE(IsNull(sendTrack)); ASSERT_EQ(types[level], sendTrack.GetMediaType()); // These might have been in the SDP, or might have been randomly // chosen by JsepSessionImpl ASSERT_FALSE(IsNull(recvTrack)); ASSERT_EQ(types[level], recvTrack.GetMediaType()); if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += sendTrack.GetStreamIds()[0]; ASSERT_NE(std::string::npos, answer.find(msidAttr)) << "Did not find " << msidAttr << " in answer"; } } if (types.size() == 1 && types[0] == SdpMediaSection::kApplication) { ASSERT_EQ(std::string::npos, answer.find("a=ssrc")) << "Data channel should not contain SSRC"; } } std::cerr << "Answerer transceivers:" << std::endl; DumpTransceivers(*mSessionAns); } void SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS) { std::vector transceiversBefore = GetTransceivers(*mSessionOff); JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); if (checkFlags & CHECK_SUCCESS) { ASSERT_FALSE(result.mError.isSome()); } CheckTransceiverInvariants(transceiversBefore, GetTransceivers(*mSessionOff)); if (checkFlags & CHECK_TRACKS) { // Verify that the right stuff is in the tracks. ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); for (const auto& transceiver : GetTransceivers(*mSessionOff)) { if (!transceiver.HasLevel()) { continue; } const auto& sendTrack(transceiver.mSendTrack); const auto& recvTrack(transceiver.mRecvTrack); size_t level = transceiver.GetLevel(); ASSERT_FALSE(IsNull(sendTrack)); ASSERT_EQ(types[level], sendTrack.GetMediaType()); // These might have been in the SDP, or might have been randomly // chosen by JsepSessionImpl ASSERT_FALSE(IsNull(recvTrack)); ASSERT_EQ(types[level], recvTrack.GetMediaType()); if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += recvTrack.GetStreamIds()[0]; // Track id will not match, and will eventually not be present at all ASSERT_NE(std::string::npos, answer.find(msidAttr)) << "Did not find " << msidAttr << " in answer"; } } } std::cerr << "Offerer transceivers:" << std::endl; DumpTransceivers(*mSessionOff); } std::string GetTransportId(const JsepSession& session, size_t level) { for (const auto& transceiver : GetTransceivers(session)) { if (transceiver.HasLevel() && transceiver.GetLevel() == level) { return transceiver.mTransport.mTransportId; } } return std::string(); } typedef enum { RTP = 1, RTCP = 2 } ComponentType; class CandidateSet { public: CandidateSet() {} void Gather(JsepSession& session, ComponentType maxComponent = RTCP) { for (const auto& transceiver : GetTransceivers(session)) { if (transceiver.HasOwnTransport()) { Gather(session, transceiver.mTransport.mTransportId, RTP); if (transceiver.mTransport.mComponents > 1) { Gather(session, transceiver.mTransport.mTransportId, RTCP); } } } FinishGathering(session); } void Gather(JsepSession& session, const std::string& transportId, ComponentType component) { static uint16_t port = 1000; std::vector candidates; for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { ++port; std::ostringstream candidate; candidate << "0 " << static_cast(component) << " UDP 9999 192.168.0.1 " << port << " typ host"; std::string mid; uint16_t level = 0; bool skipped; session.AddLocalIceCandidate(kAEqualsCandidate + candidate.str(), transportId, "", &level, &mid, &skipped); if (!skipped) { mCandidatesToTrickle.push_back(std::tuple( level, mid, kAEqualsCandidate + candidate.str())); candidates.push_back(candidate.str()); } } // Stomp existing candidates mCandidates[transportId][component] = candidates; // Stomp existing defaults mDefaultCandidates[transportId][component] = std::make_pair("192.168.0.1", port); session.UpdateDefaultCandidate( mDefaultCandidates[transportId][RTP].first, mDefaultCandidates[transportId][RTP].second, // Will be empty string if not present, which is how we indicate // that there is no default for RTCP mDefaultCandidates[transportId][RTCP].first, mDefaultCandidates[transportId][RTCP].second, transportId); } void FinishGathering(JsepSession& session) const { // Copy so we can be terse and use [] for (auto idAndCandidates : mDefaultCandidates) { ASSERT_EQ(1U, idAndCandidates.second.count(RTP)); // do a final UpdateDefaultCandidate here in case candidates were // cleared during renegotiation. session.UpdateDefaultCandidate( idAndCandidates.second[RTP].first, idAndCandidates.second[RTP].second, // Will be empty string if not present, which is how we indicate // that there is no default for RTCP idAndCandidates.second[RTCP].first, idAndCandidates.second[RTCP].second, idAndCandidates.first); std::string mid; uint16_t level = 0; bool skipped; session.AddLocalIceCandidate("", idAndCandidates.first, "", &level, &mid, &skipped); } } void Trickle(JsepSession& session) { std::string transportId; for (const auto& levelMidAndCandidate : mCandidatesToTrickle) { auto [level, mid, candidate] = levelMidAndCandidate; std::cerr << "trickling candidate: " << candidate << " level: " << level << " mid: " << mid << std::endl; Maybe lev = Some(level); session.AddRemoteIceCandidate(candidate, mid, lev, "", &transportId); } session.AddRemoteIceCandidate("", "", Maybe(), "", &transportId); mCandidatesToTrickle.clear(); } void CheckRtpCandidates(bool expectRtpCandidates, const SdpMediaSection& msection, const std::string& transportId, const std::string& context) const { auto& attrs = msection.GetAttributeList(); ASSERT_EQ(expectRtpCandidates, attrs.HasAttribute(SdpAttribute::kCandidateAttribute)) << context << " (level " << msection.GetLevel() << ")"; if (expectRtpCandidates) { // Copy so we can be terse and use [] auto expectedCandidates = mCandidates; ASSERT_LE(kNumCandidatesPerComponent, expectedCandidates[transportId][RTP].size()); auto& candidates = attrs.GetCandidate(); ASSERT_LE(kNumCandidatesPerComponent, candidates.size()) << context << " (level " << msection.GetLevel() << ")"; for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { ASSERT_EQ(expectedCandidates[transportId][RTP][i], candidates[i]) << context << " (level " << msection.GetLevel() << ")"; } } } void CheckRtcpCandidates(bool expectRtcpCandidates, const SdpMediaSection& msection, const std::string& transportId, const std::string& context) const { auto& attrs = msection.GetAttributeList(); if (expectRtcpCandidates) { // Copy so we can be terse and use [] auto expectedCandidates = mCandidates; ASSERT_LE(kNumCandidatesPerComponent, expectedCandidates[transportId][RTCP].size()); ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kCandidateAttribute)) << context << " (level " << msection.GetLevel() << ")"; auto& candidates = attrs.GetCandidate(); ASSERT_EQ(kNumCandidatesPerComponent * 2, candidates.size()) << context << " (level " << msection.GetLevel() << ")"; for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { ASSERT_EQ(expectedCandidates[transportId][RTCP][i], candidates[i + kNumCandidatesPerComponent]) << context << " (level " << msection.GetLevel() << ")"; } } } void CheckDefaultRtpCandidate(bool expectDefault, const SdpMediaSection& msection, const std::string& transportId, const std::string& context) const { Address expectedAddress = "0.0.0.0"; Port expectedPort = 9U; if (expectDefault) { // Copy so we can be terse and use [] auto defaultCandidates = mDefaultCandidates; expectedAddress = defaultCandidates[transportId][RTP].first; expectedPort = defaultCandidates[transportId][RTP].second; } // if bundle-only attribute is present, expect port 0 const SdpAttributeList& attrs = msection.GetAttributeList(); if (attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute)) { expectedPort = 0U; } ASSERT_EQ(expectedAddress, msection.GetConnection().GetAddress()) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(expectedPort, msection.GetPort()) << context << " (level " << msection.GetLevel() << ")"; } void CheckDefaultRtcpCandidate(bool expectDefault, const SdpMediaSection& msection, const std::string& transportId, const std::string& context) const { if (expectDefault) { // Copy so we can be terse and use [] auto defaultCandidates = mDefaultCandidates; ASSERT_TRUE(msection.GetAttributeList().HasAttribute( SdpAttribute::kRtcpAttribute)) << context << " (level " << msection.GetLevel() << ")"; auto& rtcpAttr = msection.GetAttributeList().GetRtcp(); ASSERT_EQ(defaultCandidates[transportId][RTCP].second, rtcpAttr.mPort) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(sdp::kInternet, rtcpAttr.mNetType) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(sdp::kIPv4, rtcpAttr.mAddrType) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(defaultCandidates[transportId][RTCP].first, rtcpAttr.mAddress) << context << " (level " << msection.GetLevel() << ")"; } else { ASSERT_FALSE(msection.GetAttributeList().HasAttribute( SdpAttribute::kRtcpAttribute)) << context << " (level " << msection.GetLevel() << ")"; } } private: typedef size_t Level; typedef std::string TransportId; typedef std::string Mid; typedef std::string Candidate; typedef std::string Address; typedef uint16_t Port; // Default candidates are put into the m-line, c-line, and rtcp // attribute for endpoints that don't support ICE. std::map>> mDefaultCandidates; std::map>> mCandidates; // Level/mid/candidate tuples that need to be trickled std::vector> mCandidatesToTrickle; }; // For streaming parse errors std::string GetParseErrors( const UniquePtr& results) const { std::stringstream output; auto errors = std::move(results->Errors()); for (auto error : errors) { output << error.first << ": " << error.second << std::endl; } return output.str(); } void CheckEndOfCandidates(bool expectEoc, const SdpMediaSection& msection, const std::string& context) { if (expectEoc) { ASSERT_TRUE(msection.GetAttributeList().HasAttribute( SdpAttribute::kEndOfCandidatesAttribute)) << context << " (level " << msection.GetLevel() << ")"; } else { ASSERT_FALSE(msection.GetAttributeList().HasAttribute( SdpAttribute::kEndOfCandidatesAttribute)) << context << " (level " << msection.GetLevel() << ")"; } } void CheckTransceiversAreBundled(const JsepSession& session, const std::string& context) { for (const auto& transceiver : GetTransceivers(session)) { ASSERT_TRUE(transceiver.HasBundleLevel()) << context; ASSERT_EQ(0U, transceiver.BundleLevel()) << context; ASSERT_NE("", transceiver.mTransport.mTransportId); } } void DisableMsid(std::string* sdp) const { while (true) { size_t pos = sdp->find("a=msid"); if (pos == std::string::npos) { break; } (*sdp)[pos + 2] = 'X'; // garble, a=Xsid } } void DisableBundle(std::string* sdp) const { size_t pos = sdp->find("a=group:BUNDLE"); ASSERT_NE(std::string::npos, pos); (*sdp)[pos + 11] = 'G'; // garble, a=group:BUNGLE } void DisableMsection(std::string* sdp, size_t level) const { UniquePtr parsed(Parse(*sdp)); ASSERT_TRUE(parsed.get()); ASSERT_LT(level, parsed->GetMediaSectionCount()); SdpHelper::DisableMsection(parsed.get(), &parsed->GetMediaSection(level)); (*sdp) = parsed->ToString(); } void CopyTransportAttributes(std::string* sdp, size_t src_level, size_t dst_level) { UniquePtr parsed(Parse(*sdp)); ASSERT_TRUE(parsed.get()); ASSERT_LT(src_level, parsed->GetMediaSectionCount()); ASSERT_LT(dst_level, parsed->GetMediaSectionCount()); nsresult rv = mSdpHelper.CopyTransportParams(2, parsed->GetMediaSection(src_level), &parsed->GetMediaSection(dst_level)); ASSERT_EQ(NS_OK, rv); (*sdp) = parsed->ToString(); } void ReplaceInSdp(std::string* sdp, const char* searchStr, const char* replaceStr) const { if (searchStr[0] == '\0') return; size_t pos = 0; while ((pos = sdp->find(searchStr, pos)) != std::string::npos) { sdp->replace(pos, strlen(searchStr), replaceStr); pos += strlen(replaceStr); } } void ValidateDisabledMSection(const SdpMediaSection* msection) { ASSERT_EQ(1U, msection->GetFormats().size()); auto& attrs = msection->GetAttributeList(); ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kMidAttribute)); ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kDirectionAttribute)); ASSERT_FALSE(attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute)); ASSERT_EQ(SdpDirectionAttribute::kInactive, msection->GetDirectionAttribute().mValue); ASSERT_EQ(3U, attrs.Count()); if (msection->GetMediaType() == SdpMediaSection::kAudio) { ASSERT_EQ("0", msection->GetFormats()[0]); const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("0")); ASSERT_TRUE(rtpmap); ASSERT_EQ("0", rtpmap->pt); ASSERT_EQ("PCMU", rtpmap->name); } else if (msection->GetMediaType() == SdpMediaSection::kVideo) { ASSERT_EQ("120", msection->GetFormats()[0]); const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("120")); ASSERT_TRUE(rtpmap); ASSERT_EQ("120", rtpmap->pt); ASSERT_EQ("VP8", rtpmap->name); } else if (msection->GetMediaType() == SdpMediaSection::kApplication) { if (msection->GetProtocol() == SdpMediaSection::kUdpDtlsSctp || msection->GetProtocol() == SdpMediaSection::kTcpDtlsSctp) { // draft 21 format ASSERT_EQ("webrtc-datachannel", msection->GetFormats()[0]); ASSERT_FALSE(msection->GetSctpmap()); ASSERT_EQ(0U, msection->GetSctpPort()); } else { // old draft 05 format ASSERT_EQ("0", msection->GetFormats()[0]); const SdpSctpmapAttributeList::Sctpmap* sctpmap(msection->GetSctpmap()); ASSERT_TRUE(sctpmap); ASSERT_EQ("0", sctpmap->pt); ASSERT_EQ("rejected", sctpmap->name); ASSERT_EQ(0U, sctpmap->streams); } } else { // Not that we would have any test which tests this... ASSERT_EQ("19", msection->GetFormats()[0]); const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("19")); ASSERT_TRUE(rtpmap); ASSERT_EQ("19", rtpmap->pt); ASSERT_EQ("reserved", rtpmap->name); } ASSERT_FALSE(msection->GetAttributeList().HasAttribute( SdpAttribute::kMsidAttribute)); } void ValidateSetupAttribute(const JsepSessionImpl& side, const SdpSetupAttribute::Role expectedRole) { auto sdp = GetParsedLocalDescription(side); for (size_t i = 0; sdp && i < sdp->GetMediaSectionCount(); ++i) { if (sdp->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kSetupAttribute)) { auto role = sdp->GetMediaSection(i).GetAttributeList().GetSetup().mRole; ASSERT_EQ(expectedRole, role); } } } void DumpTrack(const JsepTrack& track) { const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); std::cerr << " type=" << track.GetMediaType() << std::endl; if (!details) { std::cerr << " not negotiated" << std::endl; return; } std::cerr << " encodings=" << std::endl; for (size_t i = 0; i < details->GetEncodingCount(); ++i) { const JsepTrackEncoding& encoding = details->GetEncoding(i); std::cerr << " id=" << encoding.mRid << std::endl; for (const auto& codec : encoding.GetCodecs()) { std::cerr << " " << codec->mName << " enabled(" << (codec->mEnabled ? "yes" : "no") << ")"; if (track.GetMediaType() == SdpMediaSection::kAudio) { const JsepAudioCodecDescription* audioCodec = static_cast(codec.get()); std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled ? "yes" : "no") << ")"; } if (track.GetMediaType() == SdpMediaSection::kVideo) { const JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); std::cerr << " rtx(" << (videoCodec->mRtxEnabled ? videoCodec->mRtxPayloadType : "no") << ")"; } std::cerr << std::endl; } } } void DumpTransport(const JsepTransport& transport) { std::cerr << " id=" << transport.mTransportId << std::endl; std::cerr << " components=" << transport.mComponents << std::endl; } void DumpTransceivers(const JsepSessionImpl& session) { for (const auto& transceiver : GetTransceivers(session)) { std::cerr << "Transceiver "; if (transceiver.HasLevel()) { std::cerr << transceiver.GetLevel() << std::endl; } else { std::cerr << "" << std::endl; } if (transceiver.HasBundleLevel()) { std::cerr << "(bundle level is " << transceiver.BundleLevel() << ")" << std::endl; } if (!IsNull(transceiver.mSendTrack)) { std::cerr << "Sending-->" << std::endl; DumpTrack(transceiver.mSendTrack); } if (!IsNull(transceiver.mRecvTrack)) { std::cerr << "Receiving-->" << std::endl; DumpTrack(transceiver.mRecvTrack); } std::cerr << "Transport-->" << std::endl; DumpTransport(transceiver.mTransport); } } UniquePtr Parse(const std::string& sdp) const { SipccSdpParser parser; auto results = parser.Parse(sdp); UniquePtr parsed = std::move(results->Sdp()); EXPECT_TRUE(parsed.get()) << "Should have valid SDP" << std::endl << "Errors were: " << GetParseErrors(results); return parsed; } std::string SetExtmap(const std::string& aSdp, const std::string& aUri, uint16_t aId, uint16_t* aOldId = nullptr) { UniquePtr munge(Parse(aSdp)); for (size_t i = 0; i < munge->GetMediaSectionCount(); ++i) { auto& attrs = munge->GetMediaSection(i).GetAttributeList(); if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) { auto extmap = attrs.GetExtmap(); for (auto it = extmap.mExtmaps.begin(); it != extmap.mExtmaps.end(); ++it) { if (it->extensionname == aUri) { if (aOldId) { *aOldId = it->entry; } if (aId) { it->entry = aId; } else { extmap.mExtmaps.erase(it); } break; } } attrs.SetAttribute(extmap.Clone()); } } return munge->ToString(); } uint16_t GetExtmap(const std::string& aSdp, const std::string& aUri) { UniquePtr parsed(Parse(aSdp)); for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) { auto& attrs = parsed->GetMediaSection(i).GetAttributeList(); if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) { auto extmap = attrs.GetExtmap(); for (auto& ext : extmap.mExtmaps) { if (ext.extensionname == aUri) { return ext.entry; } } } } return 0; } void SwapOfferAnswerRoles() { mSessionOff.swap(mSessionAns); mOffCandidates.swap(mAnsCandidates); mOffererTransport.swap(mAnswererTransport); } UniquePtr mSessionOff; UniquePtr mOffCandidates; UniquePtr mSessionAns; UniquePtr mAnsCandidates; std::vector types; std::vector> mGatheredCandidates; FakeUuidGenerator mUuidGen; private: void ValidateTransport(TransportData& source, const std::string& sdp_str, sdp::SdpType type) { UniquePtr sdp(Parse(sdp_str)); ASSERT_TRUE(!!sdp); size_t num_m_sections = sdp->GetMediaSectionCount(); for (size_t i = 0; i < num_m_sections; ++i) { auto& msection = sdp->GetMediaSection(i); if (msection.GetMediaType() == SdpMediaSection::kApplication) { if (!(msection.GetProtocol() == SdpMediaSection::kUdpDtlsSctp || msection.GetProtocol() == SdpMediaSection::kTcpDtlsSctp)) { // old draft 05 format ASSERT_EQ(SdpMediaSection::kDtlsSctp, msection.GetProtocol()); } } else { ASSERT_EQ(SdpMediaSection::kUdpTlsRtpSavpf, msection.GetProtocol()); } const SdpAttributeList& attrs = msection.GetAttributeList(); bool bundle_only = attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute); // port 0 only means disabled when the bundle-only attribute is missing if (!bundle_only && msection.GetPort() == 0) { ValidateDisabledMSection(&msection); continue; } if (mSdpHelper.OwnsTransport(*sdp, i, type)) { const SdpAttributeList& attrs = msection.GetAttributeList(); ASSERT_FALSE(attrs.GetIceUfrag().empty()); ASSERT_FALSE(attrs.GetIcePwd().empty()); const SdpFingerprintAttributeList& fps = attrs.GetFingerprint(); for (auto fp = fps.mFingerprints.begin(); fp != fps.mFingerprints.end(); ++fp) { std::string alg_str = "None"; if (fp->hashFunc == SdpFingerprintAttributeList::kSha1) { alg_str = "sha-1"; } else if (fp->hashFunc == SdpFingerprintAttributeList::kSha256) { alg_str = "sha-256"; } ASSERT_EQ(source.mFingerprints[alg_str], fp->fingerprint); } ASSERT_EQ(source.mFingerprints.size(), fps.mFingerprints.size()); } } } std::string mLastError; SdpHelper mSdpHelper; UniquePtr mOffererTransport; UniquePtr mAnswererTransport; }; TEST_F(JsepSessionTestBase, CreateDestroy) {} TEST_P(JsepSessionTest, CreateOffer) { AddTracks(*mSessionOff); CreateOffer(); } TEST_P(JsepSessionTest, CreateOfferSetLocal) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); } TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemote) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); } TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswer) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); } TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswerSetLocal) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); } TEST_P(JsepSessionTest, FullCall) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); } TEST_P(JsepSessionTest, GetDescriptions) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string desc = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); ASSERT_EQ(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPending); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); SetRemoteOffer(offer); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionCurrent); ASSERT_EQ(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPending); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); desc = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetLocalDescription(kJsepDescriptionPending); ASSERT_EQ(0U, desc.size()); desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPending); ASSERT_EQ(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_EQ(0U, desc.size()); SetRemoteAnswer(answer); desc = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPending); ASSERT_EQ(0U, desc.size()); desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPending); ASSERT_EQ(0U, desc.size()); desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); ASSERT_NE(0U, desc.size()); } TEST_P(JsepSessionTest, RenegotiationNoChange) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer); SetRemoteOffer(reoffer); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer); SetRemoteAnswer(reanswer); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } // Disabled: See Bug 1329028 TEST_P(JsepSessionTest, DISABLED_RenegotiationSwappedRolesNoChange) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); auto offererTransceivers = GetTransceivers(*mSessionOff); auto answererTransceivers = GetTransceivers(*mSessionAns); SwapOfferAnswerRoles(); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer); SetRemoteOffer(reoffer); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer); SetRemoteAnswer(reanswer); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_TRUE(Equals(offererTransceivers, newAnswererTransceivers)); ASSERT_TRUE(Equals(answererTransceivers, newOffererTransceivers)); } static void RemoveLastN(std::vector& aTransceivers, size_t aNum) { while (aNum--) { // erase doesn't take reverse_iterator :( aTransceivers.erase(--aTransceivers.end()); } } TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); std::vector extraTypes; extraTypes.push_back(SdpMediaSection::kAudio); extraTypes.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, extraTypes); types.insert(types.end(), extraTypes.begin(), extraTypes.end()); OfferAnswer(CHECK_SUCCESS); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_LE(2U, newOffererTransceivers.size()); RemoveLastN(newOffererTransceivers, 2); ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_LE(2U, newAnswererTransceivers.size()); RemoveLastN(newAnswererTransceivers, 2); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); std::vector extraTypes; extraTypes.push_back(SdpMediaSection::kAudio); extraTypes.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionAns, extraTypes); types.insert(types.end(), extraTypes.begin(), extraTypes.end()); // We need to add a recvonly m-section to the offer for this to work mSessionOff->AddTransceiver( JsepTransceiver(SdpMediaSection::kAudio, mUuidGen, SdpDirectionAttribute::Direction::kRecvonly)); mSessionOff->AddTransceiver( JsepTransceiver(SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::Direction::kRecvonly)); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_LE(2U, newOffererTransceivers.size()); RemoveLastN(newOffererTransceivers, 2); ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_LE(2U, newAnswererTransceivers.size()); RemoveLastN(newAnswererTransceivers, 2); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationBothAddTrack) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); std::vector extraTypes; extraTypes.push_back(SdpMediaSection::kAudio); extraTypes.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionAns, extraTypes); AddTracks(*mSessionOff, extraTypes); types.insert(types.end(), extraTypes.begin(), extraTypes.end()); OfferAnswer(CHECK_SUCCESS); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_LE(2U, newOffererTransceivers.size()); RemoveLastN(newOffererTransceivers, 2); ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_LE(2U, newAnswererTransceivers.size()); RemoveLastN(newAnswererTransceivers, 2); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (GetParam() == "datachannel") { return; } OfferAnswer(); auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty()); ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty()); auto firstOffId = GetFirstLocalStreamId(*mSessionOff); auto firstAnsId = GetFirstLocalStreamId(*mSessionAns); auto offererTransceivers = GetTransceivers(*mSessionOff); auto answererTransceivers = GetTransceivers(*mSessionAns); std::vector extraTypes; extraTypes.push_back(SdpMediaSection::kAudio); extraTypes.push_back(SdpMediaSection::kVideo); AddTracksToStream(*mSessionOff, firstOffId, extraTypes); AddTracksToStream(*mSessionAns, firstAnsId, extraTypes); types.insert(types.end(), extraTypes.begin(), extraTypes.end()); OfferAnswer(CHECK_SUCCESS); oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty()); ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty()); if (oHasStream) { ASSERT_STREQ(firstOffId.c_str(), GetFirstLocalStreamId(*mSessionOff).c_str()); } if (aHasStream) { ASSERT_STREQ(firstAnsId.c_str(), GetFirstLocalStreamId(*mSessionAns).c_str()); auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); } } // The JSEP draft explicitly forbids changing the msid on an m-section, but // that is a bug. TEST_P(JsepSessionTest, RenegotiationOffererChangesMsid) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); std::string offer = CreateOffer(); SetLocalOffer(offer); JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); ASSERT_TRUE(transceiver); if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { return; } std::string streamId = transceiver->mSendTrack.GetStreamIds()[0]; std::string msidToReplace("a=msid:"); msidToReplace += streamId; size_t msidOffset = offer.find(msidToReplace); ASSERT_NE(std::string::npos, msidOffset); offer.replace(msidOffset, msidToReplace.size(), "a=msid:foo"); SetRemoteOffer(offer); transceiver = GetNegotiatedTransceiver(*mSessionAns, 0); ASSERT_EQ("foo", transceiver->mRecvTrack.GetStreamIds()[0]); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); } // The JSEP draft explicitly forbids changing the msid on an m-section, but // that is a bug. TEST_P(JsepSessionTest, RenegotiationAnswererChangesMsid) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); ASSERT_TRUE(transceiver); if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { return; } std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); transceiver = GetNegotiatedTransceiver(*mSessionAns, 0); ASSERT_TRUE(transceiver); if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { return; } std::string streamId = transceiver->mSendTrack.GetStreamIds()[0]; std::string msidToReplace("a=msid:"); msidToReplace += streamId; size_t msidOffset = answer.find(msidToReplace); ASSERT_NE(std::string::npos, msidOffset); answer.replace(msidOffset, msidToReplace.size(), "a=msid:foo"); SetRemoteAnswer(answer); transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); ASSERT_EQ("foo", transceiver->mRecvTrack.GetStreamIds()[0]); } TEST_P(JsepSessionTest, RenegotiationOffererStopsTransceiver) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.back() == SdpMediaSection::kApplication) { return; } OfferAnswer(); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); JsepTransceiver lastTransceiver = GetTransceivers(*mSessionOff).back(); // Avoid bundle transport side effects; don't stop the BUNDLE-tag! lastTransceiver.Stop(); mSessionOff->SetTransceiver(lastTransceiver); JsepTrack removedTrack(lastTransceiver.mSendTrack); OfferAnswer(CHECK_SUCCESS); // Last m-section should be disabled auto offer = GetParsedLocalDescription(*mSessionOff); const SdpMediaSection* msection = &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ValidateDisabledMSection(msection); // Last m-section should be disabled auto answer = GetParsedLocalDescription(*mSessionAns); msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ValidateDisabledMSection(msection); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); RemoveLastN(origOffererTransceivers, 1); // Ignore this one RemoveLastN(newOffererTransceivers, 1); // Ignore this one RemoveLastN(origAnswererTransceivers, 1); // Ignore this one RemoveLastN(newAnswererTransceivers, 1); // Ignore this one ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationAnswererStopsTransceiver) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.back() == SdpMediaSection::kApplication) { return; } OfferAnswer(); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); // Avoid bundle transport side effects; don't stop the BUNDLE-tag! GetTransceivers(*mSessionAns).back().Stop(); JsepTrack removedTrack(GetTransceivers(*mSessionAns).back().mSendTrack); OfferAnswer(CHECK_SUCCESS); // Last m-section should be sendrecv auto offer = GetParsedLocalDescription(*mSessionOff); const SdpMediaSection* msection = &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_TRUE(msection->IsSending()); // Last m-section should be disabled auto answer = GetParsedLocalDescription(*mSessionAns); msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ValidateDisabledMSection(msection); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); RemoveLastN(origOffererTransceivers, 1); // Ignore this one RemoveLastN(newOffererTransceivers, 1); // Ignore this one RemoveLastN(origAnswererTransceivers, 1); // Ignore this one RemoveLastN(newAnswererTransceivers, 1); // Ignore this one ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationBothStopSameTransceiver) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.back() == SdpMediaSection::kApplication) { return; } OfferAnswer(); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); // Avoid bundle transport side effects; don't stop the BUNDLE-tag! GetTransceivers(*mSessionOff).back().Stop(); JsepTrack removedTrackOffer(GetTransceivers(*mSessionOff).back().mSendTrack); GetTransceivers(*mSessionAns).back().Stop(); JsepTrack removedTrackAnswer(GetTransceivers(*mSessionAns).back().mSendTrack); OfferAnswer(CHECK_SUCCESS); // Last m-section should be disabled auto offer = GetParsedLocalDescription(*mSessionOff); const SdpMediaSection* msection = &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ValidateDisabledMSection(msection); // Last m-section should be disabled auto answer = GetParsedLocalDescription(*mSessionAns); msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); ASSERT_TRUE(msection); ValidateDisabledMSection(msection); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); RemoveLastN(origOffererTransceivers, 1); // Ignore this one RemoveLastN(newOffererTransceivers, 1); // Ignore this one RemoveLastN(origAnswererTransceivers, 1); // Ignore this one RemoveLastN(newAnswererTransceivers, 1); // Ignore this one ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverThenAddTrack) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.back() == SdpMediaSection::kApplication) { return; } SdpMediaSection::MediaType removedType = types.back(); OfferAnswer(); // Avoid bundle transport side effects; don't stop the BUNDLE-tag! GetTransceivers(*mSessionOff).back().Stop(); JsepTrack removedTrackOffer(GetTransceivers(*mSessionOff).back().mSendTrack); GetTransceivers(*mSessionOff).back().Stop(); JsepTrack removedTrackAnswer(GetTransceivers(*mSessionOff).back().mSendTrack); OfferAnswer(CHECK_SUCCESS); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); std::vector extraTypes; extraTypes.push_back(removedType); AddTracks(*mSessionAns, extraTypes); AddTracks(*mSessionOff, extraTypes); types.insert(types.end(), extraTypes.begin(), extraTypes.end()); OfferAnswer(CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(origOffererTransceivers.size() + 1, newOffererTransceivers.size()); ASSERT_EQ(origAnswererTransceivers.size() + 1, newAnswererTransceivers.size()); // Ensure that the m-section was re-used; no gaps ASSERT_EQ(origOffererTransceivers.back().GetLevel(), newOffererTransceivers.back().GetLevel()); ASSERT_EQ(origAnswererTransceivers.back().GetLevel(), newAnswererTransceivers.back().GetLevel()); } TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverDifferentMsection) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.size() < 2) { return; } if (GetTransceivers(*mSessionOff)[0].GetMediaType() == SdpMediaSection::kApplication || GetTransceivers(*mSessionOff)[1].GetMediaType() == SdpMediaSection::kApplication) { return; } OfferAnswer(); GetTransceivers(*mSessionOff)[0].Stop(); GetTransceivers(*mSessionOff)[1].Stop(); OfferAnswer(CHECK_SUCCESS); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsStopped()); } TEST_P(JsepSessionTest, RenegotiationOffererChangesStreamId) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (GetTransceivers(*mSessionOff)[0].GetMediaType() == SdpMediaSection::kApplication) { return; } OfferAnswer(); GetTransceivers(*mSessionOff)[0].mSendTrack.UpdateStreamIds( std::vector(1, "newstream")); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ("newstream", GetTransceivers(*mSessionAns)[0].mRecvTrack.GetStreamIds()[0]); } TEST_P(JsepSessionTest, RenegotiationAnswererChangesStreamId) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (GetTransceivers(*mSessionOff)[0].GetMediaType() == SdpMediaSection::kApplication) { return; } OfferAnswer(); GetTransceivers(*mSessionAns)[0].mSendTrack.UpdateStreamIds( std::vector(1, "newstream")); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ("newstream", GetTransceivers(*mSessionOff)[0].mRecvTrack.GetStreamIds()[0]); } // Tests whether auto-assigned remote msids (ie; what happens when the other // side doesn't use msid attributes) are stable across renegotiation. TEST_P(JsepSessionTest, RenegotiationAutoAssignedMsidIsStable) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size()); for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { ASSERT_FALSE(IsNull(origOffererTransceivers[i].mRecvTrack)); ASSERT_FALSE(IsNull(origAnswererTransceivers[i].mSendTrack)); // These should not match since we've monkeyed with the msid ASSERT_NE(origOffererTransceivers[i].mRecvTrack.GetStreamIds(), origAnswererTransceivers[i].mSendTrack.GetStreamIds()); } offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); } TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); // check all the audio tracks to make sure they have 2 codecs (109 and 101), // and dtmf is enabled on all audio tracks std::vector tracks; for (const auto& transceiver : GetTransceivers(*mSessionOff)) { tracks.push_back(transceiver.mSendTrack); tracks.push_back(transceiver.mRecvTrack); } for (const JsepTrack& track : tracks) { if (track.GetMediaType() != SdpMediaSection::kAudio) { continue; } const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); ASSERT_EQ(1U, details->GetEncodingCount()); const JsepTrackEncoding& encoding = details->GetEncoding(0); ASSERT_EQ(5U, encoding.GetCodecs().size()); ASSERT_TRUE(encoding.HasFormat("109")); ASSERT_TRUE(encoding.HasFormat("101")); for (const auto& codec : encoding.GetCodecs()) { ASSERT_TRUE(codec); // we can cast here because we've already checked for audio track const JsepAudioCodecDescription* audioCodec = static_cast(codec.get()); ASSERT_TRUE(audioCodec->mDtmfEnabled); } } std::string offer = CreateOffer(); ReplaceInSdp(&offer, "8 101", "8"); ReplaceInSdp(&offer, "a=fmtp:101 0-15\r\n", ""); ReplaceInSdp(&offer, "a=rtpmap:101 telephone-event/8000/1\r\n", ""); std::cerr << "modified OFFER: " << offer << std::endl; SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); // check all the audio tracks to make sure they have 1 codec (109), // and dtmf is disabled on all audio tracks tracks.clear(); for (const auto& transceiver : GetTransceivers(*mSessionOff)) { tracks.push_back(transceiver.mSendTrack); tracks.push_back(transceiver.mRecvTrack); } for (const JsepTrack& track : tracks) { if (track.GetMediaType() != SdpMediaSection::kAudio) { continue; } const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); ASSERT_EQ(1U, details->GetEncodingCount()); const JsepTrackEncoding& encoding = details->GetEncoding(0); ASSERT_EQ(4U, encoding.GetCodecs().size()); ASSERT_TRUE(encoding.HasFormat("109")); // we can cast here because we've already checked for audio track const JsepAudioCodecDescription* audioCodec = static_cast( encoding.GetCodecs()[0].get()); ASSERT_TRUE(audioCodec); ASSERT_FALSE(audioCodec->mDtmfEnabled); } } // Tests behavior when the answerer does not use msid in the initial exchange, // but does on renegotiation. TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { ASSERT_EQ(origOffererTransceivers[i].mRecvTrack.GetMediaType(), newOffererTransceivers[i].mRecvTrack.GetMediaType()); ASSERT_TRUE(Equals(origOffererTransceivers[i].mSendTrack, newOffererTransceivers[i].mSendTrack)); ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport, newOffererTransceivers[i].mTransport)); if (origOffererTransceivers[i].mRecvTrack.GetMediaType() == SdpMediaSection::kApplication) { ASSERT_TRUE(Equals(origOffererTransceivers[i].mRecvTrack, newOffererTransceivers[i].mRecvTrack)); } else { // This should be the only difference ASSERT_FALSE(Equals(origOffererTransceivers[i].mRecvTrack, newOffererTransceivers[i].mRecvTrack)); } } } TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer, CHECK_SUCCESS); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { ASSERT_EQ(origOffererTransceivers[i].mRecvTrack.GetMediaType(), newOffererTransceivers[i].mRecvTrack.GetMediaType()); ASSERT_TRUE(Equals(origOffererTransceivers[i].mSendTrack, newOffererTransceivers[i].mSendTrack)); ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport, newOffererTransceivers[i].mTransport)); if (origOffererTransceivers[i].mRecvTrack.GetMediaType() == SdpMediaSection::kApplication) { ASSERT_TRUE(Equals(origOffererTransceivers[i].mRecvTrack, newOffererTransceivers[i].mRecvTrack)); } else { // This should be the only difference ASSERT_FALSE(Equals(origOffererTransceivers[i].mRecvTrack, newOffererTransceivers[i].mRecvTrack)); } } } // Tests behavior when offerer does not use bundle on the initial offer/answer, // but does on renegotiation. TEST_P(JsepSessionTest, RenegotiationOffererEnablesBundle) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.size() < 2) { // No bundle will happen here. return; } std::string offer = CreateOffer(); DisableBundle(&offer); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); OfferAnswer(); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); for (size_t i = 0; i < newOffererTransceivers.size(); ++i) { // No bundle initially ASSERT_FALSE(origOffererTransceivers[i].HasBundleLevel()); ASSERT_FALSE(origAnswererTransceivers[i].HasBundleLevel()); if (i != 0) { ASSERT_FALSE(Equals(origOffererTransceivers[0].mTransport, origOffererTransceivers[i].mTransport)); ASSERT_FALSE(Equals(origAnswererTransceivers[0].mTransport, origAnswererTransceivers[i].mTransport)); } // Verify that bundle worked after renegotiation ASSERT_TRUE(newOffererTransceivers[i].HasBundleLevel()); ASSERT_TRUE(newAnswererTransceivers[i].HasBundleLevel()); ASSERT_TRUE(Equals(newOffererTransceivers[0].mTransport, newOffererTransceivers[i].mTransport)); ASSERT_TRUE(Equals(newAnswererTransceivers[0].mTransport, newAnswererTransceivers[i].mTransport)); } } TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.size() < 2) { return; } OfferAnswer(); auto stopped = GetTransceiverByLevel(*mSessionOff, 0); stopped->Stop(); mSessionOff->SetTransceiver(*stopped); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); OfferAnswer(CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); Maybe ot0 = GetTransceiverByLevel(newOffererTransceivers, 0); Maybe at0 = GetTransceiverByLevel(newAnswererTransceivers, 0); ASSERT_FALSE(ot0->HasBundleLevel()); ASSERT_FALSE(at0->HasBundleLevel()); ASSERT_FALSE( Equals(ot0->mTransport, GetTransceiverByLevel(origOffererTransceivers, 0)->mTransport)); ASSERT_FALSE( Equals(at0->mTransport, GetTransceiverByLevel(origAnswererTransceivers, 0)->mTransport)); ASSERT_EQ(0U, ot0->mTransport.mComponents); ASSERT_EQ(0U, at0->mTransport.mComponents); for (size_t i = 1; i < types.size() - 1; ++i) { Maybe ot = GetTransceiverByLevel(newOffererTransceivers, i); Maybe at = GetTransceiverByLevel(newAnswererTransceivers, i); ASSERT_TRUE(ot->HasBundleLevel()); ASSERT_TRUE(at->HasBundleLevel()); ASSERT_EQ(1U, ot->BundleLevel()); ASSERT_EQ(1U, at->BundleLevel()); ASSERT_FALSE(Equals(ot0->mTransport, ot->mTransport)); ASSERT_FALSE(Equals(at0->mTransport, at->mTransport)); } } TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); if (types.size() < 2) { return; } OfferAnswer(); std::vector origOffererTransceivers = GetTransceivers(*mSessionOff); std::vector origAnswererTransceivers = GetTransceivers(*mSessionAns); auto stopped = GetTransceiverByLevel(*mSessionAns, 0); stopped->Stop(); mSessionAns->SetTransceiver(*stopped); OfferAnswer(CHECK_SUCCESS); auto newOffererTransceivers = GetTransceivers(*mSessionOff); auto newAnswererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); Maybe ot0 = GetTransceiverByLevel(newOffererTransceivers, 0); Maybe at0 = GetTransceiverByLevel(newAnswererTransceivers, 0); ASSERT_FALSE(ot0->HasBundleLevel()); ASSERT_FALSE(at0->HasBundleLevel()); ASSERT_FALSE( Equals(ot0->mTransport, GetTransceiverByLevel(origOffererTransceivers, 0)->mTransport)); ASSERT_FALSE( Equals(at0->mTransport, GetTransceiverByLevel(origAnswererTransceivers, 0)->mTransport)); ASSERT_EQ(0U, ot0->mTransport.mComponents); ASSERT_EQ(0U, at0->mTransport.mComponents); // With bundle-policy "balanced", this ends up being somewhat complicated. // The first m-section of each type is _not_ marked bundle-only, // but subsequent m-sections of that type are. This means that if there is // more than one type, there is more than one transport, which means there's // a fallback when the bundle-tag is disabled (the first such transport). We // should be using that fallback when it exists. Maybe fallbackTransport; for (size_t level = 1; level != types.size(); ++level) { if (types[level] != types[0]) { fallbackTransport = Some(level); break; } } for (size_t i = 1; i < newOffererTransceivers.size(); ++i) { auto ot = GetTransceiverByLevel(newOffererTransceivers, i); auto at = GetTransceiverByLevel(newAnswererTransceivers, i); // If there is no fallback, the bundle level will be left pointing at the // dead transport at index 0. size_t expectedBundleLevel = fallbackTransport.valueOr(0); auto otWithTransport = GetTransceiverByLevel(newOffererTransceivers, expectedBundleLevel); auto atWithTransport = GetTransceiverByLevel(newAnswererTransceivers, expectedBundleLevel); ASSERT_TRUE(ot->HasBundleLevel()); ASSERT_TRUE(at->HasBundleLevel()); ASSERT_EQ(expectedBundleLevel, ot->BundleLevel()); ASSERT_EQ(expectedBundleLevel, at->BundleLevel()); // TODO: When creating an answer where we have rejected the bundle // transport, we do not do a good job of creating a sensible SDP. Mainly, // when we remove the rejected mid from the bundle group, we can leave a // bundle-only mid as the first one when others are available. ASSERT_TRUE(Equals(otWithTransport->mTransport, ot->mTransport)); ASSERT_TRUE(Equals(atWithTransport->mTransport, at->mTransport)); } } TEST_P(JsepSessionTest, ParseRejectsBadMediaFormat) { AddTracks(*mSessionOff); if (types.front() == SdpMediaSection::MediaType::kApplication) { return; } std::string offer = CreateOffer(); UniquePtr munge(Parse(offer)); SdpMediaSection& mediaSection = munge->GetMediaSection(0); mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1); std::string sdpString = munge->ToString(); JsepSession::Result result = mSessionOff->SetLocalDescription(kJsepSdpOffer, sdpString); ASSERT_EQ(dom::PCError::OperationError, *result.mError); } TEST_P(JsepSessionTest, FullCallWithCandidates) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); mOffCandidates->Gather(*mSessionOff); UniquePtr localOffer( Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending))); for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionOff, i); bool bundleOnly = localOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute); mOffCandidates->CheckRtpCandidates( !bundleOnly, localOffer->GetMediaSection(i), id, "Local offer after gathering should have RTP candidates " "(unless bundle-only)"); mOffCandidates->CheckDefaultRtpCandidate( !bundleOnly, localOffer->GetMediaSection(i), id, "Local offer after gathering should have a default RTP candidate " "(unless bundle-only)"); mOffCandidates->CheckRtcpCandidates( !bundleOnly && types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), id, "Local offer after gathering should have RTCP candidates " "(unless m=application or bundle-only)"); mOffCandidates->CheckDefaultRtcpCandidate( !bundleOnly && types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), id, "Local offer after gathering should have a default RTCP candidate " "(unless m=application or bundle-only)"); CheckEndOfCandidates( !bundleOnly, localOffer->GetMediaSection(i), "Local offer after gathering should have an end-of-candidates " "(unless bundle-only)"); } SetRemoteOffer(offer); mOffCandidates->Trickle(*mSessionAns); UniquePtr remoteOffer( Parse(mSessionAns->GetRemoteDescription(kJsepDescriptionPending))); for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionOff, i); bool bundleOnly = remoteOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute); mOffCandidates->CheckRtpCandidates( !bundleOnly, remoteOffer->GetMediaSection(i), id, "Remote offer after trickle should have RTP candidates " "(unless bundle-only)"); mOffCandidates->CheckDefaultRtpCandidate( false, remoteOffer->GetMediaSection(i), id, "Remote offer after trickle should not have a default RTP candidate."); mOffCandidates->CheckRtcpCandidates( !bundleOnly && types[i] != SdpMediaSection::kApplication, remoteOffer->GetMediaSection(i), id, "Remote offer after trickle should have RTCP candidates " "(unless m=application or bundle-only)"); mOffCandidates->CheckDefaultRtcpCandidate( false, remoteOffer->GetMediaSection(i), id, "Remote offer after trickle should not have a default RTCP candidate."); CheckEndOfCandidates( true, remoteOffer->GetMediaSection(i), "Remote offer after trickle should have an end-of-candidates."); } AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); // This will gather candidates that mSessionAns knows it doesn't need. // They should not be present in the SDP. mAnsCandidates->Gather(*mSessionAns); UniquePtr localAnswer( Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); std::string id0 = GetTransportId(*mSessionAns, 0); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionAns, i); mAnsCandidates->CheckRtpCandidates( i == 0, localAnswer->GetMediaSection(i), id, "Local answer after gathering should have RTP candidates on level 0."); mAnsCandidates->CheckDefaultRtpCandidate( true, localAnswer->GetMediaSection(i), id0, "Local answer after gathering should have a default RTP candidate " "on all levels that matches transport level 0."); mAnsCandidates->CheckRtcpCandidates( false, localAnswer->GetMediaSection(i), id, "Local answer after gathering should not have RTCP candidates " "(because we're answering with rtcp-mux)"); mAnsCandidates->CheckDefaultRtcpCandidate( false, localAnswer->GetMediaSection(i), id, "Local answer after gathering should not have a default RTCP candidate " "(because we're answering with rtcp-mux)"); CheckEndOfCandidates( i == 0, localAnswer->GetMediaSection(i), "Local answer after gathering should have an end-of-candidates only for" " level 0."); } SetRemoteAnswer(answer); mAnsCandidates->Trickle(*mSessionOff); UniquePtr remoteAnswer( Parse(mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent))); for (size_t i = 0; i < remoteAnswer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionAns, i); mAnsCandidates->CheckRtpCandidates( i == 0, remoteAnswer->GetMediaSection(i), id, "Remote answer after trickle should have RTP candidates on level 0."); mAnsCandidates->CheckDefaultRtpCandidate( false, remoteAnswer->GetMediaSection(i), id, "Remote answer after trickle should not have a default RTP candidate."); mAnsCandidates->CheckRtcpCandidates( false, remoteAnswer->GetMediaSection(i), id, "Remote answer after trickle should not have RTCP candidates " "(because we're answering with rtcp-mux)"); mAnsCandidates->CheckDefaultRtcpCandidate( false, remoteAnswer->GetMediaSection(i), id, "Remote answer after trickle should not have a default RTCP " "candidate."); CheckEndOfCandidates( true, remoteAnswer->GetMediaSection(i), "Remote answer after trickle should have an end-of-candidates."); } } TEST_P(JsepSessionTest, RenegotiationWithCandidates) { AddTracks(*mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); mOffCandidates->Gather(*mSessionOff); SetRemoteOffer(offer); mOffCandidates->Trickle(*mSessionAns); AddTracks(*mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); mAnsCandidates->Gather(*mSessionAns); SetRemoteAnswer(answer); mAnsCandidates->Trickle(*mSessionOff); offer = CreateOffer(); SetLocalOffer(offer); UniquePtr parsedOffer(Parse(offer)); std::string id0 = GetTransportId(*mSessionOff, 0); for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionOff, i); mOffCandidates->CheckRtpCandidates( i == 0, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should have RTP candidates on level 0" " only."); mOffCandidates->CheckDefaultRtpCandidate( i == 0, parsedOffer->GetMediaSection(i), id0, "Local reoffer before gathering should have a default RTP candidate " "on level 0 only."); mOffCandidates->CheckRtcpCandidates( false, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should not have RTCP candidates."); mOffCandidates->CheckDefaultRtcpCandidate( false, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should not have a default RTCP " "candidate."); CheckEndOfCandidates( i == 0, parsedOffer->GetMediaSection(i), "Local reoffer before gathering should have an end-of-candidates " "(level 0 only)"); } // mSessionAns should generate a reoffer that is similar std::string otherOffer; JsepOfferOptions defaultOptions; JsepSession::Result result = mSessionAns->CreateOffer(defaultOptions, &otherOffer); ASSERT_FALSE(result.mError.isSome()); parsedOffer = Parse(otherOffer); id0 = GetTransportId(*mSessionAns, 0); for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionAns, i); mAnsCandidates->CheckRtpCandidates( i == 0, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should have RTP candidates on level 0" " only. (previous answerer)"); mAnsCandidates->CheckDefaultRtpCandidate( i == 0, parsedOffer->GetMediaSection(i), id0, "Local reoffer before gathering should have a default RTP candidate " "on level 0 only. (previous answerer)"); mAnsCandidates->CheckRtcpCandidates( false, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should not have RTCP candidates." " (previous answerer)"); mAnsCandidates->CheckDefaultRtcpCandidate( false, parsedOffer->GetMediaSection(i), id, "Local reoffer before gathering should not have a default RTCP " "candidate. (previous answerer)"); CheckEndOfCandidates( i == 0, parsedOffer->GetMediaSection(i), "Local reoffer before gathering should have an end-of-candidates " "(level 0 only)"); } // Ok, let's continue with the renegotiation SetRemoteOffer(offer); // PeerConnection will not re-gather for RTP, but it will for RTCP in case // the answerer decides to turn off rtcp-mux. if (types[0] != SdpMediaSection::kApplication) { mOffCandidates->Gather(*mSessionOff, GetTransportId(*mSessionOff, 0), RTCP); } // Since the remaining levels were bundled, PeerConnection will re-gather for // both RTP and RTCP, in case the answerer rejects bundle, provided // bundle-only isn't being used. UniquePtr localOffer( Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending))); for (size_t level = 1; level < types.size(); ++level) { std::string id = GetTransportId(*mSessionOff, level); bool bundleOnly = localOffer->GetMediaSection(level).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute); if (!id.empty() && !bundleOnly) { mOffCandidates->Gather(*mSessionOff, id, RTP); if (types[level] != SdpMediaSection::kApplication) { mOffCandidates->Gather(*mSessionOff, id, RTCP); } } } mOffCandidates->FinishGathering(*mSessionOff); localOffer = Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending)); mOffCandidates->Trickle(*mSessionAns); for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionOff, i); bool bundleOnly = localOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute); mOffCandidates->CheckRtpCandidates( !bundleOnly, localOffer->GetMediaSection(i), id, "Local reoffer after gathering should have RTP candidates " "(unless bundle-only)"); mOffCandidates->CheckDefaultRtpCandidate( !bundleOnly, localOffer->GetMediaSection(i), id, "Local reoffer after gathering should have a default RTP candidate " "(unless bundle-only)"); mOffCandidates->CheckRtcpCandidates( !bundleOnly && (types[i] != SdpMediaSection::kApplication), localOffer->GetMediaSection(i), id, "Local reoffer after gathering should have RTCP candidates " "(unless m=application or bundle-only)"); mOffCandidates->CheckDefaultRtcpCandidate( !bundleOnly && (types[i] != SdpMediaSection::kApplication), localOffer->GetMediaSection(i), id, "Local reoffer after gathering should have a default RTCP candidate " "(unless m=application or bundle-only)"); CheckEndOfCandidates( !bundleOnly, localOffer->GetMediaSection(i), "Local reoffer after gathering should have an end-of-candidates " "(unless bundle-only)"); } UniquePtr remoteOffer( Parse(mSessionAns->GetRemoteDescription(kJsepDescriptionPending))); for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { bool bundleOnly = remoteOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute); std::string id = GetTransportId(*mSessionOff, i); mOffCandidates->CheckRtpCandidates( !bundleOnly, remoteOffer->GetMediaSection(i), id, "Remote reoffer after trickle should have RTP candidates " "(unless bundle-only)"); mOffCandidates->CheckDefaultRtpCandidate( i == 0, remoteOffer->GetMediaSection(i), id, "Remote reoffer should have a default RTP candidate on level 0 " "(because it was gathered last offer/answer)."); mOffCandidates->CheckRtcpCandidates( !bundleOnly && types[i] != SdpMediaSection::kApplication, remoteOffer->GetMediaSection(i), id, "Remote reoffer after trickle should have RTCP candidates " "(unless m=application or bundle-only)"); mOffCandidates->CheckDefaultRtcpCandidate( false, remoteOffer->GetMediaSection(i), id, "Remote reoffer should not have a default RTCP candidate."); CheckEndOfCandidates(true, remoteOffer->GetMediaSection(i), "Remote reoffer should have an end-of-candidates."); } answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); // No candidates should be gathered at the answerer, but default candidates // should be set. mAnsCandidates->FinishGathering(*mSessionAns); UniquePtr localAnswer( Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); id0 = GetTransportId(*mSessionAns, 0); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionAns, 0); mAnsCandidates->CheckRtpCandidates( i == 0, localAnswer->GetMediaSection(i), id, "Local reanswer after gathering should have RTP candidates on level " "0."); mAnsCandidates->CheckDefaultRtpCandidate( true, localAnswer->GetMediaSection(i), id0, "Local reanswer after gathering should have a default RTP candidate " "on all levels that matches transport level 0."); mAnsCandidates->CheckRtcpCandidates( false, localAnswer->GetMediaSection(i), id, "Local reanswer after gathering should not have RTCP candidates " "(because we're reanswering with rtcp-mux)"); mAnsCandidates->CheckDefaultRtcpCandidate( false, localAnswer->GetMediaSection(i), id, "Local reanswer after gathering should not have a default RTCP " "candidate (because we're reanswering with rtcp-mux)"); CheckEndOfCandidates( i == 0, localAnswer->GetMediaSection(i), "Local reanswer after gathering should have an end-of-candidates only " "for level 0."); } UniquePtr remoteAnswer( Parse(mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent))); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { std::string id = GetTransportId(*mSessionAns, 0); mAnsCandidates->CheckRtpCandidates( i == 0, remoteAnswer->GetMediaSection(i), id, "Remote reanswer after trickle should have RTP candidates on level 0."); mAnsCandidates->CheckDefaultRtpCandidate( i == 0, remoteAnswer->GetMediaSection(i), id, "Remote reanswer should have a default RTP candidate on level 0 " "(because it was gathered last offer/answer)."); mAnsCandidates->CheckRtcpCandidates( false, remoteAnswer->GetMediaSection(i), id, "Remote reanswer after trickle should not have RTCP candidates " "(because we're reanswering with rtcp-mux)"); mAnsCandidates->CheckDefaultRtcpCandidate( false, remoteAnswer->GetMediaSection(i), id, "Remote reanswer after trickle should not have a default RTCP " "candidate."); CheckEndOfCandidates(i == 0, remoteAnswer->GetMediaSection(i), "Remote reanswer after trickle should have an " "end-of-candidates on level 0 only."); } } TEST_P(JsepSessionTest, RenegotiationAnswererSendonly) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); UniquePtr parsedAnswer(Parse(answer)); for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) { SdpMediaSection& msection = parsedAnswer->GetMediaSection(i); if (msection.GetMediaType() != SdpMediaSection::kApplication) { msection.SetReceiving(false); } } answer = parsedAnswer->ToString(); SetRemoteAnswer(answer); for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) { if (track.GetMediaType() != SdpMediaSection::kApplication) { ASSERT_FALSE(track.GetActive()); } } ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); } TEST_P(JsepSessionTest, RenegotiationAnswererInactive) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); UniquePtr parsedAnswer(Parse(answer)); for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) { SdpMediaSection& msection = parsedAnswer->GetMediaSection(i); if (msection.GetMediaType() != SdpMediaSection::kApplication) { msection.SetReceiving(false); msection.SetSending(false); } } answer = parsedAnswer->ToString(); SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) { if (track.GetMediaType() != SdpMediaSection::kApplication) { ASSERT_FALSE(track.GetActive()); } } ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); } INSTANTIATE_TEST_SUITE_P( Variants, JsepSessionTest, ::testing::Values("audio", "video", "datachannel", "audio,video", "video,audio", "audio,datachannel", "video,datachannel", "video,audio,datachannel", "audio,video,datachannel", "datachannel,audio", "datachannel,video", "datachannel,audio,video", "datachannel,video,audio", "audio,datachannel,video", "video,datachannel,audio", "audio,audio", "video,video", "audio,audio,video", "audio,video,video", "audio,audio,video,video", "audio,audio,video,video,datachannel")); // offerToReceiveXxx variants TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines) { mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kAudio, mUuidGen, SdpDirectionAttribute::kRecvonly)); mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); std::string offer = CreateOffer(); UniquePtr parsedOffer(Parse(offer)); ASSERT_TRUE(!!parsedOffer); ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, parsedOffer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, parsedOffer->GetMediaSection(0).GetAttributeList().GetDirection()); ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( SdpAttribute::kSsrcAttribute)); ASSERT_EQ(SdpMediaSection::kVideo, parsedOffer->GetMediaSection(1).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, parsedOffer->GetMediaSection(1).GetAttributeList().GetDirection()); ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute( SdpAttribute::kSsrcAttribute)); ASSERT_EQ(SdpMediaSection::kVideo, parsedOffer->GetMediaSection(2).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, parsedOffer->GetMediaSection(2).GetAttributeList().GetDirection()); ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute( SdpAttribute::kSsrcAttribute)); ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); SetLocalOffer(offer, CHECK_SUCCESS); AddTracks(*mSessionAns, "audio,video"); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); UniquePtr parsedAnswer(Parse(answer)); ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, parsedAnswer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendonly, parsedAnswer->GetMediaSection(0).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, parsedAnswer->GetMediaSection(1).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendonly, parsedAnswer->GetMediaSection(1).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, parsedAnswer->GetMediaSection(2).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kInactive, parsedAnswer->GetMediaSection(2).GetAttributeList().GetDirection()); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); std::vector transceivers(GetTransceivers(*mSessionOff)); ASSERT_EQ(3U, transceivers.size()); for (const auto& transceiver : transceivers) { const auto& msection = parsedOffer->GetMediaSection(transceiver.GetLevel()); const auto& ssrcs = msection.GetAttributeList().GetSsrc().mSsrcs; ASSERT_EQ(1U, ssrcs.size()); } } TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines) { AddTracks(*mSessionOff, "audio,video,video"); SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly); SetDirection(*mSessionOff, 2, SdpDirectionAttribute::kSendonly); std::string offer = CreateOffer(); UniquePtr outputSdp(Parse(offer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(3U, outputSdp->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, outputSdp->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendonly, outputSdp->GetMediaSection(0).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, outputSdp->GetMediaSection(1).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, outputSdp->GetMediaSection(1).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, outputSdp->GetMediaSection(2).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendonly, outputSdp->GetMediaSection(2).GetAttributeList().GetDirection()); ASSERT_TRUE(outputSdp->GetMediaSection(0).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); ASSERT_TRUE(outputSdp->GetMediaSection(1).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); ASSERT_TRUE(outputSdp->GetMediaSection(2).GetAttributeList().HasAttribute( SdpAttribute::kRtcpMuxAttribute)); SetLocalOffer(offer, CHECK_SUCCESS); AddTracks(*mSessionAns, "audio,video"); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); outputSdp = Parse(answer); ASSERT_EQ(3U, outputSdp->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, outputSdp->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, outputSdp->GetMediaSection(0).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, outputSdp->GetMediaSection(1).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, outputSdp->GetMediaSection(1).GetAttributeList().GetDirection()); ASSERT_EQ(SdpMediaSection::kVideo, outputSdp->GetMediaSection(2).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, outputSdp->GetMediaSection(2).GetAttributeList().GetDirection()); } TEST_F(JsepSessionTest, OfferToReceiveAudioNotUsed) { mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kAudio, mUuidGen, SdpDirectionAttribute::kRecvonly)); OfferAnswer(CHECK_SUCCESS); UniquePtr offer( Parse(mSessionOff->GetLocalDescription(kJsepDescriptionCurrent))); ASSERT_TRUE(offer.get()); ASSERT_EQ(1U, offer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, offer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, offer->GetMediaSection(0).GetAttributeList().GetDirection()); UniquePtr answer( Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); ASSERT_TRUE(answer.get()); ASSERT_EQ(1U, answer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, answer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kInactive, answer->GetMediaSection(0).GetAttributeList().GetDirection()); } TEST_F(JsepSessionTest, OfferToReceiveVideoNotUsed) { mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); OfferAnswer(CHECK_SUCCESS); UniquePtr offer( Parse(mSessionOff->GetLocalDescription(kJsepDescriptionCurrent))); ASSERT_TRUE(offer.get()); ASSERT_EQ(1U, offer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kVideo, offer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kRecvonly, offer->GetMediaSection(0).GetAttributeList().GetDirection()); UniquePtr answer( Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); ASSERT_TRUE(answer.get()); ASSERT_EQ(1U, answer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kVideo, answer->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpDirectionAttribute::kInactive, answer->GetMediaSection(0).GetAttributeList().GetDirection()); } TEST_F(JsepSessionTest, CreateOfferNoDatachannelDefault) { JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); audio.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(audio); JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); video.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(video); std::string offer = CreateOffer(); UniquePtr outputSdp(Parse(offer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kAudio, outputSdp->GetMediaSection(0).GetMediaType()); ASSERT_EQ(SdpMediaSection::kVideo, outputSdp->GetMediaSection(1).GetMediaType()); } TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); audio.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(audio); JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); video.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(video); std::string offer = CreateOffer(); UniquePtr outputSdp(Parse(offer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); const auto& video_section = outputSdp->GetMediaSection(1); ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); const auto& video_attrs = video_section.GetAttributeList(); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); ASSERT_EQ(10U, video_section.GetFormats().size()); ASSERT_EQ("120", video_section.GetFormats()[0]); ASSERT_EQ("124", video_section.GetFormats()[1]); ASSERT_EQ("121", video_section.GetFormats()[2]); ASSERT_EQ("125", video_section.GetFormats()[3]); ASSERT_EQ("126", video_section.GetFormats()[4]); ASSERT_EQ("127", video_section.GetFormats()[5]); ASSERT_EQ("97", video_section.GetFormats()[6]); ASSERT_EQ("98", video_section.GetFormats()[7]); ASSERT_EQ("123", video_section.GetFormats()[8]); ASSERT_EQ("122", video_section.GetFormats()[9]); // Validate rtpmap ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); auto& rtpmaps = video_attrs.GetRtpmap(); ASSERT_TRUE(rtpmaps.HasEntry("120")); ASSERT_TRUE(rtpmaps.HasEntry("124")); ASSERT_TRUE(rtpmaps.HasEntry("121")); ASSERT_TRUE(rtpmaps.HasEntry("125")); ASSERT_TRUE(rtpmaps.HasEntry("126")); ASSERT_TRUE(rtpmaps.HasEntry("127")); ASSERT_TRUE(rtpmaps.HasEntry("97")); ASSERT_TRUE(rtpmaps.HasEntry("98")); ASSERT_TRUE(rtpmaps.HasEntry("123")); ASSERT_TRUE(rtpmaps.HasEntry("122")); const auto& vp8_entry = rtpmaps.GetEntry("120"); const auto& vp8_rtx_entry = rtpmaps.GetEntry("124"); const auto& vp9_entry = rtpmaps.GetEntry("121"); const auto& vp9_rtx_entry = rtpmaps.GetEntry("125"); const auto& h264_1_entry = rtpmaps.GetEntry("126"); const auto& h264_1_rtx_entry = rtpmaps.GetEntry("127"); const auto& h264_0_entry = rtpmaps.GetEntry("97"); const auto& h264_0_rtx_entry = rtpmaps.GetEntry("98"); const auto& ulpfec_0_entry = rtpmaps.GetEntry("123"); const auto& red_0_entry = rtpmaps.GetEntry("122"); ASSERT_EQ("VP8", vp8_entry.name); ASSERT_EQ("rtx", vp8_rtx_entry.name); ASSERT_EQ("VP9", vp9_entry.name); ASSERT_EQ("rtx", vp9_rtx_entry.name); ASSERT_EQ("H264", h264_1_entry.name); ASSERT_EQ("rtx", h264_1_rtx_entry.name); ASSERT_EQ("H264", h264_0_entry.name); ASSERT_EQ("rtx", h264_0_rtx_entry.name); ASSERT_EQ("red", red_0_entry.name); ASSERT_EQ("ulpfec", ulpfec_0_entry.name); // Validate fmtps ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); auto& fmtps = video_attrs.GetFmtp().mFmtps; ASSERT_EQ(9U, fmtps.size()); // VP8 const SdpFmtpAttributeList::Parameters* vp8_params = video_section.FindFmtp("120"); ASSERT_TRUE(vp8_params); ASSERT_EQ(SdpRtpmapAttributeList::kVP8, vp8_params->codec_type); auto& parsed_vp8_params = *static_cast(vp8_params); ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs); ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr); // VP8 RTX const SdpFmtpAttributeList::Parameters* vp8_rtx_params = video_section.FindFmtp("124"); ASSERT_TRUE(vp8_rtx_params); ASSERT_EQ(SdpRtpmapAttributeList::kRtx, vp8_rtx_params->codec_type); const auto& parsed_vp8_rtx_params = *static_cast(vp8_rtx_params); ASSERT_EQ((uint32_t)120, parsed_vp8_rtx_params.apt); // VP9 const SdpFmtpAttributeList::Parameters* vp9_params = video_section.FindFmtp("121"); ASSERT_TRUE(vp9_params); ASSERT_EQ(SdpRtpmapAttributeList::kVP9, vp9_params->codec_type); const auto& parsed_vp9_params = *static_cast(vp9_params); ASSERT_EQ((uint32_t)12288, parsed_vp9_params.max_fs); ASSERT_EQ((uint32_t)60, parsed_vp9_params.max_fr); // VP9 RTX const SdpFmtpAttributeList::Parameters* vp9_rtx_params = video_section.FindFmtp("125"); ASSERT_TRUE(vp9_rtx_params); ASSERT_EQ(SdpRtpmapAttributeList::kRtx, vp9_rtx_params->codec_type); const auto& parsed_vp9_rtx_params = *static_cast(vp9_rtx_params); ASSERT_EQ((uint32_t)121, parsed_vp9_rtx_params.apt); // H264 packetization mode 1 const SdpFmtpAttributeList::Parameters* h264_1_params = video_section.FindFmtp("126"); ASSERT_TRUE(h264_1_params); ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_1_params->codec_type); const auto& parsed_h264_1_params = *static_cast(h264_1_params); ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_1_params.profile_level_id); ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed); ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode); // H264 packetization mode 1 RTX const SdpFmtpAttributeList::Parameters* h264_1_rtx_params = video_section.FindFmtp("127"); ASSERT_TRUE(h264_1_rtx_params); ASSERT_EQ(SdpRtpmapAttributeList::kRtx, h264_1_rtx_params->codec_type); const auto& parsed_h264_1_rtx_params = *static_cast( h264_1_rtx_params); ASSERT_EQ((uint32_t)126, parsed_h264_1_rtx_params.apt); // H264 packetization mode 0 const SdpFmtpAttributeList::Parameters* h264_0_params = video_section.FindFmtp("97"); ASSERT_TRUE(h264_0_params); ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_0_params->codec_type); const auto& parsed_h264_0_params = *static_cast(h264_0_params); ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_0_params.profile_level_id); ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed); ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode); // H264 packetization mode 0 RTX const SdpFmtpAttributeList::Parameters* h264_0_rtx_params = video_section.FindFmtp("98"); ASSERT_TRUE(h264_0_rtx_params); ASSERT_EQ(SdpRtpmapAttributeList::kRtx, h264_0_rtx_params->codec_type); const auto& parsed_h264_0_rtx_params = *static_cast( h264_0_rtx_params); ASSERT_EQ((uint32_t)97, parsed_h264_0_rtx_params.apt); // red const SdpFmtpAttributeList::Parameters* red_params = video_section.FindFmtp("122"); ASSERT_TRUE(red_params); ASSERT_EQ(SdpRtpmapAttributeList::kRed, red_params->codec_type); const auto& parsed_red_params = *static_cast(red_params); ASSERT_EQ(5U, parsed_red_params.encodings.size()); ASSERT_EQ(120, parsed_red_params.encodings[0]); ASSERT_EQ(121, parsed_red_params.encodings[1]); ASSERT_EQ(126, parsed_red_params.encodings[2]); ASSERT_EQ(97, parsed_red_params.encodings[3]); ASSERT_EQ(123, parsed_red_params.encodings[4]); } TEST_F(JsepSessionTest, ValidateOfferedAudioCodecParams) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); audio.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(audio); JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); video.mSendTrack.UpdateStreamIds( std::vector(1, "offerer_stream")); mSessionOff->AddTransceiver(video); std::string offer = CreateOffer(); UniquePtr outputSdp(Parse(offer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); auto& audio_section = outputSdp->GetMediaSection(0); ASSERT_EQ(SdpMediaSection::kAudio, audio_section.GetMediaType()); auto& audio_attrs = audio_section.GetAttributeList(); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, audio_attrs.GetDirection()); ASSERT_EQ(5U, audio_section.GetFormats().size()); ASSERT_EQ("109", audio_section.GetFormats()[0]); ASSERT_EQ("9", audio_section.GetFormats()[1]); ASSERT_EQ("0", audio_section.GetFormats()[2]); ASSERT_EQ("8", audio_section.GetFormats()[3]); ASSERT_EQ("101", audio_section.GetFormats()[4]); // Validate rtpmap ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); auto& rtpmaps = audio_attrs.GetRtpmap(); ASSERT_TRUE(rtpmaps.HasEntry("109")); ASSERT_TRUE(rtpmaps.HasEntry("9")); ASSERT_TRUE(rtpmaps.HasEntry("0")); ASSERT_TRUE(rtpmaps.HasEntry("8")); ASSERT_TRUE(rtpmaps.HasEntry("101")); auto& opus_entry = rtpmaps.GetEntry("109"); auto& g722_entry = rtpmaps.GetEntry("9"); auto& pcmu_entry = rtpmaps.GetEntry("0"); auto& pcma_entry = rtpmaps.GetEntry("8"); auto& telephone_event_entry = rtpmaps.GetEntry("101"); ASSERT_EQ("opus", opus_entry.name); ASSERT_EQ("G722", g722_entry.name); ASSERT_EQ("PCMU", pcmu_entry.name); ASSERT_EQ("PCMA", pcma_entry.name); ASSERT_EQ("telephone-event", telephone_event_entry.name); // Validate fmtps ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); auto& fmtps = audio_attrs.GetFmtp().mFmtps; ASSERT_EQ(2U, fmtps.size()); // opus const SdpFmtpAttributeList::Parameters* opus_params = audio_section.FindFmtp("109"); ASSERT_TRUE(opus_params); ASSERT_EQ(SdpRtpmapAttributeList::kOpus, opus_params->codec_type); auto& parsed_opus_params = *static_cast(opus_params); ASSERT_EQ((uint32_t)48000, parsed_opus_params.maxplaybackrate); ASSERT_EQ((uint32_t)1, parsed_opus_params.stereo); ASSERT_EQ((uint32_t)0, parsed_opus_params.useInBandFec); ASSERT_EQ((uint32_t)0, parsed_opus_params.maxAverageBitrate); ASSERT_EQ((uint32_t)0, parsed_opus_params.useDTX); ASSERT_EQ((uint32_t)0, parsed_opus_params.useCbr); ASSERT_EQ((uint32_t)0, parsed_opus_params.frameSizeMs); ASSERT_EQ((uint32_t)0, parsed_opus_params.minFrameSizeMs); ASSERT_EQ((uint32_t)0, parsed_opus_params.maxFrameSizeMs); // dtmf const SdpFmtpAttributeList::Parameters* dtmf_params = audio_section.FindFmtp("101"); ASSERT_TRUE(dtmf_params); ASSERT_EQ(SdpRtpmapAttributeList::kTelephoneEvent, dtmf_params->codec_type); auto& parsed_dtmf_params = *static_cast( dtmf_params); ASSERT_EQ("0-15", parsed_dtmf_params.dtmfTones); } TEST_F(JsepSessionTest, ValidateNoFmtpLineForRedInOfferAndAnswer) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video"); std::string offer = CreateOffer(); // look for line with fmtp:122 and remove it size_t start = offer.find("a=fmtp:122"); size_t end = offer.find("\r\n", start); offer.replace(start, end + 2 - start, ""); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video"); std::string answer = CreateAnswer(); // because parsing will throw out the malformed fmtp, make sure it is not // in the answer sdp string ASSERT_EQ(std::string::npos, answer.find("a=fmtp:122")); UniquePtr outputSdp(Parse(answer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); auto& video_section = outputSdp->GetMediaSection(1); ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); auto& video_attrs = video_section.GetAttributeList(); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); ASSERT_EQ(10U, video_section.GetFormats().size()); ASSERT_EQ("120", video_section.GetFormats()[0]); ASSERT_EQ("124", video_section.GetFormats()[1]); ASSERT_EQ("121", video_section.GetFormats()[2]); ASSERT_EQ("125", video_section.GetFormats()[3]); ASSERT_EQ("126", video_section.GetFormats()[4]); ASSERT_EQ("127", video_section.GetFormats()[5]); ASSERT_EQ("97", video_section.GetFormats()[6]); ASSERT_EQ("98", video_section.GetFormats()[7]); ASSERT_EQ("123", video_section.GetFormats()[8]); ASSERT_EQ("122", video_section.GetFormats()[9]); // Validate rtpmap ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); auto& rtpmaps = video_attrs.GetRtpmap(); ASSERT_TRUE(rtpmaps.HasEntry("120")); ASSERT_TRUE(rtpmaps.HasEntry("124")); ASSERT_TRUE(rtpmaps.HasEntry("121")); ASSERT_TRUE(rtpmaps.HasEntry("125")); ASSERT_TRUE(rtpmaps.HasEntry("126")); ASSERT_TRUE(rtpmaps.HasEntry("127")); ASSERT_TRUE(rtpmaps.HasEntry("97")); ASSERT_TRUE(rtpmaps.HasEntry("98")); ASSERT_TRUE(rtpmaps.HasEntry("123")); ASSERT_TRUE(rtpmaps.HasEntry("122")); // Validate fmtps ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); auto& fmtps = video_attrs.GetFmtp().mFmtps; ASSERT_EQ(8U, fmtps.size()); ASSERT_EQ("126", fmtps[0].format); ASSERT_EQ("97", fmtps[1].format); ASSERT_EQ("120", fmtps[2].format); ASSERT_EQ("124", fmtps[3].format); ASSERT_EQ("121", fmtps[4].format); ASSERT_EQ("125", fmtps[5].format); ASSERT_EQ("127", fmtps[6].format); ASSERT_EQ("98", fmtps[7].format); SetLocalAnswer(answer); SetRemoteAnswer(answer); auto offerTransceivers = GetTransceivers(*mSessionOff); ASSERT_EQ(2U, offerTransceivers.size()); ASSERT_FALSE(IsNull(offerTransceivers[1].mSendTrack)); ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); ASSERT_TRUE(offerTransceivers[1].mSendTrack.GetNegotiatedDetails()); ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(6U, offerTransceivers[1] .mSendTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); ASSERT_EQ(6U, offerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); auto answerTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(2U, answerTransceivers.size()); ASSERT_FALSE(IsNull(answerTransceivers[1].mSendTrack)); ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); ASSERT_TRUE(answerTransceivers[1].mSendTrack.GetNegotiatedDetails()); ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(6U, answerTransceivers[1] .mSendTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); ASSERT_EQ(6U, answerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); } TEST_F(JsepSessionTest, ValidateAnsweredCodecParamsNoRed) { // TODO(bug 1099351): Once fixed, we can allow red in this offer, // which will also cause multiple codecs in answer. For now, // red/ulpfec for video are behind a pref to mitigate potential for // errors. SetCodecEnabled(*mSessionOff, "red", false); for (auto& codec : mSessionAns->Codecs()) { if (codec->mName == "H264") { JsepVideoCodecDescription* h264 = static_cast(codec.get()); h264->mProfileLevelId = 0x42a00d; // Switch up the pts if (h264->mDefaultPt == "126") { h264->mDefaultPt = "97"; } else { h264->mDefaultPt = "126"; } } } types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video"); std::string answer = CreateAnswer(); UniquePtr outputSdp(Parse(answer)); ASSERT_TRUE(!!outputSdp); ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); auto& video_section = outputSdp->GetMediaSection(1); ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); auto& video_attrs = video_section.GetAttributeList(); ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); ASSERT_EQ(4U, video_section.GetFormats().size()); ASSERT_EQ("120", video_section.GetFormats()[0]); ASSERT_EQ("124", video_section.GetFormats()[1]); ASSERT_EQ("121", video_section.GetFormats()[2]); ASSERT_EQ("125", video_section.GetFormats()[3]); // Validate rtpmap ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); auto& rtpmaps = video_attrs.GetRtpmap(); ASSERT_TRUE(rtpmaps.HasEntry("120")); ASSERT_TRUE(rtpmaps.HasEntry("121")); auto& vp8_entry = rtpmaps.GetEntry("120"); auto& vp9_entry = rtpmaps.GetEntry("121"); ASSERT_EQ("VP8", vp8_entry.name); ASSERT_EQ("VP9", vp9_entry.name); // Validate fmtps ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); auto& fmtps = video_attrs.GetFmtp().mFmtps; ASSERT_EQ(4U, fmtps.size()); // VP8 ASSERT_EQ("120", fmtps[0].format); ASSERT_TRUE(!!fmtps[0].parameters); ASSERT_EQ(SdpRtpmapAttributeList::kVP8, fmtps[0].parameters->codec_type); auto& parsed_vp8_params = *static_cast( fmtps[0].parameters.get()); ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs); ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr); // VP9 ASSERT_EQ("121", fmtps[2].format); ASSERT_TRUE(!!fmtps[2].parameters); ASSERT_EQ(SdpRtpmapAttributeList::kVP9, fmtps[2].parameters->codec_type); auto& parsed_vp9_params = *static_cast( fmtps[2].parameters.get()); ASSERT_EQ((uint32_t)12288, parsed_vp9_params.max_fs); ASSERT_EQ((uint32_t)60, parsed_vp9_params.max_fr); SetLocalAnswer(answer); SetRemoteAnswer(answer); auto offerTransceivers = GetTransceivers(*mSessionOff); ASSERT_EQ(2U, offerTransceivers.size()); ASSERT_FALSE(IsNull(offerTransceivers[1].mSendTrack)); ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); ASSERT_TRUE(offerTransceivers[1].mSendTrack.GetNegotiatedDetails()); ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(2U, offerTransceivers[1] .mSendTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); ASSERT_EQ(2U, offerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); auto answerTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(2U, answerTransceivers.size()); ASSERT_FALSE(IsNull(answerTransceivers[1].mSendTrack)); ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); ASSERT_TRUE(answerTransceivers[1].mSendTrack.GetNegotiatedDetails()); ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(2U, answerTransceivers[1] .mSendTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); ASSERT_EQ(2U, answerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetEncoding(0) .GetCodecs() .size()); #if 0 // H264 packetization mode 1 ASSERT_EQ("126", fmtps[1].format); ASSERT_TRUE(fmtps[1].parameters); ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type); auto& parsed_h264_1_params = *static_cast( fmtps[1].parameters.get()); ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_1_params.profile_level_id); ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed); ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode); // H264 packetization mode 0 ASSERT_EQ("97", fmtps[2].format); ASSERT_TRUE(fmtps[2].parameters); ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[2].parameters->codec_type); auto& parsed_h264_0_params = *static_cast( fmtps[2].parameters.get()); ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_0_params.profile_level_id); ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed); ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode); #endif } TEST_F(JsepSessionTest, OfferWithBundleGroupNoTags) { AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); size_t i = offer.find("a=group:BUNDLE"); offer.insert(i, "a=group:BUNDLE\r\n"); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); } static void Replace(const std::string& toReplace, const std::string& with, std::string* in) { size_t pos = in->find(toReplace); ASSERT_NE(std::string::npos, pos); in->replace(pos, toReplace.size(), with); } static void ReplaceAll(const std::string& toReplace, const std::string& with, std::string* in) { while (in->find(toReplace) != std::string::npos) { Replace(toReplace, with, in); } } static void GetCodec(JsepSession& session, size_t transceiverIndex, sdp::Direction direction, size_t encodingIndex, size_t codecIndex, UniquePtr* codecOut) { codecOut->reset(); ASSERT_LT(transceiverIndex, JsepSessionTest::GetTransceivers(session).size()); JsepTransceiver transceiver( JsepSessionTest::GetTransceivers(session)[transceiverIndex]); JsepTrack& track = (direction == sdp::kSend) ? transceiver.mSendTrack : transceiver.mRecvTrack; ASSERT_TRUE(track.GetNegotiatedDetails()); ASSERT_LT(encodingIndex, track.GetNegotiatedDetails()->GetEncodingCount()); ASSERT_LT(codecIndex, track.GetNegotiatedDetails() ->GetEncoding(encodingIndex) .GetCodecs() .size()); codecOut->reset(track.GetNegotiatedDetails() ->GetEncoding(encodingIndex) .GetCodecs()[codecIndex] ->Clone()); } static void ForceH264(JsepSession& session, uint32_t profileLevelId) { for (auto& codec : session.Codecs()) { if (codec->mName == "H264") { JsepVideoCodecDescription* h264 = static_cast(codec.get()); h264->mProfileLevelId = profileLevelId; } else { codec->mEnabled = false; } } } TEST_F(JsepSessionTest, TestH264Negotiation) { ForceH264(*mSessionOff, 0x42e00b); ForceH264(*mSessionAns, 0x42e00d); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); UniquePtr offererSendCodec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00d, offererVideoSendCodec->mProfileLevelId); UniquePtr offererRecvCodec; GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); UniquePtr answererSendCodec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); UniquePtr answererRecvCodec; GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00d, answererVideoRecvCodec->mProfileLevelId); } TEST_F(JsepSessionTest, TestH264NegotiationFails) { ForceH264(*mSessionOff, 0x42000b); ForceH264(*mSessionAns, 0x42e00d); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionOff, 0)); ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionAns, 0)); } TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault) { ForceH264(*mSessionOff, 0x42000d); ForceH264(*mSessionAns, 0x42000d); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); Replace("profile-level-id=42000d", "some-unknown-param=0", &offer); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); UniquePtr answererSendCodec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec.get())); ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId); } TEST_F(JsepSessionTest, TestH264NegotiationOffererNoFmtp) { ForceH264(*mSessionOff, 0x42000d); ForceH264(*mSessionAns, 0x42001e); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); Replace("a=fmtp", "a=oops", &offer); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); UniquePtr answererSendCodec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec.get())); ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId); UniquePtr answererRecvCodec; GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec.get())); ASSERT_EQ((uint32_t)0x420010, answererVideoRecvCodec->mProfileLevelId); } TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByOffererWithLowLevel) { ForceH264(*mSessionOff, 0x42e00b); ForceH264(*mSessionAns, 0x42e00d); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &offer); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); // Offerer doesn't know about the shenanigans we've pulled here, so will // behave normally, and we test the normal behavior elsewhere. UniquePtr answererSendCodec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); UniquePtr answererRecvCodec; GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId); } TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByOffererWithHighLevel) { ForceH264(*mSessionOff, 0x42e00d); ForceH264(*mSessionAns, 0x42e00b); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &offer); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); // Offerer doesn't know about the shenanigans we've pulled here, so will // behave normally, and we test the normal behavior elsewhere. UniquePtr answererSendCodec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); UniquePtr answererRecvCodec; GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId); } TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByAnswererWithLowLevel) { ForceH264(*mSessionOff, 0x42e00d); ForceH264(*mSessionAns, 0x42e00b); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &answer); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); UniquePtr offererSendCodec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); UniquePtr offererRecvCodec; GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); // Answerer doesn't know we've pulled these shenanigans, it should act as if // it did not set level-asymmetry-required, and we already check that // elsewhere } TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByAnswererWithHighLevel) { ForceH264(*mSessionOff, 0x42e00b); ForceH264(*mSessionAns, 0x42e00d); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer(CreateOffer()); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer(CreateAnswer()); Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &answer); SetRemoteAnswer(answer, CHECK_SUCCESS); SetLocalAnswer(answer, CHECK_SUCCESS); UniquePtr offererSendCodec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); UniquePtr offererRecvCodec; GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec.get())); ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); // Answerer doesn't know we've pulled these shenanigans, it should act as if // it did not set level-asymmetry-required, and we already check that // elsewhere } TEST_P(JsepSessionTest, TestRejectMline) { // We need to do this before adding tracks types = BuildTypes(GetParam()); SdpMediaSection::MediaType type = types.front(); switch (type) { case SdpMediaSection::kAudio: // Sabotage audio EnsureNegotiationFailure(types.front(), "opus"); break; case SdpMediaSection::kVideo: // Sabotage video EnsureNegotiationFailure(types.front(), "H264"); break; case SdpMediaSection::kApplication: // Sabotage datachannel EnsureNegotiationFailure(types.front(), "webrtc-datachannel"); break; default: ASSERT_TRUE(false) << "Unknown media type"; } AddTracks(*mSessionOff); AddTracks(*mSessionAns); std::string offer = CreateOffer(); mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); std::string answer = CreateAnswer(); UniquePtr outputSdp(Parse(answer)); ASSERT_TRUE(!!outputSdp); ASSERT_NE(0U, outputSdp->GetMediaSectionCount()); SdpMediaSection* failed_section = nullptr; for (size_t i = 0; i < outputSdp->GetMediaSectionCount(); ++i) { if (outputSdp->GetMediaSection(i).GetMediaType() == type) { failed_section = &outputSdp->GetMediaSection(i); } } ASSERT_TRUE(failed_section) << "Failed type was entirely absent from SDP"; auto& failed_attrs = failed_section->GetAttributeList(); ASSERT_EQ(SdpDirectionAttribute::kInactive, failed_attrs.GetDirection()); ASSERT_EQ(0U, failed_section->GetPort()); mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); size_t numRejected = std::count(types.begin(), types.end(), type); size_t numAccepted = types.size() - numRejected; if (type == SdpMediaSection::MediaType::kApplication) { ASSERT_TRUE(GetDatachannelTransceiver(*mSessionOff)); ASSERT_FALSE( GetDatachannelTransceiver(*mSessionOff)->mRecvTrack.GetActive()); ASSERT_TRUE(GetDatachannelTransceiver(*mSessionAns)); ASSERT_FALSE( GetDatachannelTransceiver(*mSessionAns)->mRecvTrack.GetActive()); } else { ASSERT_EQ(types.size(), GetLocalTracks(*mSessionOff).size()); ASSERT_EQ(numAccepted, GetRemoteTracks(*mSessionOff).size()); ASSERT_EQ(types.size(), GetLocalTracks(*mSessionAns).size()); ASSERT_EQ(types.size(), GetRemoteTracks(*mSessionAns).size()); } } TEST_F(JsepSessionTest, NegotiationNoMlines) { OfferAnswer(); } TEST_F(JsepSessionTest, TestIceLite) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); UniquePtr parsedOffer(Parse(offer)); parsedOffer->GetAttributeList().SetAttribute( new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute)); std::ostringstream os; parsedOffer->Serialize(os); SetRemoteOffer(os.str(), CHECK_SUCCESS); ASSERT_TRUE(mSessionAns->RemoteIsIceLite()); ASSERT_FALSE(mSessionOff->RemoteIsIceLite()); } TEST_F(JsepSessionTest, TestIceOptions) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); ASSERT_EQ(1U, mSessionOff->GetIceOptions().size()); ASSERT_EQ("trickle", mSessionOff->GetIceOptions()[0]); ASSERT_EQ(1U, mSessionAns->GetIceOptions().size()); ASSERT_EQ("trickle", mSessionAns->GetIceOptions()[0]); } TEST_F(JsepSessionTest, TestIceRestart) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); JsepOfferOptions options; options.mIceRestart = Some(true); std::string reoffer = CreateOffer(Some(options)); SetLocalOffer(reoffer, CHECK_SUCCESS); SetRemoteOffer(reoffer, CHECK_SUCCESS); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer, CHECK_SUCCESS); SetRemoteAnswer(reanswer, CHECK_SUCCESS); UniquePtr parsedOffer(Parse(offer)); ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); UniquePtr parsedReoffer(Parse(reoffer)); ASSERT_EQ(1U, parsedReoffer->GetMediaSectionCount()); UniquePtr parsedAnswer(Parse(answer)); ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount()); UniquePtr parsedReanswer(Parse(reanswer)); ASSERT_EQ(1U, parsedReanswer->GetMediaSectionCount()); // verify ice pwd/ufrag are present in offer/answer and reoffer/reanswer auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); auto& reofferMediaAttrs = parsedReoffer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(reofferMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); ASSERT_TRUE(reofferMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); auto& reanswerMediaAttrs = parsedReanswer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(reanswerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); ASSERT_TRUE( reanswerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); // make sure offer/reoffer ice pwd/ufrag changed on ice restart ASSERT_NE(offerMediaAttrs.GetIcePwd().c_str(), reofferMediaAttrs.GetIcePwd().c_str()); ASSERT_NE(offerMediaAttrs.GetIceUfrag().c_str(), reofferMediaAttrs.GetIceUfrag().c_str()); // make sure answer/reanswer ice pwd/ufrag changed on ice restart ASSERT_NE(answerMediaAttrs.GetIcePwd().c_str(), reanswerMediaAttrs.GetIcePwd().c_str()); ASSERT_NE(answerMediaAttrs.GetIceUfrag().c_str(), reanswerMediaAttrs.GetIceUfrag().c_str()); auto offererTransceivers = GetTransceivers(*mSessionOff); auto answererTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(reofferMediaAttrs.GetIceUfrag(), offererTransceivers[0].mTransport.mLocalUfrag); ASSERT_EQ(reofferMediaAttrs.GetIceUfrag(), answererTransceivers[0].mTransport.mIce->GetUfrag()); ASSERT_EQ(reofferMediaAttrs.GetIcePwd(), offererTransceivers[0].mTransport.mLocalPwd); ASSERT_EQ(reofferMediaAttrs.GetIcePwd(), answererTransceivers[0].mTransport.mIce->GetPassword()); ASSERT_EQ(reanswerMediaAttrs.GetIceUfrag(), answererTransceivers[0].mTransport.mLocalUfrag); ASSERT_EQ(reanswerMediaAttrs.GetIceUfrag(), offererTransceivers[0].mTransport.mIce->GetUfrag()); ASSERT_EQ(reanswerMediaAttrs.GetIcePwd(), answererTransceivers[0].mTransport.mLocalPwd); ASSERT_EQ(reanswerMediaAttrs.GetIcePwd(), offererTransceivers[0].mTransport.mIce->GetPassword()); } TEST_F(JsepSessionTest, TestAnswererIndicatingIceRestart) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); // reoffer, but we'll improperly indicate an ice restart in the answer by // modifying the ice pwd and ufrag std::string reoffer = CreateOffer(); SetLocalOffer(reoffer, CHECK_SUCCESS); SetRemoteOffer(reoffer, CHECK_SUCCESS); std::string reanswer = CreateAnswer(); // change the ice pwd and ufrag ReplaceInSdp(&reanswer, "a=ice-ufrag:", "a=ice-ufrag:bad-"); ReplaceInSdp(&reanswer, "a=ice-pwd:", "a=ice-pwd:bad-"); SetLocalAnswer(reanswer, CHECK_SUCCESS); JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, reanswer); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); } TEST_F(JsepSessionTest, TestExtmap) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); // ssrc-audio-level will be extmap 1 for both // csrc-audio-level will be 2 for both // mid will be 3 for both // video related extensions take 4 - 7 mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 mSessionAns->AddAudioRtpExtension("bar"); // Default mapping of 8 std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); UniquePtr parsedOffer(Parse(offer)); ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(5U, offerExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", offerExtmap[0].extensionname); ASSERT_EQ(1U, offerExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", offerExtmap[1].extensionname); ASSERT_EQ(2U, offerExtmap[1].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", offerExtmap[2].extensionname); ASSERT_EQ(3U, offerExtmap[2].entry); ASSERT_EQ("foo", offerExtmap[3].extensionname); ASSERT_EQ(8U, offerExtmap[3].entry); ASSERT_EQ("bar", offerExtmap[4].extensionname); ASSERT_EQ(9U, offerExtmap[4].entry); UniquePtr parsedAnswer(Parse(answer)); ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount()); auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& answerExtmap = answerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(3U, answerExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", answerExtmap[0].extensionname); ASSERT_EQ(1U, answerExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", answerExtmap[1].extensionname); ASSERT_EQ(3U, answerExtmap[1].entry); // We ensure that the entry for "bar" matches what was in the offer ASSERT_EQ("bar", answerExtmap[2].extensionname); ASSERT_EQ(9U, answerExtmap[2].entry); } TEST_F(JsepSessionTest, TestExtmapDefaults) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); UniquePtr parsedOffer(Parse(offer)); ASSERT_EQ(2U, parsedOffer->GetMediaSectionCount()); auto& offerAudioMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE( offerAudioMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerAudioExtmap = offerAudioMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(3U, offerAudioExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", offerAudioExtmap[0].extensionname); ASSERT_EQ(1U, offerAudioExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", offerAudioExtmap[1].extensionname); ASSERT_EQ(2U, offerAudioExtmap[1].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", offerAudioExtmap[2].extensionname); auto& offerVideoMediaAttrs = parsedOffer->GetMediaSection(1).GetAttributeList(); ASSERT_TRUE( offerVideoMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerVideoExtmap = offerVideoMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(5U, offerVideoExtmap.size()); ASSERT_EQ(3U, offerVideoExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", offerVideoExtmap[0].extensionname); ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", offerVideoExtmap[1].extensionname); ASSERT_EQ(4U, offerVideoExtmap[1].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", offerVideoExtmap[2].extensionname); ASSERT_EQ(5U, offerVideoExtmap[2].entry); ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", offerVideoExtmap[3].extensionname); ASSERT_EQ(6U, offerVideoExtmap[3].entry); ASSERT_EQ( "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" "extensions-01", offerVideoExtmap[4].extensionname); ASSERT_EQ(7U, offerVideoExtmap[4].entry); UniquePtr parsedAnswer(Parse(answer)); ASSERT_EQ(2U, parsedAnswer->GetMediaSectionCount()); auto& answerAudioMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE( answerAudioMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& answerAudioExtmap = answerAudioMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(2U, answerAudioExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", answerAudioExtmap[0].extensionname); ASSERT_EQ(1U, answerAudioExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", answerAudioExtmap[1].extensionname); ASSERT_EQ(3U, answerAudioExtmap[1].entry); auto& answerVideoMediaAttrs = parsedAnswer->GetMediaSection(1).GetAttributeList(); ASSERT_TRUE( answerVideoMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& answerVideoExtmap = answerVideoMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(4U, answerVideoExtmap.size()); ASSERT_EQ(3U, answerVideoExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", answerVideoExtmap[0].extensionname); ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", answerVideoExtmap[1].extensionname); ASSERT_EQ(4U, answerVideoExtmap[1].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", answerVideoExtmap[2].extensionname); ASSERT_EQ(5U, answerVideoExtmap[2].entry); ASSERT_EQ( "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" "extensions-01", answerVideoExtmap[3].extensionname); ASSERT_EQ(7U, answerVideoExtmap[3].entry); } TEST_F(JsepSessionTest, TestExtmapWithDuplicates) { AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); // ssrc-audio-level will be extmap 1 for both // csrc-audio-level will be 2 for both // mid will be 3 for both // video related extensions take 4 - 7 mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored mSessionOff->AddAudioRtpExtension("baz"); // Default mapping of 10 mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored std::string offer = CreateOffer(); UniquePtr parsedOffer(Parse(offer)); ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(6U, offerExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", offerExtmap[0].extensionname); ASSERT_EQ(1U, offerExtmap[0].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", offerExtmap[1].extensionname); ASSERT_EQ(2U, offerExtmap[1].entry); ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", offerExtmap[2].extensionname); ASSERT_EQ(3U, offerExtmap[2].entry); ASSERT_EQ("foo", offerExtmap[3].extensionname); ASSERT_EQ(8U, offerExtmap[3].entry); ASSERT_EQ("bar", offerExtmap[4].extensionname); ASSERT_EQ(9U, offerExtmap[4].entry); ASSERT_EQ("baz", offerExtmap[5].extensionname); ASSERT_EQ(10U, offerExtmap[5].entry); } TEST_F(JsepSessionTest, TestExtmapZeroId) { AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string sdp = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" "c=IN IP4 0\r\n" "a=rtpmap:100 VP8/90000\r\n" "a=extmap:0 urn:ietf:params:rtp-hdrext:toffset\r\n"; auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); ASSERT_EQ( "Description contains invalid extension id 0 on level 0 which is" " unsupported until 2-byte rtp header extensions are supported in" " webrtc.org", mSessionAns->GetLastError()); } TEST_F(JsepSessionTest, TestExtmapInvalidId) { AddTracks(*mSessionAns, "video"); std::string sdp = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" "c=IN IP4 0\r\n" "a=rtpmap:100 VP8/90000\r\n" "a=extmap:15 urn:ietf:params:rtp-hdrext:toffset\r\n"; auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); ASSERT_EQ( "Description contains invalid extension id 15 on level 0 which is" " unsupported until 2-byte rtp header extensions are supported in" " webrtc.org", mSessionAns->GetLastError()); } TEST_F(JsepSessionTest, TestExtmapDuplicateId) { AddTracks(*mSessionAns, "video"); std::string sdp = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" "c=IN IP4 0\r\n" "a=rtpmap:100 VP8/90000\r\n" "a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n" "a=extmap:2 " "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n"; auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); ASSERT_EQ("Description contains duplicate extension id 2 on level 0", mSessionAns->GetLastError()); } TEST_F(JsepSessionTest, TestNegotiatedExtmapStability) { AddTracks(*mSessionAns, "audio,video"); std::string sdp = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" "c=IN IP4 51.81.107.13\r\n" "a=sendrecv\r\n" "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" "a=mid:audio\r\n" "a=rtcp-mux\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=setup:active\r\n" "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n" "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" "c=IN IP4 0\r\n" "a=sendrecv\r\n" "a=rtpmap:100 VP8/90000\r\n" "a=extmap:12 urn:ietf:params:rtp-hdrext:toffset\r\n" "a=extmap:13 " "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n"; auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_FALSE(result.mError.isSome()); auto answer = CreateAnswer(); result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); ASSERT_FALSE(result.mError.isSome()); // Verify that we've negotiated the right extmap based on the unusual values // in the offer. auto transceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(2U, transceivers.size()); auto* audioSend = transceivers[0].mSendTrack.GetNegotiatedDetails(); auto* audioRecv = transceivers[0].mRecvTrack.GetNegotiatedDetails(); auto* videoSend = transceivers[1].mSendTrack.GetNegotiatedDetails(); auto* videoRecv = transceivers[1].mRecvTrack.GetNegotiatedDetails(); ASSERT_TRUE(audioSend); ASSERT_TRUE(audioRecv); ASSERT_TRUE(videoSend); ASSERT_TRUE(videoRecv); ASSERT_EQ( 11U, audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); ASSERT_EQ( 11U, audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); ASSERT_EQ(12U, videoSend->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); ASSERT_EQ(12U, videoRecv->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); ASSERT_EQ( 13U, videoSend ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") ->entry); ASSERT_EQ( 13U, videoRecv ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") ->entry); SwapOfferAnswerRoles(); // Make sure a reoffer uses the negotiated extmap auto reoffer = CreateOffer(); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:11")); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:12")); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:13")); } TEST_F(JsepSessionTest, TestNegotiatedExtmapCollision) { AddTracks(*mSessionAns, "audio"); // ssrc-audio-level will be extmap 1 for both // csrc-audio-level will be 2 for both // mid will be 3 for both mSessionAns->AddAudioRtpExtension("foo"); mSessionAns->AddAudioRtpExtension("bar"); mSessionAns->AddAudioRtpExtension("baz"); // Set up an offer that uses the same extmap entries, but for different // things, causing collisions. std::string sdp = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" "c=IN IP4 51.81.107.13\r\n" "a=sendrecv\r\n" "a=extmap:1 foo\r\n" "a=extmap:2 bar\r\n" "a=extmap:3 baz\r\n" "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" "a=mid:audio\r\n" "a=rtcp-mux\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=setup:active\r\n" "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n"; auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_FALSE(result.mError.isSome()); auto answer = CreateAnswer(); result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); ASSERT_FALSE(result.mError.isSome()); // Verify that we've negotiated the right extmap based on the unusual values // in the offer. auto transceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(1U, transceivers.size()); auto* audioSend = transceivers[0].mSendTrack.GetNegotiatedDetails(); auto* audioRecv = transceivers[0].mRecvTrack.GetNegotiatedDetails(); ASSERT_TRUE(audioSend); ASSERT_TRUE(audioRecv); ASSERT_EQ(1U, audioSend->GetExt("foo")->entry); ASSERT_EQ(1U, audioRecv->GetExt("foo")->entry); ASSERT_EQ(2U, audioSend->GetExt("bar")->entry); ASSERT_EQ(2U, audioRecv->GetExt("bar")->entry); ASSERT_EQ(3U, audioSend->GetExt("baz")->entry); ASSERT_EQ(3U, audioRecv->GetExt("baz")->entry); ASSERT_EQ( 11U, audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); ASSERT_EQ( 11U, audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); SwapOfferAnswerRoles(); // Make sure a reoffer uses the negotiated extmap auto reoffer = CreateOffer(); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:1 foo")); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:2 bar")); ASSERT_NE(std::string::npos, reoffer.find("a=extmap:3 baz")); ASSERT_NE( std::string::npos, reoffer.find("a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level")); // Ensure no duplicates ASSERT_EQ(std::string::npos, reoffer.find("a=extmap:1 ", reoffer.find("a=extmap:1 ") + 1)); ASSERT_EQ(std::string::npos, reoffer.find("a=extmap:2 ", reoffer.find("a=extmap:2 ") + 1)); ASSERT_EQ(std::string::npos, reoffer.find("a=extmap:3 ", reoffer.find("a=extmap:3 ") + 1)); ASSERT_EQ(std::string::npos, reoffer.find("a=extmap:11 ", reoffer.find("a=extmap:11 ") + 1)); } TEST_F(JsepSessionTest, TestExtmapAnswerChangesId) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer, ALL_CHECKS); SetRemoteOffer(offer, ALL_CHECKS); std::string answer = CreateAnswer(); std::string mungedAnswer = SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14); JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, mungedAnswer); ASSERT_TRUE(result.mError.isSome()); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); ASSERT_NE(mSessionOff->GetLastError().find( "Answer changed id for extmap attribute at level 0"), std::string::npos); } TEST_F(JsepSessionTest, TestExtmapChangeId) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); // RFC 5285 does not seem to forbid giving a pre-existing extension a new id, // as long as that id has never been used before { std::string offer = CreateOffer(); SetLocalOffer(offer, ALL_CHECKS); uint16_t oldId = 0; std::string mungedOffer = SetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14, &oldId); ASSERT_NE(oldId, 0); SetRemoteOffer(mungedOffer, ALL_CHECKS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, ALL_CHECKS); std::string mungedAnswer = SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", oldId); SetRemoteAnswer(mungedAnswer, ALL_CHECKS); } // Make sure going back to the previous id works OfferAnswer(); } TEST_F(JsepSessionTest, TestExtmapSwap) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); std::string offer = CreateOffer(); uint16_t midId = GetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid"); uint16_t ssrcLevelId = 0; std::string mungedOffer = SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId, &ssrcLevelId); mungedOffer = SetExtmap(mungedOffer, "urn:ietf:params:rtp-hdrext:sdes:mid", ssrcLevelId); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, mungedOffer); ASSERT_TRUE(result.mError.isSome()); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); ASSERT_NE(mSessionAns->GetLastError().find( "Remote description attempted to remap RTP extension id"), std::string::npos); } TEST_F(JsepSessionTest, TestExtmapReuse) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); std::string offer = CreateOffer(); UniquePtr munge(Parse(offer)); ASSERT_EQ(1U, munge->GetMediaSectionCount()); auto& offerMediaAttrs = munge->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto offerExtmap = offerMediaAttrs.GetExtmap(); for (auto& ext : offerExtmap.mExtmaps) { if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { ext.extensionname = "foo"; } } offerMediaAttrs.SetAttribute(offerExtmap.Clone()); std::string sdpString = munge->ToString(); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdpString); ASSERT_TRUE(result.mError.isSome()); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); ASSERT_NE(mSessionAns->GetLastError().find( "Remote description attempted to remap RTP extension id"), std::string::npos); } TEST_F(JsepSessionTest, TestExtmapReuseAfterRenegotiation) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); // Renegotiate without ssrc-audio-level { std::string offer = CreateOffer(); SetLocalOffer(offer, ALL_CHECKS); // Passing 0 removes urn:ietf:params:rtp-hdrext:ssrc-audio-level std::string mungedOffer = SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 0); SetRemoteOffer(mungedOffer, ALL_CHECKS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, ALL_CHECKS); SetRemoteAnswer(answer, ALL_CHECKS); } // Make sure trying to reuse the id for ssrc-audio-level fails, even though we // did not use it last round. { std::string offer = CreateOffer(); UniquePtr munge(Parse(offer)); ASSERT_EQ(1U, munge->GetMediaSectionCount()); auto& offerMediaAttrs = munge->GetMediaSection(0).GetAttributeList(); ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto offerExtmap = offerMediaAttrs.GetExtmap(); for (auto& ext : offerExtmap.mExtmaps) { if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { ext.extensionname = "foo"; } } offerMediaAttrs.SetAttribute(offerExtmap.Clone()); std::string sdpString = munge->ToString(); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdpString); ASSERT_TRUE(result.mError.isSome()); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); ASSERT_NE(mSessionAns->GetLastError().find( "Remote description attempted to remap RTP extension id"), std::string::npos); } } TEST_F(JsepSessionTest, TestRtcpFbStar) { AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer = CreateOffer(); UniquePtr parsedOffer(Parse(offer)); auto* rtcpfbs = new SdpRtcpFbAttributeList; rtcpfbs->PushEntry("*", SdpRtcpFbAttributeList::kNack); parsedOffer->GetMediaSection(0).GetAttributeList().SetAttribute(rtcpfbs); offer = parsedOffer->ToString(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); ASSERT_EQ(1U, GetRemoteTracks(*mSessionAns).size()); JsepTrack track = GetRemoteTracks(*mSessionAns)[0]; ASSERT_TRUE(track.GetNegotiatedDetails()); auto* details = track.GetNegotiatedDetails(); for (const auto& codec : details->GetEncoding(0).GetCodecs()) { const JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); ASSERT_EQ(1U, videoCodec->mNackFbTypes.size()); ASSERT_EQ("", videoCodec->mNackFbTypes[0]); } } TEST_F(JsepSessionTest, TestUniquePayloadTypes) { // The audio payload types will all appear more than once, but the video // payload types will be unique. AddTracks(*mSessionOff, "audio,audio,video"); AddTracks(*mSessionAns, "audio,audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); auto offerTransceivers = GetTransceivers(*mSessionOff); auto answerTransceivers = GetTransceivers(*mSessionAns); ASSERT_EQ(3U, offerTransceivers.size()); ASSERT_EQ(3U, answerTransceivers.size()); ASSERT_FALSE(IsNull(offerTransceivers[0].mRecvTrack)); ASSERT_TRUE(offerTransceivers[0].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(0U, offerTransceivers[0] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(0U, offerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); ASSERT_FALSE(IsNull(offerTransceivers[2].mRecvTrack)); ASSERT_TRUE(offerTransceivers[2].mRecvTrack.GetNegotiatedDetails()); ASSERT_NE(0U, offerTransceivers[2] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); ASSERT_FALSE(IsNull(answerTransceivers[0].mRecvTrack)); ASSERT_TRUE(answerTransceivers[0].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(0U, answerTransceivers[0] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); ASSERT_EQ(0U, answerTransceivers[1] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); ASSERT_FALSE(IsNull(answerTransceivers[2].mRecvTrack)); ASSERT_TRUE(answerTransceivers[2].mRecvTrack.GetNegotiatedDetails()); ASSERT_NE(0U, answerTransceivers[2] .mRecvTrack.GetNegotiatedDetails() ->GetUniquePayloadTypes() .size()); } TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer(CreateOffer()); SetLocalOffer(offer); ReplaceAll("fingerprint:sha", "fingerprint:foo", &offer); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); ASSERT_NE("", mSessionAns->GetLastError()); } TEST(H264ProfileLevelIdTest, TestLevelComparisons) { ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x421D0B), // 1b JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1 ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0 JsepVideoCodecDescription::GetSaneH264Level(0x421D0B)); // 1b ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0 JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1 ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x640009), // 1b JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1 ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0 JsepVideoCodecDescription::GetSaneH264Level(0x640009)); // 1b ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0 JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1 } TEST(H264ProfileLevelIdTest, TestLevelSetting) { uint32_t profileLevelId = 0x420D0A; JsepVideoCodecDescription::SetSaneH264Level( JsepVideoCodecDescription::GetSaneH264Level(0x42100B), &profileLevelId); ASSERT_EQ((uint32_t)0x421D0B, profileLevelId); JsepVideoCodecDescription::SetSaneH264Level( JsepVideoCodecDescription::GetSaneH264Level(0x42000A), &profileLevelId); ASSERT_EQ((uint32_t)0x420D0A, profileLevelId); profileLevelId = 0x6E100A; JsepVideoCodecDescription::SetSaneH264Level( JsepVideoCodecDescription::GetSaneH264Level(0x640009), &profileLevelId); ASSERT_EQ((uint32_t)0x6E1009, profileLevelId); JsepVideoCodecDescription::SetSaneH264Level( JsepVideoCodecDescription::GetSaneH264Level(0x64000B), &profileLevelId); ASSERT_EQ((uint32_t)0x6E100B, profileLevelId); } TEST_F(JsepSessionTest, StronglyPreferredCodec) { for (auto& codec : mSessionAns->Codecs()) { if (codec->mName == "H264") { codec->mStronglyPreferred = true; } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); OfferAnswer(); UniquePtr codec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("H264", codec->mName); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("H264", codec->mName); } TEST_F(JsepSessionTest, LowDynamicPayloadType) { SetPayloadTypeNumber(*mSessionOff, "opus", "12"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); UniquePtr codec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("12", codec->mDefaultPt); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("12", codec->mDefaultPt); } TEST_F(JsepSessionTest, TestOfferPTAsymmetry) { SetPayloadTypeNumber(*mSessionAns, "opus", "105"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); JsepOfferOptions options; // Ensure that mSessionAns is appropriately configured. Also ensure that // creating an offer with 105 does not prompt mSessionAns to ignore the // PT in the offer. std::string offer; JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_NE(std::string::npos, offer.find("a=rtpmap:105 opus")) << offer; OfferAnswer(); // Answerer should use what the offerer suggested UniquePtr codec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("109", codec->mDefaultPt); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("109", codec->mDefaultPt); // Answerer should not change when it reoffers result = mSessionAns->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_NE(std::string::npos, offer.find("a=rtpmap:109 opus")) << offer; } TEST_F(JsepSessionTest, TestAnswerPTAsymmetry) { // JsepSessionImpl will never answer with an asymmetric payload type // (tested in TestOfferPTAsymmetry), so we have to rewrite SDP a little. types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); Replace("a=rtpmap:109 opus", "a=rtpmap:105 opus", &offer); Replace("m=audio 9 UDP/TLS/RTP/SAVPF 109", "m=audio 9 UDP/TLS/RTP/SAVPF 105", &offer); ReplaceAll("a=fmtp:109", "a=fmtp:105", &offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); UniquePtr codec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("105", codec->mDefaultPt); GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("109", codec->mDefaultPt); GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("105", codec->mDefaultPt); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("105", codec->mDefaultPt); // Offerer should use 105 for reoffers offer = CreateOffer(); ASSERT_NE(std::string::npos, offer.find("a=rtpmap:105 opus")) << offer; ASSERT_EQ(std::string::npos, offer.find("a=rtpmap:109 opus")) << offer; ASSERT_NE(std::string::npos, offer.find("a=fmtp:105")) << offer; ASSERT_EQ(std::string::npos, offer.find("a=fmtp:109")) << offer; } TEST_F(JsepSessionTest, PayloadTypeClash) { // Set up a scenario where mSessionAns' favorite codec (opus) is unsupported // by mSessionOff, and mSessionOff uses that payload type for something else. SetCodecEnabled(*mSessionOff, "opus", false); SetPayloadTypeNumber(*mSessionOff, "opus", "0"); SetPayloadTypeNumber(*mSessionOff, "G722", "109"); SetPayloadTypeNumber(*mSessionAns, "opus", "109"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); UniquePtr codec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("G722", codec->mName); ASSERT_EQ("109", codec->mDefaultPt); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("G722", codec->mName); ASSERT_EQ("109", codec->mDefaultPt); // Now, make sure that mSessionAns does not put a=rtpmap:109 opus in a // reoffer, since pt 109 is taken for PCMU (the answerer still supports opus, // and will reoffer it, but it should choose a new payload type for it) JsepOfferOptions options; std::string reoffer; JsepSession::Result result = mSessionAns->CreateOffer(options, &reoffer); ASSERT_FALSE(result.mError.isSome()); ASSERT_EQ(std::string::npos, reoffer.find("a=rtpmap:109 opus")) << reoffer; ASSERT_NE(std::string::npos, reoffer.find(" opus")) << reoffer; } TEST_P(JsepSessionTest, TestGlareRollback) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); JsepOfferOptions options; std::string offer; ASSERT_FALSE(mSessionAns->CreateOffer(options, &offer).mError.isSome()); ASSERT_FALSE( mSessionAns->SetLocalDescription(kJsepSdpOffer, offer).mError.isSome()); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionAns->GetState()); ASSERT_FALSE(mSessionOff->CreateOffer(options, &offer).mError.isSome()); ASSERT_FALSE( mSessionOff->SetLocalDescription(kJsepSdpOffer, offer).mError.isSome()); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer).mError); ASSERT_FALSE( mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); } TEST_P(JsepSessionTest, TestRejectOfferRollback) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); ASSERT_FALSE( mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError.isSome()); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); for (const auto& transceiver : GetTransceivers(*mSessionAns)) { ASSERT_EQ(0U, transceiver.mRecvTrack.GetStreamIds().size()); } ASSERT_FALSE( mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); OfferAnswer(); } TEST_P(JsepSessionTest, TestInvalidRollback) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); std::string offer = CreateOffer(); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); SetLocalOffer(offer); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); SetRemoteOffer(offer); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); std::string answer = CreateAnswer(); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); SetLocalAnswer(answer); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError); SetRemoteAnswer(answer); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); ASSERT_EQ(dom::PCError::InvalidStateError, *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); } size_t GetActiveTransportCount(const JsepSession& session) { size_t activeTransportCount = 0; for (const auto& transceiver : JsepSessionTest::GetTransceivers(session)) { if (!transceiver.HasBundleLevel() || (transceiver.BundleLevel() == transceiver.GetLevel())) { activeTransportCount += transceiver.mTransport.mComponents; } } return activeTransportCount; } TEST_P(JsepSessionTest, TestBalancedBundle) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); mSessionOff->SetBundlePolicy(kBundleBalanced); std::string offer = CreateOffer(); UniquePtr parsedOffer = std::move(SipccSdpParser().Parse(offer)->Sdp()); ASSERT_TRUE(parsedOffer.get()); std::map firstByType; for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { SdpMediaSection& msection(parsedOffer->GetMediaSection(i)); bool firstOfType = !firstByType.count(msection.GetMediaType()); if (firstOfType) { firstByType[msection.GetMediaType()] = &msection; } ASSERT_EQ(!firstOfType, msection.GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute)); } SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers"); CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers"); EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff)); EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns)); } TEST_P(JsepSessionTest, TestMaxBundle) { AddTracks(*mSessionOff); AddTracks(*mSessionAns); mSessionOff->SetBundlePolicy(kBundleMaxBundle); std::string offer = CreateOffer(); UniquePtr parsedOffer = std::move(SipccSdpParser().Parse(offer)->Sdp()); ASSERT_TRUE(parsedOffer.get()); ASSERT_FALSE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute)); ASSERT_NE(0U, parsedOffer->GetMediaSection(0).GetPort()); for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) { ASSERT_TRUE(parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute)); ASSERT_EQ(0U, parsedOffer->GetMediaSection(i).GetPort()); } SetLocalOffer(offer); for (const auto& transceiver : GetTransceivers(*mSessionOff)) { if (transceiver.GetLevel() == 0) { // We do not set the bundle-level in have-local-offer unless the // m-section is bundle-only. ASSERT_FALSE(transceiver.HasBundleLevel()); } else { ASSERT_TRUE(transceiver.HasBundleLevel()); ASSERT_EQ(0U, transceiver.BundleLevel()); } ASSERT_NE("", transceiver.mTransport.mTransportId); } SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers"); CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers"); EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff)); EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns)); } TEST_F(JsepSessionTest, TestNonDefaultProtocol) { AddTracks(*mSessionOff, "audio,video,datachannel"); AddTracks(*mSessionAns, "audio,video,datachannel"); std::string offer; ASSERT_FALSE( mSessionOff->CreateOffer(JsepOfferOptions(), &offer).mError.isSome()); offer.replace(offer.find("UDP/TLS/RTP/SAVPF"), strlen("UDP/TLS/RTP/SAVPF"), "RTP/SAVPF"); offer.replace(offer.find("UDP/TLS/RTP/SAVPF"), strlen("UDP/TLS/RTP/SAVPF"), "RTP/SAVPF"); mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); std::string answer; mSessionAns->CreateAnswer(JsepAnswerOptions(), &answer); UniquePtr parsedAnswer = Parse(answer); ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedAnswer->GetMediaSection(0).GetProtocol()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedAnswer->GetMediaSection(1).GetProtocol()); mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); // Make sure reoffer uses the same protocol as before mSessionOff->CreateOffer(JsepOfferOptions(), &offer); UniquePtr parsedOffer = Parse(offer); ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedOffer->GetMediaSection(0).GetProtocol()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedOffer->GetMediaSection(1).GetProtocol()); // Make sure reoffer from other side uses the same protocol as before mSessionAns->CreateOffer(JsepOfferOptions(), &offer); parsedOffer = Parse(offer); ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedOffer->GetMediaSection(0).GetProtocol()); ASSERT_EQ(SdpMediaSection::kRtpSavpf, parsedOffer->GetMediaSection(1).GetProtocol()); } TEST_F(JsepSessionTest, CreateOfferNoVideoStreamRecvVideo) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferNoAudioStreamRecvAudio) { types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferNoVideoStream) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(0U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferNoAudioStream) { types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(0U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferDontReceiveAudio) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(0U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferDontReceiveVideo) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(0U)); CreateOffer(Some(options)); } TEST_F(JsepSessionTest, CreateOfferRemoveAudioTrack) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); SetDirection(*mSessionOff, 1, SdpDirectionAttribute::kSendonly); JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); ASSERT_FALSE(IsNull(removedTrack)); CreateOffer(); } TEST_F(JsepSessionTest, CreateOfferDontReceiveAudioRemoveAudioTrack) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly); JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); ASSERT_FALSE(IsNull(removedTrack)); CreateOffer(); } TEST_F(JsepSessionTest, CreateOfferDontReceiveVideoRemoveVideoTrack) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(0U)); JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); ASSERT_FALSE(IsNull(removedTrack)); CreateOffer(Some(options)); } static const std::string strSampleCandidate = "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n"; static const unsigned short nSamplelevel = 2; TEST_F(JsepSessionTest, CreateOfferAddCandidate) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); uint16_t level; std::string mid; bool skipped; nsresult rv; rv = mSessionOff->AddLocalIceCandidate(strSampleCandidate, GetTransportId(*mSessionOff, 0), "", &level, &mid, &skipped); ASSERT_EQ(NS_OK, rv); } TEST_F(JsepSessionTest, AddIceCandidateEarly) { uint16_t level; std::string mid; bool skipped; nsresult rv; rv = mSessionOff->AddLocalIceCandidate(strSampleCandidate, GetTransportId(*mSessionOff, 0), "", &level, &mid, &skipped); // This can't succeed without a local description ASSERT_NE(NS_OK, rv); } TEST_F(JsepSessionTest, OfferAnswerDontAddAudioStreamOnAnswerNoOptions) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); std::string offer = CreateOffer(Some(options)); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); } TEST_F(JsepSessionTest, OfferAnswerDontAddVideoStreamOnAnswerNoOptions) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); CreateOffer(Some(options)); std::string offer = CreateOffer(Some(options)); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); } TEST_F(JsepSessionTest, OfferAnswerDontAddAudioVideoStreamsOnAnswerNoOptions) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); OfferAnswer(); } TEST_F(JsepSessionTest, OfferAndAnswerWithExtraCodec) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); UniquePtr munge = Parse(answer); SdpMediaSection& mediaSection = munge->GetMediaSection(0); mediaSection.AddCodec("8", "PCMA", 8000, 1); std::string sdpString = munge->ToString(); SetLocalAnswer(sdpString); SetRemoteAnswer(answer); } TEST_F(JsepSessionTest, AddCandidateInHaveLocalOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string mid; std::string transportId; JsepSession::Result result = mSessionOff->AddRemoteIceCandidate( strSampleCandidate, mid, Some(nSamplelevel), "", &transportId); ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); } TEST_F(JsepSessionTest, SetLocalWithoutCreateOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); JsepSession::Result result = mSessionAns->SetLocalDescription(kJsepSdpOffer, offer); ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); } TEST_F(JsepSessionTest, SetLocalWithoutCreateAnswer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetRemoteOffer(offer); JsepSession::Result result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, offer); ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); } // Test for Bug 843595 TEST_F(JsepSessionTest, missingUfrag) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); std::string ufrag = "ice-ufrag"; std::size_t pos = offer.find(ufrag); ASSERT_NE(pos, std::string::npos); offer.replace(pos, ufrag.length(), "ice-ufrog"); JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); } TEST_F(JsepSessionTest, AudioOnlyCalleeNoRtcpMux) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); std::string rtcp_mux = "a=rtcp-mux\r\n"; std::size_t pos = offer.find(rtcp_mux); ASSERT_NE(pos, std::string::npos); offer.replace(pos, rtcp_mux.length(), ""); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); pos = answer.find(rtcp_mux); ASSERT_EQ(pos, std::string::npos); } // This test comes from Bug 810220 TEST_F(JsepSessionTest, AudioOnlyG711Call) { std::string offer = "v=0\r\n" "o=- 1 1 IN IP4 148.147.200.251\r\n" "s=-\r\n" "b=AS:64\r\n" "t=0 0\r\n" "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" "m=audio 9000 UDP/TLS/RTP/SAVPF 0 8 126\r\n" "c=IN IP4 148.147.200.251\r\n" "b=TIAS:64000\r\n" "a=rtpmap:0 PCMU/8000\r\n" "a=rtpmap:8 PCMA/8000\r\n" "a=rtpmap:126 telephone-event/8000\r\n" "a=candidate:0 1 udp 2130706432 148.147.200.251 9000 typ host\r\n" "a=candidate:0 2 udp 2130706432 148.147.200.251 9005 typ host\r\n" "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" "a=setup:active\r\n" "a=sendrecv\r\n"; types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); // They didn't offer opus, so our answer shouldn't include it. ASSERT_EQ(answer.find(" opus/"), std::string::npos); // They also didn't offer video or application ASSERT_EQ(answer.find("video"), std::string::npos); ASSERT_EQ(answer.find("application"), std::string::npos); // We should answer with PCMU and telephone-event ASSERT_NE(answer.find(" PCMU/8000"), std::string::npos); // Double-check the directionality ASSERT_NE(answer.find("\r\na=sendrecv"), std::string::npos); } TEST_F(JsepSessionTest, AudioOnlyG722Only) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string audio = "m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n"; std::size_t pos = offer.find(audio); ASSERT_NE(pos, std::string::npos); offer.replace(pos, audio.length(), "m=audio 65375 UDP/TLS/RTP/SAVPF 9\r\n"); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("UDP/TLS/RTP/SAVPF 9\r"), std::string::npos); ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("a=rtpmap:9 G722/8000"), std::string::npos); } TEST_F(JsepSessionTest, AudioOnlyG722Rejected) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string audio = "m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n"; std::size_t pos = offer.find(audio); ASSERT_NE(pos, std::string::npos); offer.replace(pos, audio.length(), "m=audio 65375 UDP/TLS/RTP/SAVPF 0 8\r\n"); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("UDP/TLS/RTP/SAVPF 0 8\r"), std::string::npos); ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("a=rtpmap:0 PCMU/8000"), std::string::npos); ASSERT_EQ(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("a=rtpmap:109 opus/48000/2"), std::string::npos); ASSERT_EQ(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("a=rtpmap:9 G722/8000"), std::string::npos); } // This test doesn't make sense for bundle TEST_F(JsepSessionTest, DISABLED_FullCallAudioNoMuxVideoMux) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string rtcp_mux = "a=rtcp-mux\r\n"; std::size_t pos = offer.find(rtcp_mux); ASSERT_NE(pos, std::string::npos); offer.replace(pos, rtcp_mux.length(), ""); SetRemoteOffer(offer); std::string answer = CreateAnswer(); size_t match = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("\r\na=rtcp-mux"); ASSERT_NE(match, std::string::npos); match = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) .find("\r\na=rtcp-mux", match + 1); ASSERT_EQ(match, std::string::npos); } // Disabled pending resolution of bug 818640. // Actually, this test is completely broken; you can't just call // SetRemote/CreateAnswer over and over again. TEST_F(JsepSessionTest, DISABLED_OfferAllDynamicTypes) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); std::string offer; for (int i = 96; i < 128; i++) { std::stringstream ss; ss << i; std::cout << "Trying dynamic pt = " << i << std::endl; offer = "v=0\r\n" "o=- 1 1 IN IP4 148.147.200.251\r\n" "s=-\r\n" "b=AS:64\r\n" "t=0 0\r\n" "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" "m=audio 9000 RTP/AVP " + ss.str() + "\r\n" "c=IN IP4 148.147.200.251\r\n" "b=TIAS:64000\r\n" "a=rtpmap:" + ss.str() + " opus/48000/2\r\n" "a=candidate:0 1 udp 2130706432 148.147.200.251 9000 typ host\r\n" "a=candidate:0 2 udp 2130706432 148.147.200.251 9005 typ host\r\n" "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" "a=sendrecv\r\n"; SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); ASSERT_NE(answer.find(ss.str() + " opus/"), std::string::npos); } } TEST_F(JsepSessionTest, ipAddrAnyOffer) { std::string offer = "v=0\r\n" "o=- 1 1 IN IP4 127.0.0.1\r\n" "s=-\r\n" "b=AS:64\r\n" "t=0 0\r\n" "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" "m=audio 9000 UDP/TLS/RTP/SAVPF 99\r\n" "c=IN IP4 0.0.0.0\r\n" "a=rtpmap:99 opus/48000/2\r\n" "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" "a=setup:active\r\n" "a=sendrecv\r\n"; types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); ASSERT_NE(answer.find("a=sendrecv"), std::string::npos); } static void CreateSDPForBigOTests(std::string& offer, const std::string& number) { offer = "v=0\r\n" "o=- "; offer += number; offer += " "; offer += number; offer += " IN IP4 127.0.0.1\r\n" "s=-\r\n" "b=AS:64\r\n" "t=0 0\r\n" "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" "m=audio 9000 RTP/AVP 99\r\n" "c=IN IP4 0.0.0.0\r\n" "a=rtpmap:99 opus/48000/2\r\n" "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" "a=setup:active\r\n" "a=sendrecv\r\n"; } TEST_F(JsepSessionTest, BigOValues) { std::string offer; CreateSDPForBigOTests(offer, "12345678901234567"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); SetRemoteOffer(offer, CHECK_SUCCESS); } TEST_F(JsepSessionTest, BigOValuesExtraChars) { std::string offer; CreateSDPForBigOTests(offer, "12345678901234567FOOBAR"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); // The signaling state will remain "stable" because the unparsable // SDP leads to a failure in SetRemoteDescription. SetRemoteOffer(offer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } TEST_F(JsepSessionTest, BigOValuesTooBig) { std::string offer; CreateSDPForBigOTests(offer, "18446744073709551615"); types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionAns, "audio"); // The signaling state will remain "stable" because the unparsable // SDP leads to a failure in SetRemoteDescription. SetRemoteOffer(offer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } TEST_F(JsepSessionTest, SetLocalAnswerInStable) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); // The signaling state will remain "stable" because the // SetLocalDescription call fails. SetLocalAnswer(offer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); } TEST_F(JsepSessionTest, SetRemoteAnswerInStable) { const std::string answer = "v=0\r\n" "o=Mozilla-SIPUA 4949 0 IN IP4 10.86.255.143\r\n" "s=SIP Call\r\n" "t=0 0\r\n" "a=ice-ufrag:qkEP\r\n" "a=ice-pwd:ed6f9GuHjLcoCN6sC/Eh7fVl\r\n" "m=audio 16384 RTP/AVP 0 8 9 101\r\n" "c=IN IP4 10.86.255.143\r\n" "a=rtpmap:0 PCMU/8000\r\n" "a=rtpmap:8 PCMA/8000\r\n" "a=rtpmap:9 G722/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n" "a=fmtp:101 0-15\r\n" "a=sendrecv\r\n" "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n" "a=candidate:2 2 UDP 2130706431 192.168.2.2 50006 typ host\r\n" "m=video 1024 RTP/AVP 97\r\n" "c=IN IP4 10.86.255.143\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=fmtp:97 profile-level-id=42E00C\r\n" "a=sendrecv\r\n" "a=candidate:1 1 UDP 2130706431 192.168.2.3 50007 typ host\r\n" "a=candidate:2 2 UDP 2130706431 192.168.2.4 50008 typ host\r\n"; // The signaling state will remain "stable" because the // SetRemoteDescription call fails. JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); } TEST_F(JsepSessionTest, SetLocalAnswerInHaveLocalOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); // The signaling state will remain "have-local-offer" because the // SetLocalDescription call fails. JsepSession::Result result = mSessionOff->SetLocalDescription(kJsepSdpAnswer, offer); ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); } TEST_F(JsepSessionTest, SetRemoteOfferInHaveLocalOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); // The signaling state will remain "have-local-offer" because the // SetRemoteDescription call fails. JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, offer); ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); } TEST_F(JsepSessionTest, SetLocalOfferInHaveRemoteOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); // The signaling state will remain "have-remote-offer" because the // SetLocalDescription call fails. JsepSession::Result result = mSessionAns->SetLocalDescription(kJsepSdpOffer, offer); ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); } TEST_F(JsepSessionTest, SetRemoteAnswerInHaveRemoteOffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); std::string offer = CreateOffer(); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); // The signaling state will remain "have-remote-offer" because the // SetRemoteDescription call fails. JsepSession::Result result = mSessionAns->SetRemoteDescription(kJsepSdpAnswer, offer); ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); } TEST_F(JsepSessionTest, RtcpFbInOffer) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); std::string offer = CreateOffer(); std::map expected; expected["nack"] = false; expected["nack pli"] = false; expected["ccm fir"] = false; size_t prev = 0; size_t found = 0; for (;;) { found = offer.find('\n', found + 1); if (found == std::string::npos) break; std::string line = offer.substr(prev, (found - prev)); // ensure no other rtcp-fb values are present if (line.find("a=rtcp-fb:") != std::string::npos) { size_t space = line.find(' '); // strip trailing \r\n std::string value = line.substr(space + 1, line.length() - space - 2); std::map::iterator entry = expected.find(value); ASSERT_NE(entry, expected.end()); entry->second = true; } prev = found + 1; } // ensure all values are present for (std::map::iterator it = expected.begin(); it != expected.end(); ++it) { ASSERT_EQ(it->second, true); } } // In this test we will change the offer SDP's a=setup value // from actpass to passive. This will force the answer to do active. TEST_F(JsepSessionTest, AudioCallForceDtlsRoles) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); std::string actpass = "\r\na=setup:actpass"; size_t match = offer.find(actpass); ASSERT_NE(match, std::string::npos); offer.replace(match, actpass.length(), "\r\na=setup:passive"); SetLocalOffer(offer); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); std::string answer = CreateAnswer(); match = answer.find("\r\na=setup:active"); ASSERT_NE(match, std::string::npos); SetLocalAnswer(answer); SetRemoteAnswer(answer); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } // In this test we will change the offer SDP's a=setup value // from actpass to active. This will force the answer to do passive. TEST_F(JsepSessionTest, AudioCallReverseDtlsRoles) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); std::string actpass = "\r\na=setup:actpass"; size_t match = offer.find(actpass); ASSERT_NE(match, std::string::npos); offer.replace(match, actpass.length(), "\r\na=setup:active"); SetLocalOffer(offer); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); std::string answer = CreateAnswer(); match = answer.find("\r\na=setup:passive"); ASSERT_NE(match, std::string::npos); SetLocalAnswer(answer); SetRemoteAnswer(answer); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } // In this test we will change the answer SDP's a=setup value // from active to passive. This will make both sides do // active and should not connect. TEST_F(JsepSessionTest, AudioCallMismatchDtlsRoles) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); std::string actpass = "\r\na=setup:actpass"; size_t match = offer.find(actpass); ASSERT_NE(match, std::string::npos); SetLocalOffer(offer); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); std::string answer = CreateAnswer(); SetLocalAnswer(answer); std::string active = "\r\na=setup:active"; match = answer.find(active); ASSERT_NE(match, std::string::npos); answer.replace(match, active.length(), "\r\na=setup:passive"); SetRemoteAnswer(answer); // This is as good as it gets in a JSEP test (w/o starting DTLS) ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, GetTransceivers(*mSessionOff)[0].mTransport.mDtls->GetRole()); ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, GetTransceivers(*mSessionAns)[0].mTransport.mDtls->GetRole()); } // Verify that missing a=setup in offer gets rejected TEST_F(JsepSessionTest, AudioCallOffererNoSetup) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string actpass = "\r\na=setup:actpass"; size_t match = offer.find(actpass); ASSERT_NE(match, std::string::npos); offer.replace(match, actpass.length(), ""); // The signaling state will remain "stable" because the unparsable // SDP leads to a failure in SetRemoteDescription. SetRemoteOffer(offer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); } // In this test we will change the answer SDP to remove the // a=setup line, which results in active being assumed. TEST_F(JsepSessionTest, AudioCallAnswerNoSetup) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); size_t match = offer.find("\r\na=setup:actpass"); ASSERT_NE(match, std::string::npos); SetLocalOffer(offer); SetRemoteOffer(offer); ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); std::string answer = CreateAnswer(); SetLocalAnswer(answer); std::string active = "\r\na=setup:active"; match = answer.find(active); ASSERT_NE(match, std::string::npos); answer.replace(match, active.length(), ""); SetRemoteAnswer(answer); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); // This is as good as it gets in a JSEP test (w/o starting DTLS) ASSERT_EQ(JsepDtlsTransport::kJsepDtlsServer, GetTransceivers(*mSessionOff)[0].mTransport.mDtls->GetRole()); ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, GetTransceivers(*mSessionAns)[0].mTransport.mDtls->GetRole()); } // Verify that 'holdconn' gets rejected TEST_F(JsepSessionTest, AudioCallDtlsRoleHoldconn) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); std::string actpass = "\r\na=setup:actpass"; size_t match = offer.find(actpass); ASSERT_NE(match, std::string::npos); offer.replace(match, actpass.length(), "\r\na=setup:holdconn"); // The signaling state will remain "stable" because the unparsable // SDP leads to a failure in SetRemoteDescription. SetRemoteOffer(offer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); } // Verify that 'actpass' in answer gets rejected TEST_F(JsepSessionTest, AudioCallAnswererUsesActpass) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); std::string active = "\r\na=setup:active"; size_t match = answer.find(active); ASSERT_NE(match, std::string::npos); answer.replace(match, active.length(), "\r\na=setup:actpass"); // The signaling state will remain "stable" because the unparsable // SDP leads to a failure in SetRemoteDescription. SetRemoteAnswer(answer, NO_CHECKS); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); } // Verify that 'actpass' in reoffer from previous answerer doesn't result // in a role switch. TEST_F(JsepSessionTest, AudioCallPreviousAnswererUsesActpassInReoffer) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); SwapOfferAnswerRoles(); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive); } // Disabled: See Bug 1329028 TEST_F(JsepSessionTest, DISABLED_AudioCallOffererAttemptsSetupRoleSwitch) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer); std::string actpass = "\r\na=setup:actpass"; size_t match = reoffer.find(actpass); ASSERT_NE(match, std::string::npos); reoffer.replace(match, actpass.length(), "\r\na=setup:active"); // This is expected to fail. SetRemoteOffer(reoffer, NO_CHECKS); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } // Disabled: See Bug 1329028 TEST_F(JsepSessionTest, DISABLED_AudioCallAnswererAttemptsSetupRoleSwitch) { types.push_back(SdpMediaSection::kAudio); AddTracks(*mSessionOff, "audio"); AddTracks(*mSessionAns, "audio"); OfferAnswer(); ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer); SetRemoteOffer(reoffer); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer); std::string actpass = "\r\na=setup:active"; size_t match = reanswer.find(actpass); ASSERT_NE(match, std::string::npos); reanswer.replace(match, actpass.length(), "\r\na=setup:passive"); // This is expected to fail. SetRemoteAnswer(reanswer, NO_CHECKS); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); } // Remove H.264 P1 and VP8 from offer, check answer negotiates H.264 P0 TEST_F(JsepSessionTest, OfferWithOnlyH264P0) { for (auto& codec : mSessionOff->Codecs()) { if (codec->mName != "H264" || codec->mDefaultPt == "126") { codec->mEnabled = false; } } types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); ASSERT_EQ(offer.find("a=rtpmap:126 H264/90000"), std::string::npos); ASSERT_EQ(offer.find("a=rtpmap:120 VP8/90000"), std::string::npos); SetLocalOffer(offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); size_t match = answer.find("\r\na=setup:active"); ASSERT_NE(match, std::string::npos); // validate answer SDP ASSERT_NE(answer.find("a=rtpmap:97 H264/90000"), std::string::npos); ASSERT_NE(answer.find("a=rtcp-fb:97 nack"), std::string::npos); ASSERT_NE(answer.find("a=rtcp-fb:97 nack pli"), std::string::npos); ASSERT_NE(answer.find("a=rtcp-fb:97 ccm fir"), std::string::npos); // Ensure VP8 and P1 removed ASSERT_EQ(answer.find("a=rtpmap:126 H264/90000"), std::string::npos); ASSERT_EQ(answer.find("a=rtpmap:120 VP8/90000"), std::string::npos); ASSERT_EQ(answer.find("a=rtcp-fb:120"), std::string::npos); ASSERT_EQ(answer.find("a=rtcp-fb:126"), std::string::npos); } // Test negotiating an answer which has only H.264 P1 // Which means replace VP8 with H.264 P1 in answer TEST_F(JsepSessionTest, AnswerWithoutVP8) { types.push_back(SdpMediaSection::kAudio); types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); for (auto& codec : mSessionOff->Codecs()) { if (codec->mName != "H264" || codec->mDefaultPt == "126") { codec->mEnabled = false; } } std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); } // Ok. Hear me out. // The JSEP spec specifies very different behavior for the following two cases: // 1. AddTrack caused a transceiver to be created. // 2. The transceiver was not created as a side-effect of AddTrack. // // All together now... // // SADFACE :( // // Ok, enough of that. The upshot is we need to test two different codepaths for // the same thing here. Most of this unit-test suite tests the "magic" case // (case 1 above). Case 2 (the non-magic case) is simpler, so we have just a // handful of tests. TEST_F(JsepSessionTest, OffererNoAddTrackMagic) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff, NO_ADDTRACK_MAGIC); AddTracks(*mSessionAns); // Offerer's transceivers aren't "magic"; they will not associate with the // remote side's m-sections automatically. But, since they went into the // offer, everything works normally. OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); } TEST_F(JsepSessionTest, AnswererNoAddTrackMagic) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns, NO_ADDTRACK_MAGIC); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); // Since answerer's transceivers aren't "magic", they cannot automatically be // attached to the offerer's m-sections. ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); SwapOfferAnswerRoles(); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); } // JSEP has rules about when a disabled m-section can be reused; the gist is // that the m-section has to be negotiated disabled, then it becomes a candidate // for reuse on the next renegotiation. Stopping a transceiver does not allow // you to reuse on the next negotiation. TEST_F(JsepSessionTest, OffererRecycle) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); GetTransceivers(*mSessionOff)[0].Stop(); AddTracks(*mSessionOff, "audio"); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); OfferAnswer(CHECK_SUCCESS); // It is too soon to recycle msection 0, so the new track should have been // given a new msection. ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); UniquePtr offer = GetParsedLocalDescription(*mSessionOff); ASSERT_EQ(3U, offer->GetMediaSectionCount()); ValidateDisabledMSection(&offer->GetMediaSection(0)); UniquePtr answer = GetParsedLocalDescription(*mSessionAns); ASSERT_EQ(3U, answer->GetMediaSectionCount()); ValidateDisabledMSection(&answer->GetMediaSection(0)); // Ok. Now renegotiating should recycle m-section 0. AddTracks(*mSessionOff, "audio"); ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); OfferAnswer(CHECK_SUCCESS); // Transceiver 3 should now be attached to m-section 0 ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[3].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[3].IsStopped()); ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[3].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); } TEST_F(JsepSessionTest, RecycleAnswererStopsTransceiver) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); GetTransceivers(*mSessionAns)[0].Stop(); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); UniquePtr offer = GetParsedLocalDescription(*mSessionOff); ASSERT_EQ(2U, offer->GetMediaSectionCount()); UniquePtr answer = GetParsedLocalDescription(*mSessionAns); ASSERT_EQ(2U, answer->GetMediaSectionCount()); ValidateDisabledMSection(&answer->GetMediaSection(0)); // Renegotiating should recycle m-section 0. AddTracks(*mSessionOff, "audio"); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); OfferAnswer(CHECK_SUCCESS); // Transceiver 3 should now be attached to m-section 0 ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); } // TODO: Have a test where offerer stops, and answerer adds a track and reoffers // once Nils' role swap code lands. // TODO: Have a test where answerer stops and adds a track. TEST_F(JsepSessionTest, OffererRecycleNoMagic) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); GetTransceivers(*mSessionOff)[0].Stop(); OfferAnswer(CHECK_SUCCESS); // Ok. Now renegotiating should recycle m-section 0. AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); OfferAnswer(CHECK_SUCCESS); // Transceiver 2 should now be attached to m-section 0 ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); } TEST_F(JsepSessionTest, OffererRecycleNoMagicAnswererStopsTransceiver) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); GetTransceivers(*mSessionAns)[0].Stop(); OfferAnswer(CHECK_SUCCESS); // Ok. Now renegotiating should recycle m-section 0. AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); OfferAnswer(CHECK_SUCCESS); // Transceiver 2 should now be attached to m-section 0 ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); } TEST_F(JsepSessionTest, RecycleRollback) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); GetTransceivers(*mSessionOff)[0].Stop(); OfferAnswer(CHECK_SUCCESS); AddTracks(*mSessionOff, "audio"); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); std::string offer = CreateOffer(); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); SetLocalOffer(offer, CHECK_SUCCESS); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); // This should now be associated ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].IsAssociated()); ASSERT_FALSE( mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); // Rollback should not change the levels of any of these, since those are set // in CreateOffer. ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); // This should no longer be associated ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); } TEST_F(JsepSessionTest, AddTrackMagicWithNullReplaceTrack) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); AddTracks(*mSessionAns, "audio"); AddTracks(*mSessionOff, "audio"); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); // Ok, transceiver 2 is "magical". Ensure it still has this "magical" // auto-matching property even if we null it out with replaceTrack. GetTransceivers(*mSessionAns)[2].mSendTrack.ClearStreamIds(); GetTransceivers(*mSessionAns)[2].mJsDirection = SdpDirectionAttribute::Direction::kRecvonly; OfferAnswer(CHECK_SUCCESS); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].HasAddTrackMagic()); } // Flipside of AddTrackMagicWithNullReplaceTrack; we want to check that // auto-matching does not work for transceivers that were created without a // track, but were later given a track with replaceTrack. TEST_F(JsepSessionTest, NoAddTrackMagicReplaceTrack) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); AddTracks(*mSessionOff, "audio"); mSessionAns->AddTransceiver( JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen)); GetTransceivers(*mSessionAns)[2].mSendTrack.UpdateStreamIds({"newstream"}); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); OfferAnswer(CHECK_SUCCESS); ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[3].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); } TEST_F(JsepSessionTest, AddTrackDoesNotMakeTransceiverMagical) { types = BuildTypes("audio,video"); AddTracks(*mSessionOff); AddTracks(*mSessionAns); OfferAnswer(); ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); AddTracks(*mSessionOff, "audio"); mSessionAns->AddTransceiver( JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen)); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); // This attaches a track to the third transceiver, but does _not_ set the // addTrack magic bit, meaning it will not auto-pair with the track added // to the offerer. AddTracks(*mSessionAns, "audio"); ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); OfferAnswer(CHECK_SUCCESS); // The offer's new transceiver does not pair up with the transceiver we added ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[3].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); } TEST_F(JsepSessionTest, ComplicatedRemoteRollback) { AddTracks(*mSessionOff, "audio,audio,audio,video"); AddTracks(*mSessionAns, "video,video"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); // Three recvonly for audio, one sendrecv for video, and one (unmapped) for // the second video track. ASSERT_EQ(5U, GetTransceivers(*mSessionAns).size()); // First video transceiver; auto matched with offer ASSERT_EQ(3U, GetTransceivers(*mSessionAns)[0].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].HasAddTrackMagic()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].OnlyExistsBecauseOfSetRemote()); // Second video transceiver, not matched with offer ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].HasAddTrackMagic()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].OnlyExistsBecauseOfSetRemote()); // Audio transceiver, created due to application of SetRemote ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); // Audio transceiver, created due to application of SetRemote ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[3].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].OnlyExistsBecauseOfSetRemote()); // Audio transceiver, created due to application of SetRemote ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[4].GetLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].IsStopped()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasAddTrackMagic()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].OnlyExistsBecauseOfSetRemote()); // This will prevent rollback from eating this transceiver, even though we // call replaceTrack(null) on it. AddTracks(*mSessionAns, "audio"); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); GetTransceivers(*mSessionAns)[2].mSendTrack.ClearStreamIds(); GetTransceivers(*mSessionAns)[2].mJsDirection = SdpDirectionAttribute::Direction::kRecvonly; // We do nothing with the second audio transceiver; when we rollback, it will // be marked as removed. // This will not cause the third audio transceiver to stick around; having a // track is _not_ enough to preserve it. It must have addTrack "magic"! GetTransceivers(*mSessionAns)[4].mSendTrack.UpdateStreamIds({"newstream"}); // Create a fourth audio transceiver. Rollback will leave it alone, since we // created it. mSessionAns->AddTransceiver( JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen, SdpDirectionAttribute::Direction::kRecvonly)); ASSERT_FALSE( mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError.isSome()); // Two of these (3 and 4) will be marked removed, if this all worked ASSERT_EQ(6U, GetTransceivers(*mSessionAns).size()); // First video transceiver ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].HasAddTrackMagic()); ASSERT_FALSE(IsNull(GetTransceivers(*mSessionAns)[0].mSendTrack)); ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsRemoved()); // Second video transceiver ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].HasAddTrackMagic()); ASSERT_FALSE(IsNull(GetTransceivers(*mSessionAns)[1].mSendTrack)); ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsRemoved()); // First audio transceiver, kept because AddTrack touched it, even though we // removed the send track after. ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[2].mSendTrack)); ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsRemoved()); // Second audio transceiver should be gone. ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[3].mSendTrack)); ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsRemoved()); // Third audio transceiver should also be gone. ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasLevel()); ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasAddTrackMagic()); ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[4].mSendTrack)); ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsRemoved()); // Fourth audio transceiver, created after SetRemote ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].HasLevel()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].IsStopped()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].HasAddTrackMagic()); ASSERT_TRUE( GetTransceivers(*mSessionAns)[5].mSendTrack.GetStreamIds().empty()); } TEST_F(JsepSessionTest, LocalRollback) { AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); ASSERT_FALSE( mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsAssociated()); } TEST_F(JsepSessionTest, JsStopsTransceiverBeforeAnswer) { AddTracks(*mSessionOff, "audio,video"); AddTracks(*mSessionAns, "audio,video"); std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); // Now JS decides to stop a transceiver. Make sure transport stuff is still // ready to go when the answer is set. This should only prevent the flow of // media for that transceiver. GetTransceivers(*mSessionOff)[0].Stop(); SetRemoteAnswer(answer, CHECK_SUCCESS); ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[0].mTransport.mComponents); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].mSendTrack.GetActive()); ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].mRecvTrack.GetActive()); } TEST_F(JsepSessionTest, TestOfferPTAsymmetryRtxApt) { for (auto& codec : mSessionAns->Codecs()) { if (codec->mName == "VP8") { JsepVideoCodecDescription* vp8 = static_cast(codec.get()); vp8->EnableRtx("42"); break; } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); JsepOfferOptions options; // Ensure that mSessionAns is appropriately configured. std::string offer; JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_NE(std::string::npos, offer.find("a=rtpmap:42 rtx")) << offer; OfferAnswer(); // Answerer should use what the offerer suggested UniquePtr codec; GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); JsepVideoCodecDescription* vp8 = static_cast(codec.get()); ASSERT_EQ("120", vp8->mDefaultPt); ASSERT_EQ("124", vp8->mRtxPayloadType); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); vp8 = static_cast(codec.get()); ASSERT_EQ("120", vp8->mDefaultPt); ASSERT_EQ("124", vp8->mRtxPayloadType); // Answerer should not change back when it reoffers result = mSessionAns->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_NE(std::string::npos, offer.find("a=rtpmap:124 rtx")) << offer; } TEST_F(JsepSessionTest, TestAnswerPTAsymmetryRtx) { // JsepSessionImpl will never answer with an asymmetric payload type // (tested in TestOfferPTAsymmetry), so we have to rewrite SDP a little. types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer = CreateOffer(); SetLocalOffer(offer); Replace("a=rtpmap:120 VP8", "a=rtpmap:119 VP8", &offer); Replace("m=video 9 UDP/TLS/RTP/SAVPF 120", "m=video 9 UDP/TLS/RTP/SAVPF 119", &offer); ReplaceAll("a=fmtp:120", "a=fmtp:119", &offer); ReplaceAll("a=fmtp:122 120", "a=fmtp:122 119", &offer); ReplaceAll("a=fmtp:124 apt=120", "a=fmtp:124 apt=119", &offer); ReplaceAll("a=rtcp-fb:120", "a=rtcp-fb:119", &offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); UniquePtr codec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("119", codec->mDefaultPt); JsepVideoCodecDescription* vp8 = static_cast(codec.get()); ASSERT_EQ("124", vp8->mRtxPayloadType); GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("120", codec->mDefaultPt); vp8 = static_cast(codec.get()); ASSERT_EQ("124", vp8->mRtxPayloadType); GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("119", codec->mDefaultPt); vp8 = static_cast(codec.get()); ASSERT_EQ("124", vp8->mRtxPayloadType); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("119", codec->mDefaultPt); vp8 = static_cast(codec.get()); ASSERT_EQ("124", vp8->mRtxPayloadType); } TEST_F(JsepSessionTest, TestAnswerPTAsymmetryRtxApt) { // JsepSessionImpl will never answer with an asymmetric payload type // so we have to rewrite SDP a little. types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); std::string offer = CreateOffer(); SetLocalOffer(offer); Replace("a=rtpmap:124 rtx", "a=rtpmap:42 rtx", &offer); Replace("m=video 9 UDP/TLS/RTP/SAVPF 120 124", "m=video 9 UDP/TLS/RTP/SAVPF 120 42", &offer); ReplaceAll("a=fmtp:124", "a=fmtp:42", &offer); SetRemoteOffer(offer); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); UniquePtr codec; GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("120", codec->mDefaultPt); JsepVideoCodecDescription* vp8 = static_cast(codec.get()); ASSERT_EQ("42", vp8->mRtxPayloadType); GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); ASSERT_EQ("120", codec->mDefaultPt); vp8 = static_cast(codec.get()); ASSERT_EQ("124", vp8->mRtxPayloadType); GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); vp8 = static_cast(codec.get()); ASSERT_EQ("120", vp8->mDefaultPt); ASSERT_EQ("42", vp8->mRtxPayloadType); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("VP8", codec->mName); vp8 = static_cast(codec.get()); ASSERT_EQ("120", vp8->mDefaultPt); ASSERT_EQ("42", vp8->mRtxPayloadType); } TEST_F(JsepSessionTest, TestOfferNoRtx) { for (auto& codec : mSessionOff->Codecs()) { if (codec->Type() == SdpMediaSection::kVideo) { JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); videoCodec->mRtxEnabled = false; } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); JsepOfferOptions options; std::string offer; JsepSession::Result result = mSessionOff->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_EQ(std::string::npos, offer.find("rtx")) << offer; OfferAnswer(); // Answerer should use what the offerer suggested UniquePtr codec; for (size_t i = 0; i < 4; ++i) { GetCodec(*mSessionAns, 0, sdp::kSend, 0, i, &codec); ASSERT_TRUE(codec); JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); ASSERT_FALSE(videoCodec->mRtxEnabled); GetCodec(*mSessionAns, 0, sdp::kRecv, 0, i, &codec); ASSERT_TRUE(codec); videoCodec = static_cast(codec.get()); ASSERT_FALSE(videoCodec->mRtxEnabled); } } TEST_F(JsepSessionTest, TestOneWayRtx) { for (auto& codec : mSessionAns->Codecs()) { if (codec->Type() == SdpMediaSection::kVideo) { JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); videoCodec->mRtxEnabled = false; } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); JsepOfferOptions options; std::string offer; JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_EQ(std::string::npos, offer.find("rtx")) << offer; OfferAnswer(); // If the answerer does not support rtx, the offerer should not send it, // but it is too late to turn off recv on the offerer side. UniquePtr codec; for (size_t i = 0; i < 4; ++i) { GetCodec(*mSessionOff, 0, sdp::kSend, 0, i, &codec); ASSERT_TRUE(codec); JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); ASSERT_FALSE(videoCodec->mRtxEnabled); GetCodec(*mSessionOff, 0, sdp::kRecv, 0, i, &codec); ASSERT_TRUE(codec); videoCodec = static_cast(codec.get()); ASSERT_TRUE(videoCodec->mRtxEnabled); } } TEST_F(JsepSessionTest, TestRtxNoSsrcGroup) { mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); OfferAnswer(CHECK_SUCCESS); std::string offer = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); ASSERT_EQ(std::string::npos, offer.find("FID")) << offer; std::string answer = mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); ASSERT_EQ(std::string::npos, answer.find("FID")) << answer; } TEST_F(JsepSessionTest, TestRtxSsrcGroupOnlyOffered) { mSessionOff->AddTransceiver(JsepTransceiver( SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kSendonly)); OfferAnswer(CHECK_SUCCESS); std::string offer = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); ASSERT_NE(std::string::npos, offer.find("FID")) << offer; std::string answer = mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); ASSERT_EQ(std::string::npos, answer.find("FID")) << answer; } TEST_F(JsepSessionTest, TestOfferRtxNoMsid) { for (auto& codec : mSessionOff->Codecs()) { if (codec->mName == "VP8") { JsepVideoCodecDescription* vp8 = static_cast(codec.get()); vp8->EnableRtx("42"); break; } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); mSessionOff->ForEachTransceiver([this](JsepTransceiver& aTransceiver) { if (!IsNull(aTransceiver.mSendTrack)) { std::vector empty; aTransceiver.mSendTrack.UpdateStreamIds(empty); } }); // MSID stream absence should not influence FID ssrc-group JsepOfferOptions options; std::string offer; JsepSession::Result result = mSessionOff->CreateOffer(options, &offer); ASSERT_FALSE(result.mError.isSome()); ASSERT_NE(std::string::npos, offer.find("FID")) << offer; } TEST_F(JsepSessionTest, TestDuplicatePayloadTypes) { for (auto& codec : mSessionOff->Codecs()) { if (codec->Type() == SdpMediaSection::kVideo) { JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); videoCodec->mRtxPayloadType = "97"; videoCodec->EnableFec("97", "97"); } } types.push_back(SdpMediaSection::kVideo); AddTracks(*mSessionOff, "video"); AddTracks(*mSessionAns, "video"); OfferAnswer(); std::vector directions = {sdp::kSend, sdp::kRecv}; for (auto direction : directions) { UniquePtr codec; std::set payloadTypes; std::string redPt, ulpfecPt; for (size_t i = 0; i < 4; ++i) { GetCodec(*mSessionOff, 0, direction, 0, i, &codec); ASSERT_TRUE(codec); JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); ASSERT_TRUE(payloadTypes.insert(videoCodec->mDefaultPt).second); ASSERT_TRUE(payloadTypes.insert(videoCodec->mRtxPayloadType).second); // ULPFEC and RED payload types are the same for each codec, so we only // check them for the first one. if (i == 0) { ASSERT_TRUE(payloadTypes.insert(videoCodec->mREDPayloadType).second); ASSERT_TRUE(payloadTypes.insert(videoCodec->mULPFECPayloadType).second); redPt = videoCodec->mREDPayloadType; ulpfecPt = videoCodec->mULPFECPayloadType; } else { ASSERT_TRUE(redPt == videoCodec->mREDPayloadType); ASSERT_TRUE(ulpfecPt == videoCodec->mULPFECPayloadType); } } } } TEST_F(JsepSessionTest, TestTransportAttributeValidation) { const std::string sdpTemplate = "v=0\r\n" "o=- 6 2 IN IP4 1r\r\n" "t=0 0a\r\n" "a=group:BUNDLE audio video\r\n" "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" "c=IN IP4 51.81.107.13\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n" "a=sendrecv\r\n" "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" "a=mid:audio\r\n" "a=rtcp-mux\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n" "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" "c=IN IP4 51.81.107.13\r\n" "a=rtpmap:100 VP8/90000\r\n" "a=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\n" "a=mid:video\r\n" "a=ice-ufrag:Xp\r\n" "a=ice-pwd:he\r\n" "a=setup:actpass\r\n" "a=fingerprint:sha-256 " "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" "08:D2:F7:9D:F5:E2:C1:15\r\n"; // Control case { auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdpTemplate); ASSERT_FALSE(result.mError.isSome()); } // Missing ufrag { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( SdpAttribute::kIceUfragAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing pwd, bundle tag { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( SdpAttribute::kIcePwdAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing setup, bundle tag { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( SdpAttribute::kSetupAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Invalid setup attribute (holdconn), bundle tag { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(0).GetAttributeList().SetAttribute( new SdpSetupAttribute(SdpSetupAttribute::kHoldconn)); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing fingerprint, bundle tag { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( SdpAttribute::kFingerprintAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Unknown fingerprint algorithm { std::string mungedSdp = sdpTemplate; ReplaceAll("fingerprint:sha", "fingerprint:foo", &mungedSdp); UniquePtr parsed = Parse(mungedSdp); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing pwd, bundled msection without bundle-only { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kIcePwdAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing setup, bundled msection without bundle-only { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kSetupAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing fingerprint, bundled msection without bundle-only { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kFingerprintAttribute); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_TRUE(result.mError.isSome()); } // Missing ufrag attribute, bundle-only msection { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kIceUfragAttribute); parsed->GetMediaSection(1).GetAttributeList().SetAttribute( new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_FALSE(result.mError.isSome()); } // Missing pwd attribute, bundle-only msection { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kIcePwdAttribute); parsed->GetMediaSection(1).GetAttributeList().SetAttribute( new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_FALSE(result.mError.isSome()); } // Missing fingerprint attribute, bundle-only msection { UniquePtr parsed = Parse(sdpTemplate); parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( SdpAttribute::kFingerprintAttribute); parsed->GetMediaSection(1).GetAttributeList().SetAttribute( new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); auto sdp = parsed->ToString(); auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); ASSERT_FALSE(result.mError.isSome()); } } } // namespace mozilla