summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/jsep/JsepSessionImpl.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/webrtc/jsep/JsepSessionImpl.cpp')
-rw-r--r--dom/media/webrtc/jsep/JsepSessionImpl.cpp2457
1 files changed, 2457 insertions, 0 deletions
diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.cpp b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
new file mode 100644
index 0000000000..bb792e7764
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
@@ -0,0 +1,2457 @@
+/* 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 "jsep/JsepSessionImpl.h"
+
+#include <stdlib.h>
+
+#include <bitset>
+#include <iterator>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "transport/logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/net/DataChannelProtocol.h"
+#include "nsDebug.h"
+#include "nspr.h"
+#include "nss.h"
+#include "pk11pub.h"
+
+#include "api/rtp_parameters.h"
+
+#include "jsep/JsepTrack.h"
+#include "jsep/JsepTransport.h"
+#include "sdp/HybridSdpParser.h"
+#include "sdp/SipccSdp.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("jsep")
+
+#define JSEP_SET_ERROR(error) \
+ do { \
+ std::ostringstream os; \
+ os << error; \
+ mLastError = os.str(); \
+ MOZ_MTLOG(ML_ERROR, "[" << mName << "]: " << mLastError); \
+ } while (0);
+
+static std::bitset<128> GetForbiddenSdpPayloadTypes() {
+ std::bitset<128> forbidden(0);
+ forbidden[1] = true;
+ forbidden[2] = true;
+ forbidden[19] = true;
+ for (uint16_t i = 64; i < 96; ++i) {
+ forbidden[i] = true;
+ }
+ return forbidden;
+}
+
+static std::string GetRandomHex(size_t words) {
+ std::ostringstream os;
+
+ for (size_t i = 0; i < words; ++i) {
+ uint32_t rand;
+ SECStatus rv = PK11_GenerateRandom(reinterpret_cast<unsigned char*>(&rand),
+ sizeof(rand));
+ if (rv != SECSuccess) {
+ MOZ_CRASH();
+ return "";
+ }
+
+ os << std::hex << std::setfill('0') << std::setw(8) << rand;
+ }
+ return os.str();
+}
+
+JsepSessionImpl::JsepSessionImpl(const JsepSessionImpl& aOrig)
+ : JsepSession(aOrig),
+ JsepSessionCopyableStuff(aOrig),
+ mUuidGen(aOrig.mUuidGen->Clone()),
+ mGeneratedOffer(aOrig.mGeneratedOffer ? aOrig.mGeneratedOffer->Clone()
+ : nullptr),
+ mGeneratedAnswer(aOrig.mGeneratedAnswer ? aOrig.mGeneratedAnswer->Clone()
+ : nullptr),
+ mCurrentLocalDescription(aOrig.mCurrentLocalDescription
+ ? aOrig.mCurrentLocalDescription->Clone()
+ : nullptr),
+ mCurrentRemoteDescription(aOrig.mCurrentRemoteDescription
+ ? aOrig.mCurrentRemoteDescription->Clone()
+ : nullptr),
+ mPendingLocalDescription(aOrig.mPendingLocalDescription
+ ? aOrig.mPendingLocalDescription->Clone()
+ : nullptr),
+ mPendingRemoteDescription(aOrig.mPendingRemoteDescription
+ ? aOrig.mPendingRemoteDescription->Clone()
+ : nullptr),
+ mSdpHelper(&mLastError),
+ mParser(new HybridSdpParser()) {
+ for (const auto& codec : aOrig.mSupportedCodecs) {
+ mSupportedCodecs.emplace_back(codec->Clone());
+ }
+}
+
+nsresult JsepSessionImpl::Init() {
+ mLastError.clear();
+
+ MOZ_ASSERT(!mSessionId, "Init called more than once");
+
+ nsresult rv = SetupIds();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mEncodeTrackId =
+ Preferences::GetBool("media.peerconnection.sdp.encode_track_id", true);
+
+ mIceUfrag = GetRandomHex(1);
+ mIcePwd = GetRandomHex(4);
+ return NS_OK;
+}
+
+static void GetIceCredentials(
+ const Sdp& aSdp,
+ std::set<std::pair<std::string, std::string>>* aCredentials) {
+ for (size_t i = 0; i < aSdp.GetMediaSectionCount(); ++i) {
+ const SdpAttributeList& attrs = aSdp.GetMediaSection(i).GetAttributeList();
+ if (attrs.HasAttribute(SdpAttribute::kIceUfragAttribute) &&
+ attrs.HasAttribute(SdpAttribute::kIcePwdAttribute)) {
+ aCredentials->insert(
+ std::make_pair(attrs.GetIceUfrag(), attrs.GetIcePwd()));
+ }
+ }
+}
+
+std::set<std::pair<std::string, std::string>>
+JsepSessionImpl::GetLocalIceCredentials() const {
+ std::set<std::pair<std::string, std::string>> result;
+ if (mCurrentLocalDescription) {
+ GetIceCredentials(*mCurrentLocalDescription, &result);
+ }
+ if (mPendingLocalDescription) {
+ GetIceCredentials(*mPendingLocalDescription, &result);
+ }
+ return result;
+}
+
+void JsepSessionImpl::AddTransceiver(const JsepTransceiver& aTransceiver) {
+ mLastError.clear();
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "]: Adding transceiver " << aTransceiver.GetUuid());
+#ifdef DEBUG
+ if (aTransceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ // Make sure we don't add more than one DataChannel transceiver
+ for (const auto& transceiver : mTransceivers) {
+ MOZ_ASSERT(transceiver.GetMediaType() != SdpMediaSection::kApplication);
+ }
+ }
+#endif
+ mTransceivers.push_back(aTransceiver);
+ InitTransceiver(mTransceivers.back());
+}
+
+void JsepSessionImpl::InitTransceiver(JsepTransceiver& aTransceiver) {
+ mLastError.clear();
+
+ if (aTransceiver.GetMediaType() != SdpMediaSection::kApplication) {
+ // Make sure we have an ssrc. Might already be set.
+ aTransceiver.mSendTrack.EnsureSsrcs(mSsrcGenerator, 1U);
+ aTransceiver.mSendTrack.SetCNAME(mCNAME);
+
+ // Make sure we have identifiers for send track, just in case.
+ // (man I hate this)
+ if (mEncodeTrackId) {
+ aTransceiver.mSendTrack.SetTrackId(aTransceiver.GetUuid());
+ }
+ } else {
+ // Datachannel transceivers should always be sendrecv. Just set it instead
+ // of asserting.
+ aTransceiver.mJsDirection = SdpDirectionAttribute::kSendrecv;
+ }
+
+ aTransceiver.mSendTrack.PopulateCodecs(mSupportedCodecs);
+ aTransceiver.mRecvTrack.PopulateCodecs(mSupportedCodecs);
+ // We do not set mLevel yet, we do that either on createOffer, or setRemote
+}
+
+nsresult JsepSessionImpl::SetBundlePolicy(JsepBundlePolicy policy) {
+ mLastError.clear();
+
+ if (mBundlePolicy == policy) {
+ return NS_OK;
+ }
+
+ if (mCurrentLocalDescription) {
+ JSEP_SET_ERROR(
+ "Changing the bundle policy is only supported before the "
+ "first SetLocalDescription.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mBundlePolicy = policy;
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddDtlsFingerprint(
+ const std::string& algorithm, const std::vector<uint8_t>& value) {
+ mLastError.clear();
+ JsepDtlsFingerprint fp;
+
+ fp.mAlgorithm = algorithm;
+ fp.mValue = value;
+
+ mDtlsFingerprints.push_back(fp);
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddRtpExtension(
+ JsepMediaType mediaType, const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ mLastError.clear();
+
+ for (auto& ext : mRtpExtensions) {
+ if (ext.mExtmap.direction == direction &&
+ ext.mExtmap.extensionname == extensionName) {
+ if (ext.mMediaType != mediaType) {
+ ext.mMediaType = JsepMediaType::kAudioVideo;
+ }
+ return NS_OK;
+ }
+ }
+
+ uint16_t freeEntry = GetNeverUsedExtmapEntry();
+
+ if (freeEntry == 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JsepExtmapMediaType extMediaType = {
+ mediaType,
+ {freeEntry, direction,
+ // do we want to specify direction?
+ direction != SdpDirectionAttribute::kSendrecv, extensionName, ""}};
+
+ mRtpExtensions.push_back(extMediaType);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddAudioRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kAudio, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::AddVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kVideo, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::AddAudioVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kAudioVideo, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::CreateOfferMsection(const JsepOfferOptions& options,
+ JsepTransceiver& transceiver,
+ Sdp* local) {
+ SdpMediaSection::Protocol protocol(
+ SdpHelper::GetProtocolForMediaType(transceiver.GetMediaType()));
+
+ const Sdp* answer(GetAnswer());
+ const SdpMediaSection* lastAnswerMsection = nullptr;
+
+ if (answer &&
+ (local->GetMediaSectionCount() < answer->GetMediaSectionCount())) {
+ lastAnswerMsection =
+ &answer->GetMediaSection(local->GetMediaSectionCount());
+ // Use the protocol the answer used, even if it is not what we would have
+ // used.
+ protocol = lastAnswerMsection->GetProtocol();
+ }
+
+ SdpMediaSection* msection = &local->AddMediaSection(
+ transceiver.GetMediaType(), transceiver.mJsDirection, 0, protocol,
+ sdp::kIPv4, "0.0.0.0");
+
+ // Some of this stuff (eg; mid) sticks around even if disabled
+ if (lastAnswerMsection) {
+ MOZ_ASSERT(lastAnswerMsection->GetMediaType() ==
+ transceiver.GetMediaType());
+ nsresult rv = mSdpHelper.CopyStickyParams(*lastAnswerMsection, msection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (transceiver.IsStopped()) {
+ SdpHelper::DisableMsection(local, msection);
+ return NS_OK;
+ }
+
+ msection->SetPort(9);
+
+ // We don't do this in AddTransportAttributes because that is also used for
+ // making answers, and we don't want to unconditionally set rtcp-mux or
+ // rtcp-rsize there.
+ if (mSdpHelper.HasRtcp(msection->GetProtocol())) {
+ // Set RTCP-MUX.
+ msection->GetAttributeList().SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+ // Set RTCP-RSIZE
+ if (msection->GetMediaType() == SdpMediaSection::MediaType::kVideo &&
+ Preferences::GetBool("media.navigator.video.offer_rtcp_rsize", false)) {
+ msection->GetAttributeList().SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+ }
+ }
+
+ nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.AddToOffer(mSsrcGenerator, msection);
+ transceiver.mRecvTrack.AddToOffer(mSsrcGenerator, msection);
+
+ AddExtmap(msection);
+
+ std::string mid;
+ // We do not set the mid on the transceiver, that happens when a description
+ // is set.
+ if (transceiver.IsAssociated()) {
+ mid = transceiver.GetMid();
+ } else {
+ mid = GetNewMid();
+ }
+
+ msection->GetAttributeList().SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::SetupBundle(Sdp* sdp) const {
+ std::vector<std::string> mids;
+ std::set<SdpMediaSection::MediaType> observedTypes;
+
+ // This has the effect of changing the bundle level if the first m-section
+ // goes from disabled to enabled. This is kinda inefficient.
+
+ for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
+ auto& attrs = sdp->GetMediaSection(i).GetAttributeList();
+ if ((sdp->GetMediaSection(i).GetPort() != 0) &&
+ attrs.HasAttribute(SdpAttribute::kMidAttribute)) {
+ bool useBundleOnly = false;
+ switch (mBundlePolicy) {
+ case kBundleMaxCompat:
+ // We don't use bundle-only for max-compat
+ break;
+ case kBundleBalanced:
+ // balanced means we use bundle-only on everything but the first
+ // m-section of a given type
+ if (observedTypes.count(sdp->GetMediaSection(i).GetMediaType())) {
+ useBundleOnly = true;
+ }
+ observedTypes.insert(sdp->GetMediaSection(i).GetMediaType());
+ break;
+ case kBundleMaxBundle:
+ // max-bundle means we use bundle-only on everything but the first
+ // m-section
+ useBundleOnly = !mids.empty();
+ break;
+ }
+
+ if (useBundleOnly) {
+ attrs.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute));
+ // Set port to 0 for sections with bundle-only attribute. (mjf)
+ sdp->GetMediaSection(i).SetPort(0);
+ }
+
+ mids.push_back(attrs.GetMid());
+ }
+ }
+
+ if (!mids.empty()) {
+ UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
+ groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids);
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+ }
+}
+
+JsepSession::Result JsepSessionImpl::CreateOffer(
+ const JsepOfferOptions& options, std::string* offer) {
+ mLastError.clear();
+
+ if (mState != kJsepStateStable && mState != kJsepStateHaveLocalOffer) {
+ JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
+ // Spec doesn't seem to say this is an error. It probably should.
+ return dom::PCError::InvalidStateError;
+ }
+
+ // This is one of those places where CreateOffer sets some state.
+ SetIceRestarting(options.mIceRestart.isSome() && *(options.mIceRestart));
+
+ UniquePtr<Sdp> sdp;
+
+ // Make the basic SDP that is common to offer/answer.
+ nsresult rv = CreateGenericSDP(&sdp);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ for (size_t level = 0;
+ Maybe<JsepTransceiver> transceiver = GetTransceiverForLocal(level);
+ ++level) {
+ rv = CreateOfferMsection(options, *transceiver, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ SetTransceiver(*transceiver);
+ }
+
+ SetupBundle(sdp.get());
+
+ if (mCurrentLocalDescription) {
+ rv = CopyPreviousTransportParams(*GetAnswer(), *mCurrentLocalDescription,
+ *sdp, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ *offer = sdp->ToString();
+ mGeneratedOffer = std::move(sdp);
+ ++mSessionVersion;
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: CreateOffer \nSDP=\n" << *offer);
+
+ return Result();
+}
+
+std::string JsepSessionImpl::GetLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ std::ostringstream os;
+ mozilla::Sdp* sdp = GetParsedLocalDescription(type);
+ if (sdp) {
+ sdp->Serialize(os);
+ }
+ return os.str();
+}
+
+std::string JsepSessionImpl::GetRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ std::ostringstream os;
+ mozilla::Sdp* sdp = GetParsedRemoteDescription(type);
+ if (sdp) {
+ sdp->Serialize(os);
+ }
+ return os.str();
+}
+
+void JsepSessionImpl::AddExtmap(SdpMediaSection* msection) {
+ auto extensions = GetRtpExtensions(*msection);
+
+ if (!extensions.empty()) {
+ SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList;
+ extmap->mExtmaps = extensions;
+ msection->GetAttributeList().SetAttribute(extmap);
+ }
+}
+
+std::vector<SdpExtmapAttributeList::Extmap> JsepSessionImpl::GetRtpExtensions(
+ const SdpMediaSection& msection) {
+ std::vector<SdpExtmapAttributeList::Extmap> result;
+ JsepMediaType mediaType = JsepMediaType::kNone;
+ switch (msection.GetMediaType()) {
+ case SdpMediaSection::kAudio:
+ mediaType = JsepMediaType::kAudio;
+ break;
+ case SdpMediaSection::kVideo:
+ mediaType = JsepMediaType::kVideo;
+ if (msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kRidAttribute)) {
+ // We need RID support
+ // TODO: Would it be worth checking that the direction is sane?
+ AddVideoRtpExtension(webrtc::RtpExtension::kRidUri,
+ SdpDirectionAttribute::kSendonly);
+
+ if (mRtxIsAllowed &&
+ Preferences::GetBool("media.peerconnection.video.use_rtx", false)) {
+ AddVideoRtpExtension(webrtc::RtpExtension::kRepairedRidUri,
+ SdpDirectionAttribute::kSendonly);
+ }
+ }
+ break;
+ default:;
+ }
+ if (mediaType != JsepMediaType::kNone) {
+ for (auto ext = mRtpExtensions.begin(); ext != mRtpExtensions.end();
+ ++ext) {
+ if (ext->mMediaType == mediaType ||
+ ext->mMediaType == JsepMediaType::kAudioVideo) {
+ result.push_back(ext->mExtmap);
+ }
+ }
+ }
+ return result;
+}
+
+std::string JsepSessionImpl::GetNewMid() {
+ std::string mid;
+
+ do {
+ std::ostringstream osMid;
+ osMid << mMidCounter++;
+ mid = osMid.str();
+ } while (mUsedMids.count(mid));
+
+ mUsedMids.insert(mid);
+ return mid;
+}
+
+void JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection,
+ SdpMediaSection* msection) {
+ auto negotiatedRtpExtensions = GetRtpExtensions(*msection);
+ mSdpHelper.NegotiateAndAddExtmaps(remoteMsection, negotiatedRtpExtensions,
+ msection);
+}
+
+uint16_t JsepSessionImpl::GetNeverUsedExtmapEntry() {
+ uint16_t result = 1;
+
+ // Walk the set in order, and return the first "hole" we find
+ for (const auto used : mExtmapEntriesEverUsed) {
+ if (result != used) {
+ MOZ_ASSERT(result < used);
+ break;
+ }
+
+ // RFC 5285 says entries >= 4096 are used in offers to force the answerer
+ // to pick, so we do not want to actually use these
+ if (used == 4095) {
+ JSEP_SET_ERROR(
+ "Too many rtp extensions have been added. "
+ "That's 4095. Who _does_ that?");
+ return 0;
+ }
+
+ result = used + 1;
+ }
+
+ mExtmapEntriesEverUsed.insert(result);
+ return result;
+}
+
+JsepSession::Result JsepSessionImpl::CreateAnswer(
+ const JsepAnswerOptions& options, std::string* answer) {
+ mLastError.clear();
+
+ if (mState != kJsepStateHaveRemoteOffer) {
+ JSEP_SET_ERROR("Cannot create answer in state " << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ UniquePtr<Sdp> sdp;
+
+ // Make the basic SDP that is common to offer/answer.
+ nsresult rv = CreateGenericSDP(&sdp);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ const Sdp& offer = *mPendingRemoteDescription;
+
+ // Copy the bundle groups into our answer
+ UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
+ mSdpHelper.GetBundleGroups(offer, &groupAttr->mGroups);
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+
+ for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+ // The transceivers are already in place, due to setRemote
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ MOZ_ASSERT(false);
+ return dom::PCError::OperationError;
+ }
+ rv = CreateAnswerMsection(options, *transceiver, offer.GetMediaSection(i),
+ sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ SetTransceiver(*transceiver);
+ }
+
+ // Ensure that each bundle-group starts with a mid that has a transport, in
+ // case we've disabled what the offerer wanted to use. If the group doesn't
+ // contain anything that has a transport, remove it.
+ groupAttr.reset(new SdpGroupAttributeList);
+ std::vector<SdpGroupAttributeList::Group> bundleGroups;
+ mSdpHelper.GetBundleGroups(*sdp, &bundleGroups);
+ for (auto& group : bundleGroups) {
+ for (auto& mid : group.tags) {
+ const SdpMediaSection* msection =
+ mSdpHelper.FindMsectionByMid(offer, mid);
+
+ if (msection && !msection->GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute)) {
+ std::swap(group.tags[0], mid);
+ groupAttr->mGroups.push_back(group);
+ break;
+ }
+ }
+ }
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+
+ if (mCurrentLocalDescription) {
+ // per discussion with bwc, 3rd parm here should be offer, not *sdp. (mjf)
+ rv = CopyPreviousTransportParams(*GetAnswer(), *mCurrentRemoteDescription,
+ offer, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ *answer = sdp->ToString();
+ mGeneratedAnswer = std::move(sdp);
+ ++mSessionVersion;
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: CreateAnswer \nSDP=\n" << *answer);
+
+ return Result();
+}
+
+nsresult JsepSessionImpl::CreateAnswerMsection(
+ const JsepAnswerOptions& options, JsepTransceiver& transceiver,
+ const SdpMediaSection& remoteMsection, Sdp* sdp) {
+ MOZ_ASSERT(transceiver.GetMediaType() == remoteMsection.GetMediaType());
+ SdpDirectionAttribute::Direction direction =
+ reverse(remoteMsection.GetDirection()) & transceiver.mJsDirection;
+ SdpMediaSection& msection =
+ sdp->AddMediaSection(remoteMsection.GetMediaType(), direction, 9,
+ remoteMsection.GetProtocol(), sdp::kIPv4, "0.0.0.0");
+
+ nsresult rv = mSdpHelper.CopyStickyParams(remoteMsection, &msection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mSdpHelper.MsectionIsDisabled(remoteMsection) ||
+ // JS might have stopped this
+ transceiver.IsStopped()) {
+ SdpHelper::DisableMsection(sdp, &msection);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(transceiver.IsAssociated());
+ if (msection.GetAttributeList().GetMid().empty()) {
+ msection.GetAttributeList().SetAttribute(new SdpStringAttribute(
+ SdpAttribute::kMidAttribute, transceiver.GetMid()));
+ }
+
+ MOZ_ASSERT(transceiver.GetMid() == msection.GetAttributeList().GetMid());
+
+ SdpSetupAttribute::Role role;
+ if (transceiver.mTransport.mDtls && !IsIceRestarting()) {
+ role = (transceiver.mTransport.mDtls->mRole ==
+ JsepDtlsTransport::kJsepDtlsClient)
+ ? SdpSetupAttribute::kActive
+ : SdpSetupAttribute::kPassive;
+ } else {
+ rv = DetermineAnswererSetupRole(remoteMsection, &role);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = AddTransportAttributes(&msection, role);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+ transceiver.mRecvTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+
+ // Add extmap attributes. This logic will probably be moved to the track,
+ // since it can be specified on a per-sender basis in JS.
+ // We will need some validation to ensure that the ids are identical for
+ // RTP streams that are bundled together, though (bug 1406529).
+ AddCommonExtmaps(remoteMsection, &msection);
+
+ if (msection.GetFormats().empty()) {
+ // Could not negotiate anything. Disable m-section.
+ SdpHelper::DisableMsection(sdp, &msection);
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::DetermineAnswererSetupRole(
+ const SdpMediaSection& remoteMsection, SdpSetupAttribute::Role* rolep) {
+ // Determine the role.
+ // RFC 5763 says:
+ //
+ // The endpoint MUST use the setup attribute defined in [RFC4145].
+ // The endpoint that is the offerer MUST use the setup attribute
+ // value of setup:actpass and be prepared to receive a client_hello
+ // before it receives the answer. The answerer MUST use either a
+ // setup attribute value of setup:active or setup:passive. Note that
+ // if the answerer uses setup:passive, then the DTLS handshake will
+ // not begin until the answerer is received, which adds additional
+ // latency. setup:active allows the answer and the DTLS handshake to
+ // occur in parallel. Thus, setup:active is RECOMMENDED. Whichever
+ // party is active MUST initiate a DTLS handshake by sending a
+ // ClientHello over each flow (host/port quartet).
+ //
+ // We default to assuming that the offerer is passive and we are active.
+ SdpSetupAttribute::Role role = SdpSetupAttribute::kActive;
+
+ if (remoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kSetupAttribute)) {
+ switch (remoteMsection.GetAttributeList().GetSetup().mRole) {
+ case SdpSetupAttribute::kActive:
+ role = SdpSetupAttribute::kPassive;
+ break;
+ case SdpSetupAttribute::kPassive:
+ case SdpSetupAttribute::kActpass:
+ role = SdpSetupAttribute::kActive;
+ break;
+ case SdpSetupAttribute::kHoldconn:
+ // This should have been caught by ParseSdp
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR(
+ "The other side used an illegal setup attribute"
+ " (\"holdconn\").");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ *rolep = role;
+ return NS_OK;
+}
+
+JsepSession::Result JsepSessionImpl::SetLocalDescription(
+ JsepSdpType type, const std::string& constSdp) {
+ mLastError.clear();
+ std::string sdp = constSdp;
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SetLocalDescription type=" << type
+ << "\nSDP=\n"
+ << sdp);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ if (!mGeneratedOffer) {
+ JSEP_SET_ERROR(
+ "Cannot set local offer when createOffer has not been called.");
+ return dom::PCError::InvalidModificationError;
+ }
+ if (sdp.empty()) {
+ sdp = mGeneratedOffer->ToString();
+ }
+ if (mState == kJsepStateHaveLocalOffer) {
+ // Rollback previous offer before applying the new one.
+ SetLocalDescription(kJsepSdpRollback, "");
+ MOZ_ASSERT(mState == kJsepStateStable);
+ }
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ if (!mGeneratedAnswer) {
+ JSEP_SET_ERROR(
+ "Cannot set local answer when createAnswer has not been called.");
+ return dom::PCError::InvalidModificationError;
+ }
+ if (sdp.empty()) {
+ sdp = mGeneratedAnswer->ToString();
+ }
+ break;
+ case kJsepSdpRollback:
+ if (mState != kJsepStateHaveLocalOffer) {
+ JSEP_SET_ERROR("Cannot rollback local description in "
+ << GetStateStr(mState));
+ // Currently, spec allows this in any state except stable, and
+ // sRD(rollback) and sLD(rollback) do exactly the same thing.
+ return dom::PCError::InvalidStateError;
+ }
+
+ mPendingLocalDescription.reset();
+ SetState(kJsepStateStable);
+ RollbackLocalOffer();
+ return Result();
+ }
+
+ switch (mState) {
+ case kJsepStateStable:
+ if (type != kJsepSdpOffer) {
+ JSEP_SET_ERROR("Cannot set local answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ case kJsepStateHaveRemoteOffer:
+ if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+ JSEP_SET_ERROR("Cannot set local offer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ default:
+ JSEP_SET_ERROR("Cannot set local offer or answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ UniquePtr<Sdp> parsed;
+ nsresult rv = ParseSdp(sdp, &parsed);
+ // Needs to be RTCError with sdp-syntax-error
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ // Check that content hasn't done anything unsupported with the SDP
+ rv = ValidateLocalDescription(*parsed, type);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidModificationError);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = ValidateOffer(*parsed);
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = ValidateAnswer(*mPendingRemoteDescription, *parsed);
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ if (type == kJsepSdpOffer) {
+ // Save in case we need to rollback
+ mOldTransceivers = mTransceivers;
+ }
+
+ SdpHelper::BundledMids bundledMids;
+ rv = mSdpHelper.GetBundledMids(*parsed, &bundledMids);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ SdpHelper::BundledMids remoteBundledMids;
+ if (type != kJsepSdpOffer) {
+ rv = mSdpHelper.GetBundledMids(*mPendingRemoteDescription,
+ &remoteBundledMids);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return dom::PCError::OperationError;
+ }
+
+ const auto& msection = parsed->GetMediaSection(i);
+ transceiver->Associate(msection.GetAttributeList().GetMid());
+ transceiver->mRecvTrack.RecvTrackSetLocal(msection);
+
+ if (mSdpHelper.MsectionIsDisabled(msection)) {
+ transceiver->mTransport.Close();
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ bool hasOwnTransport = mSdpHelper.OwnsTransport(
+ msection, bundledMids,
+ (type == kJsepSdpOffer) ? sdp::kOffer : sdp::kAnswer);
+ if (type != kJsepSdpOffer) {
+ const auto& remoteMsection =
+ mPendingRemoteDescription->GetMediaSection(i);
+ // Don't allow the answer to override what the offer allowed for
+ hasOwnTransport &= mSdpHelper.OwnsTransport(
+ remoteMsection, remoteBundledMids, sdp::kOffer);
+ }
+
+ if (hasOwnTransport) {
+ EnsureHasOwnTransport(parsed->GetMediaSection(i), *transceiver);
+ }
+
+ if (type == kJsepSdpOffer) {
+ if (!hasOwnTransport) {
+ auto it = bundledMids.find(transceiver->GetMid());
+ if (it != bundledMids.end()) {
+ transceiver->SetBundleLevel(it->second->GetLevel());
+ }
+ }
+ } else {
+ auto it = remoteBundledMids.find(transceiver->GetMid());
+ if (it != remoteBundledMids.end()) {
+ transceiver->SetBundleLevel(it->second->GetLevel());
+ }
+ }
+ SetTransceiver(*transceiver);
+ }
+
+ CopyBundleTransports();
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = SetLocalDescriptionOffer(std::move(parsed));
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = SetLocalDescriptionAnswer(type, std::move(parsed));
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ return Result();
+}
+
+nsresult JsepSessionImpl::SetLocalDescriptionOffer(UniquePtr<Sdp> offer) {
+ MOZ_ASSERT(mState == kJsepStateStable);
+ mPendingLocalDescription = std::move(offer);
+ mIsPendingOfferer = Some(true);
+ SetState(kJsepStateHaveLocalOffer);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetLocalDescriptionAnswer(JsepSdpType type,
+ UniquePtr<Sdp> answer) {
+ MOZ_ASSERT(mState == kJsepStateHaveRemoteOffer);
+ mPendingLocalDescription = std::move(answer);
+
+ nsresult rv = HandleNegotiatedSession(mPendingLocalDescription,
+ mPendingRemoteDescription);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCurrentRemoteDescription = std::move(mPendingRemoteDescription);
+ mCurrentLocalDescription = std::move(mPendingLocalDescription);
+ MOZ_ASSERT(mIsPendingOfferer.isSome() && !*mIsPendingOfferer);
+ mIsPendingOfferer.reset();
+ mIsCurrentOfferer = Some(false);
+
+ SetState(kJsepStateStable);
+ return NS_OK;
+}
+
+JsepSession::Result JsepSessionImpl::SetRemoteDescription(
+ JsepSdpType type, const std::string& sdp) {
+ mLastError.clear();
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SetRemoteDescription type=" << type
+ << "\nSDP=\n"
+ << sdp);
+
+ if (mState == kJsepStateHaveRemoteOffer && type == kJsepSdpOffer) {
+ // Rollback previous offer before applying the new one.
+ SetRemoteDescription(kJsepSdpRollback, "");
+ MOZ_ASSERT(mState == kJsepStateStable);
+ }
+
+ if (type == kJsepSdpRollback) {
+ if (mState != kJsepStateHaveRemoteOffer) {
+ JSEP_SET_ERROR("Cannot rollback remote description in "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ mPendingRemoteDescription.reset();
+ SetState(kJsepStateStable);
+ RollbackRemoteOffer();
+
+ return Result();
+ }
+
+ switch (mState) {
+ case kJsepStateStable:
+ if (type != kJsepSdpOffer) {
+ JSEP_SET_ERROR("Cannot set remote answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ case kJsepStateHaveLocalOffer:
+ case kJsepStateHaveRemotePranswer:
+ if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+ JSEP_SET_ERROR("Cannot set remote offer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ default:
+ JSEP_SET_ERROR("Cannot set remote offer or answer in current state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ // Parse.
+ UniquePtr<Sdp> parsed;
+ nsresult rv = ParseSdp(sdp, &parsed);
+ // Needs to be RTCError with sdp-syntax-error
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ rv = ValidateRemoteDescription(*parsed);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = ValidateOffer(*parsed);
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = ValidateAnswer(*mPendingLocalDescription, *parsed);
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ bool iceLite =
+ parsed->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+ // check for mismatch ufrag/pwd indicating ice restart
+ // can't just check the first one because it might be disabled
+ bool iceRestarting = false;
+ if (mCurrentRemoteDescription.get()) {
+ for (size_t i = 0; !iceRestarting &&
+ i < mCurrentRemoteDescription->GetMediaSectionCount();
+ ++i) {
+ const SdpMediaSection& newMsection = parsed->GetMediaSection(i);
+ const SdpMediaSection& oldMsection =
+ mCurrentRemoteDescription->GetMediaSection(i);
+
+ if (mSdpHelper.MsectionIsDisabled(newMsection) ||
+ mSdpHelper.MsectionIsDisabled(oldMsection)) {
+ continue;
+ }
+
+ iceRestarting = mSdpHelper.IceCredentialsDiffer(newMsection, oldMsection);
+ }
+ }
+
+ std::vector<std::string> iceOptions;
+ if (parsed->GetAttributeList().HasAttribute(
+ SdpAttribute::kIceOptionsAttribute)) {
+ iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
+ }
+
+ if (type == kJsepSdpOffer) {
+ // Save in case we need to rollback.
+ mOldTransceivers = mTransceivers;
+ for (auto& transceiver : mTransceivers) {
+ if (!transceiver.IsNegotiated()) {
+ // We chose a level for this transceiver, but never negotiated it.
+ // Discard this state.
+ transceiver.ClearLevel();
+ }
+ }
+ }
+
+ // TODO(bug 1095780): Note that we create remote tracks even when
+ // They contain only codecs we can't negotiate or other craziness.
+ rv = UpdateTransceiversFromRemoteDescription(*parsed);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ MOZ_ASSERT(GetTransceiverForLevel(i));
+ }
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = SetRemoteDescriptionOffer(std::move(parsed));
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = SetRemoteDescriptionAnswer(type, std::move(parsed));
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ mRemoteIsIceLite = iceLite;
+ mIceOptions = iceOptions;
+ SetIceRestarting(iceRestarting);
+ return Result();
+}
+
+nsresult JsepSessionImpl::HandleNegotiatedSession(
+ const UniquePtr<Sdp>& local, const UniquePtr<Sdp>& remote) {
+ // local ufrag/pwd has been negotiated; we will never go back to the old ones
+ mOldIceUfrag.clear();
+ mOldIcePwd.clear();
+
+ bool remoteIceLite =
+ remote->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+ mIceControlling = remoteIceLite || *mIsPendingOfferer;
+
+ const Sdp& answer = *mIsPendingOfferer ? *remote : *local;
+
+ SdpHelper::BundledMids bundledMids;
+ nsresult rv = mSdpHelper.GetBundledMids(answer, &bundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // First, set the bundle level on the transceivers
+ for (auto& [mid, transportOwner] : bundledMids) {
+ Maybe<JsepTransceiver> bundledTransceiver = GetTransceiverForMid(mid);
+ if (!bundledTransceiver) {
+ JSEP_SET_ERROR("No transceiver for bundled mid " << mid);
+ return NS_ERROR_INVALID_ARG;
+ }
+ bundledTransceiver->SetBundleLevel(transportOwner->GetLevel());
+ SetTransceiver(*bundledTransceiver);
+ }
+
+ // Now walk through the m-sections, perform negotiation, and update the
+ // transceivers.
+ for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Skip disabled m-sections.
+ if (answer.GetMediaSection(i).GetPort() == 0) {
+ transceiver->mTransport.Close();
+ transceiver->Stop();
+ transceiver->Disassociate();
+ transceiver->ClearBundleLevel();
+ transceiver->mSendTrack.SetActive(false);
+ transceiver->mRecvTrack.SetActive(false);
+ transceiver->SetCanRecycle();
+ SetTransceiver(*transceiver);
+ // Do not clear mLevel yet! That will happen on the next negotiation.
+ continue;
+ }
+
+ rv = MakeNegotiatedTransceiver(remote->GetMediaSection(i),
+ local->GetMediaSection(i), *transceiver);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetTransceiver(*transceiver);
+ }
+
+ CopyBundleTransports();
+
+ std::vector<JsepTrack*> remoteTracks;
+ for (auto& transceiver : mTransceivers) {
+ remoteTracks.push_back(&transceiver.mRecvTrack);
+ }
+ JsepTrack::SetUniquePayloadTypes(remoteTracks);
+
+ mNegotiations++;
+
+ mGeneratedAnswer.reset();
+ mGeneratedOffer.reset();
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::MakeNegotiatedTransceiver(
+ const SdpMediaSection& remote, const SdpMediaSection& local,
+ JsepTransceiver& transceiver) {
+ const SdpMediaSection& answer = *mIsPendingOfferer ? remote : local;
+
+ bool sending = false;
+ bool receiving = false;
+
+ // JS could stop the transceiver after the answer was created.
+ if (!transceiver.IsStopped()) {
+ if (*mIsPendingOfferer) {
+ receiving = answer.IsSending();
+ sending = answer.IsReceiving();
+ } else {
+ sending = answer.IsSending();
+ receiving = answer.IsReceiving();
+ }
+ }
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiated m= line"
+ << " index=" << local.GetLevel() << " type="
+ << local.GetMediaType() << " sending=" << sending
+ << " receiving=" << receiving);
+
+ transceiver.SetNegotiated();
+
+ // Ensure that this is finalized in case we need to copy it below
+ nsresult rv =
+ FinalizeTransport(remote.GetAttributeList(), answer.GetAttributeList(),
+ &transceiver.mTransport);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.SetActive(sending);
+ rv = transceiver.mSendTrack.Negotiate(answer, remote, local);
+ if (NS_FAILED(rv)) {
+ JSEP_SET_ERROR("Answer had no codecs in common with offer in m-section "
+ << local.GetLevel());
+ return rv;
+ }
+
+ JsepTrack& recvTrack = transceiver.mRecvTrack;
+ recvTrack.SetActive(receiving);
+ rv = recvTrack.Negotiate(answer, remote, local);
+ if (NS_FAILED(rv)) {
+ JSEP_SET_ERROR("Answer had no codecs in common with offer in m-section "
+ << local.GetLevel());
+ return rv;
+ }
+
+ if (transceiver.HasBundleLevel() && recvTrack.GetSsrcs().empty() &&
+ recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+ // TODO(bug 1105005): Once we have urn:ietf:params:rtp-hdrext:sdes:mid
+ // support, we should only fire this warning if that extension was not
+ // negotiated.
+ MOZ_MTLOG(ML_ERROR, "[" << mName
+ << "]: Bundled m-section has no ssrc "
+ "attributes. This may cause media packets to be "
+ "dropped.");
+ }
+
+ if (transceiver.mTransport.mComponents == 2) {
+ // RTCP MUX or not.
+ // TODO(bug 1095743): verify that the PTs are consistent with mux.
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: RTCP-MUX is off");
+ }
+
+ if (answer.GetAttributeList().HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ const auto extmaps = answer.GetAttributeList().GetExtmap().mExtmaps;
+ for (const auto& negotiatedExtension : extmaps) {
+ if (negotiatedExtension.entry == 0) {
+ MOZ_ASSERT(false, "This should have been caught sooner");
+ continue;
+ }
+
+ mExtmapEntriesEverNegotiated[negotiatedExtension.entry] =
+ negotiatedExtension.extensionname;
+
+ for (auto& originalExtension : mRtpExtensions) {
+ if (negotiatedExtension.extensionname ==
+ originalExtension.mExtmap.extensionname) {
+ // Update extmap to match what was negotiated
+ originalExtension.mExtmap.entry = negotiatedExtension.entry;
+ mExtmapEntriesEverUsed.insert(negotiatedExtension.entry);
+ } else if (originalExtension.mExtmap.entry ==
+ negotiatedExtension.entry) {
+ // If this extmap entry was claimed for a different extension, update
+ // it to a new value so we don't end up with a duplicate.
+ originalExtension.mExtmap.entry = GetNeverUsedExtmapEntry();
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::EnsureHasOwnTransport(const SdpMediaSection& msection,
+ JsepTransceiver& transceiver) {
+ JsepTransport& transport = transceiver.mTransport;
+
+ if (!transceiver.HasOwnTransport()) {
+ // Transceiver didn't own this transport last time, it won't now either
+ transport.Close();
+ }
+
+ transport.mLocalUfrag = msection.GetAttributeList().GetIceUfrag();
+ transport.mLocalPwd = msection.GetAttributeList().GetIcePwd();
+
+ transceiver.ClearBundleLevel();
+
+ if (!transport.mComponents) {
+ if (mSdpHelper.HasRtcp(msection.GetProtocol())) {
+ transport.mComponents = 2;
+ } else {
+ transport.mComponents = 1;
+ }
+ }
+
+ if (transport.mTransportId.empty()) {
+ // TODO: Once we use different ICE ufrag/pass for each m-section, we can
+ // use that here.
+ std::ostringstream os;
+ os << "transport_" << mTransportIdCounter++;
+ transport.mTransportId = os.str();
+ }
+}
+
+void JsepSessionImpl::CopyBundleTransports() {
+ for (auto& transceiver : mTransceivers) {
+ if (transceiver.HasBundleLevel()) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "] Transceiver " << transceiver.GetLevel()
+ << " is in a bundle; transceiver "
+ << transceiver.BundleLevel() << " owns the transport.");
+ Maybe<const JsepTransceiver> transportOwner =
+ GetTransceiverForLevel(transceiver.BundleLevel());
+ MOZ_ASSERT(transportOwner);
+ if (transportOwner) {
+ transceiver.mTransport = transportOwner->mTransport;
+ }
+ } else if (transceiver.HasLevel()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "] Transceiver "
+ << transceiver.GetLevel()
+ << " is not necessarily in a bundle.");
+ }
+ if (transceiver.HasLevel()) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "] Transceiver " << transceiver.GetLevel()
+ << " transport-id: " << transceiver.mTransport.mTransportId
+ << " components: " << transceiver.mTransport.mComponents);
+ }
+ }
+}
+
+nsresult JsepSessionImpl::FinalizeTransport(const SdpAttributeList& remote,
+ const SdpAttributeList& answer,
+ JsepTransport* transport) const {
+ if (!transport->mComponents) {
+ return NS_OK;
+ }
+
+ if (!transport->mIce || transport->mIce->mUfrag != remote.GetIceUfrag() ||
+ transport->mIce->mPwd != remote.GetIcePwd()) {
+ UniquePtr<JsepIceTransport> ice = MakeUnique<JsepIceTransport>();
+ transport->mDtls = nullptr;
+
+ // We do sanity-checking for these in ParseSdp
+ ice->mUfrag = remote.GetIceUfrag();
+ ice->mPwd = remote.GetIcePwd();
+ transport->mIce = std::move(ice);
+ }
+
+ if (remote.HasAttribute(SdpAttribute::kCandidateAttribute)) {
+ transport->mIce->mCandidates = remote.GetCandidate();
+ }
+
+ if (!transport->mDtls) {
+ // RFC 5763 says:
+ //
+ // The endpoint MUST use the setup attribute defined in [RFC4145].
+ // The endpoint that is the offerer MUST use the setup attribute
+ // value of setup:actpass and be prepared to receive a client_hello
+ // before it receives the answer. The answerer MUST use either a
+ // setup attribute value of setup:active or setup:passive. Note that
+ // if the answerer uses setup:passive, then the DTLS handshake will
+ // not begin until the answerer is received, which adds additional
+ // latency. setup:active allows the answer and the DTLS handshake to
+ // occur in parallel. Thus, setup:active is RECOMMENDED. Whichever
+ // party is active MUST initiate a DTLS handshake by sending a
+ // ClientHello over each flow (host/port quartet).
+ UniquePtr<JsepDtlsTransport> dtls = MakeUnique<JsepDtlsTransport>();
+ dtls->mFingerprints = remote.GetFingerprint();
+ if (!answer.HasAttribute(mozilla::SdpAttribute::kSetupAttribute)) {
+ dtls->mRole = *mIsPendingOfferer ? JsepDtlsTransport::kJsepDtlsServer
+ : JsepDtlsTransport::kJsepDtlsClient;
+ } else {
+ if (*mIsPendingOfferer) {
+ dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+ ? JsepDtlsTransport::kJsepDtlsServer
+ : JsepDtlsTransport::kJsepDtlsClient;
+ } else {
+ dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+ ? JsepDtlsTransport::kJsepDtlsClient
+ : JsepDtlsTransport::kJsepDtlsServer;
+ }
+ }
+
+ transport->mDtls = std::move(dtls);
+ }
+
+ if (answer.HasAttribute(SdpAttribute::kRtcpMuxAttribute)) {
+ transport->mComponents = 1;
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddTransportAttributes(
+ SdpMediaSection* msection, SdpSetupAttribute::Role dtlsRole) {
+ if (mIceUfrag.empty() || mIcePwd.empty()) {
+ JSEP_SET_ERROR("Missing ICE ufrag or password");
+ return NS_ERROR_FAILURE;
+ }
+
+ SdpAttributeList& attrList = msection->GetAttributeList();
+ attrList.SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIceUfragAttribute, mIceUfrag));
+ attrList.SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIcePwdAttribute, mIcePwd));
+
+ msection->GetAttributeList().SetAttribute(new SdpSetupAttribute(dtlsRole));
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::CopyPreviousTransportParams(
+ const Sdp& oldAnswer, const Sdp& offerersPreviousSdp, const Sdp& newOffer,
+ Sdp* newLocal) {
+ for (size_t i = 0; i < oldAnswer.GetMediaSectionCount(); ++i) {
+ if (!mSdpHelper.MsectionIsDisabled(newLocal->GetMediaSection(i)) &&
+ mSdpHelper.AreOldTransportParamsValid(oldAnswer, offerersPreviousSdp,
+ newOffer, i)) {
+ // If newLocal is an offer, this will be the number of components we used
+ // last time, and if it is an answer, this will be the number of
+ // components we've decided we're using now.
+ Maybe<const JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return NS_ERROR_FAILURE;
+ }
+ size_t numComponents = transceiver->mTransport.mComponents;
+ nsresult rv = mSdpHelper.CopyTransportParams(
+ numComponents, mCurrentLocalDescription->GetMediaSection(i),
+ &newLocal->GetMediaSection(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ParseSdp(const std::string& sdp,
+ UniquePtr<Sdp>* parsedp) {
+ auto results = mParser->Parse(sdp);
+ auto parsed = std::move(results->Sdp());
+ mLastSdpParsingErrors = results->Errors();
+ if (!parsed) {
+ std::string error = results->ParserName() + " Failed to parse SDP: ";
+ mSdpHelper.AppendSdpParseErrors(mLastSdpParsingErrors, &error);
+ JSEP_SET_ERROR(error);
+ return NS_ERROR_INVALID_ARG;
+ }
+ // Verify that the JSEP rules for all SDP are followed
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ if (mSdpHelper.MsectionIsDisabled(parsed->GetMediaSection(i))) {
+ // Disabled, let this stuff slide.
+ continue;
+ }
+
+ const SdpMediaSection& msection(parsed->GetMediaSection(i));
+ auto& mediaAttrs = msection.GetAttributeList();
+
+ if (mediaAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ mediaAttrs.GetMid().length() > 16) {
+ JSEP_SET_ERROR(
+ "Invalid description, mid length greater than 16 "
+ "unsupported until 2-byte rtp header extensions are "
+ "supported in webrtc.org");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ std::set<uint16_t> extIds;
+ for (const auto& ext : mediaAttrs.GetExtmap().mExtmaps) {
+ uint16_t id = ext.entry;
+
+ if (id < 1 || id > 14) {
+ JSEP_SET_ERROR("Description contains invalid extension id "
+ << id << " on level " << i
+ << " which is unsupported until 2-byte rtp"
+ " header extensions are supported in webrtc.org");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (extIds.find(id) != extIds.end()) {
+ JSEP_SET_ERROR("Description contains duplicate extension id "
+ << id << " on level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ extIds.insert(id);
+ }
+ }
+
+ static const std::bitset<128> forbidden = GetForbiddenSdpPayloadTypes();
+ if (msection.GetMediaType() == SdpMediaSection::kAudio ||
+ msection.GetMediaType() == SdpMediaSection::kVideo) {
+ // Sanity-check that payload type can work with RTP
+ for (const std::string& fmt : msection.GetFormats()) {
+ uint16_t payloadType;
+ if (!SdpHelper::GetPtAsInt(fmt, &payloadType)) {
+ JSEP_SET_ERROR("Payload type \""
+ << fmt << "\" is not a 16-bit unsigned int at level "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (payloadType > 127) {
+ JSEP_SET_ERROR("audio/video payload type \""
+ << fmt << "\" is too large at level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (forbidden.test(payloadType)) {
+ JSEP_SET_ERROR("Illegal audio/video payload type \""
+ << fmt << "\" at level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ *parsedp = std::move(parsed);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr<Sdp> offer) {
+ MOZ_ASSERT(mState == kJsepStateStable);
+
+ mPendingRemoteDescription = std::move(offer);
+ mIsPendingOfferer = Some(false);
+
+ SetState(kJsepStateHaveRemoteOffer);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type,
+ UniquePtr<Sdp> answer) {
+ MOZ_ASSERT(mState == kJsepStateHaveLocalOffer ||
+ mState == kJsepStateHaveRemotePranswer);
+
+ mPendingRemoteDescription = std::move(answer);
+
+ nsresult rv = HandleNegotiatedSession(mPendingLocalDescription,
+ mPendingRemoteDescription);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCurrentRemoteDescription = std::move(mPendingRemoteDescription);
+ mCurrentLocalDescription = std::move(mPendingLocalDescription);
+ MOZ_ASSERT(mIsPendingOfferer.isSome() && *mIsPendingOfferer);
+ mIsPendingOfferer.reset();
+ mIsCurrentOfferer = Some(true);
+
+ SetState(kJsepStateStable);
+ return NS_OK;
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForLevel(
+ size_t level) const {
+ return FindTransceiver([level](const JsepTransceiver& transceiver) {
+ return transceiver.HasLevel() && (transceiver.GetLevel() == level);
+ });
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForMid(
+ const std::string& mid) const {
+ return FindTransceiver([mid](const JsepTransceiver& transceiver) {
+ return transceiver.IsAssociated() && (transceiver.GetMid() == mid);
+ });
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForLocal(size_t level) {
+ if (Maybe<JsepTransceiver> transceiver = GetTransceiverForLevel(level)) {
+ if (transceiver->CanRecycle() &&
+ transceiver->GetMediaType() != SdpMediaSection::kApplication) {
+ // Attempt to recycle. If this fails, the old transceiver stays put.
+ transceiver->Disassociate();
+ Maybe<JsepTransceiver> newTransceiver =
+ FindUnassociatedTransceiver(transceiver->GetMediaType(), false);
+ if (newTransceiver) {
+ newTransceiver->SetLevel(level);
+ transceiver->ClearLevel();
+ transceiver->mSendTrack.ClearRids();
+ SetTransceiver(*newTransceiver);
+ SetTransceiver(*transceiver);
+ return newTransceiver;
+ }
+ }
+
+ SetTransceiver(*transceiver);
+ return transceiver;
+ }
+
+ // There is no transceiver for |level| right now.
+
+ // Look for an RTP transceiver
+ for (auto& transceiver : mTransceivers) {
+ if (transceiver.GetMediaType() != SdpMediaSection::kApplication &&
+ !transceiver.IsStopped() && !transceiver.HasLevel()) {
+ transceiver.SetLevel(level);
+ return Some(transceiver);
+ }
+ }
+
+ // Ok, look for a datachannel
+ for (auto& transceiver : mTransceivers) {
+ if (!transceiver.IsStopped() && !transceiver.HasLevel()) {
+ transceiver.SetLevel(level);
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForRemote(
+ const SdpMediaSection& msection) {
+ size_t level = msection.GetLevel();
+ Maybe<JsepTransceiver> transceiver = GetTransceiverForLevel(level);
+ if (transceiver) {
+ if (!transceiver->CanRecycle()) {
+ return transceiver;
+ }
+ transceiver->Disassociate();
+ transceiver->ClearLevel();
+ transceiver->mSendTrack.ClearRids();
+ SetTransceiver(*transceiver);
+ }
+
+ // No transceiver for |level|
+ transceiver = FindUnassociatedTransceiver(msection.GetMediaType(), true);
+ if (transceiver) {
+ transceiver->SetLevel(level);
+ SetTransceiver(*transceiver);
+ return transceiver;
+ }
+
+ // Make a new transceiver
+ JsepTransceiver newTransceiver(msection.GetMediaType(), *mUuidGen,
+ SdpDirectionAttribute::kRecvonly);
+ newTransceiver.SetLevel(level);
+ newTransceiver.SetOnlyExistsBecauseOfSetRemote(true);
+ AddTransceiver(newTransceiver);
+ return Some(mTransceivers.back());
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverWithTransport(
+ const std::string& transportId) const {
+ for (const auto& transceiver : mTransceivers) {
+ if (transceiver.HasOwnTransport() &&
+ (transceiver.mTransport.mTransportId == transportId)) {
+ MOZ_ASSERT(transceiver.HasLevel(),
+ "Transceiver has a transport, but no level!");
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+nsresult JsepSessionImpl::UpdateTransceiversFromRemoteDescription(
+ const Sdp& remote) {
+ // Iterate over the sdp, updating remote tracks as we go
+ for (size_t i = 0; i < remote.GetMediaSectionCount(); ++i) {
+ const SdpMediaSection& msection = remote.GetMediaSection(i);
+
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForRemote(msection));
+ if (!transceiver) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSdpHelper.MsectionIsDisabled(msection)) {
+ if (msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kMidAttribute)) {
+ transceiver->Associate(msection.GetAttributeList().GetMid());
+ }
+ if (!transceiver->IsAssociated()) {
+ transceiver->Associate(GetNewMid());
+ } else {
+ mUsedMids.insert(transceiver->GetMid());
+ }
+ } else {
+ transceiver->mTransport.Close();
+ transceiver->Disassociate();
+ // This cannot be rolled back.
+ transceiver->Stop();
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ if (msection.GetMediaType() == SdpMediaSection::MediaType::kApplication) {
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ transceiver->mSendTrack.SendTrackSetRemote(mSsrcGenerator, msection);
+
+ // Interop workaround for endpoints that don't support msid.
+ // Ensures that there is a default stream id set, provided the remote is
+ // sending.
+ // TODO(bug 1426005): Remove this, or at least move it to JsepTrack.
+ transceiver->mRecvTrack.UpdateStreamIds({mDefaultRemoteStreamId});
+
+ // This will process a=msid if present, or clear the stream ids if the
+ // msection is not sending. If the msection is sending, and there are no
+ // a=msid, the previously set default will stay.
+ transceiver->mRecvTrack.RecvTrackSetRemote(remote, msection);
+ SetTransceiver(*transceiver);
+ }
+
+ return NS_OK;
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::FindUnassociatedTransceiver(
+ SdpMediaSection::MediaType type, bool magic) {
+ // Look through transceivers that are not mapped to an m-section
+ for (auto& transceiver : mTransceivers) {
+ if (type == SdpMediaSection::kApplication &&
+ type == transceiver.GetMediaType()) {
+ transceiver.RestartDatachannelTransceiver();
+ return Some(transceiver);
+ }
+ if (!transceiver.IsStopped() && !transceiver.HasLevel() &&
+ (!magic || transceiver.HasAddTrackMagic()) &&
+ (transceiver.GetMediaType() == type)) {
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+void JsepSessionImpl::RollbackLocalOffer() {
+ for (size_t i = 0; i < mTransceivers.size(); ++i) {
+ auto& transceiver = mTransceivers[i];
+ if (mOldTransceivers.size() > i) {
+ transceiver.Rollback(mOldTransceivers[i], false);
+ mOldTransceivers[i] = transceiver;
+ continue;
+ }
+
+ JsepTransceiver temp(transceiver.GetMediaType(), *mUuidGen);
+ InitTransceiver(temp);
+ transceiver.Rollback(temp, false);
+ mOldTransceivers.push_back(transceiver);
+ }
+
+ mTransceivers = std::move(mOldTransceivers);
+}
+
+void JsepSessionImpl::RollbackRemoteOffer() {
+ for (size_t i = 0; i < mTransceivers.size(); ++i) {
+ auto& transceiver = mTransceivers[i];
+ if (mOldTransceivers.size() > i) {
+ // Some stuff cannot be rolled back. Save this information.
+ transceiver.Rollback(mOldTransceivers[i], true);
+ mOldTransceivers[i] = transceiver;
+ continue;
+ }
+
+ // New transceiver!
+ // We rollback even for transceivers we will remove, just to ensure we end
+ // up at the starting state.
+ JsepTransceiver temp(transceiver.GetMediaType(), *mUuidGen);
+ InitTransceiver(temp);
+ transceiver.Rollback(temp, true);
+
+ if (transceiver.OnlyExistsBecauseOfSetRemote()) {
+ transceiver.Stop();
+ transceiver.SetRemoved();
+ }
+ mOldTransceivers.push_back(transceiver);
+ }
+
+ mTransceivers = std::move(mOldTransceivers);
+}
+
+nsresult JsepSessionImpl::ValidateLocalDescription(const Sdp& description,
+ JsepSdpType type) {
+ Sdp* generated = nullptr;
+ // TODO(bug 1095226): Better checking.
+ if (type == kJsepSdpOffer) {
+ generated = mGeneratedOffer.get();
+ } else {
+ generated = mGeneratedAnswer.get();
+ }
+
+ if (!generated) {
+ JSEP_SET_ERROR(
+ "Calling SetLocal without first calling CreateOffer/Answer"
+ " is not supported.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (description.GetMediaSectionCount() != generated->GetMediaSectionCount()) {
+ JSEP_SET_ERROR("Changing the number of m-sections is not allowed");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (size_t i = 0; i < description.GetMediaSectionCount(); ++i) {
+ auto& origMsection = generated->GetMediaSection(i);
+ auto& finalMsection = description.GetMediaSection(i);
+ if (origMsection.GetMediaType() != finalMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Changing the media-type of m-sections is not allowed");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // These will be present in reoffer
+ if (!mCurrentLocalDescription) {
+ if (finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kCandidateAttribute)) {
+ JSEP_SET_ERROR("Adding your own candidate attributes is not supported");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kEndOfCandidatesAttribute)) {
+ JSEP_SET_ERROR("Why are you trying to set a=end-of-candidates?");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(finalMsection)) {
+ continue;
+ }
+
+ if (!finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kMidAttribute)) {
+ JSEP_SET_ERROR("Local descriptions must have a=mid attributes.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (finalMsection.GetAttributeList().GetMid() !=
+ origMsection.GetAttributeList().GetMid()) {
+ JSEP_SET_ERROR("Changing the mid of m-sections is not allowed.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // TODO(bug 1095218): Check msid
+ // TODO(bug 1095226): Check ice-ufrag and ice-pwd
+ // TODO(bug 1095226): Check fingerprints
+ // TODO(bug 1095226): Check payload types (at least ensure that payload
+ // types we don't actually support weren't added)
+ // TODO(bug 1095226): Check ice-options?
+ }
+
+ if (description.GetAttributeList().HasAttribute(
+ SdpAttribute::kIceLiteAttribute)) {
+ JSEP_SET_ERROR("Running ICE in lite mode is unsupported");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ValidateRemoteDescription(const Sdp& description) {
+ if (!mCurrentLocalDescription) {
+ // Initial offer; nothing to validate besides the stuff in ParseSdp
+ return NS_OK;
+ }
+
+ if (mCurrentLocalDescription->GetMediaSectionCount() >
+ description.GetMediaSectionCount()) {
+ JSEP_SET_ERROR(
+ "New remote description has fewer m-sections than the "
+ "previous remote description.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (size_t i = 0; i < description.GetMediaSectionCount(); ++i) {
+ const SdpAttributeList& attrs =
+ description.GetMediaSection(i).GetAttributeList();
+
+ if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ for (const auto& ext : attrs.GetExtmap().mExtmaps) {
+ if (mExtmapEntriesEverNegotiated.count(ext.entry) &&
+ mExtmapEntriesEverNegotiated[ext.entry] != ext.extensionname) {
+ JSEP_SET_ERROR(
+ "Remote description attempted to remap RTP extension id "
+ << ext.entry << " from "
+ << mExtmapEntriesEverNegotiated[ext.entry] << " to "
+ << ext.extensionname);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ if (!mCurrentRemoteDescription) {
+ // No further checking for initial answers
+ return NS_OK;
+ }
+
+ // These are solely to check that bundle is valid
+ SdpHelper::BundledMids bundledMids;
+ nsresult rv = GetNegotiatedBundledMids(&bundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SdpHelper::BundledMids newBundledMids;
+ rv = mSdpHelper.GetBundledMids(description, &newBundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check for partial ice restart, which is not supported
+ Maybe<bool> iceCredsDiffer;
+ for (size_t i = 0; i < mCurrentRemoteDescription->GetMediaSectionCount();
+ ++i) {
+ const SdpMediaSection& newMsection = description.GetMediaSection(i);
+ const SdpMediaSection& oldMsection =
+ mCurrentRemoteDescription->GetMediaSection(i);
+
+ if (mSdpHelper.MsectionIsDisabled(newMsection) ||
+ mSdpHelper.MsectionIsDisabled(oldMsection)) {
+ continue;
+ }
+
+ if (oldMsection.GetMediaType() != newMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Remote description changes the media type of m-line "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool differ = mSdpHelper.IceCredentialsDiffer(newMsection, oldMsection);
+
+ if (mIsPendingOfferer.isSome() && *mIsPendingOfferer && differ &&
+ !IsIceRestarting()) {
+ JSEP_SET_ERROR(
+ "Remote description indicates ICE restart but offer did not "
+ "request ICE restart (new remote description changes either "
+ "the ice-ufrag or ice-pwd)");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Detect whether all the creds are the same or all are different
+ if (!iceCredsDiffer.isSome()) {
+ // for the first msection capture whether creds are different or same
+ iceCredsDiffer = mozilla::Some(differ);
+ } else if (iceCredsDiffer.isSome() && *iceCredsDiffer != differ) {
+ // subsequent msections must match the first sections
+ JSEP_SET_ERROR(
+ "Partial ICE restart is unsupported at this time "
+ "(new remote description changes either the ice-ufrag "
+ "or ice-pwd on fewer than all msections)");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ValidateOffer(const Sdp& offer) {
+ return mSdpHelper.ValidateTransportAttributes(offer, sdp::kOffer);
+}
+
+nsresult JsepSessionImpl::ValidateAnswer(const Sdp& offer, const Sdp& answer) {
+ if (offer.GetMediaSectionCount() != answer.GetMediaSectionCount()) {
+ JSEP_SET_ERROR("Offer and answer have different number of m-lines "
+ << "(" << offer.GetMediaSectionCount() << " vs "
+ << answer.GetMediaSectionCount() << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv = mSdpHelper.ValidateTransportAttributes(answer, sdp::kAnswer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+ const SdpMediaSection& offerMsection = offer.GetMediaSection(i);
+ const SdpMediaSection& answerMsection = answer.GetMediaSection(i);
+
+ if (offerMsection.GetMediaType() != answerMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Answer and offer have different media types at m-line "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(answerMsection)) {
+ continue;
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(offerMsection)) {
+ JSEP_SET_ERROR(
+ "Answer tried to enable an m-section that was disabled in the offer");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!offerMsection.IsSending() && answerMsection.IsReceiving()) {
+ JSEP_SET_ERROR("Answer tried to set recv when offer did not set send");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!offerMsection.IsReceiving() && answerMsection.IsSending()) {
+ JSEP_SET_ERROR("Answer tried to set send when offer did not set recv");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const SdpAttributeList& answerAttrs(answerMsection.GetAttributeList());
+ const SdpAttributeList& offerAttrs(offerMsection.GetAttributeList());
+ if (answerAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ offerAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ offerAttrs.GetMid() != answerAttrs.GetMid()) {
+ JSEP_SET_ERROR("Answer changes mid for level, was \'"
+ << offerMsection.GetAttributeList().GetMid()
+ << "\', now \'"
+ << answerMsection.GetAttributeList().GetMid() << "\'");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Sanity check extmap
+ if (answerAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ if (!offerAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ JSEP_SET_ERROR("Answer adds extmap attributes to level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (const auto& ansExt : answerAttrs.GetExtmap().mExtmaps) {
+ bool found = false;
+ for (const auto& offExt : offerAttrs.GetExtmap().mExtmaps) {
+ if (ansExt.extensionname == offExt.extensionname) {
+ if ((ansExt.direction & reverse(offExt.direction)) !=
+ ansExt.direction) {
+ // FIXME we do not return an error here, because Chrome up to
+ // version 57 is actually tripping over this if they are the
+ // answerer. See bug 1355010 for details.
+ MOZ_MTLOG(ML_WARNING,
+ "[" << mName
+ << "]: Answer has inconsistent"
+ " direction on extmap attribute at level "
+ << i << " (" << ansExt.extensionname
+ << "). Offer had " << offExt.direction
+ << ", answer had " << ansExt.direction << ".");
+ // return NS_ERROR_INVALID_ARG;
+ }
+
+ if (offExt.entry < 4096 && (offExt.entry != ansExt.entry)) {
+ JSEP_SET_ERROR("Answer changed id for extmap attribute at level "
+ << i << " (" << offExt.extensionname << ") from "
+ << offExt.entry << " to " << ansExt.entry << ".");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (ansExt.entry >= 4096) {
+ JSEP_SET_ERROR("Answer used an invalid id ("
+ << ansExt.entry
+ << ") for extmap attribute at level " << i << " ("
+ << ansExt.extensionname << ").");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ JSEP_SET_ERROR("Answer has extmap "
+ << ansExt.extensionname
+ << " at "
+ "level "
+ << i << " that was not present in offer.");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::CreateGenericSDP(UniquePtr<Sdp>* sdpp) {
+ // draft-ietf-rtcweb-jsep-08 Section 5.2.1:
+ // o The second SDP line MUST be an "o=" line, as specified in
+ // [RFC4566], Section 5.2. The value of the <username> field SHOULD
+ // be "-". The value of the <sess-id> field SHOULD be a
+ // cryptographically random number. To ensure uniqueness, this
+ // number SHOULD be at least 64 bits long. The value of the <sess-
+ // version> field SHOULD be zero. The value of the <nettype>
+ // <addrtype> <unicast-address> tuple SHOULD be set to a non-
+ // meaningful address, such as IN IP4 0.0.0.0, to prevent leaking the
+ // local address in this field. As mentioned in [RFC4566], the
+ // entire o= line needs to be unique, but selecting a random number
+ // for <sess-id> is sufficient to accomplish this.
+ //
+ // Historical note: we used to report the actual version number here, after
+ // "SDPARTA-", but that becomes a problem starting with version 100, since
+ // some services parse 100 as "10" and give us legacy/broken behavior. So
+ // we're freezing the version number at 99.0 in this string.
+ auto origin = SdpOrigin("mozilla...THIS_IS_SDPARTA-99.0", mSessionId,
+ mSessionVersion, sdp::kIPv4, "0.0.0.0");
+
+ UniquePtr<Sdp> sdp = MakeUnique<SipccSdp>(origin);
+
+ if (mDtlsFingerprints.empty()) {
+ JSEP_SET_ERROR("Missing DTLS fingerprint");
+ return NS_ERROR_FAILURE;
+ }
+
+ UniquePtr<SdpFingerprintAttributeList> fpl =
+ MakeUnique<SdpFingerprintAttributeList>();
+ for (auto& dtlsFingerprint : mDtlsFingerprints) {
+ fpl->PushEntry(dtlsFingerprint.mAlgorithm, dtlsFingerprint.mValue);
+ }
+ sdp->GetAttributeList().SetAttribute(fpl.release());
+
+ auto* iceOpts = new SdpOptionsAttribute(SdpAttribute::kIceOptionsAttribute);
+ iceOpts->PushEntry("trickle");
+ sdp->GetAttributeList().SetAttribute(iceOpts);
+
+ // This assumes content doesn't add a bunch of msid attributes with a
+ // different semantic in mind.
+ std::vector<std::string> msids;
+ msids.push_back("*");
+ mSdpHelper.SetupMsidSemantic(msids, sdp.get());
+
+ *sdpp = std::move(sdp);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetupIds() {
+ SECStatus rv = PK11_GenerateRandom(
+ reinterpret_cast<unsigned char*>(&mSessionId), sizeof(mSessionId));
+ // RFC 3264 says that session-ids MUST be representable as a _signed_
+ // 64 bit number, meaning the MSB cannot be set.
+ mSessionId = mSessionId >> 1;
+ if (rv != SECSuccess) {
+ JSEP_SET_ERROR("Failed to generate session id: " << rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mUuidGen->Generate(&mDefaultRemoteStreamId)) {
+ JSEP_SET_ERROR("Failed to generate default uuid for streams");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mUuidGen->Generate(&mCNAME)) {
+ JSEP_SET_ERROR("Failed to generate CNAME");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::SetDefaultCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs) {
+ mSupportedCodecs.clear();
+
+ for (const auto& codec : aPreferredCodecs) {
+ mSupportedCodecs.emplace_back(codec->Clone());
+ }
+}
+
+void JsepSessionImpl::SetState(JsepSignalingState state) {
+ if (state == mState) return;
+
+ MOZ_MTLOG(ML_NOTICE, "[" << mName << "]: " << GetStateStr(mState) << " -> "
+ << GetStateStr(state));
+ mState = state;
+}
+
+JsepSession::Result JsepSessionImpl::AddRemoteIceCandidate(
+ const std::string& candidate, const std::string& mid,
+ const Maybe<uint16_t>& level, const std::string& ufrag,
+ std::string* transportId) {
+ mLastError.clear();
+ if (!mCurrentRemoteDescription && !mPendingRemoteDescription) {
+ JSEP_SET_ERROR("Cannot add ICE candidate when there is no remote SDP");
+ return dom::PCError::InvalidStateError;
+ }
+
+ if (mid.empty() && !level.isSome() && candidate.empty()) {
+ // Set end-of-candidates on SDP
+ if (mCurrentRemoteDescription) {
+ nsresult rv = mSdpHelper.SetIceGatheringComplete(
+ mCurrentRemoteDescription.get(), ufrag);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ if (mPendingRemoteDescription) {
+ // If we had an error when adding the candidate to the current
+ // description, we stomp them here. This is deliberate.
+ nsresult rv = mSdpHelper.SetIceGatheringComplete(
+ mPendingRemoteDescription.get(), ufrag);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+ return Result();
+ }
+
+ Maybe<JsepTransceiver> transceiver;
+ if (!mid.empty()) {
+ transceiver = GetTransceiverForMid(mid);
+ } else if (level.isSome()) {
+ transceiver = GetTransceiverForLevel(level.value());
+ }
+
+ if (!transceiver) {
+ JSEP_SET_ERROR("Cannot set ICE candidate for level="
+ << level << " mid=" << mid << ": No such transceiver.");
+ return dom::PCError::OperationError;
+ }
+
+ if (level.isSome() && transceiver->GetLevel() != level.value()) {
+ MOZ_MTLOG(ML_WARNING, "Mismatch between mid and level - \""
+ << mid << "\" is not the mid for level "
+ << level);
+ }
+
+ *transportId = transceiver->mTransport.mTransportId;
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ if (mCurrentRemoteDescription) {
+ rv =
+ mSdpHelper.AddCandidateToSdp(mCurrentRemoteDescription.get(), candidate,
+ transceiver->GetLevel(), ufrag);
+ }
+
+ if (mPendingRemoteDescription) {
+ // If we had an error when adding the candidate to the current description,
+ // we stomp them here. This is deliberate.
+ rv =
+ mSdpHelper.AddCandidateToSdp(mPendingRemoteDescription.get(), candidate,
+ transceiver->GetLevel(), ufrag);
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ return Result();
+}
+
+nsresult JsepSessionImpl::AddLocalIceCandidate(const std::string& candidate,
+ const std::string& transportId,
+ const std::string& ufrag,
+ uint16_t* level,
+ std::string* mid,
+ bool* skipped) {
+ mLastError.clear();
+ *skipped = true;
+ if (!mCurrentLocalDescription && !mPendingLocalDescription) {
+ JSEP_SET_ERROR("Cannot add ICE candidate when there is no local SDP");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ Maybe<const JsepTransceiver> transceiver =
+ GetTransceiverWithTransport(transportId);
+ if (!transceiver || !transceiver->IsAssociated()) {
+ // mainly here to make some testing less complicated, but also just in case
+ return NS_OK;
+ }
+
+ *level = transceiver->GetLevel();
+ *mid = transceiver->GetMid();
+
+ nsresult rv = NS_ERROR_INVALID_ARG;
+ if (mCurrentLocalDescription) {
+ rv = mSdpHelper.AddCandidateToSdp(mCurrentLocalDescription.get(), candidate,
+ *level, ufrag);
+ }
+
+ if (mPendingLocalDescription) {
+ // If we had an error when adding the candidate to the current description,
+ // we stomp them here. This is deliberate.
+ rv = mSdpHelper.AddCandidateToSdp(mPendingLocalDescription.get(), candidate,
+ *level, ufrag);
+ }
+
+ *skipped = false;
+ return rv;
+}
+
+nsresult JsepSessionImpl::UpdateDefaultCandidate(
+ const std::string& defaultCandidateAddr, uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort, const std::string& transportId) {
+ mLastError.clear();
+
+ mozilla::Sdp* sdp =
+ GetParsedLocalDescription(kJsepDescriptionPendingOrCurrent);
+
+ if (!sdp) {
+ JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (const auto& transceiver : mTransceivers) {
+ // We set the default address for bundled m-sections, but not candidate
+ // attributes. Ugh.
+ if (transceiver.mTransport.mTransportId == transportId) {
+ MOZ_ASSERT(transceiver.HasLevel(),
+ "Transceiver has a transport, but no level! "
+ "This should never happen.");
+ std::string defaultRtcpCandidateAddrCopy(defaultRtcpCandidateAddr);
+ if (mState == kJsepStateStable) {
+ if (transceiver.mTransport.mComponents == 1) {
+ // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
+ defaultRtcpCandidateAddrCopy = "";
+ defaultRtcpCandidatePort = 0;
+ }
+ }
+
+ size_t level = transceiver.GetLevel();
+ if (level >= sdp->GetMediaSectionCount()) {
+ MOZ_ASSERT(false, "Transceiver's level is too large!");
+ JSEP_SET_ERROR("Transceiver's level is too large!");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto& msection = sdp->GetMediaSection(level);
+
+ // Do not add default candidate to a bundle-only m-section, sinice that
+ // might confuse endpoints that do not support bundle-only.
+ if (!msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute)) {
+ mSdpHelper.SetDefaultAddresses(
+ defaultCandidateAddr, defaultCandidatePort,
+ defaultRtcpCandidateAddrCopy, defaultRtcpCandidatePort, &msection);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::GetNegotiatedBundledMids(
+ SdpHelper::BundledMids* bundledMids) {
+ const Sdp* answerSdp = GetAnswer();
+
+ if (!answerSdp) {
+ return NS_OK;
+ }
+
+ return mSdpHelper.GetBundledMids(*answerSdp, bundledMids);
+}
+
+mozilla::Sdp* JsepSessionImpl::GetParsedLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ if (type == kJsepDescriptionPending) {
+ return mPendingLocalDescription.get();
+ } else if (mPendingLocalDescription &&
+ type == kJsepDescriptionPendingOrCurrent) {
+ return mPendingLocalDescription.get();
+ }
+ return mCurrentLocalDescription.get();
+}
+
+mozilla::Sdp* JsepSessionImpl::GetParsedRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ if (type == kJsepDescriptionPending) {
+ return mPendingRemoteDescription.get();
+ } else if (mPendingRemoteDescription &&
+ type == kJsepDescriptionPendingOrCurrent) {
+ return mPendingRemoteDescription.get();
+ }
+ return mCurrentRemoteDescription.get();
+}
+
+const Sdp* JsepSessionImpl::GetAnswer() const {
+ return (mIsCurrentOfferer.isSome() && *mIsCurrentOfferer)
+ ? mCurrentRemoteDescription.get()
+ : mCurrentLocalDescription.get();
+}
+
+void JsepSessionImpl::SetIceRestarting(bool restarting) {
+ if (restarting) {
+ // not restarting -> restarting
+ if (!IsIceRestarting()) {
+ // We don't set this more than once, so the old ufrag/pwd is preserved
+ // even if we CreateOffer({iceRestart:true}) multiple times in a row.
+ mOldIceUfrag = mIceUfrag;
+ mOldIcePwd = mIcePwd;
+ }
+ mIceUfrag = GetRandomHex(1);
+ mIcePwd = GetRandomHex(4);
+ } else if (IsIceRestarting()) {
+ // restarting -> not restarting, restore old ufrag/pwd
+ mIceUfrag = mOldIceUfrag;
+ mIcePwd = mOldIcePwd;
+ mOldIceUfrag.clear();
+ mOldIcePwd.clear();
+ }
+}
+
+nsresult JsepSessionImpl::Close() {
+ mLastError.clear();
+ SetState(kJsepStateClosed);
+ return NS_OK;
+}
+
+const std::string JsepSessionImpl::GetLastError() const { return mLastError; }
+
+const std::vector<std::pair<size_t, std::string>>&
+JsepSessionImpl::GetLastSdpParsingErrors() const {
+ return mLastSdpParsingErrors;
+}
+
+bool JsepSessionImpl::CheckNegotiationNeeded() const {
+ MOZ_ASSERT(mState == kJsepStateStable);
+
+ for (const auto& transceiver : mTransceivers) {
+ if (transceiver.IsStopped()) {
+ if (transceiver.IsAssociated()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because of "
+ "stopped transceiver that still has a mid.");
+ return true;
+ }
+ continue;
+ }
+
+ if (!transceiver.IsAssociated()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because of "
+ "unassociated (but not stopped) transceiver.");
+ return true;
+ }
+
+ if (!mCurrentLocalDescription || !mCurrentRemoteDescription) {
+ MOZ_CRASH(
+ "Transceivers should not be associated if we're in stable "
+ "before the first negotiation.");
+ continue;
+ }
+
+ if (!transceiver.HasLevel()) {
+ MOZ_CRASH("Associated transceivers should always have a level.");
+ continue;
+ }
+
+ if (transceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ continue;
+ }
+
+ size_t level = transceiver.GetLevel();
+ if (NS_WARN_IF(mCurrentLocalDescription->GetMediaSectionCount() <= level) ||
+ NS_WARN_IF(mCurrentRemoteDescription->GetMediaSectionCount() <=
+ level)) {
+ MOZ_ASSERT(false);
+ continue;
+ }
+
+ const SdpMediaSection& local =
+ mCurrentLocalDescription->GetMediaSection(level);
+ const SdpMediaSection& remote =
+ mCurrentRemoteDescription->GetMediaSection(level);
+
+ if (transceiver.mJsDirection & sdp::kSend) {
+ std::vector<std::string> sdpMsids;
+ if (local.GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute)) {
+ for (const auto& msidAttr : local.GetAttributeList().GetMsid().mMsids) {
+ if (msidAttr.identifier != "-") {
+ sdpMsids.push_back(msidAttr.identifier);
+ }
+ }
+ }
+ std::sort(sdpMsids.begin(), sdpMsids.end());
+
+ std::vector<std::string> jsepMsids;
+ for (const auto& jsepMsid : transceiver.mSendTrack.GetStreamIds()) {
+ jsepMsids.push_back(jsepMsid);
+ }
+ std::sort(jsepMsids.begin(), jsepMsids.end());
+
+ if (!std::equal(sdpMsids.begin(), sdpMsids.end(), jsepMsids.begin(),
+ jsepMsids.end())) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName
+ << "]: Negotiation needed because transceiver "
+ "is sending, and the local SDP has different "
+ "msids than the send track");
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SDP msids = [");
+ for (const auto& msid : sdpMsids) {
+ MOZ_MTLOG(ML_DEBUG, msid << ", ");
+ }
+ MOZ_MTLOG(ML_DEBUG, "]");
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: JSEP msids = [");
+ for (const auto& msid : jsepMsids) {
+ MOZ_MTLOG(ML_DEBUG, msid << ", ");
+ }
+ MOZ_MTLOG(ML_DEBUG, "]");
+ return true;
+ }
+ }
+
+ if (mIsCurrentOfferer.isSome() && *mIsCurrentOfferer) {
+ if ((local.GetDirection() != transceiver.mJsDirection) &&
+ reverse(remote.GetDirection()) != transceiver.mJsDirection) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because "
+ "the direction on our offer, and the remote "
+ "answer, does not "
+ "match the direction on a transceiver.");
+ return true;
+ }
+ } else if (local.GetDirection() !=
+ (transceiver.mJsDirection & reverse(remote.GetDirection()))) {
+ MOZ_MTLOG(
+ ML_DEBUG,
+ "[" << mName
+ << "]: Negotiation needed because "
+ "the direction on our answer doesn't match the direction on a "
+ "transceiver, even though the remote offer would have allowed "
+ "it.");
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace mozilla