summaryrefslogtreecommitdiffstats
path: root/media/webrtc/signaling/test/jsep_session_unittest.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'media/webrtc/signaling/test/jsep_session_unittest.cpp')
-rw-r--r--media/webrtc/signaling/test/jsep_session_unittest.cpp4235
1 files changed, 4235 insertions, 0 deletions
diff --git a/media/webrtc/signaling/test/jsep_session_unittest.cpp b/media/webrtc/signaling/test/jsep_session_unittest.cpp
new file mode 100644
index 000000000..d29400771
--- /dev/null
+++ b/media/webrtc/signaling/test/jsep_session_unittest.cpp
@@ -0,0 +1,4235 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <iostream>
+#include <map>
+
+#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<std::string>
+{
+public:
+ JsepSessionTest()
+ : mSessionOff("Offerer", MakeUnique<FakeUuidGenerator>()),
+ mSessionAns("Answerer", MakeUnique<FakeUuidGenerator>())
+ {
+ 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<std::string, std::vector<uint8_t> > mFingerprints;
+ };
+
+ void
+ AddDtlsFingerprint(const std::string& alg, JsepSessionImpl* session,
+ TransportData* tdata)
+ {
+ std::vector<uint8_t> 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<JsepOfferOptions> 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<SdpMediaSection::MediaType>
+ BuildTypes(const std::string& mediatypes)
+ {
+ std::vector<SdpMediaSection::MediaType> 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<SdpMediaSection::MediaType>& 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<SdpMediaSection::MediaType>& 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<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
+ side.AddTrack(mst);
+ }
+ }
+
+ bool HasMediaStream(std::vector<RefPtr<JsepTrack>> 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<std::string>
+ GetMediaStreamIds(std::vector<RefPtr<JsepTrack>> tracks) const {
+ std::vector<std::string> 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<std::string>
+ GetLocalMediaStreamIds(JsepSessionImpl& side) const {
+ return GetMediaStreamIds(side.GetLocalTracks());
+ }
+
+ std::vector<std::string>
+ GetRemoteMediaStreamIds(JsepSessionImpl& side) const {
+ return GetMediaStreamIds(side.GetRemoteTracks());
+ }
+
+ std::vector<std::string>
+ sortUniqueStrVector(std::vector<std::string> 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<std::string>
+ GetLocalUniqueStreamIds(JsepSessionImpl& side) const {
+ return sortUniqueStrVector(GetLocalMediaStreamIds(side));
+ }
+
+ std::vector<std::string>
+ GetRemoteUniqueStreamIds(JsepSessionImpl& side) const {
+ return sortUniqueStrVector(GetRemoteMediaStreamIds(side));
+ }
+
+ RefPtr<JsepTrack> 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<JsepTrack>(nullptr);
+ }
+
+ RefPtr<JsepTrack> GetTrackOff(size_t index,
+ SdpMediaSection::MediaType type) {
+ return GetTrack(mSessionOff, type, index);
+ }
+
+ RefPtr<JsepTrack> 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<JsepTrackPair> 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<f1.mFingerprints.size(); ++i) {
+ if (!Equals(f1.mFingerprints[i], f2.mFingerprints[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool Equals(const UniquePtr<JsepDtlsTransport>& t1,
+ const UniquePtr<JsepDtlsTransport>& 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<JsepIceTransport>& t1,
+ const UniquePtr<JsepIceTransport>& 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<JsepTransport>& t1,
+ const RefPtr<JsepTransport>& 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<Sdp> 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<JsepOfferOptions> 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<SdpMediaSection::MediaType>& 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<std::string> candidates;
+ for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) {
+ ++port;
+ std::ostringstream candidate;
+ candidate << "0 " << static_cast<uint16_t>(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, Candidate>(
+ 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<Level,
+ std::map<ComponentType,
+ std::pair<Address, Port>>> mDefaultCandidates;
+ std::map<Level,
+ std::map<ComponentType,
+ std::vector<Candidate>>> mCandidates;
+ // Level/mid/candidate tuples that need to be trickled
+ std::vector<Tuple<Level, Mid, Candidate>> 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<Sdp> 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<const JsepAudioCodecDescription*>(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<Sdp>
+ Parse(const std::string& sdp) const
+ {
+ SipccSdpParser parser;
+ UniquePtr<Sdp> 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<SdpMediaSection::MediaType> types;
+ std::vector<std::pair<std::string, uint16_t>> mGatheredCandidates;
+
+private:
+ void
+ ValidateTransport(TransportData& source, const std::string& sdp_str)
+ {
+ UniquePtr<Sdp> 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<SdpMediaSection::MediaType> 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<SdpMediaSection::MediaType> 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<SdpMediaSection::MediaType> 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<SdpMediaSection::MediaType> 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<JsepTrack> 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<JsepTrack> 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<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
+ ASSERT_TRUE(removedTrackAnswer);
+ ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
+ removedTrackAnswer->GetTrackId()));
+
+ RefPtr<JsepTrack> 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<JsepTrack> removedTrackAnswer = GetTrackAns(0, removedType);
+ ASSERT_TRUE(removedTrackAnswer);
+ ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
+ removedTrackAnswer->GetTrackId()));
+
+ RefPtr<JsepTrack> 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<SdpMediaSection::MediaType> 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<JsepTrack> 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<JsepTrack> 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<JsepTrack> removedTrack = GetTrackOff(0, types.front());
+ ASSERT_TRUE(removedTrack);
+ ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(),
+ removedTrack->GetTrackId()));
+ RefPtr<JsepTrack> 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<JsepTrack*> 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<JsepAudioCodecDescription*>(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<JsepTrack*> 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<JsepAudioCodecDescription*>(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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<JsepTrack>& 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<Sdp> 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<JsepTrack>& 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<size_t>(1U));
+ options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
+ options.mDontOfferDataChannel = Some(true);
+ std::string offer = CreateOffer(Some(options));
+
+ UniquePtr<Sdp> 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<Sdp> 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<JsepTrackPair> 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<size_t>(0U));
+ options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
+ options.mDontOfferDataChannel = Some(true);
+ std::string offer = CreateOffer(Some(options));
+
+ UniquePtr<Sdp> 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<size_t>(1);
+
+ OfferAnswer(CHECK_SUCCESS, Some(options));
+
+ UniquePtr<Sdp> 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<Sdp> 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<size_t>(1);
+
+ OfferAnswer(CHECK_SUCCESS, Some(options));
+
+ UniquePtr<Sdp> 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<Sdp> 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<JsepTrack> msta(
+ new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+ mSessionOff.AddTrack(msta);
+
+ RefPtr<JsepTrack> mstv1(
+ new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+ mSessionOff.AddTrack(mstv1);
+
+ std::string offer = CreateOffer();
+
+ UniquePtr<Sdp> 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<JsepTrack> msta(
+ new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+ mSessionOff.AddTrack(msta);
+ RefPtr<JsepTrack> mstv1(
+ new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
+ mSessionOff.AddTrack(mstv1);
+
+ std::string offer = CreateOffer();
+
+ UniquePtr<Sdp> 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<const SdpFmtpAttributeList::VP8Parameters*>(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<const SdpFmtpAttributeList::VP8Parameters*>(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<const SdpFmtpAttributeList::H264Parameters*>(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<const SdpFmtpAttributeList::H264Parameters*>(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<const SdpFmtpAttributeList::RedParameters*>(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<JsepTrack> msta(
+ new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+ mSessionOff.AddTrack(msta);
+ RefPtr<JsepTrack> mstv1(
+ new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
+ mSessionOff.AddTrack(mstv1);
+
+ std::string offer = CreateOffer();
+
+ UniquePtr<Sdp> 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<const SdpFmtpAttributeList::OpusParameters*>(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<const SdpFmtpAttributeList::TelephoneEventParameters*>
+ (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<JsepVideoCodecDescription*>(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<JsepTrack> msta(
+ new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+ mSessionOff.AddTrack(msta);
+ RefPtr<JsepTrack> mstv1(
+ new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+ mSessionOff.AddTrack(mstv1);
+
+ std::string offer = CreateOffer();
+ SetLocalOffer(offer);
+ SetRemoteOffer(offer);
+
+ RefPtr<JsepTrack> msta_ans(
+ new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1"));
+ mSessionAns.AddTrack(msta);
+ RefPtr<JsepTrack> mstv1_ans(
+ new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1"));
+ mSessionAns.AddTrack(mstv1);
+
+ std::string answer = CreateAnswer();
+
+ UniquePtr<Sdp> 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<const SdpFmtpAttributeList::VP8Parameters*>(
+ 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<const SdpFmtpAttributeList::H264Parameters*>(
+ 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<const SdpFmtpAttributeList::H264Parameters*>(
+ 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<JsepTrack> 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<JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<const JsepVideoCodecDescription*>(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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<Sdp> 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<JsepTrack> 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<const JsepVideoCodecDescription*>(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<JsepTransport>& 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<Sdp> parsedOffer = parser.Parse(offer);
+ ASSERT_TRUE(parsedOffer.get());
+
+ std::map<SdpMediaSection::MediaType, SdpMediaSection*> 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<Sdp> 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<Sdp> 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<Sdp> 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();
+}