/* 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 "logging.h" #include "signaling/src/jsep/JsepSessionImpl.h" #include #include #include #include #include "nspr.h" #include "nss.h" #include "pk11pub.h" #include "nsDebug.h" #include #include #include "signaling/src/jsep/JsepTrack.h" #include "signaling/src/jsep/JsepTrack.h" #include "signaling/src/jsep/JsepTransport.h" #include "signaling/src/sdp/Sdp.h" #include "signaling/src/sdp/SipccSdp.h" #include "signaling/src/sdp/SipccSdpParser.h" #include "mozilla/net/DataChannelProtocol.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, 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; } nsresult JsepSessionImpl::Init() { mLastError.clear(); MOZ_ASSERT(!mSessionId, "Init called more than once"); nsresult rv = SetupIds(); NS_ENSURE_SUCCESS(rv, rv); SetupDefaultCodecs(); SetupDefaultRtpExtensions(); return NS_OK; } // Helper function to find the track for a given m= section. template typename std::vector::iterator FindTrackByLevel(std::vector& tracks, size_t level) { for (auto t = tracks.begin(); t != tracks.end(); ++t) { if (t->mAssignedMLine.isSome() && (*t->mAssignedMLine == level)) { return t; } } return tracks.end(); } template typename std::vector::iterator FindTrackByIds(std::vector& tracks, const std::string& streamId, const std::string& trackId) { for (auto t = tracks.begin(); t != tracks.end(); ++t) { if (t->mTrack->GetStreamId() == streamId && (t->mTrack->GetTrackId() == trackId)) { return t; } } return tracks.end(); } template typename std::vector::iterator FindUnassignedTrackByType(std::vector& tracks, SdpMediaSection::MediaType type) { for (auto t = tracks.begin(); t != tracks.end(); ++t) { if (!t->mAssignedMLine.isSome() && (t->mTrack->GetMediaType() == type)) { return t; } } return tracks.end(); } nsresult JsepSessionImpl::AddTrack(const RefPtr& track) { mLastError.clear(); MOZ_ASSERT(track->GetDirection() == sdp::kSend); if (track->GetMediaType() != SdpMediaSection::kApplication) { track->SetCNAME(mCNAME); if (track->GetSsrcs().empty()) { uint32_t ssrc; nsresult rv = CreateSsrc(&ssrc); NS_ENSURE_SUCCESS(rv, rv); track->AddSsrc(ssrc); } } track->PopulateCodecs(mSupportedCodecs.values); JsepSendingTrack strack; strack.mTrack = track; mLocalTracks.push_back(strack); return NS_OK; } nsresult JsepSessionImpl::RemoveTrack(const std::string& streamId, const std::string& trackId) { if (mState != kJsepStateStable) { JSEP_SET_ERROR("Removing tracks outside of stable is unsupported."); return NS_ERROR_UNEXPECTED; } auto track = FindTrackByIds(mLocalTracks, streamId, trackId); if (track == mLocalTracks.end()) { return NS_ERROR_INVALID_ARG; } mLocalTracks.erase(track); return NS_OK; } nsresult JsepSessionImpl::SetIceCredentials(const std::string& ufrag, const std::string& pwd) { mLastError.clear(); mIceUfrag = ufrag; mIcePwd = pwd; return NS_OK; } nsresult JsepSessionImpl::SetBundlePolicy(JsepBundlePolicy policy) { mLastError.clear(); 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& value) { mLastError.clear(); JsepDtlsFingerprint fp; fp.mAlgorithm = algorithm; fp.mValue = value; mDtlsFingerprints.push_back(fp); return NS_OK; } nsresult JsepSessionImpl::AddRtpExtension(std::vector& extensions, const std::string& extensionName, SdpDirectionAttribute::Direction direction) { mLastError.clear(); if (extensions.size() + 1 > UINT16_MAX) { JSEP_SET_ERROR("Too many rtp extensions have been added"); return NS_ERROR_FAILURE; } SdpExtmapAttributeList::Extmap extmap = { static_cast(extensions.size() + 1), direction, direction != SdpDirectionAttribute::kSendrecv, // do we want to specify direction? extensionName, "" }; extensions.push_back(extmap); return NS_OK; } nsresult JsepSessionImpl::AddAudioRtpExtension(const std::string& extensionName, SdpDirectionAttribute::Direction direction) { return AddRtpExtension(mAudioRtpExtensions, extensionName, direction); } nsresult JsepSessionImpl::AddVideoRtpExtension(const std::string& extensionName, SdpDirectionAttribute::Direction direction) { return AddRtpExtension(mVideoRtpExtensions, extensionName, direction); } template std::vector> GetTracks(const std::vector& wrappedTracks) { std::vector> result; for (auto i = wrappedTracks.begin(); i != wrappedTracks.end(); ++i) { result.push_back(i->mTrack); } return result; } nsresult JsepSessionImpl::ReplaceTrack(const std::string& oldStreamId, const std::string& oldTrackId, const std::string& newStreamId, const std::string& newTrackId) { auto it = FindTrackByIds(mLocalTracks, oldStreamId, oldTrackId); if (it == mLocalTracks.end()) { JSEP_SET_ERROR("Track " << oldStreamId << "/" << oldTrackId << " was never added."); return NS_ERROR_INVALID_ARG; } if (FindTrackByIds(mLocalTracks, newStreamId, newTrackId) != mLocalTracks.end()) { JSEP_SET_ERROR("Track " << newStreamId << "/" << newTrackId << " was already added."); return NS_ERROR_INVALID_ARG; } it->mTrack->SetStreamId(newStreamId); it->mTrack->SetTrackId(newTrackId); return NS_OK; } nsresult JsepSessionImpl::SetParameters(const std::string& streamId, const std::string& trackId, const std::vector& constraints) { auto it = FindTrackByIds(mLocalTracks, streamId, trackId); if (it == mLocalTracks.end()) { JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added."); return NS_ERROR_INVALID_ARG; } // Add RtpStreamId Extmap // SdpDirectionAttribute::Direction is a bitmask SdpDirectionAttribute::Direction addVideoExt = SdpDirectionAttribute::kInactive; for (auto constraintEntry: constraints) { if (constraintEntry.rid != "") { if (it->mTrack->GetMediaType() == SdpMediaSection::kVideo) { addVideoExt = static_cast(addVideoExt | it->mTrack->GetDirection()); } } } if (addVideoExt != SdpDirectionAttribute::kInactive) { AddVideoRtpExtension("urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", addVideoExt); } it->mTrack->SetJsConstraints(constraints); return NS_OK; } nsresult JsepSessionImpl::GetParameters(const std::string& streamId, const std::string& trackId, std::vector* outConstraints) { auto it = FindTrackByIds(mLocalTracks, streamId, trackId); if (it == mLocalTracks.end()) { JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added."); return NS_ERROR_INVALID_ARG; } it->mTrack->GetJsConstraints(outConstraints); return NS_OK; } std::vector> JsepSessionImpl::GetLocalTracks() const { return GetTracks(mLocalTracks); } std::vector> JsepSessionImpl::GetRemoteTracks() const { return GetTracks(mRemoteTracks); } std::vector> JsepSessionImpl::GetRemoteTracksAdded() const { return GetTracks(mRemoteTracksAdded); } std::vector> JsepSessionImpl::GetRemoteTracksRemoved() const { return GetTracks(mRemoteTracksRemoved); } nsresult JsepSessionImpl::SetupOfferMSections(const JsepOfferOptions& options, Sdp* sdp) { // First audio, then video, then datachannel, for interop // TODO(bug 1121756): We need to group these by stream-id, _then_ by media // type, according to the spec. However, this is not going to interop with // older versions of Firefox if a video-only stream is added before an // audio-only stream. // We should probably wait until 38 is ESR before trying to do this. nsresult rv = SetupOfferMSectionsByType( SdpMediaSection::kAudio, options.mOfferToReceiveAudio, sdp); NS_ENSURE_SUCCESS(rv, rv); rv = SetupOfferMSectionsByType( SdpMediaSection::kVideo, options.mOfferToReceiveVideo, sdp); NS_ENSURE_SUCCESS(rv, rv); if (!(options.mDontOfferDataChannel.isSome() && *options.mDontOfferDataChannel)) { rv = SetupOfferMSectionsByType( SdpMediaSection::kApplication, Maybe(), sdp); NS_ENSURE_SUCCESS(rv, rv); } if (!sdp->GetMediaSectionCount()) { JSEP_SET_ERROR("Cannot create an offer with no local tracks, " "no offerToReceiveAudio/Video, and no DataChannel."); return NS_ERROR_INVALID_ARG; } return NS_OK; } nsresult JsepSessionImpl::SetupOfferMSectionsByType(SdpMediaSection::MediaType mediatype, Maybe offerToReceiveMaybe, Sdp* sdp) { // Convert the Maybe into a size_t*, since that is more readable, especially // when using it as an in/out param. size_t offerToReceiveCount; size_t* offerToReceiveCountPtr = nullptr; if (offerToReceiveMaybe) { offerToReceiveCount = *offerToReceiveMaybe; offerToReceiveCountPtr = &offerToReceiveCount; } // Make sure every local track has an m-section nsresult rv = BindLocalTracks(mediatype, sdp); NS_ENSURE_SUCCESS(rv, rv); // Make sure that m-sections that previously had a remote track have the // recv bit set. Only matters for renegotiation. rv = BindRemoteTracks(mediatype, sdp, offerToReceiveCountPtr); NS_ENSURE_SUCCESS(rv, rv); // If we need more recv sections, start setting the recv bit on other // msections. If not, disable msections that have no tracks. rv = SetRecvAsNeededOrDisable(mediatype, sdp, offerToReceiveCountPtr); NS_ENSURE_SUCCESS(rv, rv); // If we still don't have enough recv m-sections, add some. if (offerToReceiveCountPtr && *offerToReceiveCountPtr) { rv = AddRecvonlyMsections(mediatype, *offerToReceiveCountPtr, sdp); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult JsepSessionImpl::BindLocalTracks(SdpMediaSection::MediaType mediatype, Sdp* sdp) { for (JsepSendingTrack& track : mLocalTracks) { if (mediatype != track.mTrack->GetMediaType()) { continue; } SdpMediaSection* msection; if (track.mAssignedMLine.isSome()) { msection = &sdp->GetMediaSection(*track.mAssignedMLine); } else { nsresult rv = GetFreeMsectionForSend(track.mTrack->GetMediaType(), sdp, &msection); NS_ENSURE_SUCCESS(rv, rv); track.mAssignedMLine = Some(msection->GetLevel()); } track.mTrack->AddToOffer(msection); } return NS_OK; } nsresult JsepSessionImpl::BindRemoteTracks(SdpMediaSection::MediaType mediatype, Sdp* sdp, size_t* offerToReceive) { for (JsepReceivingTrack& track : mRemoteTracks) { if (mediatype != track.mTrack->GetMediaType()) { continue; } if (!track.mAssignedMLine.isSome()) { MOZ_ASSERT(false); continue; } auto& msection = sdp->GetMediaSection(*track.mAssignedMLine); if (mSdpHelper.MsectionIsDisabled(msection)) { // TODO(bug 1095226) Content probably disabled this? Should we allow // content to do this? continue; } track.mTrack->AddToOffer(&msection); if (offerToReceive && *offerToReceive) { --(*offerToReceive); } } return NS_OK; } nsresult JsepSessionImpl::SetRecvAsNeededOrDisable(SdpMediaSection::MediaType mediatype, Sdp* sdp, size_t* offerToRecv) { for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) { auto& msection = sdp->GetMediaSection(i); if (mSdpHelper.MsectionIsDisabled(msection) || msection.GetMediaType() != mediatype || msection.IsReceiving()) { continue; } if (offerToRecv) { if (*offerToRecv) { SetupOfferToReceiveMsection(&msection); --(*offerToRecv); continue; } } else if (msection.IsSending()) { SetupOfferToReceiveMsection(&msection); continue; } if (!msection.IsSending()) { // Unused m-section, and no reason to offer to recv on it mSdpHelper.DisableMsection(sdp, &msection); } } return NS_OK; } void JsepSessionImpl::SetupOfferToReceiveMsection(SdpMediaSection* offer) { // Create a dummy recv track, and have it add codecs, set direction, etc. RefPtr dummy = new JsepTrack(offer->GetMediaType(), "", "", sdp::kRecv); dummy->PopulateCodecs(mSupportedCodecs.values); dummy->AddToOffer(offer); } nsresult JsepSessionImpl::AddRecvonlyMsections(SdpMediaSection::MediaType mediatype, size_t count, Sdp* sdp) { while (count--) { nsresult rv = CreateOfferMSection( mediatype, mSdpHelper.GetProtocolForMediaType(mediatype), SdpDirectionAttribute::kRecvonly, sdp); NS_ENSURE_SUCCESS(rv, rv); SetupOfferToReceiveMsection( &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1)); } return NS_OK; } // This function creates a skeleton SDP based on the old descriptions // (ie; all m-sections are inactive). nsresult JsepSessionImpl::AddReofferMsections(const Sdp& oldLocalSdp, const Sdp& oldAnswer, Sdp* newSdp) { nsresult rv; for (size_t i = 0; i < oldLocalSdp.GetMediaSectionCount(); ++i) { // We do not set the direction in this function (or disable when previously // disabled), that happens in |SetupOfferMSectionsByType| rv = CreateOfferMSection(oldLocalSdp.GetMediaSection(i).GetMediaType(), oldLocalSdp.GetMediaSection(i).GetProtocol(), SdpDirectionAttribute::kInactive, newSdp); NS_ENSURE_SUCCESS(rv, rv); rv = mSdpHelper.CopyStickyParams(oldAnswer.GetMediaSection(i), &newSdp->GetMediaSection(i)); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } void JsepSessionImpl::SetupBundle(Sdp* sdp) const { std::vector mids; std::set 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 (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)); } mids.push_back(attrs.GetMid()); } } if (mids.size() > 1) { UniquePtr groupAttr(new SdpGroupAttributeList); groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids); sdp->GetAttributeList().SetAttribute(groupAttr.release()); } } nsresult JsepSessionImpl::GetRemoteIds(const Sdp& sdp, const SdpMediaSection& msection, std::string* streamId, std::string* trackId) { nsresult rv = mSdpHelper.GetIdsFromMsid(sdp, msection, streamId, trackId); if (rv == NS_ERROR_NOT_AVAILABLE) { *streamId = mDefaultRemoteStreamId; if (!mDefaultRemoteTrackIdsByLevel.count(msection.GetLevel())) { // Generate random track ids. if (!mUuidGen->Generate(trackId)) { JSEP_SET_ERROR("Failed to generate UUID for JsepTrack"); return NS_ERROR_FAILURE; } mDefaultRemoteTrackIdsByLevel[msection.GetLevel()] = *trackId; } else { *trackId = mDefaultRemoteTrackIdsByLevel[msection.GetLevel()]; } return NS_OK; } if (NS_SUCCEEDED(rv)) { // If, for whatever reason, the other end renegotiates with an msid where // there wasn't one before, don't allow the old default to pop up again // later. mDefaultRemoteTrackIdsByLevel.erase(msection.GetLevel()); } return rv; } nsresult JsepSessionImpl::CreateOffer(const JsepOfferOptions& options, std::string* offer) { mLastError.clear(); if (mState != kJsepStateStable) { JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } // Undo track assignments from a previous call to CreateOffer // (ie; if the track has not been negotiated yet, it doesn't necessarily need // to stay in the same m-section that it was in) for (JsepSendingTrack& trackWrapper : mLocalTracks) { if (!trackWrapper.mTrack->GetNegotiatedDetails()) { trackWrapper.mAssignedMLine.reset(); } } UniquePtr sdp; // Make the basic SDP that is common to offer/answer. nsresult rv = CreateGenericSDP(&sdp); NS_ENSURE_SUCCESS(rv, rv); if (mCurrentLocalDescription) { rv = AddReofferMsections(*mCurrentLocalDescription, *GetAnswer(), sdp.get()); NS_ENSURE_SUCCESS(rv, rv); } // Ensure that we have all the m-sections we need, and disable extras rv = SetupOfferMSections(options, sdp.get()); NS_ENSURE_SUCCESS(rv, rv); SetupBundle(sdp.get()); if (mCurrentLocalDescription) { rv = CopyPreviousTransportParams(*GetAnswer(), *mCurrentLocalDescription, *sdp, sdp.get()); NS_ENSURE_SUCCESS(rv,rv); } *offer = sdp->ToString(); mGeneratedLocalDescription = Move(sdp); ++mSessionVersion; return NS_OK; } std::string JsepSessionImpl::GetLocalDescription() const { std::ostringstream os; mozilla::Sdp* sdp = GetParsedLocalDescription(); if (sdp) { sdp->Serialize(os); } return os.str(); } std::string JsepSessionImpl::GetRemoteDescription() const { std::ostringstream os; mozilla::Sdp* sdp = GetParsedRemoteDescription(); if (sdp) { sdp->Serialize(os); } return os.str(); } void JsepSessionImpl::AddExtmap(SdpMediaSection* msection) const { const auto* extensions = GetRtpExtensions(msection->GetMediaType()); if (extensions && !extensions->empty()) { SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList; extmap->mExtmaps = *extensions; msection->GetAttributeList().SetAttribute(extmap); } } void JsepSessionImpl::AddMid(const std::string& mid, SdpMediaSection* msection) const { msection->GetAttributeList().SetAttribute(new SdpStringAttribute( SdpAttribute::kMidAttribute, mid)); } const std::vector* JsepSessionImpl::GetRtpExtensions(SdpMediaSection::MediaType type) const { switch (type) { case SdpMediaSection::kAudio: return &mAudioRtpExtensions; case SdpMediaSection::kVideo: return &mVideoRtpExtensions; default: return nullptr; } } void JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection, SdpMediaSection* msection) { auto* ourExtensions = GetRtpExtensions(remoteMsection.GetMediaType()); if (ourExtensions) { mSdpHelper.AddCommonExtmaps(remoteMsection, *ourExtensions, msection); } } nsresult JsepSessionImpl::CreateAnswer(const JsepAnswerOptions& options, std::string* answer) { mLastError.clear(); if (mState != kJsepStateHaveRemoteOffer) { JSEP_SET_ERROR("Cannot create answer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } // This is the heart of the negotiation code. Depressing that it's // so bad. // // Here's the current algorithm: // 1. Walk through all the m-lines on the other side. // 2. For each m-line, walk through all of our local tracks // in sequence and see if any are unassigned. If so, assign // them and mark it sendrecv, otherwise it's recvonly. // 3. Just replicate their media attributes. // 4. Profit. UniquePtr sdp; // Make the basic SDP that is common to offer/answer. nsresult rv = CreateGenericSDP(&sdp); NS_ENSURE_SUCCESS(rv, rv); const Sdp& offer = *mPendingRemoteDescription; // Copy the bundle groups into our answer UniquePtr groupAttr(new SdpGroupAttributeList); mSdpHelper.GetBundleGroups(offer, &groupAttr->mGroups); sdp->GetAttributeList().SetAttribute(groupAttr.release()); // Disable send for local tracks if the offer no longer allows it // (i.e., the m-section is recvonly, inactive or disabled) for (JsepSendingTrack& trackWrapper : mLocalTracks) { if (!trackWrapper.mAssignedMLine.isSome()) { continue; } // Get rid of all m-line assignments that have not been negotiated if (!trackWrapper.mTrack->GetNegotiatedDetails()) { trackWrapper.mAssignedMLine.reset(); continue; } if (!offer.GetMediaSection(*trackWrapper.mAssignedMLine).IsReceiving()) { trackWrapper.mAssignedMLine.reset(); } } size_t numMsections = offer.GetMediaSectionCount(); for (size_t i = 0; i < numMsections; ++i) { const SdpMediaSection& remoteMsection = offer.GetMediaSection(i); rv = CreateAnswerMSection(options, i, remoteMsection, sdp.get()); NS_ENSURE_SUCCESS(rv, rv); } 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,rv); } *answer = sdp->ToString(); mGeneratedLocalDescription = Move(sdp); ++mSessionVersion; return NS_OK; } nsresult JsepSessionImpl::CreateOfferMSection(SdpMediaSection::MediaType mediatype, SdpMediaSection::Protocol proto, SdpDirectionAttribute::Direction dir, Sdp* sdp) { SdpMediaSection* msection = &sdp->AddMediaSection(mediatype, dir, 0, proto, sdp::kIPv4, "0.0.0.0"); return EnableOfferMsection(msection); } nsresult JsepSessionImpl::GetFreeMsectionForSend( SdpMediaSection::MediaType type, Sdp* sdp, SdpMediaSection** msectionOut) { for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) { SdpMediaSection& msection = sdp->GetMediaSection(i); // draft-ietf-rtcweb-jsep-08 says we should reclaim disabled m-sections // regardless of media type. This breaks some pretty fundamental rules of // SDP offer/answer, so we probably should not do it. if (msection.GetMediaType() != type) { continue; } if (FindTrackByLevel(mLocalTracks, i) != mLocalTracks.end()) { // Not free continue; } if (mSdpHelper.MsectionIsDisabled(msection)) { // Was disabled; revive nsresult rv = EnableOfferMsection(&msection); NS_ENSURE_SUCCESS(rv, rv); } *msectionOut = &msection; return NS_OK; } // Ok, no pre-existing m-section. Make a new one. nsresult rv = CreateOfferMSection(type, mSdpHelper.GetProtocolForMediaType(type), SdpDirectionAttribute::kInactive, sdp); NS_ENSURE_SUCCESS(rv, rv); *msectionOut = &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1); return NS_OK; } nsresult JsepSessionImpl::CreateAnswerMSection(const JsepAnswerOptions& options, size_t mlineIndex, const SdpMediaSection& remoteMsection, Sdp* sdp) { SdpMediaSection& msection = sdp->AddMediaSection(remoteMsection.GetMediaType(), SdpDirectionAttribute::kInactive, 9, remoteMsection.GetProtocol(), sdp::kIPv4, "0.0.0.0"); nsresult rv = mSdpHelper.CopyStickyParams(remoteMsection, &msection); NS_ENSURE_SUCCESS(rv, rv); if (mSdpHelper.MsectionIsDisabled(remoteMsection)) { mSdpHelper.DisableMsection(sdp, &msection); return NS_OK; } SdpSetupAttribute::Role role; rv = DetermineAnswererSetupRole(remoteMsection, &role); NS_ENSURE_SUCCESS(rv, rv); rv = AddTransportAttributes(&msection, role); NS_ENSURE_SUCCESS(rv, rv); rv = SetRecvonlySsrc(&msection); NS_ENSURE_SUCCESS(rv, rv); // Only attempt to match up local tracks if the offerer has elected to // receive traffic. if (remoteMsection.IsReceiving()) { rv = BindMatchingLocalTrackToAnswer(&msection); NS_ENSURE_SUCCESS(rv, rv); } if (remoteMsection.IsSending()) { BindMatchingRemoteTrackToAnswer(&msection); } if (!msection.IsReceiving() && !msection.IsSending()) { mSdpHelper.DisableMsection(sdp, &msection); return NS_OK; } // Add extmap attributes. AddCommonExtmaps(remoteMsection, &msection); if (msection.GetFormats().empty()) { // Could not negotiate anything. Disable m-section. mSdpHelper.DisableMsection(sdp, &msection); } return NS_OK; } nsresult JsepSessionImpl::SetRecvonlySsrc(SdpMediaSection* msection) { // If previous m-sections are disabled, we do not call this function for them while (mRecvonlySsrcs.size() <= msection->GetLevel()) { uint32_t ssrc; nsresult rv = CreateSsrc(&ssrc); NS_ENSURE_SUCCESS(rv, rv); mRecvonlySsrcs.push_back(ssrc); } std::vector ssrcs; ssrcs.push_back(mRecvonlySsrcs[msection->GetLevel()]); msection->SetSsrcs(ssrcs, mCNAME); return NS_OK; } nsresult JsepSessionImpl::BindMatchingLocalTrackToAnswer(SdpMediaSection* msection) { auto track = FindTrackByLevel(mLocalTracks, msection->GetLevel()); if (track == mLocalTracks.end()) { track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType()); } if (track == mLocalTracks.end() && msection->GetMediaType() == SdpMediaSection::kApplication) { // If we are offered datachannel, we need to play along even if no track // for it has been added yet. std::string streamId; std::string trackId; if (!mUuidGen->Generate(&streamId) || !mUuidGen->Generate(&trackId)) { JSEP_SET_ERROR("Failed to generate UUIDs for datachannel track"); return NS_ERROR_FAILURE; } AddTrack(RefPtr( new JsepTrack(SdpMediaSection::kApplication, streamId, trackId))); track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType()); MOZ_ASSERT(track != mLocalTracks.end()); } if (track != mLocalTracks.end()) { track->mAssignedMLine = Some(msection->GetLevel()); track->mTrack->AddToAnswer( mPendingRemoteDescription->GetMediaSection(msection->GetLevel()), msection); } return NS_OK; } nsresult JsepSessionImpl::BindMatchingRemoteTrackToAnswer(SdpMediaSection* msection) { auto it = FindTrackByLevel(mRemoteTracks, msection->GetLevel()); if (it == mRemoteTracks.end()) { MOZ_ASSERT(false); JSEP_SET_ERROR("Failed to find remote track for local answer m-section"); return NS_ERROR_FAILURE; } it->mTrack->AddToAnswer( mPendingRemoteDescription->GetMediaSection(msection->GetLevel()), 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; } nsresult JsepSessionImpl::SetLocalDescription(JsepSdpType type, const std::string& sdp) { mLastError.clear(); MOZ_MTLOG(ML_DEBUG, "SetLocalDescription type=" << type << "\nSDP=\n" << sdp); if (type == kJsepSdpRollback) { if (mState != kJsepStateHaveLocalOffer) { JSEP_SET_ERROR("Cannot rollback local description in " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } mPendingLocalDescription.reset(); SetState(kJsepStateStable); mTransports = mOldTransports; mOldTransports.clear(); return NS_OK; } switch (mState) { case kJsepStateStable: if (type != kJsepSdpOffer) { JSEP_SET_ERROR("Cannot set local answer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } mIsOfferer = true; break; case kJsepStateHaveRemoteOffer: if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) { JSEP_SET_ERROR("Cannot set local offer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } break; default: JSEP_SET_ERROR("Cannot set local offer or answer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } UniquePtr parsed; nsresult rv = ParseSdp(sdp, &parsed); NS_ENSURE_SUCCESS(rv, rv); // Check that content hasn't done anything unsupported with the SDP rv = ValidateLocalDescription(*parsed); NS_ENSURE_SUCCESS(rv, rv); // Create transport objects. mOldTransports = mTransports; // Save in case we need to rollback mTransports.clear(); for (size_t t = 0; t < parsed->GetMediaSectionCount(); ++t) { mTransports.push_back(RefPtr(new JsepTransport)); InitTransport(parsed->GetMediaSection(t), mTransports[t].get()); } switch (type) { case kJsepSdpOffer: rv = SetLocalDescriptionOffer(Move(parsed)); break; case kJsepSdpAnswer: case kJsepSdpPranswer: rv = SetLocalDescriptionAnswer(type, Move(parsed)); break; case kJsepSdpRollback: MOZ_CRASH(); // Handled above } return rv; } nsresult JsepSessionImpl::SetLocalDescriptionOffer(UniquePtr offer) { MOZ_ASSERT(mState == kJsepStateStable); mPendingLocalDescription = Move(offer); SetState(kJsepStateHaveLocalOffer); return NS_OK; } nsresult JsepSessionImpl::SetLocalDescriptionAnswer(JsepSdpType type, UniquePtr answer) { MOZ_ASSERT(mState == kJsepStateHaveRemoteOffer); mPendingLocalDescription = Move(answer); nsresult rv = ValidateAnswer(*mPendingRemoteDescription, *mPendingLocalDescription); NS_ENSURE_SUCCESS(rv, rv); rv = HandleNegotiatedSession(mPendingLocalDescription, mPendingRemoteDescription); NS_ENSURE_SUCCESS(rv, rv); mCurrentRemoteDescription = Move(mPendingRemoteDescription); mCurrentLocalDescription = Move(mPendingLocalDescription); mWasOffererLastTime = mIsOfferer; SetState(kJsepStateStable); return NS_OK; } nsresult JsepSessionImpl::SetRemoteDescription(JsepSdpType type, const std::string& sdp) { mLastError.clear(); mRemoteTracksAdded.clear(); mRemoteTracksRemoved.clear(); MOZ_MTLOG(ML_DEBUG, "SetRemoteDescription type=" << type << "\nSDP=\n" << sdp); if (type == kJsepSdpRollback) { if (mState != kJsepStateHaveRemoteOffer) { JSEP_SET_ERROR("Cannot rollback remote description in " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } mPendingRemoteDescription.reset(); SetState(kJsepStateStable); // Update the remote tracks to what they were before the SetRemote return SetRemoteTracksFromDescription(mCurrentRemoteDescription.get()); } switch (mState) { case kJsepStateStable: if (type != kJsepSdpOffer) { JSEP_SET_ERROR("Cannot set remote answer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } mIsOfferer = false; break; case kJsepStateHaveLocalOffer: case kJsepStateHaveRemotePranswer: if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) { JSEP_SET_ERROR("Cannot set remote offer in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } break; default: JSEP_SET_ERROR("Cannot set remote offer or answer in current state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } // Parse. UniquePtr parsed; nsresult rv = ParseSdp(sdp, &parsed); NS_ENSURE_SUCCESS(rv, rv); rv = ValidateRemoteDescription(*parsed); NS_ENSURE_SUCCESS(rv, rv); 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 iceOptions; if (parsed->GetAttributeList().HasAttribute( SdpAttribute::kIceOptionsAttribute)) { iceOptions = parsed->GetAttributeList().GetIceOptions().mValues; } switch (type) { case kJsepSdpOffer: rv = SetRemoteDescriptionOffer(Move(parsed)); break; case kJsepSdpAnswer: case kJsepSdpPranswer: rv = SetRemoteDescriptionAnswer(type, Move(parsed)); break; case kJsepSdpRollback: MOZ_CRASH(); // Handled above } if (NS_SUCCEEDED(rv)) { mRemoteIsIceLite = iceLite; mIceOptions = iceOptions; mRemoteIceIsRestarting = iceRestarting; } return rv; } nsresult JsepSessionImpl::HandleNegotiatedSession(const UniquePtr& local, const UniquePtr& remote) { bool remoteIceLite = remote->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute); mIceControlling = remoteIceLite || mIsOfferer; const Sdp& answer = mIsOfferer ? *remote : *local; SdpHelper::BundledMids bundledMids; nsresult rv = mSdpHelper.GetBundledMids(answer, &bundledMids); NS_ENSURE_SUCCESS(rv, rv); if (mTransports.size() < local->GetMediaSectionCount()) { JSEP_SET_ERROR("Fewer transports set up than m-lines"); MOZ_ASSERT(false); return NS_ERROR_FAILURE; } for (JsepSendingTrack& trackWrapper : mLocalTracks) { trackWrapper.mTrack->ClearNegotiatedDetails(); } for (JsepReceivingTrack& trackWrapper : mRemoteTracks) { trackWrapper.mTrack->ClearNegotiatedDetails(); } std::vector trackPairs; // Now walk through the m-sections, make sure they match, and create // track pairs that describe the media to be set up. for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) { // Skip disabled m-sections. if (answer.GetMediaSection(i).GetPort() == 0) { mTransports[i]->Close(); continue; } // The transport details are not necessarily on the m-section we're // currently processing. size_t transportLevel = i; bool usingBundle = false; { const SdpMediaSection& answerMsection(answer.GetMediaSection(i)); if (answerMsection.GetAttributeList().HasAttribute( SdpAttribute::kMidAttribute)) { if (bundledMids.count(answerMsection.GetAttributeList().GetMid())) { const SdpMediaSection* masterBundleMsection = bundledMids[answerMsection.GetAttributeList().GetMid()]; transportLevel = masterBundleMsection->GetLevel(); usingBundle = true; if (i != transportLevel) { mTransports[i]->Close(); } } } } RefPtr transport = mTransports[transportLevel]; rv = FinalizeTransport( remote->GetMediaSection(transportLevel).GetAttributeList(), answer.GetMediaSection(transportLevel).GetAttributeList(), transport); NS_ENSURE_SUCCESS(rv, rv); JsepTrackPair trackPair; rv = MakeNegotiatedTrackPair(remote->GetMediaSection(i), local->GetMediaSection(i), transport, usingBundle, transportLevel, &trackPair); NS_ENSURE_SUCCESS(rv, rv); trackPairs.push_back(trackPair); } JsepTrack::SetUniquePayloadTypes(GetTracks(mRemoteTracks)); // Ouch, this probably needs some dirty bit instead of just clearing // stuff for renegotiation. mNegotiatedTrackPairs = trackPairs; mGeneratedLocalDescription.reset(); mNegotiations++; return NS_OK; } nsresult JsepSessionImpl::MakeNegotiatedTrackPair(const SdpMediaSection& remote, const SdpMediaSection& local, const RefPtr& transport, bool usingBundle, size_t transportLevel, JsepTrackPair* trackPairOut) { MOZ_ASSERT(transport->mComponents); const SdpMediaSection& answer = mIsOfferer ? remote : local; bool sending; bool receiving; if (mIsOfferer) { receiving = answer.IsSending(); sending = answer.IsReceiving(); } else { sending = answer.IsSending(); receiving = answer.IsReceiving(); } MOZ_MTLOG(ML_DEBUG, "Negotiated m= line" << " index=" << local.GetLevel() << " type=" << local.GetMediaType() << " sending=" << sending << " receiving=" << receiving); trackPairOut->mLevel = local.GetLevel(); MOZ_ASSERT(mRecvonlySsrcs.size() > local.GetLevel(), "Failed to set the default ssrc for an active m-section"); trackPairOut->mRecvonlySsrc = mRecvonlySsrcs[local.GetLevel()]; if (usingBundle) { trackPairOut->mBundleLevel = Some(transportLevel); } auto sendTrack = FindTrackByLevel(mLocalTracks, local.GetLevel()); if (sendTrack != mLocalTracks.end()) { sendTrack->mTrack->Negotiate(answer, remote); sendTrack->mTrack->SetActive(sending); trackPairOut->mSending = sendTrack->mTrack; } else if (sending) { JSEP_SET_ERROR("Failed to find local track for level " << local.GetLevel() << " in local SDP. This should never happen."); NS_ASSERTION(false, "Failed to find local track for level"); return NS_ERROR_FAILURE; } auto recvTrack = FindTrackByLevel(mRemoteTracks, local.GetLevel()); if (recvTrack != mRemoteTracks.end()) { recvTrack->mTrack->Negotiate(answer, remote); recvTrack->mTrack->SetActive(receiving); trackPairOut->mReceiving = recvTrack->mTrack; if (receiving && trackPairOut->mBundleLevel.isSome() && recvTrack->mTrack->GetSsrcs().empty() && recvTrack->mTrack->GetMediaType() != SdpMediaSection::kApplication) { MOZ_MTLOG(ML_ERROR, "Bundled m-section has no ssrc attributes. " "This may cause media packets to be dropped."); } } else if (receiving) { JSEP_SET_ERROR("Failed to find remote track for level " << local.GetLevel() << " in remote SDP. This should never happen."); NS_ASSERTION(false, "Failed to find remote track for level"); return NS_ERROR_FAILURE; } trackPairOut->mRtpTransport = transport; if (transport->mComponents == 2) { // RTCP MUX or not. // TODO(bug 1095743): verify that the PTs are consistent with mux. MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is off"); trackPairOut->mRtcpTransport = transport; } return NS_OK; } void JsepSessionImpl::InitTransport(const SdpMediaSection& msection, JsepTransport* transport) { if (mSdpHelper.MsectionIsDisabled(msection)) { transport->Close(); return; } if (mSdpHelper.HasRtcp(msection.GetProtocol())) { transport->mComponents = 2; } else { transport->mComponents = 1; } if (msection.GetAttributeList().HasAttribute(SdpAttribute::kMidAttribute)) { transport->mTransportId = msection.GetAttributeList().GetMid(); } else { std::ostringstream os; os << "level_" << msection.GetLevel() << "(no mid)"; transport->mTransportId = os.str(); } } nsresult JsepSessionImpl::FinalizeTransport(const SdpAttributeList& remote, const SdpAttributeList& answer, const RefPtr& transport) { UniquePtr ice = MakeUnique(); // We do sanity-checking for these in ParseSdp ice->mUfrag = remote.GetIceUfrag(); ice->mPwd = remote.GetIcePwd(); if (remote.HasAttribute(SdpAttribute::kCandidateAttribute)) { ice->mCandidates = remote.GetCandidate(); } // 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 dtls = MakeUnique(); dtls->mFingerprints = remote.GetFingerprint(); if (!answer.HasAttribute(mozilla::SdpAttribute::kSetupAttribute)) { dtls->mRole = mIsOfferer ? JsepDtlsTransport::kJsepDtlsServer : JsepDtlsTransport::kJsepDtlsClient; } else { if (mIsOfferer) { dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive) ? JsepDtlsTransport::kJsepDtlsServer : JsepDtlsTransport::kJsepDtlsClient; } else { dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive) ? JsepDtlsTransport::kJsepDtlsClient : JsepDtlsTransport::kJsepDtlsServer; } } transport->mIce = Move(ice); transport->mDtls = 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) && !mRemoteIceIsRestarting ) { // 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. size_t numComponents = mTransports[i]->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* parsedp) { UniquePtr parsed = mParser.Parse(sdp); if (!parsed) { std::string error = "Failed to parse SDP: "; mSdpHelper.appendSdpParseErrors(mParser.GetParseErrors(), &error); JSEP_SET_ERROR(error); return NS_ERROR_INVALID_ARG; } // Verify that the JSEP rules for all SDP are followed if (!parsed->GetMediaSectionCount()) { JSEP_SET_ERROR("Description has no media sections"); return NS_ERROR_INVALID_ARG; } std::set trackIds; 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.GetIceUfrag().empty()) { JSEP_SET_ERROR("Invalid description, no ice-ufrag attribute"); return NS_ERROR_INVALID_ARG; } if (mediaAttrs.GetIcePwd().empty()) { JSEP_SET_ERROR("Invalid description, no ice-pwd attribute"); return NS_ERROR_INVALID_ARG; } if (!mediaAttrs.HasAttribute(SdpAttribute::kFingerprintAttribute)) { JSEP_SET_ERROR("Invalid description, no fingerprint attribute"); return NS_ERROR_INVALID_ARG; } const SdpFingerprintAttributeList& fingerprints( mediaAttrs.GetFingerprint()); if (fingerprints.mFingerprints.empty()) { JSEP_SET_ERROR("Invalid description, no supported fingerprint algorithms " "present"); return NS_ERROR_INVALID_ARG; } if (mediaAttrs.HasAttribute(SdpAttribute::kSetupAttribute) && mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kHoldconn) { JSEP_SET_ERROR("Description has illegal setup attribute " "\"holdconn\" at level " << i); return NS_ERROR_INVALID_ARG; } auto& formats = parsed->GetMediaSection(i).GetFormats(); for (auto f = formats.begin(); f != formats.end(); ++f) { uint16_t pt; if (!SdpHelper::GetPtAsInt(*f, &pt)) { JSEP_SET_ERROR("Payload type \"" << *f << "\" is not a 16-bit unsigned int at level " << i); return NS_ERROR_INVALID_ARG; } } std::string streamId; std::string trackId; nsresult rv = mSdpHelper.GetIdsFromMsid(*parsed, parsed->GetMediaSection(i), &streamId, &trackId); if (NS_SUCCEEDED(rv)) { if (trackIds.count(trackId)) { JSEP_SET_ERROR("track id:" << trackId << " appears in more than one m-section at level " << i); return NS_ERROR_INVALID_ARG; } trackIds.insert(trackId); } else if (rv != NS_ERROR_NOT_AVAILABLE) { // Error has already been set return rv; } 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; // TODO (bug 1204099): Make this check for reserved ranges. if (!SdpHelper::GetPtAsInt(fmt, &payloadType) || payloadType > 127) { JSEP_SET_ERROR("audio/video payload type is too large: " << fmt); return NS_ERROR_INVALID_ARG; } if (forbidden.test(payloadType)) { JSEP_SET_ERROR("Illegal audio/video payload type: " << fmt); return NS_ERROR_INVALID_ARG; } } } } *parsedp = Move(parsed); return NS_OK; } nsresult JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr offer) { MOZ_ASSERT(mState == kJsepStateStable); // TODO(bug 1095780): Note that we create remote tracks even when // They contain only codecs we can't negotiate or other craziness. nsresult rv = SetRemoteTracksFromDescription(offer.get()); NS_ENSURE_SUCCESS(rv, rv); mPendingRemoteDescription = Move(offer); SetState(kJsepStateHaveRemoteOffer); return NS_OK; } nsresult JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type, UniquePtr answer) { MOZ_ASSERT(mState == kJsepStateHaveLocalOffer || mState == kJsepStateHaveRemotePranswer); mPendingRemoteDescription = Move(answer); nsresult rv = ValidateAnswer(*mPendingLocalDescription, *mPendingRemoteDescription); NS_ENSURE_SUCCESS(rv, rv); // TODO(bug 1095780): Note that this creates remote tracks even if // we offered sendonly and other side offered sendrecv or recvonly. rv = SetRemoteTracksFromDescription(mPendingRemoteDescription.get()); NS_ENSURE_SUCCESS(rv, rv); rv = HandleNegotiatedSession(mPendingLocalDescription, mPendingRemoteDescription); NS_ENSURE_SUCCESS(rv, rv); mCurrentRemoteDescription = Move(mPendingRemoteDescription); mCurrentLocalDescription = Move(mPendingLocalDescription); mWasOffererLastTime = mIsOfferer; SetState(kJsepStateStable); return NS_OK; } nsresult JsepSessionImpl::SetRemoteTracksFromDescription(const Sdp* remoteDescription) { // Unassign all remote tracks for (auto i = mRemoteTracks.begin(); i != mRemoteTracks.end(); ++i) { i->mAssignedMLine.reset(); } // This will not exist if we're rolling back the first remote description if (remoteDescription) { size_t numMlines = remoteDescription->GetMediaSectionCount(); nsresult rv; // Iterate over the sdp, re-assigning or creating remote tracks as we go for (size_t i = 0; i < numMlines; ++i) { const SdpMediaSection& msection = remoteDescription->GetMediaSection(i); if (mSdpHelper.MsectionIsDisabled(msection) || !msection.IsSending()) { continue; } std::vector::iterator track; if (msection.GetMediaType() == SdpMediaSection::kApplication) { // Datachannel doesn't have msid, just search by type track = FindUnassignedTrackByType(mRemoteTracks, msection.GetMediaType()); } else { std::string streamId; std::string trackId; rv = GetRemoteIds(*remoteDescription, msection, &streamId, &trackId); NS_ENSURE_SUCCESS(rv, rv); track = FindTrackByIds(mRemoteTracks, streamId, trackId); } if (track == mRemoteTracks.end()) { RefPtr track; rv = CreateReceivingTrack(i, *remoteDescription, msection, &track); NS_ENSURE_SUCCESS(rv, rv); JsepReceivingTrack rtrack; rtrack.mTrack = track; rtrack.mAssignedMLine = Some(i); mRemoteTracks.push_back(rtrack); mRemoteTracksAdded.push_back(rtrack); } else { track->mAssignedMLine = Some(i); } } } // Remove any unassigned remote track ids for (size_t i = 0; i < mRemoteTracks.size();) { if (!mRemoteTracks[i].mAssignedMLine.isSome()) { mRemoteTracksRemoved.push_back(mRemoteTracks[i]); mRemoteTracks.erase(mRemoteTracks.begin() + i); } else { ++i; } } return NS_OK; } nsresult JsepSessionImpl::ValidateLocalDescription(const Sdp& description) { // TODO(bug 1095226): Better checking. if (!mGeneratedLocalDescription) { JSEP_SET_ERROR("Calling SetLocal without first calling CreateOffer/Answer" " is not supported."); return NS_ERROR_UNEXPECTED; } if (description.GetMediaSectionCount() != mGeneratedLocalDescription->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 = mGeneratedLocalDescription->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; } } // 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 (!mCurrentRemoteDescription || !mCurrentLocalDescription) { // Not renegotiation; checks for whether a remote answer are consistent // with our offer are handled in ValidateAnswer() return NS_OK; } if (mCurrentRemoteDescription->GetMediaSectionCount() > description.GetMediaSectionCount()) { JSEP_SET_ERROR("New remote description has fewer m-sections than the " "previous remote description."); return NS_ERROR_INVALID_ARG; } // 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 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); // 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::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; } 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 (!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; } } return NS_OK; } nsresult JsepSessionImpl::CreateReceivingTrack(size_t mline, const Sdp& sdp, const SdpMediaSection& msection, RefPtr* track) { std::string streamId; std::string trackId; nsresult rv = GetRemoteIds(sdp, msection, &streamId, &trackId); NS_ENSURE_SUCCESS(rv, rv); *track = new JsepTrack(msection.GetMediaType(), streamId, trackId, sdp::kRecv); (*track)->SetCNAME(mSdpHelper.GetCNAME(msection)); (*track)->PopulateCodecs(mSupportedCodecs.values); return NS_OK; } nsresult JsepSessionImpl::CreateGenericSDP(UniquePtr* 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 field SHOULD // be "-". The value of the field SHOULD be a // cryptographically random number. To ensure uniqueness, this // number SHOULD be at least 64 bits long. The value of the field SHOULD be zero. The value of the // 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 is sufficient to accomplish this. auto origin = SdpOrigin("mozilla...THIS_IS_SDPARTA-" MOZ_APP_UA_VERSION, mSessionId, mSessionVersion, sdp::kIPv4, "0.0.0.0"); UniquePtr sdp = MakeUnique(origin); if (mDtlsFingerprints.empty()) { JSEP_SET_ERROR("Missing DTLS fingerprint"); return NS_ERROR_FAILURE; } UniquePtr fpl = MakeUnique(); for (auto fp = mDtlsFingerprints.begin(); fp != mDtlsFingerprints.end(); ++fp) { fpl->PushEntry(fp->mAlgorithm, fp->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 msids; msids.push_back("*"); mSdpHelper.SetupMsidSemantic(msids, sdp.get()); *sdpp = Move(sdp); return NS_OK; } nsresult JsepSessionImpl::SetupIds() { SECStatus rv = PK11_GenerateRandom( reinterpret_cast(&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; } nsresult JsepSessionImpl::CreateSsrc(uint32_t* ssrc) { do { SECStatus rv = PK11_GenerateRandom( reinterpret_cast(ssrc), sizeof(uint32_t)); if (rv != SECSuccess) { JSEP_SET_ERROR("Failed to generate SSRC, error=" << rv); return NS_ERROR_FAILURE; } } while (mSsrcs.count(*ssrc)); mSsrcs.insert(*ssrc); return NS_OK; } void JsepSessionImpl::SetupDefaultCodecs() { // Supported audio codecs. // Per jmspeex on IRC: // For 32KHz sampling, 28 is ok, 32 is good, 40 should be really good // quality. Note that 1-2Kbps will be wasted on a stereo Opus channel // with mono input compared to configuring it for mono. // If we reduce bitrate enough Opus will low-pass us; 16000 will kill a // 9KHz tone. This should be adaptive when we're at the low-end of video // bandwidth (say <100Kbps), and if we're audio-only, down to 8 or // 12Kbps. mSupportedCodecs.values.push_back(new JsepAudioCodecDescription( "109", "opus", 48000, 2, 960, #ifdef WEBRTC_GONK // TODO Move this elsewhere to be adaptive to rate - Bug 1207925 16000 // B2G uses lower capture sampling rate #else 40000 #endif )); mSupportedCodecs.values.push_back(new JsepAudioCodecDescription( "9", "G722", 8000, 1, 320, 64000)); // packet size and bitrate values below copied from sipcc. // May need reevaluation from a media expert. mSupportedCodecs.values.push_back( new JsepAudioCodecDescription("0", "PCMU", 8000, 1, 8000 / 50, // frequency / 50 8 * 8000 * 1 // 8 * frequency * channels )); mSupportedCodecs.values.push_back( new JsepAudioCodecDescription("8", "PCMA", 8000, 1, 8000 / 50, // frequency / 50 8 * 8000 * 1 // 8 * frequency * channels )); // note: because telephone-event is effectively a marker codec that indicates // that dtmf rtp packets may be passed, the packetSize and bitRate fields // don't make sense here. For now, use zero. (mjf) mSupportedCodecs.values.push_back( new JsepAudioCodecDescription("101", "telephone-event", 8000, 1, 0, // packetSize doesn't make sense here 0 // bitRate doesn't make sense here )); // Supported video codecs. // Note: order here implies priority for building offers! JsepVideoCodecDescription* vp8 = new JsepVideoCodecDescription( "120", "VP8", 90000 ); // Defaults for mandatory params vp8->mConstraints.maxFs = 12288; // Enough for 2048x1536 vp8->mConstraints.maxFps = 60; mSupportedCodecs.values.push_back(vp8); JsepVideoCodecDescription* vp9 = new JsepVideoCodecDescription( "121", "VP9", 90000 ); // Defaults for mandatory params vp9->mConstraints.maxFs = 12288; // Enough for 2048x1536 vp9->mConstraints.maxFps = 60; mSupportedCodecs.values.push_back(vp9); JsepVideoCodecDescription* h264_1 = new JsepVideoCodecDescription( "126", "H264", 90000 ); h264_1->mPacketizationMode = 1; // Defaults for mandatory params h264_1->mProfileLevelId = 0x42E00D; mSupportedCodecs.values.push_back(h264_1); JsepVideoCodecDescription* h264_0 = new JsepVideoCodecDescription( "97", "H264", 90000 ); h264_0->mPacketizationMode = 0; // Defaults for mandatory params h264_0->mProfileLevelId = 0x42E00D; mSupportedCodecs.values.push_back(h264_0); JsepVideoCodecDescription* red = new JsepVideoCodecDescription( "122", // payload type "red", // codec name 90000 // clock rate (match other video codecs) ); mSupportedCodecs.values.push_back(red); JsepVideoCodecDescription* ulpfec = new JsepVideoCodecDescription( "123", // payload type "ulpfec", // codec name 90000 // clock rate (match other video codecs) ); mSupportedCodecs.values.push_back(ulpfec); mSupportedCodecs.values.push_back(new JsepApplicationCodecDescription( "5000", "webrtc-datachannel", WEBRTC_DATACHANNEL_STREAMS_DEFAULT )); // Update the redundant encodings for the RED codec with the supported // codecs. Note: only uses the video codecs. red->UpdateRedundantEncodings(mSupportedCodecs.values); } void JsepSessionImpl::SetupDefaultRtpExtensions() { AddAudioRtpExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level", SdpDirectionAttribute::Direction::kSendonly); } void JsepSessionImpl::SetState(JsepSignalingState state) { if (state == mState) return; MOZ_MTLOG(ML_NOTICE, "[" << mName << "]: " << GetStateStr(mState) << " -> " << GetStateStr(state)); mState = state; } nsresult JsepSessionImpl::AddRemoteIceCandidate(const std::string& candidate, const std::string& mid, uint16_t level) { mLastError.clear(); mozilla::Sdp* sdp = GetParsedRemoteDescription(); if (!sdp) { JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } return mSdpHelper.AddCandidateToSdp(sdp, candidate, mid, level); } nsresult JsepSessionImpl::AddLocalIceCandidate(const std::string& candidate, uint16_t level, std::string* mid, bool* skipped) { mLastError.clear(); mozilla::Sdp* sdp = GetParsedLocalDescription(); if (!sdp) { JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } if (sdp->GetMediaSectionCount() <= level) { // mainly here to make some testing less complicated, but also just in case *skipped = true; return NS_OK; } if (mState == kJsepStateStable) { const Sdp* answer(GetAnswer()); if (mSdpHelper.IsBundleSlave(*answer, level)) { // We do not add candidate attributes to bundled m-sections unless they // are the "master" bundle m-section. *skipped = true; return NS_OK; } } nsresult rv = mSdpHelper.GetMidFromLevel(*sdp, level, mid); if (NS_FAILED(rv)) { return rv; } *skipped = false; return mSdpHelper.AddCandidateToSdp(sdp, candidate, *mid, level); } nsresult JsepSessionImpl::UpdateDefaultCandidate( const std::string& defaultCandidateAddr, uint16_t defaultCandidatePort, const std::string& defaultRtcpCandidateAddr, uint16_t defaultRtcpCandidatePort, uint16_t level) { mLastError.clear(); mozilla::Sdp* sdp = GetParsedLocalDescription(); if (!sdp) { JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } if (level >= sdp->GetMediaSectionCount()) { return NS_OK; } std::string defaultRtcpCandidateAddrCopy(defaultRtcpCandidateAddr); if (mState == kJsepStateStable && mTransports[level]->mComponents == 1) { // We know we're doing rtcp-mux by now. Don't create an rtcp attr. defaultRtcpCandidateAddrCopy = ""; defaultRtcpCandidatePort = 0; } // If offer/answer isn't done, it is too early to tell whether these defaults // need to be applied to other m-sections. SdpHelper::BundledMids bundledMids; if (mState == kJsepStateStable) { nsresult rv = GetNegotiatedBundledMids(&bundledMids); if (NS_FAILED(rv)) { MOZ_ASSERT(false); mLastError += " (This should have been caught sooner!)"; return NS_ERROR_FAILURE; } } mSdpHelper.SetDefaultAddresses( defaultCandidateAddr, defaultCandidatePort, defaultRtcpCandidateAddrCopy, defaultRtcpCandidatePort, sdp, level, bundledMids); return NS_OK; } nsresult JsepSessionImpl::EndOfLocalCandidates(uint16_t level) { mLastError.clear(); mozilla::Sdp* sdp = GetParsedLocalDescription(); if (!sdp) { JSEP_SET_ERROR("Cannot mark end of local ICE candidates in state " << GetStateStr(mState)); return NS_ERROR_UNEXPECTED; } if (level >= sdp->GetMediaSectionCount()) { return NS_OK; } // If offer/answer isn't done, it is too early to tell whether this update // needs to be applied to other m-sections. SdpHelper::BundledMids bundledMids; if (mState == kJsepStateStable) { nsresult rv = GetNegotiatedBundledMids(&bundledMids); if (NS_FAILED(rv)) { MOZ_ASSERT(false); mLastError += " (This should have been caught sooner!)"; return NS_ERROR_FAILURE; } } mSdpHelper.SetIceGatheringComplete(sdp, level, bundledMids); return NS_OK; } nsresult JsepSessionImpl::GetNegotiatedBundledMids(SdpHelper::BundledMids* bundledMids) { const Sdp* answerSdp = GetAnswer(); if (!answerSdp) { return NS_OK; } return mSdpHelper.GetBundledMids(*answerSdp, bundledMids); } nsresult JsepSessionImpl::EnableOfferMsection(SdpMediaSection* msection) { // We assert here because adding rtcp-mux to a non-disabled m-section that // did not already have rtcp-mux can cause problems. MOZ_ASSERT(mSdpHelper.MsectionIsDisabled(*msection)); 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 there. if (mSdpHelper.HasRtcp(msection->GetProtocol())) { // Set RTCP-MUX. msection->GetAttributeList().SetAttribute( new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute)); } nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass); NS_ENSURE_SUCCESS(rv, rv); rv = SetRecvonlySsrc(msection); NS_ENSURE_SUCCESS(rv, rv); AddExtmap(msection); std::ostringstream osMid; osMid << "sdparta_" << msection->GetLevel(); AddMid(osMid.str(), msection); return NS_OK; } mozilla::Sdp* JsepSessionImpl::GetParsedLocalDescription() const { if (mPendingLocalDescription) { return mPendingLocalDescription.get(); } else if (mCurrentLocalDescription) { return mCurrentLocalDescription.get(); } return nullptr; } mozilla::Sdp* JsepSessionImpl::GetParsedRemoteDescription() const { if (mPendingRemoteDescription) { return mPendingRemoteDescription.get(); } else if (mCurrentRemoteDescription) { return mCurrentRemoteDescription.get(); } return nullptr; } const Sdp* JsepSessionImpl::GetAnswer() const { return mWasOffererLastTime ? mCurrentRemoteDescription.get() : mCurrentLocalDescription.get(); } nsresult JsepSessionImpl::Close() { mLastError.clear(); SetState(kJsepStateClosed); return NS_OK; } const std::string JsepSessionImpl::GetLastError() const { return mLastError; } bool JsepSessionImpl::AllLocalTracksAreAssigned() const { for (auto i = mLocalTracks.begin(); i != mLocalTracks.end(); ++i) { if (!i->mAssignedMLine.isSome()) { return false; } } return true; } } // namespace mozilla