/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "nspr.h" #include "nss.h" #include "ssl.h" #include "mozilla/RefPtr.h" #include "mozilla/Tuple.h" #define GTEST_HAS_RTTI 0 #include "gtest/gtest.h" #include "gtest_utils.h" #include "FakeMediaStreams.h" #include "FakeMediaStreamsImpl.h" #include "FakeLogging.h" #include "signaling/src/sdp/SdpMediaSection.h" #include "signaling/src/sdp/SipccSdpParser.h" #include "signaling/src/jsep/JsepCodecDescription.h" #include "signaling/src/jsep/JsepTrack.h" #include "signaling/src/jsep/JsepSession.h" #include "signaling/src/jsep/JsepSessionImpl.h" #include "signaling/src/jsep/JsepTrack.h" #include "mtransport_test_utils.h" #include "FakeIPC.h" #include "FakeIPC.cpp" #include "TestHarness.h" namespace mozilla { static std::string kAEqualsCandidate("a=candidate:"); const static size_t kNumCandidatesPerComponent = 3; class JsepSessionTestBase : public ::testing::Test { }; class FakeUuidGenerator : public mozilla::JsepUuidGenerator { public: bool Generate(std::string* str) { std::ostringstream os; os << "FAKE_UUID_" << ++ctr; *str = os.str(); return true; } private: static uint64_t ctr; }; uint64_t FakeUuidGenerator::ctr = 1000; class JsepSessionTest : public JsepSessionTestBase, public ::testing::WithParamInterface { public: JsepSessionTest() : mSessionOff("Offerer", MakeUnique()), mSessionAns("Answerer", MakeUnique()) { EXPECT_EQ(NS_OK, mSessionOff.Init()); EXPECT_EQ(NS_OK, mSessionAns.Init()); AddTransportData(&mSessionOff, &mOffererTransport); AddTransportData(&mSessionAns, &mAnswererTransport); } protected: struct TransportData { std::string mIceUfrag; std::string mIcePwd; 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) { // Values here semi-borrowed from JSEP draft. tdata->mIceUfrag = session->GetName() + "-ufrag"; tdata->mIcePwd = session->GetName() + "-1234567890"; session->SetIceCredentials(tdata->mIceUfrag, tdata->mIcePwd); AddDtlsFingerprint("sha-1", session, tdata); AddDtlsFingerprint("sha-256", session, tdata); } std::string CreateOffer(const Maybe options = Nothing()) { JsepOfferOptions defaultOptions; const JsepOfferOptions& optionsRef = options ? *options : defaultOptions; std::string offer; nsresult rv = mSessionOff.CreateOffer(optionsRef, &offer); EXPECT_EQ(NS_OK, rv) << mSessionOff.GetLastError(); std::cerr << "OFFER: " << offer << std::endl; ValidateTransport(mOffererTransport, offer); return offer; } void AddTracks(JsepSessionImpl& side) { // Add tracks. if (types.empty()) { types = BuildTypes(GetParam()); } AddTracks(side, types); // Now that we have added streams, we expect audio, then video, then // application in the SDP, regardless of the order in which the streams were // added. std::sort(types.begin(), types.end()); } void AddTracks(JsepSessionImpl& side, const std::string& mediatypes) { AddTracks(side, BuildTypes(mediatypes)); } 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); SdpMediaSection::MediaType type; if (chunk == "audio") { type = SdpMediaSection::kAudio; } else if (chunk == "video") { type = SdpMediaSection::kVideo; } else if (chunk == "datachannel") { type = SdpMediaSection::kApplication; } else { MOZ_CRASH(); } result.push_back(type); if (comma == std::string::npos) break; ptr = comma + 1; } return result; } void AddTracks(JsepSessionImpl& side, const std::vector& mediatypes) { FakeUuidGenerator uuid_gen; std::string stream_id; std::string track_id; ASSERT_TRUE(uuid_gen.Generate(&stream_id)); AddTracksToStream(side, stream_id, mediatypes); } void AddTracksToStream(JsepSessionImpl& side, const std::string stream_id, const std::string& mediatypes) { AddTracksToStream(side, stream_id, BuildTypes(mediatypes)); } void AddTracksToStream(JsepSessionImpl& side, const std::string stream_id, const std::vector& mediatypes) { FakeUuidGenerator uuid_gen; std::string track_id; for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) { ASSERT_TRUE(uuid_gen.Generate(&track_id)); RefPtr mst(new JsepTrack(*track, stream_id, track_id)); side.AddTrack(mst); } } bool HasMediaStream(std::vector> tracks) const { for (auto i = tracks.begin(); i != tracks.end(); ++i) { if ((*i)->GetMediaType() != SdpMediaSection::kApplication) { return 1; } } return 0; } const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const { auto tracks = side.GetLocalTracks(); return (*tracks.begin())->GetStreamId(); } std::vector GetMediaStreamIds(std::vector> tracks) const { std::vector ids; for (auto i = tracks.begin(); i != tracks.end(); ++i) { // data channels don't have msid's if ((*i)->GetMediaType() == SdpMediaSection::kApplication) { continue; } ids.push_back((*i)->GetStreamId()); } return ids; } std::vector GetLocalMediaStreamIds(JsepSessionImpl& side) const { return GetMediaStreamIds(side.GetLocalTracks()); } std::vector GetRemoteMediaStreamIds(JsepSessionImpl& side) const { return GetMediaStreamIds(side.GetRemoteTracks()); } 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)); } RefPtr GetTrack(JsepSessionImpl& side, SdpMediaSection::MediaType type, size_t index) const { auto tracks = side.GetLocalTracks(); for (auto i = tracks.begin(); i != tracks.end(); ++i) { if ((*i)->GetMediaType() != type) { continue; } if (index != 0) { --index; continue; } return *i; } return RefPtr(nullptr); } RefPtr GetTrackOff(size_t index, SdpMediaSection::MediaType type) { return GetTrack(mSessionOff, type, index); } RefPtr GetTrackAns(size_t index, SdpMediaSection::MediaType type) { return GetTrack(mSessionAns, type, index); } class ComparePairsByLevel { public: bool operator()(const JsepTrackPair& lhs, const JsepTrackPair& rhs) const { return lhs.mLevel < rhs.mLevel; } }; std::vector GetTrackPairsByLevel(JsepSessionImpl& side) const { auto pairs = side.GetNegotiatedTrackPairs(); std::sort(pairs.begin(), pairs.end(), ComparePairsByLevel()); return pairs; } 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& 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 RefPtr& t1, const RefPtr& t2) const { if (!t1 && !t2) { return true; } if (!t1 || !t2) { return false; } if (t1->mTransportId != t2->mTransportId) { return false; } if (t1->mComponents != t2->mComponents) { return false; } if (!Equals(t1->mIce, t2->mIce)) { return false; } return true; } bool Equals(const JsepTrackPair& p1, const JsepTrackPair& p2) const { if (p1.mLevel != p2.mLevel) { return false; } // We don't check things like mBundleLevel, since that can change without // any changes to the transport, which is what we're really interested in. if (p1.mSending.get() != p2.mSending.get()) { return false; } if (p1.mReceiving.get() != p2.mReceiving.get()) { return false; } if (!Equals(p1.mRtpTransport, p2.mRtpTransport)) { return false; } if (!Equals(p1.mRtcpTransport, p2.mRtcpTransport)) { return false; } return true; } size_t GetTrackCount(JsepSessionImpl& side, SdpMediaSection::MediaType type) const { auto tracks = side.GetLocalTracks(); size_t result = 0; for (auto i = tracks.begin(); i != tracks.end(); ++i) { if ((*i)->GetMediaType() == type) { ++result; } } return result; } UniquePtr GetParsedLocalDescription(const JsepSessionImpl& side) const { return Parse(side.GetLocalDescription()); } 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 i = mSessionOff.Codecs().begin(); i != mSessionOff.Codecs().end(); ++i) { auto* codec = *i; if (codec->mType == type && codec->mName != codecName) { codec->mEnabled = false; } } for (auto i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end(); ++i) { auto* codec = *i; if (codec->mType == type && codec->mName == codecName) { codec->mEnabled = false; } } } std::string CreateAnswer() { JsepAnswerOptions options; std::string answer; nsresult rv = mSessionAns.CreateAnswer(options, &answer); EXPECT_EQ(NS_OK, rv); std::cerr << "ANSWER: " << answer << std::endl; ValidateTransport(mAnswererTransport, answer); 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) { nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, offer); if (checkFlags & CHECK_SUCCESS) { ASSERT_EQ(NS_OK, rv); } if (checkFlags & CHECK_TRACKS) { // Check that the transports exist. ASSERT_EQ(types.size(), mSessionOff.GetTransports().size()); auto tracks = mSessionOff.GetLocalTracks(); for (size_t i = 0; i < types.size(); ++i) { ASSERT_NE("", tracks[i]->GetStreamId()); ASSERT_NE("", tracks[i]->GetTrackId()); if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += tracks[i]->GetStreamId(); msidAttr += " "; msidAttr += tracks[i]->GetTrackId(); ASSERT_NE(std::string::npos, offer.find(msidAttr)) << "Did not find " << msidAttr << " in offer"; } } } } void SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS) { nsresult rv = mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer); if (checkFlags & CHECK_SUCCESS) { ASSERT_EQ(NS_OK, rv); } if (checkFlags & CHECK_TRACKS) { auto tracks = mSessionAns.GetRemoteTracks(); // Now verify that the right stuff is in the tracks. ASSERT_EQ(types.size(), tracks.size()); for (size_t i = 0; i < tracks.size(); ++i) { ASSERT_EQ(types[i], tracks[i]->GetMediaType()); ASSERT_NE("", tracks[i]->GetStreamId()); ASSERT_NE("", tracks[i]->GetTrackId()); if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += tracks[i]->GetStreamId(); msidAttr += " "; msidAttr += tracks[i]->GetTrackId(); 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) { nsresult rv = mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer); if (checkFlags & CHECK_SUCCESS) { ASSERT_EQ(NS_OK, rv); } if (checkFlags & CHECK_TRACKS) { // Verify that the right stuff is in the tracks. auto pairs = mSessionAns.GetNegotiatedTrackPairs(); ASSERT_EQ(types.size(), pairs.size()); for (size_t i = 0; i < types.size(); ++i) { ASSERT_TRUE(pairs[i].mSending); ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType()); ASSERT_TRUE(pairs[i].mReceiving); ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType()); ASSERT_NE("", pairs[i].mSending->GetStreamId()); ASSERT_NE("", pairs[i].mSending->GetTrackId()); // These might have been in the SDP, or might have been randomly // chosen by JsepSessionImpl ASSERT_NE("", pairs[i].mReceiving->GetStreamId()); ASSERT_NE("", pairs[i].mReceiving->GetTrackId()); if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += pairs[i].mSending->GetStreamId(); msidAttr += " "; msidAttr += pairs[i].mSending->GetTrackId(); ASSERT_NE(std::string::npos, answer.find(msidAttr)) << "Did not find " << msidAttr << " in offer"; } } } std::cerr << "OFFER pairs:" << std::endl; DumpTrackPairs(mSessionOff); } void SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS) { nsresult rv = mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer); if (checkFlags & CHECK_SUCCESS) { ASSERT_EQ(NS_OK, rv); } if (checkFlags & CHECK_TRACKS) { // Verify that the right stuff is in the tracks. auto pairs = mSessionOff.GetNegotiatedTrackPairs(); ASSERT_EQ(types.size(), pairs.size()); for (size_t i = 0; i < types.size(); ++i) { ASSERT_TRUE(pairs[i].mSending); ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType()); ASSERT_TRUE(pairs[i].mReceiving); ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType()); ASSERT_NE("", pairs[i].mSending->GetStreamId()); ASSERT_NE("", pairs[i].mSending->GetTrackId()); // These might have been in the SDP, or might have been randomly // chosen by JsepSessionImpl ASSERT_NE("", pairs[i].mReceiving->GetStreamId()); ASSERT_NE("", pairs[i].mReceiving->GetTrackId()); if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) { std::string msidAttr("a=msid:"); msidAttr += pairs[i].mReceiving->GetStreamId(); msidAttr += " "; msidAttr += pairs[i].mReceiving->GetTrackId(); ASSERT_NE(std::string::npos, answer.find(msidAttr)) << "Did not find " << msidAttr << " in answer"; } } } std::cerr << "ANSWER pairs:" << std::endl; DumpTrackPairs(mSessionAns); } typedef enum { RTP = 1, RTCP = 2 } ComponentType; class CandidateSet { public: CandidateSet() {} void Gather(JsepSession& session, const std::vector& types, ComponentType maxComponent = RTCP) { for (size_t level = 0; level < types.size(); ++level) { Gather(session, level, RTP); if (types[level] != SdpMediaSection::kApplication && maxComponent == RTCP) { Gather(session, level, RTCP); } } FinishGathering(session); } void Gather(JsepSession& session, size_t level, 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; bool skipped; session.AddLocalIceCandidate(kAEqualsCandidate + candidate.str(), level, &mid, &skipped); if (!skipped) { mCandidatesToTrickle.push_back( Tuple( level, mid, kAEqualsCandidate + candidate.str())); candidates.push_back(candidate.str()); } } // Stomp existing candidates mCandidates[level][component] = candidates; // Stomp existing defaults mDefaultCandidates[level][component] = std::make_pair("192.168.0.1", port); session.UpdateDefaultCandidate( mDefaultCandidates[level][RTP].first, mDefaultCandidates[level][RTP].second, // Will be empty string if not present, which is how we indicate // that there is no default for RTCP mDefaultCandidates[level][RTCP].first, mDefaultCandidates[level][RTCP].second, level); } void FinishGathering(JsepSession& session) const { // Copy so we can be terse and use [] for (auto levelAndCandidates : mDefaultCandidates) { ASSERT_EQ(1U, levelAndCandidates.second.count(RTP)); // do a final UpdateDefaultCandidate here in case candidates were // cleared during renegotiation. session.UpdateDefaultCandidate( levelAndCandidates.second[RTP].first, levelAndCandidates.second[RTP].second, // Will be empty string if not present, which is how we indicate // that there is no default for RTCP levelAndCandidates.second[RTCP].first, levelAndCandidates.second[RTCP].second, levelAndCandidates.first); session.EndOfLocalCandidates(levelAndCandidates.first); } } void Trickle(JsepSession& session) { for (const auto& levelMidAndCandidate : mCandidatesToTrickle) { Level level; Mid mid; Candidate candidate; Tie(level, mid, candidate) = levelMidAndCandidate; session.AddRemoteIceCandidate(candidate, mid, level); } mCandidatesToTrickle.clear(); } void CheckRtpCandidates(bool expectRtpCandidates, const SdpMediaSection& msection, size_t transportLevel, 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[transportLevel][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[transportLevel][RTP][i], candidates[i]) << context << " (level " << msection.GetLevel() << ")"; } } } void CheckRtcpCandidates(bool expectRtcpCandidates, const SdpMediaSection& msection, size_t transportLevel, 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[transportLevel][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[transportLevel][RTCP][i], candidates[i + kNumCandidatesPerComponent]) << context << " (level " << msection.GetLevel() << ")"; } } } void CheckDefaultRtpCandidate(bool expectDefault, const SdpMediaSection& msection, size_t transportLevel, const std::string& context) const { if (expectDefault) { // Copy so we can be terse and use [] auto defaultCandidates = mDefaultCandidates; ASSERT_EQ(defaultCandidates[transportLevel][RTP].first, msection.GetConnection().GetAddress()) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(defaultCandidates[transportLevel][RTP].second, msection.GetPort()) << context << " (level " << msection.GetLevel() << ")"; } else { ASSERT_EQ("0.0.0.0", msection.GetConnection().GetAddress()) << context << " (level " << msection.GetLevel() << ")"; ASSERT_EQ(9U, msection.GetPort()) << context << " (level " << msection.GetLevel() << ")"; } } void CheckDefaultRtcpCandidate(bool expectDefault, const SdpMediaSection& msection, size_t transportLevel, 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[transportLevel][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[transportLevel][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 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 SipccSdpParser& parser) const { std::stringstream output; for (auto e = parser.GetParseErrors().begin(); e != parser.GetParseErrors().end(); ++e) { output << e->first << ": " << e->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 CheckPairs(const JsepSession& session, const std::string& context) { auto pairs = session.GetNegotiatedTrackPairs(); for (JsepTrackPair& pair : pairs) { if (types.size() == 1) { ASSERT_FALSE(pair.mBundleLevel.isSome()) << context; } else { ASSERT_TRUE(pair.mBundleLevel.isSome()) << context; ASSERT_EQ(0U, *pair.mBundleLevel) << context; } } } void DisableMsid(std::string* sdp) const { size_t pos = sdp->find("a=msid-semantic"); ASSERT_NE(std::string::npos, pos); (*sdp)[pos + 2] = 'X'; // garble, a=Xsid-semantic } 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 ReplaceInSdp(std::string* sdp, const char* searchStr, const char* replaceStr) const { if (searchStr[0] == '\0') return; size_t pos; while ((pos = sdp->find(searchStr)) != std::string::npos) { sdp->replace(pos, strlen(searchStr), replaceStr); } } void ValidateDisabledMSection(const SdpMediaSection* msection) { ASSERT_EQ(1U, msection->GetFormats().size()); // Maybe validate that no attributes are present except rtpmap and // inactive? How? ASSERT_EQ(SdpDirectionAttribute::kInactive, msection->GetDirectionAttribute().mValue); 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) { ASSERT_EQ("5000", msection->GetFormats()[0]); const SdpSctpmapAttributeList::Sctpmap* sctpmap(msection->FindSctpmap("5000")); ASSERT_TRUE(sctpmap); ASSERT_EQ("5000", 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); } } void DumpTrack(const JsepTrack& track) { const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); std::cerr << " type=" << track.GetMediaType() << std::endl; 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 JsepCodecDescription* codec : encoding.GetCodecs()) { std::cerr << " " << codec->mName << " enabled(" << (codec->mEnabled?"yes":"no") << ")"; if (track.GetMediaType() == SdpMediaSection::kAudio) { const JsepAudioCodecDescription* audioCodec = static_cast(codec); std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled?"yes":"no") << ")"; } std::cerr << std::endl; } } } void DumpTrackPairs(const JsepSessionImpl& session) { auto pairs = mSessionAns.GetNegotiatedTrackPairs(); for (auto i = pairs.begin(); i != pairs.end(); ++i) { std::cerr << "Track pair " << i->mLevel << std::endl; if (i->mSending) { std::cerr << "Sending-->" << std::endl; DumpTrack(*i->mSending); } if (i->mReceiving) { std::cerr << "Receiving-->" << std::endl; DumpTrack(*i->mReceiving); } } } UniquePtr Parse(const std::string& sdp) const { SipccSdpParser parser; UniquePtr parsed = parser.Parse(sdp); EXPECT_TRUE(parsed.get()) << "Should have valid SDP" << std::endl << "Errors were: " << GetParseErrors(parser); return parsed; } JsepSessionImpl mSessionOff; CandidateSet mOffCandidates; JsepSessionImpl mSessionAns; CandidateSet mAnsCandidates; std::vector types; std::vector> mGatheredCandidates; private: void ValidateTransport(TransportData& source, const std::string& sdp_str) { 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) { ASSERT_EQ(SdpMediaSection::kDtlsSctp, msection.GetProtocol()); } else { ASSERT_EQ(SdpMediaSection::kUdpTlsRtpSavpf, msection.GetProtocol()); } if (msection.GetPort() == 0) { ValidateDisabledMSection(&msection); continue; } const SdpAttributeList& attrs = msection.GetAttributeList(); ASSERT_EQ(source.mIceUfrag, attrs.GetIceUfrag()); ASSERT_EQ(source.mIcePwd, attrs.GetIcePwd()); 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()); } } TransportData mOffererTransport; TransportData 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, RenegotiationNoChange) { AddTracks(mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(types.size(), added.size()); ASSERT_EQ(0U, removed.size()); AddTracks(mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(types.size(), added.size()); ASSERT_EQ(0U, removed.size()); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer); SetRemoteOffer(reoffer); added = mSessionAns.GetRemoteTracksAdded(); removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer); SetRemoteAnswer(reanswer); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(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); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(2U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType()); ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size()); for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(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 JsepOfferOptions options; options.mOfferToReceiveAudio = Some(GetTrackCount(mSessionOff, SdpMediaSection::kAudio) + 1); options.mOfferToReceiveVideo = Some(GetTrackCount(mSessionOff, SdpMediaSection::kVideo) + 1); std::string offer = CreateOffer(Some(options)); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); std::string answer = CreateAnswer(); SetLocalAnswer(answer, CHECK_SUCCESS); SetRemoteAnswer(answer, CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(2U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType()); ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size()); for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationBothAddTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(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); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(2U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType()); ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(2U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType()); ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size()); for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (GetParam() == "datachannel") { return; } OfferAnswer(); auto oHasStream = HasMediaStream(mSessionOff.GetLocalTracks()); auto aHasStream = HasMediaStream(mSessionAns.GetLocalTracks()); ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(mSessionOff).size()); ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(mSessionAns).size()); ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(mSessionOff).size()); ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(mSessionAns).size()); auto firstOffId = GetFirstLocalStreamId(mSessionOff); auto firstAnsId = GetFirstLocalStreamId(mSessionAns); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(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(mSessionOff.GetLocalTracks()); aHasStream = HasMediaStream(mSessionAns.GetLocalTracks()); ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(mSessionOff).size()); ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(mSessionAns).size()); ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(mSessionOff).size()); ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(mSessionAns).size()); if (oHasStream) { ASSERT_STREQ(firstOffId.c_str(), GetFirstLocalStreamId(mSessionOff).c_str()); } if (aHasStream) { ASSERT_STREQ(firstAnsId.c_str(), GetFirstLocalStreamId(mSessionAns).c_str()); } } TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); RefPtr removedTrack = GetTrackOff(0, types.front()); ASSERT_TRUE(removedTrack); ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(), removedTrack->GetTrackId())); OfferAnswer(CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); // First m-section should be recvonly auto offer = GetParsedLocalDescription(mSessionOff); auto* msection = GetMsection(*offer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); // First audio m-section should be sendonly auto answer = GetParsedLocalDescription(mSessionAns); msection = GetMsection(*answer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_FALSE(msection->IsReceiving()); ASSERT_TRUE(msection->IsSending()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); // Will be the same size since we still have a track on one side. ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); // This should be the only difference. ASSERT_TRUE(offererPairs[0].mSending); ASSERT_FALSE(newOffererPairs[0].mSending); // Remove this difference, let loop below take care of the rest offererPairs[0].mSending = nullptr; for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } // Will be the same size since we still have a track on one side. ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); // This should be the only difference. ASSERT_TRUE(answererPairs[0].mReceiving); ASSERT_FALSE(newAnswererPairs[0].mReceiving); // Remove this difference, let loop below take care of the rest answererPairs[0].mReceiving = nullptr; for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); RefPtr removedTrack = GetTrackAns(0, types.front()); ASSERT_TRUE(removedTrack); ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrack->GetStreamId(), removedTrack->GetTrackId())); OfferAnswer(CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId()); // First m-section should be sendrecv auto offer = GetParsedLocalDescription(mSessionOff); auto* msection = GetMsection(*offer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_TRUE(msection->IsSending()); // First audio m-section should be recvonly auto answer = GetParsedLocalDescription(mSessionAns); msection = GetMsection(*answer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); // Will be the same size since we still have a track on one side. ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); // This should be the only difference. ASSERT_TRUE(offererPairs[0].mReceiving); ASSERT_FALSE(newOffererPairs[0].mReceiving); // Remove this difference, let loop below take care of the rest offererPairs[0].mReceiving = nullptr; for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } // Will be the same size since we still have a track on one side. ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); // This should be the only difference. ASSERT_TRUE(answererPairs[0].mSending); ASSERT_FALSE(newAnswererPairs[0].mSending); // Remove this difference, let loop below take care of the rest answererPairs[0].mSending = nullptr; for (size_t i = 0; i < answererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); RefPtr removedTrackAnswer = GetTrackAns(0, types.front()); ASSERT_TRUE(removedTrackAnswer); ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(), removedTrackAnswer->GetTrackId())); RefPtr removedTrackOffer = GetTrackOff(0, types.front()); ASSERT_TRUE(removedTrackOffer); ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(), removedTrackOffer->GetTrackId())); OfferAnswer(CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId()); // First m-section should be recvonly auto offer = GetParsedLocalDescription(mSessionOff); auto* msection = GetMsection(*offer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); // First m-section should be inactive, and rejected auto answer = GetParsedLocalDescription(mSessionAns); msection = GetMsection(*answer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_FALSE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); ASSERT_FALSE(msection->GetPort()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1); for (size_t i = 0; i < newOffererPairs.size(); ++i) { JsepTrackPair oldPair(offererPairs[i + 1]); JsepTrackPair newPair(newOffererPairs[i]); ASSERT_EQ(oldPair.mLevel, newPair.mLevel); ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get()); ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get()); ASSERT_TRUE(oldPair.mBundleLevel.isSome()); ASSERT_TRUE(newPair.mBundleLevel.isSome()); ASSERT_EQ(0U, *oldPair.mBundleLevel); ASSERT_EQ(1U, *newPair.mBundleLevel); } ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1); for (size_t i = 0; i < newAnswererPairs.size(); ++i) { JsepTrackPair oldPair(answererPairs[i + 1]); JsepTrackPair newPair(newAnswererPairs[i]); ASSERT_EQ(oldPair.mLevel, newPair.mLevel); ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get()); ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get()); ASSERT_TRUE(oldPair.mBundleLevel.isSome()); ASSERT_TRUE(newPair.mBundleLevel.isSome()); ASSERT_EQ(0U, *oldPair.mBundleLevel); ASSERT_EQ(1U, *newPair.mBundleLevel); } } TEST_P(JsepSessionTest, RenegotiationBothRemoveThenAddTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } SdpMediaSection::MediaType removedType = types.front(); OfferAnswer(); RefPtr removedTrackAnswer = GetTrackAns(0, removedType); ASSERT_TRUE(removedTrackAnswer); ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(), removedTrackAnswer->GetTrackId())); RefPtr removedTrackOffer = GetTrackOff(0, removedType); ASSERT_TRUE(removedTrackOffer); ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(), removedTrackOffer->GetTrackId())); OfferAnswer(CHECK_SUCCESS); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(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 added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(1U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(removedType, added[0]->GetMediaType()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(1U, added.size()); ASSERT_EQ(0U, removed.size()); ASSERT_EQ(removedType, added[0]->GetMediaType()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size() + 1, newOffererPairs.size()); ASSERT_EQ(answererPairs.size() + 1, newAnswererPairs.size()); // Ensure that the m-section was re-used; no gaps for (size_t i = 0; i < newOffererPairs.size(); ++i) { ASSERT_EQ(i, newOffererPairs[i].mLevel); } for (size_t i = 0; i < newAnswererPairs.size(); ++i) { ASSERT_EQ(i, newAnswererPairs[i].mLevel); } } TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } if (types.size() < 2 || types[0] != types[1]) { // For simplicity, just run in cases where we have two of the same type return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); RefPtr removedTrackAnswer = GetTrackAns(0, types.front()); ASSERT_TRUE(removedTrackAnswer); ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(), removedTrackAnswer->GetTrackId())); // Second instance of the same type RefPtr removedTrackOffer = GetTrackOff(1, types.front()); ASSERT_TRUE(removedTrackOffer); ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(), removedTrackOffer->GetTrackId())); OfferAnswer(CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId()); // Second m-section should be recvonly auto offer = GetParsedLocalDescription(mSessionOff); auto* msection = GetMsection(*offer, types.front(), 1); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); // First m-section should be recvonly auto answer = GetParsedLocalDescription(mSessionAns); msection = GetMsection(*answer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_FALSE(msection->IsSending()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); // This should be the only difference. ASSERT_TRUE(offererPairs[0].mReceiving); ASSERT_FALSE(newOffererPairs[0].mReceiving); // Remove this difference, let loop below take care of the rest offererPairs[0].mReceiving = nullptr; // This should be the only difference. ASSERT_TRUE(offererPairs[1].mSending); ASSERT_FALSE(newOffererPairs[1].mSending); // Remove this difference, let loop below take care of the rest offererPairs[1].mSending = nullptr; for (size_t i = 0; i < newOffererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); // This should be the only difference. ASSERT_TRUE(answererPairs[0].mSending); ASSERT_FALSE(newAnswererPairs[0].mSending); // Remove this difference, let loop below take care of the rest answererPairs[0].mSending = nullptr; // This should be the only difference. ASSERT_TRUE(answererPairs[1].mReceiving); ASSERT_FALSE(newAnswererPairs[1].mReceiving); // Remove this difference, let loop below take care of the rest answererPairs[1].mReceiving = nullptr; for (size_t i = 0; i < newAnswererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationOffererReplacesTrack) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.front() == SdpMediaSection::kApplication) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); RefPtr removedTrack = GetTrackOff(0, types.front()); ASSERT_TRUE(removedTrack); ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(), removedTrack->GetTrackId())); RefPtr addedTrack( new JsepTrack(types.front(), "newstream", "newtrack")); ASSERT_EQ(NS_OK, mSessionOff.AddTrack(addedTrack)); OfferAnswer(CHECK_SUCCESS); auto added = mSessionAns.GetRemoteTracksAdded(); auto removed = mSessionAns.GetRemoteTracksRemoved(); ASSERT_EQ(1U, added.size()); ASSERT_EQ(1U, removed.size()); ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType()); ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId()); ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId()); ASSERT_EQ(addedTrack->GetMediaType(), added[0]->GetMediaType()); ASSERT_EQ(addedTrack->GetStreamId(), added[0]->GetStreamId()); ASSERT_EQ(addedTrack->GetTrackId(), added[0]->GetTrackId()); added = mSessionOff.GetRemoteTracksAdded(); removed = mSessionOff.GetRemoteTracksRemoved(); ASSERT_EQ(0U, added.size()); ASSERT_EQ(0U, removed.size()); // First audio m-section should be sendrecv auto offer = GetParsedLocalDescription(mSessionOff); auto* msection = GetMsection(*offer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_TRUE(msection->IsSending()); // First audio m-section should be sendrecv auto answer = GetParsedLocalDescription(mSessionAns); msection = GetMsection(*answer, types.front(), 0); ASSERT_TRUE(msection); ASSERT_TRUE(msection->IsReceiving()); ASSERT_TRUE(msection->IsSending()); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); ASSERT_NE(offererPairs[0].mSending->GetStreamId(), newOffererPairs[0].mSending->GetStreamId()); ASSERT_NE(offererPairs[0].mSending->GetTrackId(), newOffererPairs[0].mSending->GetTrackId()); // Skip first pair for (size_t i = 1; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); ASSERT_NE(answererPairs[0].mReceiving->GetStreamId(), newAnswererPairs[0].mReceiving->GetStreamId()); ASSERT_NE(answererPairs[0].mReceiving->GetTrackId(), newAnswererPairs[0].mReceiving->GetTrackId()); // Skip first pair for (size_t i = 1; i < newAnswererPairs.size(); ++i) { ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i])); } } // 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); auto offererPairs = GetTrackPairsByLevel(mSessionOff); // Make sure that DisableMsid actually worked, since it is kinda hacky auto answererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(offererPairs.size(), answererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(offererPairs[i].mReceiving); ASSERT_TRUE(answererPairs[i].mSending); // These should not match since we've monkeyed with the msid ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(), answererPairs[i].mSending->GetStreamId()); ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(), answererPairs[i].mSending->GetTrackId()); } offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(mSessionAns); answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs(); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i])); } } TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent) { AddTracks(mSessionOff); AddTracks(mSessionAns); OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); // check all the audio tracks to make sure they have 2 codecs (109 and 101), // and dtmf is enabled on all audio tracks for (size_t i = 0; i < offererPairs.size(); ++i) { std::vector tracks; tracks.push_back(offererPairs[i].mSending.get()); tracks.push_back(offererPairs[i].mReceiving.get()); for (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(2U, encoding.GetCodecs().size()); ASSERT_TRUE(encoding.HasFormat("109")); ASSERT_TRUE(encoding.HasFormat("101")); for (JsepCodecDescription* codec: encoding.GetCodecs()) { ASSERT_TRUE(codec); // we can cast here because we've already checked for audio track JsepAudioCodecDescription *audioCodec = static_cast(codec); ASSERT_TRUE(audioCodec->mDtmfEnabled); } } } std::string offer = CreateOffer(); ReplaceInSdp(&offer, " 109 101 ", " 109 "); 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); AddTracks(mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); // check all the audio tracks to make sure they have 1 codec (109), // and dtmf is disabled on all audio tracks for (size_t i = 0; i < newOffererPairs.size(); ++i) { std::vector tracks; tracks.push_back(newOffererPairs[i].mSending.get()); tracks.push_back(newOffererPairs[i].mReceiving.get()); for (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(1U, encoding.GetCodecs().size()); ASSERT_TRUE(encoding.HasFormat("109")); // we can cast here because we've already checked for audio track JsepAudioCodecDescription *audioCodec = static_cast(encoding.GetCodecs()[0]); 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); auto offererPairs = GetTrackPairsByLevel(mSessionOff); offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(mSessionAns); answer = CreateAnswer(); SetLocalAnswer(answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs(); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(), newOffererPairs[i].mReceiving->GetMediaType()); ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending); ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport, newOffererPairs[i].mRtpTransport)); ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport, newOffererPairs[i].mRtcpTransport)); if (offererPairs[i].mReceiving->GetMediaType() == SdpMediaSection::kApplication) { ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving); } else { // This should be the only difference ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving); } } } 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); auto offererPairs = GetTrackPairsByLevel(mSessionOff); offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); AddTracks(mSessionAns); answer = CreateAnswer(); SetLocalAnswer(answer); DisableMsid(&answer); SetRemoteAnswer(answer, CHECK_SUCCESS); auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs(); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); for (size_t i = 0; i < offererPairs.size(); ++i) { ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(), newOffererPairs[i].mReceiving->GetMediaType()); ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending); ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport, newOffererPairs[i].mRtpTransport)); ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport, newOffererPairs[i].mRtcpTransport)); if (offererPairs[i].mReceiving->GetMediaType() == SdpMediaSection::kApplication) { ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving); } else { // This should be the only difference ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving); } } } // 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); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); OfferAnswer(); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size()); ASSERT_EQ(offererPairs.size(), newOffererPairs.size()); ASSERT_EQ(answererPairs.size(), newAnswererPairs.size()); for (size_t i = 0; i < newOffererPairs.size(); ++i) { // No bundle initially ASSERT_FALSE(offererPairs[i].mBundleLevel.isSome()); ASSERT_FALSE(answererPairs[i].mBundleLevel.isSome()); if (i != 0) { ASSERT_NE(offererPairs[0].mRtpTransport.get(), offererPairs[i].mRtpTransport.get()); if (offererPairs[0].mRtcpTransport) { ASSERT_NE(offererPairs[0].mRtcpTransport.get(), offererPairs[i].mRtcpTransport.get()); } ASSERT_NE(answererPairs[0].mRtpTransport.get(), answererPairs[i].mRtpTransport.get()); if (answererPairs[0].mRtcpTransport) { ASSERT_NE(answererPairs[0].mRtcpTransport.get(), answererPairs[i].mRtcpTransport.get()); } } // Verify that bundle worked after renegotiation ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome()); ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome()); ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(), newOffererPairs[i].mRtpTransport.get()); ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(), newOffererPairs[i].mRtcpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(), newAnswererPairs[i].mRtpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(), newAnswererPairs[i].mRtcpTransport.get()); } } TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.size() < 2) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); std::string reoffer = CreateOffer(); DisableMsection(&reoffer, 0); SetLocalOffer(reoffer, CHECK_SUCCESS); SetRemoteOffer(reoffer, CHECK_SUCCESS); std::string reanswer = CreateAnswer(); SetLocalAnswer(reanswer, CHECK_SUCCESS); SetRemoteAnswer(reanswer, CHECK_SUCCESS); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size()); ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1); ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1); for (size_t i = 0; i < newOffererPairs.size(); ++i) { ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome()); ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome()); ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel); ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel); ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(), newOffererPairs[i].mRtpTransport.get()); ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(), newOffererPairs[i].mRtcpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(), newAnswererPairs[i].mRtpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(), newAnswererPairs[i].mRtcpTransport.get()); } ASSERT_NE(newOffererPairs[0].mRtpTransport.get(), offererPairs[0].mRtpTransport.get()); ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(), answererPairs[0].mRtpTransport.get()); ASSERT_LE(1U, mSessionOff.GetTransports().size()); ASSERT_LE(1U, mSessionAns.GetTransports().size()); ASSERT_EQ(0U, mSessionOff.GetTransports()[0]->mComponents); ASSERT_EQ(0U, mSessionAns.GetTransports()[0]->mComponents); } TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport) { AddTracks(mSessionOff); AddTracks(mSessionAns); if (types.size() < 2) { return; } OfferAnswer(); auto offererPairs = GetTrackPairsByLevel(mSessionOff); auto answererPairs = GetTrackPairsByLevel(mSessionAns); std::string reoffer = CreateOffer(); SetLocalOffer(reoffer, CHECK_SUCCESS); SetRemoteOffer(reoffer, CHECK_SUCCESS); std::string reanswer = CreateAnswer(); DisableMsection(&reanswer, 0); SetLocalAnswer(reanswer, CHECK_SUCCESS); SetRemoteAnswer(reanswer, CHECK_SUCCESS); auto newOffererPairs = GetTrackPairsByLevel(mSessionOff); auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns); ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size()); ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1); ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1); for (size_t i = 0; i < newOffererPairs.size(); ++i) { ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome()); ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome()); ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel); ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel); ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(), newOffererPairs[i].mRtpTransport.get()); ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(), newOffererPairs[i].mRtcpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(), newAnswererPairs[i].mRtpTransport.get()); ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(), newAnswererPairs[i].mRtcpTransport.get()); } ASSERT_NE(newOffererPairs[0].mRtpTransport.get(), offererPairs[0].mRtpTransport.get()); ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(), answererPairs[0].mRtpTransport.get()); } TEST_P(JsepSessionTest, ParseRejectsBadMediaFormat) { if (GetParam() == "datachannel") { return; } AddTracks(mSessionOff); std::string offer = CreateOffer(); UniquePtr munge(Parse(offer)); SdpMediaSection& mediaSection = munge->GetMediaSection(0); mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1); std::string sdpString = munge->ToString(); nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, sdpString); ASSERT_EQ(NS_ERROR_INVALID_ARG, rv); } TEST_P(JsepSessionTest, FullCallWithCandidates) { AddTracks(mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); mOffCandidates.Gather(mSessionOff, types); UniquePtr localOffer(Parse(mSessionOff.GetLocalDescription())); for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { mOffCandidates.CheckRtpCandidates( true, localOffer->GetMediaSection(i), i, "Local offer after gathering should have RTP candidates."); mOffCandidates.CheckDefaultRtpCandidate( true, localOffer->GetMediaSection(i), i, "Local offer after gathering should have a default RTP candidate."); mOffCandidates.CheckRtcpCandidates( types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), i, "Local offer after gathering should have RTCP candidates " "(unless m=application)"); mOffCandidates.CheckDefaultRtcpCandidate( types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), i, "Local offer after gathering should have a default RTCP candidate " "(unless m=application)"); CheckEndOfCandidates(true, localOffer->GetMediaSection(i), "Local offer after gathering should have an end-of-candidates."); } SetRemoteOffer(offer); mOffCandidates.Trickle(mSessionAns); UniquePtr remoteOffer(Parse(mSessionAns.GetRemoteDescription())); for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { mOffCandidates.CheckRtpCandidates( true, remoteOffer->GetMediaSection(i), i, "Remote offer after trickle should have RTP candidates."); mOffCandidates.CheckDefaultRtpCandidate( false, remoteOffer->GetMediaSection(i), i, "Initial remote offer should not have a default RTP candidate."); mOffCandidates.CheckRtcpCandidates( types[i] != SdpMediaSection::kApplication, remoteOffer->GetMediaSection(i), i, "Remote offer after trickle should have RTCP candidates " "(unless m=application)"); mOffCandidates.CheckDefaultRtcpCandidate( false, remoteOffer->GetMediaSection(i), i, "Initial remote offer should not have a default RTCP candidate."); CheckEndOfCandidates(false, remoteOffer->GetMediaSection(i), "Initial remote offer should not 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, types); UniquePtr localAnswer(Parse(mSessionAns.GetLocalDescription())); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { mAnsCandidates.CheckRtpCandidates( i == 0, localAnswer->GetMediaSection(i), i, "Local answer after gathering should have RTP candidates on level 0."); mAnsCandidates.CheckDefaultRtpCandidate( true, localAnswer->GetMediaSection(i), 0, "Local answer after gathering should have a default RTP candidate " "on all levels that matches transport level 0."); mAnsCandidates.CheckRtcpCandidates( false, localAnswer->GetMediaSection(i), i, "Local answer after gathering should not have RTCP candidates " "(because we're answering with rtcp-mux)"); mAnsCandidates.CheckDefaultRtcpCandidate( false, localAnswer->GetMediaSection(i), i, "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())); for (size_t i = 0; i < remoteAnswer->GetMediaSectionCount(); ++i) { mAnsCandidates.CheckRtpCandidates( i == 0, remoteAnswer->GetMediaSection(i), i, "Remote answer after trickle should have RTP candidates on level 0."); mAnsCandidates.CheckDefaultRtpCandidate( false, remoteAnswer->GetMediaSection(i), i, "Remote answer after trickle should not have a default RTP candidate."); mAnsCandidates.CheckRtcpCandidates( false, remoteAnswer->GetMediaSection(i), i, "Remote answer after trickle should not have RTCP candidates " "(because we're answering with rtcp-mux)"); mAnsCandidates.CheckDefaultRtcpCandidate( false, remoteAnswer->GetMediaSection(i), i, "Remote answer after trickle should not have a default RTCP " "candidate."); CheckEndOfCandidates(false, remoteAnswer->GetMediaSection(i), "Remote answer after trickle should not have an end-of-candidates."); } } TEST_P(JsepSessionTest, RenegotiationWithCandidates) { AddTracks(mSessionOff); std::string offer = CreateOffer(); SetLocalOffer(offer); mOffCandidates.Gather(mSessionOff, types); SetRemoteOffer(offer); mOffCandidates.Trickle(mSessionAns); AddTracks(mSessionAns); std::string answer = CreateAnswer(); SetLocalAnswer(answer); mAnsCandidates.Gather(mSessionAns, types); SetRemoteAnswer(answer); mAnsCandidates.Trickle(mSessionOff); offer = CreateOffer(); SetLocalOffer(offer); UniquePtr parsedOffer(Parse(offer)); for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { mOffCandidates.CheckRtpCandidates( i == 0, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should have RTP candidates on level 0" " only."); mOffCandidates.CheckDefaultRtpCandidate( i == 0, parsedOffer->GetMediaSection(i), 0, "Local reoffer before gathering should have a default RTP candidate " "on level 0 only."); mOffCandidates.CheckRtcpCandidates( false, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should not have RTCP candidates."); mOffCandidates.CheckDefaultRtcpCandidate( false, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should not have a default RTCP " "candidate."); CheckEndOfCandidates(false, parsedOffer->GetMediaSection(i), "Local reoffer before gathering should not have an end-of-candidates."); } // mSessionAns should generate a reoffer that is similar std::string otherOffer; JsepOfferOptions defaultOptions; nsresult rv = mSessionAns.CreateOffer(defaultOptions, &otherOffer); ASSERT_EQ(NS_OK, rv); parsedOffer = Parse(otherOffer); for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { mAnsCandidates.CheckRtpCandidates( i == 0, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should have RTP candidates on level 0" " only. (previous answerer)"); mAnsCandidates.CheckDefaultRtpCandidate( i == 0, parsedOffer->GetMediaSection(i), 0, "Local reoffer before gathering should have a default RTP candidate " "on level 0 only. (previous answerer)"); mAnsCandidates.CheckRtcpCandidates( false, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should not have RTCP candidates." " (previous answerer)"); mAnsCandidates.CheckDefaultRtcpCandidate( false, parsedOffer->GetMediaSection(i), i, "Local reoffer before gathering should not have a default RTCP " "candidate. (previous answerer)"); CheckEndOfCandidates(false, parsedOffer->GetMediaSection(i), "Local reoffer before gathering should not have an end-of-candidates. " "(previous answerer)"); } // 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, 0, RTCP); } // Since the remaining levels were bundled, PeerConnection will re-gather for // both RTP and RTCP, in case the answerer rejects bundle. for (size_t level = 1; level < types.size(); ++level) { mOffCandidates.Gather(mSessionOff, level, RTP); if (types[level] != SdpMediaSection::kApplication) { mOffCandidates.Gather(mSessionOff, level, RTCP); } } mOffCandidates.FinishGathering(mSessionOff); mOffCandidates.Trickle(mSessionAns); UniquePtr localOffer(Parse(mSessionOff.GetLocalDescription())); for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { mOffCandidates.CheckRtpCandidates( true, localOffer->GetMediaSection(i), i, "Local reoffer after gathering should have RTP candidates."); mOffCandidates.CheckDefaultRtpCandidate( true, localOffer->GetMediaSection(i), i, "Local reoffer after gathering should have a default RTP candidate."); mOffCandidates.CheckRtcpCandidates( types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), i, "Local reoffer after gathering should have RTCP candidates " "(unless m=application)"); mOffCandidates.CheckDefaultRtcpCandidate( types[i] != SdpMediaSection::kApplication, localOffer->GetMediaSection(i), i, "Local reoffer after gathering should have a default RTCP candidate " "(unless m=application)"); CheckEndOfCandidates(true, localOffer->GetMediaSection(i), "Local reoffer after gathering should have an end-of-candidates."); } UniquePtr remoteOffer(Parse(mSessionAns.GetRemoteDescription())); for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { mOffCandidates.CheckRtpCandidates( true, remoteOffer->GetMediaSection(i), i, "Remote reoffer after trickle should have RTP candidates."); mOffCandidates.CheckDefaultRtpCandidate( i == 0, remoteOffer->GetMediaSection(i), i, "Remote reoffer should have a default RTP candidate on level 0 " "(because it was gathered last offer/answer)."); mOffCandidates.CheckRtcpCandidates( types[i] != SdpMediaSection::kApplication, remoteOffer->GetMediaSection(i), i, "Remote reoffer after trickle should have RTCP candidates."); mOffCandidates.CheckDefaultRtcpCandidate( false, remoteOffer->GetMediaSection(i), i, "Remote reoffer should not have a default RTCP candidate."); CheckEndOfCandidates(false, remoteOffer->GetMediaSection(i), "Remote reoffer should not 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())); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { mAnsCandidates.CheckRtpCandidates( i == 0, localAnswer->GetMediaSection(i), i, "Local reanswer after gathering should have RTP candidates on level " "0."); mAnsCandidates.CheckDefaultRtpCandidate( true, localAnswer->GetMediaSection(i), 0, "Local reanswer after gathering should have a default RTP candidate " "on all levels that matches transport level 0."); mAnsCandidates.CheckRtcpCandidates( false, localAnswer->GetMediaSection(i), i, "Local reanswer after gathering should not have RTCP candidates " "(because we're reanswering with rtcp-mux)"); mAnsCandidates.CheckDefaultRtcpCandidate( false, localAnswer->GetMediaSection(i), i, "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())); for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { mAnsCandidates.CheckRtpCandidates( i == 0, remoteAnswer->GetMediaSection(i), i, "Remote reanswer after trickle should have RTP candidates on level 0."); mAnsCandidates.CheckDefaultRtpCandidate( i == 0, remoteAnswer->GetMediaSection(i), i, "Remote reanswer should have a default RTP candidate on level 0 " "(because it was gathered last offer/answer)."); mAnsCandidates.CheckRtcpCandidates( false, remoteAnswer->GetMediaSection(i), i, "Remote reanswer after trickle should not have RTCP candidates " "(because we're reanswering with rtcp-mux)"); mAnsCandidates.CheckDefaultRtcpCandidate( false, remoteAnswer->GetMediaSection(i), i, "Remote reanswer after trickle should not have a default RTCP " "candidate."); CheckEndOfCandidates(false, remoteAnswer->GetMediaSection(i), "Remote reanswer after trickle should not have an end-of-candidates."); } } 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 RefPtr& track : mSessionOff.GetLocalTracks()) { if (track->GetMediaType() != SdpMediaSection::kApplication) { ASSERT_FALSE(track->GetActive()); } } ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().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 RefPtr& track : mSessionOff.GetLocalTracks()) { if (track->GetMediaType() != SdpMediaSection::kApplication) { ASSERT_FALSE(track->GetActive()); } } ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().size()); } INSTANTIATE_TEST_CASE_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) { JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(1U)); options.mOfferToReceiveVideo = Some(static_cast(2U)); options.mDontOfferDataChannel = Some(true); std::string offer = CreateOffer(Some(options)); 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 trackPairs(mSessionOff.GetNegotiatedTrackPairs()); ASSERT_EQ(2U, trackPairs.size()); for (auto pair : trackPairs) { auto ssrcs = parsedOffer->GetMediaSection(pair.mLevel).GetAttributeList() .GetSsrc().mSsrcs; ASSERT_EQ(1U, ssrcs.size()); ASSERT_EQ(pair.mRecvonlySsrc, ssrcs.front().ssrc); } } TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines) { AddTracks(mSessionOff, "audio,video,video"); JsepOfferOptions options; options.mOfferToReceiveAudio = Some(static_cast(0U)); options.mOfferToReceiveVideo = Some(static_cast(1U)); options.mDontOfferDataChannel = Some(true); std::string offer = CreateOffer(Some(options)); 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) { JsepOfferOptions options; options.mOfferToReceiveAudio = Some(1); OfferAnswer(CHECK_SUCCESS, Some(options)); UniquePtr offer(Parse(mSessionOff.GetLocalDescription())); 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())); 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) { JsepOfferOptions options; options.mOfferToReceiveVideo = Some(1); OfferAnswer(CHECK_SUCCESS, Some(options)); UniquePtr offer(Parse(mSessionOff.GetLocalDescription())); 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())); 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) { RefPtr msta( new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1")); mSessionOff.AddTrack(msta); RefPtr mstv1( new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1")); mSessionOff.AddTrack(mstv1); 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); RefPtr msta( new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1")); mSessionOff.AddTrack(msta); RefPtr mstv1( new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2")); mSessionOff.AddTrack(mstv1); std::string offer = CreateOffer(); UniquePtr outputSdp(Parse(offer)); 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(6U, video_section.GetFormats().size()); ASSERT_EQ("120", video_section.GetFormats()[0]); ASSERT_EQ("121", video_section.GetFormats()[1]); ASSERT_EQ("126", video_section.GetFormats()[2]); ASSERT_EQ("97", video_section.GetFormats()[3]); ASSERT_EQ("122", video_section.GetFormats()[4]); ASSERT_EQ("123", video_section.GetFormats()[5]); // Validate rtpmap ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); auto& rtpmaps = video_attrs.GetRtpmap(); ASSERT_TRUE(rtpmaps.HasEntry("120")); ASSERT_TRUE(rtpmaps.HasEntry("121")); ASSERT_TRUE(rtpmaps.HasEntry("126")); ASSERT_TRUE(rtpmaps.HasEntry("97")); ASSERT_TRUE(rtpmaps.HasEntry("122")); ASSERT_TRUE(rtpmaps.HasEntry("123")); auto& vp8_entry = rtpmaps.GetEntry("120"); auto& vp9_entry = rtpmaps.GetEntry("121"); auto& h264_1_entry = rtpmaps.GetEntry("126"); auto& h264_0_entry = rtpmaps.GetEntry("97"); auto& red_0_entry = rtpmaps.GetEntry("122"); auto& ulpfec_0_entry = rtpmaps.GetEntry("123"); ASSERT_EQ("VP8", vp8_entry.name); ASSERT_EQ("VP9", vp9_entry.name); ASSERT_EQ("H264", h264_1_entry.name); ASSERT_EQ("H264", h264_0_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(5U, 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); // VP9 const SdpFmtpAttributeList::Parameters* vp9_params = video_section.FindFmtp("121"); ASSERT_TRUE(vp9_params); ASSERT_EQ(SdpRtpmapAttributeList::kVP9, vp9_params->codec_type); 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); // 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); 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 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); 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); // red const SdpFmtpAttributeList::Parameters* red_params = video_section.FindFmtp("122"); ASSERT_TRUE(red_params); ASSERT_EQ(SdpRtpmapAttributeList::kRed, red_params->codec_type); 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); RefPtr msta( new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1")); mSessionOff.AddTrack(msta); RefPtr mstv1( new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2")); mSessionOff.AddTrack(mstv1); 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); // 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, ValidateAnsweredCodecParams) { // 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 i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end(); ++i) { auto* codec = *i; if (codec->mName == "H264") { JsepVideoCodecDescription* h264 = static_cast(codec); 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); RefPtr msta( new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1")); mSessionOff.AddTrack(msta); RefPtr mstv1( new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1")); mSessionOff.AddTrack(mstv1); std::string offer = CreateOffer(); SetLocalOffer(offer); SetRemoteOffer(offer); RefPtr msta_ans( new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1")); mSessionAns.AddTrack(msta); RefPtr mstv1_ans( new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1")); mSessionAns.AddTrack(mstv1); 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()); // TODO(bug 1099351): Once fixed, this stuff will need to be updated. ASSERT_EQ(1U, video_section.GetFormats().size()); // ASSERT_EQ(3U, video_section.GetFormats().size()); ASSERT_EQ("120", video_section.GetFormats()[0]); // ASSERT_EQ("121", video_section.GetFormats()[1]); // ASSERT_EQ("126", video_section.GetFormats()[2]); // ASSERT_EQ("97", 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")); // ASSERT_TRUE(rtpmaps.HasEntry("126")); // ASSERT_TRUE(rtpmaps.HasEntry("97")); auto& vp8_entry = rtpmaps.GetEntry("120"); //auto& vp9_entry = rtpmaps.GetEntry("121"); // auto& h264_1_entry = rtpmaps.GetEntry("126"); // auto& h264_0_entry = rtpmaps.GetEntry("97"); ASSERT_EQ("VP8", vp8_entry.name); //ASSERT_EQ("VP9", vp9_entry.name); // ASSERT_EQ("H264", h264_1_entry.name); // ASSERT_EQ("H264", h264_0_entry.name); // Validate fmtps ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); auto& fmtps = video_attrs.GetFmtp().mFmtps; ASSERT_EQ(1U, fmtps.size()); // ASSERT_EQ(3U, 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); SetLocalAnswer(answer); SetRemoteAnswer(answer); auto offerPairs = mSessionOff.GetNegotiatedTrackPairs(); ASSERT_EQ(2U, offerPairs.size()); ASSERT_TRUE(offerPairs[1].mSending); ASSERT_TRUE(offerPairs[1].mReceiving); ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails()); ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(1U, offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0) .GetCodecs().size()); ASSERT_EQ(1U, offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0) .GetCodecs().size()); auto answerPairs = mSessionAns.GetNegotiatedTrackPairs(); ASSERT_EQ(2U, answerPairs.size()); ASSERT_TRUE(answerPairs[1].mSending); ASSERT_TRUE(answerPairs[1].mReceiving); ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails()); ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(1U, answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0) .GetCodecs().size()); ASSERT_EQ(1U, answerPairs[1].mReceiving->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 } 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 pairIndex, sdp::Direction direction, size_t encodingIndex, size_t codecIndex, const JsepCodecDescription** codecOut) { *codecOut = nullptr; ASSERT_LT(pairIndex, session.GetNegotiatedTrackPairs().size()); JsepTrackPair pair(session.GetNegotiatedTrackPairs().front()); RefPtr track( (direction == sdp::kSend) ? pair.mSending : pair.mReceiving); ASSERT_TRUE(track); ASSERT_TRUE(track->GetNegotiatedDetails()); ASSERT_LT(encodingIndex, track->GetNegotiatedDetails()->GetEncodingCount()); ASSERT_LT(codecIndex, track->GetNegotiatedDetails()->GetEncoding(encodingIndex) .GetCodecs().size()); *codecOut = track->GetNegotiatedDetails()->GetEncoding(encodingIndex) .GetCodecs()[codecIndex]; } static void ForceH264(JsepSession& session, uint32_t profileLevelId) { for (JsepCodecDescription* codec : session.Codecs()) { if (codec->mName == "H264") { JsepVideoCodecDescription* h264 = static_cast(codec); 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); const JsepCodecDescription* offererSendCodec; GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec)); ASSERT_EQ((uint32_t)0x42e00d, offererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* offererRecvCodec; GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec)); ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); const JsepCodecDescription* answererSendCodec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec)); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* answererRecvCodec; GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec)); 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(0U, mSessionOff.GetNegotiatedTrackPairs().size()); ASSERT_EQ(0U, mSessionAns.GetNegotiatedTrackPairs().size()); } 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); const JsepCodecDescription* answererSendCodec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec)); 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); const JsepCodecDescription* answererSendCodec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec)); ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* answererRecvCodec; GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec)); 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. const JsepCodecDescription* answererSendCodec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec)); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* answererRecvCodec; GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec)); 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. const JsepCodecDescription* answererSendCodec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); ASSERT_TRUE(answererSendCodec); ASSERT_EQ("H264", answererSendCodec->mName); const JsepVideoCodecDescription* answererVideoSendCodec( static_cast(answererSendCodec)); ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* answererRecvCodec; GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); ASSERT_EQ("H264", answererRecvCodec->mName); const JsepVideoCodecDescription* answererVideoRecvCodec( static_cast(answererRecvCodec)); 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); const JsepCodecDescription* offererSendCodec; GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec)); ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* offererRecvCodec; GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec)); 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); const JsepCodecDescription* offererSendCodec; GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); ASSERT_TRUE(offererSendCodec); ASSERT_EQ("H264", offererSendCodec->mName); const JsepVideoCodecDescription* offererVideoSendCodec( static_cast(offererSendCodec)); ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); const JsepCodecDescription* offererRecvCodec; GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); ASSERT_EQ("H264", offererRecvCodec->mName); const JsepVideoCodecDescription* offererVideoRecvCodec( static_cast(offererRecvCodec)); 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()); std::sort(types.begin(), types.end()); switch (types.front()) { 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() == types.front()) { 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(), types.front()); size_t numAccepted = types.size() - numRejected; ASSERT_EQ(numAccepted, mSessionOff.GetNegotiatedTrackPairs().size()); ASSERT_EQ(numAccepted, mSessionAns.GetNegotiatedTrackPairs().size()); ASSERT_EQ(types.size(), mSessionOff.GetTransports().size()); ASSERT_EQ(types.size(), mSessionOff.GetLocalTracks().size()); ASSERT_EQ(numAccepted, mSessionOff.GetRemoteTracks().size()); ASSERT_EQ(types.size(), mSessionAns.GetTransports().size()); ASSERT_EQ(types.size(), mSessionAns.GetLocalTracks().size()); ASSERT_EQ(types.size(), mSessionAns.GetRemoteTracks().size()); } TEST_F(JsepSessionTest, CreateOfferNoMlines) { JsepOfferOptions options; std::string offer; nsresult rv = mSessionOff.CreateOffer(options, &offer); ASSERT_NE(NS_OK, rv); ASSERT_NE("", mSessionOff.GetLastError()); } 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, TestExtmap) { AddTracks(mSessionOff, "audio"); AddTracks(mSessionAns, "audio"); // ssrc-audio-level will be extmap 1 for both mSessionOff.AddAudioRtpExtension("foo"); // Default mapping of 2 mSessionOff.AddAudioRtpExtension("bar"); // Default mapping of 3 mSessionAns.AddAudioRtpExtension("bar"); // Default mapping of 2 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(3U, offerExtmap.size()); ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", offerExtmap[0].extensionname); ASSERT_EQ(1U, offerExtmap[0].entry); ASSERT_EQ("foo", offerExtmap[1].extensionname); ASSERT_EQ(2U, offerExtmap[1].entry); ASSERT_EQ("bar", offerExtmap[2].extensionname); ASSERT_EQ(3U, offerExtmap[2].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(1U, answerExtmap.size()); // We ensure that the entry for "bar" matches what was in the offer ASSERT_EQ("bar", answerExtmap[0].extensionname); ASSERT_EQ(3U, answerExtmap[0].entry); } 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, mSessionAns.GetRemoteTracks().size()); RefPtr track = mSessionAns.GetRemoteTracks()[0]; ASSERT_TRUE(track->GetNegotiatedDetails()); auto* details = track->GetNegotiatedDetails(); for (const JsepCodecDescription* codec : details->GetEncoding(0).GetCodecs()) { const JsepVideoCodecDescription* videoCodec = static_cast(codec); 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 offerPairs = mSessionOff.GetNegotiatedTrackPairs(); auto answerPairs = mSessionAns.GetNegotiatedTrackPairs(); ASSERT_EQ(3U, offerPairs.size()); ASSERT_EQ(3U, answerPairs.size()); ASSERT_TRUE(offerPairs[0].mReceiving); ASSERT_TRUE(offerPairs[0].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(0U, offerPairs[0].mReceiving->GetNegotiatedDetails()-> GetUniquePayloadTypes().size()); ASSERT_TRUE(offerPairs[1].mReceiving); ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(0U, offerPairs[1].mReceiving->GetNegotiatedDetails()-> GetUniquePayloadTypes().size()); ASSERT_TRUE(offerPairs[2].mReceiving); ASSERT_TRUE(offerPairs[2].mReceiving->GetNegotiatedDetails()); ASSERT_NE(0U, offerPairs[2].mReceiving->GetNegotiatedDetails()-> GetUniquePayloadTypes().size()); ASSERT_TRUE(answerPairs[0].mReceiving); ASSERT_TRUE(answerPairs[0].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(0U, answerPairs[0].mReceiving->GetNegotiatedDetails()-> GetUniquePayloadTypes().size()); ASSERT_TRUE(answerPairs[1].mReceiving); ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails()); ASSERT_EQ(0U, answerPairs[1].mReceiving->GetNegotiatedDetails()-> GetUniquePayloadTypes().size()); ASSERT_TRUE(answerPairs[2].mReceiving); ASSERT_TRUE(answerPairs[2].mReceiving->GetNegotiatedDetails()); ASSERT_NE(0U, answerPairs[2].mReceiving->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); nsresult rv = mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer); ASSERT_NE(NS_OK, rv); 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 (JsepCodecDescription* codec : mSessionAns.Codecs()) { if (codec->mName == "H264") { codec->mStronglyPreferred = true; } } types.push_back(SdpMediaSection::kVideo); AddTracks(mSessionOff, "video"); AddTracks(mSessionAns, "video"); OfferAnswer(); const JsepCodecDescription* 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(); const JsepCodecDescription* 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, PayloadTypeClash) { // Disable this so mSessionOff doesn't have a duplicate SetCodecEnabled(mSessionOff, "PCMU", false); SetPayloadTypeNumber(mSessionOff, "opus", "0"); SetPayloadTypeNumber(mSessionAns, "PCMU", "0"); types.push_back(SdpMediaSection::kAudio); AddTracks(mSessionOff, "audio"); AddTracks(mSessionAns, "audio"); OfferAnswer(); const JsepCodecDescription* codec; GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("0", codec->mDefaultPt); GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &codec); ASSERT_TRUE(codec); ASSERT_EQ("opus", codec->mName); ASSERT_EQ("0", codec->mDefaultPt); // Now, make sure that mSessionAns does not put a=rtpmap:0 PCMU in a reoffer, // since pt 0 is taken for opus (the answerer still supports PCMU, and will // reoffer it, but it should choose a new payload type for it) JsepOfferOptions options; std::string reoffer; nsresult rv = mSessionAns.CreateOffer(options, &reoffer); ASSERT_EQ(NS_OK, rv); ASSERT_EQ(std::string::npos, reoffer.find("a=rtpmap:0 PCMU")) << reoffer; } TEST_P(JsepSessionTest, TestGlareRollback) { AddTracks(mSessionOff); AddTracks(mSessionAns); JsepOfferOptions options; std::string offer; ASSERT_EQ(NS_OK, mSessionAns.CreateOffer(options, &offer)); ASSERT_EQ(NS_OK, mSessionAns.SetLocalDescription(kJsepSdpOffer, offer)); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionAns.GetState()); ASSERT_EQ(NS_OK, mSessionOff.CreateOffer(options, &offer)); ASSERT_EQ(NS_OK, mSessionOff.SetLocalDescription(kJsepSdpOffer, offer)); ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff.GetState()); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer)); ASSERT_EQ(NS_OK, mSessionAns.SetLocalDescription(kJsepSdpRollback, "")); 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_EQ(NS_OK, mSessionAns.SetRemoteDescription(kJsepSdpRollback, "")); ASSERT_EQ(kJsepStateStable, mSessionAns.GetState()); ASSERT_EQ(types.size(), mSessionAns.GetRemoteTracksRemoved().size()); ASSERT_EQ(NS_OK, mSessionOff.SetLocalDescription(kJsepSdpRollback, "")); ASSERT_EQ(kJsepStateStable, mSessionOff.GetState()); OfferAnswer(); } TEST_P(JsepSessionTest, TestInvalidRollback) { AddTracks(mSessionOff); AddTracks(mSessionAns); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetLocalDescription(kJsepSdpRollback, "")); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetRemoteDescription(kJsepSdpRollback, "")); std::string offer = CreateOffer(); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetLocalDescription(kJsepSdpRollback, "")); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetRemoteDescription(kJsepSdpRollback, "")); SetLocalOffer(offer); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetRemoteDescription(kJsepSdpRollback, "")); SetRemoteOffer(offer); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionAns.SetLocalDescription(kJsepSdpRollback, "")); std::string answer = CreateAnswer(); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionAns.SetLocalDescription(kJsepSdpRollback, "")); SetLocalAnswer(answer); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionAns.SetLocalDescription(kJsepSdpRollback, "")); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionAns.SetRemoteDescription(kJsepSdpRollback, "")); SetRemoteAnswer(answer); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetLocalDescription(kJsepSdpRollback, "")); ASSERT_EQ(NS_ERROR_UNEXPECTED, mSessionOff.SetRemoteDescription(kJsepSdpRollback, "")); } size_t GetActiveTransportCount(const JsepSession& session) { auto transports = session.GetTransports(); size_t activeTransportCount = 0; for (RefPtr& transport : transports) { activeTransportCount += transport->mComponents; } return activeTransportCount; } TEST_P(JsepSessionTest, TestBalancedBundle) { AddTracks(mSessionOff); AddTracks(mSessionAns); mSessionOff.SetBundlePolicy(kBundleBalanced); std::string offer = CreateOffer(); SipccSdpParser parser; UniquePtr parsedOffer = parser.Parse(offer); 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); CheckPairs(mSessionOff, "Offerer pairs"); CheckPairs(mSessionAns, "Answerer pairs"); EXPECT_EQ(1U, GetActiveTransportCount(mSessionOff)); EXPECT_EQ(1U, GetActiveTransportCount(mSessionAns)); } TEST_P(JsepSessionTest, TestMaxBundle) { AddTracks(mSessionOff); AddTracks(mSessionAns); mSessionOff.SetBundlePolicy(kBundleMaxBundle); OfferAnswer(); std::string offer = mSessionOff.GetLocalDescription(); SipccSdpParser parser; UniquePtr parsedOffer = parser.Parse(offer); ASSERT_TRUE(parsedOffer.get()); ASSERT_FALSE( parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute)); for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) { ASSERT_TRUE( parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute( SdpAttribute::kBundleOnlyAttribute)); } CheckPairs(mSessionOff, "Offerer pairs"); CheckPairs(mSessionAns, "Answerer pairs"); 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_EQ(NS_OK, mSessionOff.CreateOffer(JsepOfferOptions(), &offer)); 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()); } } // namespace mozilla int main(int argc, char** argv) { // Prevents some log spew ScopedXPCOM xpcom("jsep_session_unittest"); NSS_NoDB_Init(nullptr); NSS_SetDomesticPolicy(); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }