/* 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/JsepTrack.h" #include "jsep/JsepCodecDescription.h" #include "jsep/JsepTrackEncoding.h" #include namespace mozilla { void JsepTrack::GetNegotiatedPayloadTypes( std::vector* payloadTypes) const { if (!mNegotiatedDetails) { return; } for (const auto& encoding : mNegotiatedDetails->mEncodings) { GetPayloadTypes(encoding->GetCodecs(), payloadTypes); } // Prune out dupes std::sort(payloadTypes->begin(), payloadTypes->end()); auto newEnd = std::unique(payloadTypes->begin(), payloadTypes->end()); payloadTypes->erase(newEnd, payloadTypes->end()); } /* static */ void JsepTrack::GetPayloadTypes( const std::vector>& codecs, std::vector* payloadTypes) { for (const auto& codec : codecs) { uint16_t pt; if (!codec->GetPtAsInt(&pt)) { MOZ_ASSERT(false); continue; } payloadTypes->push_back(pt); } } void JsepTrack::EnsureNoDuplicatePayloadTypes( std::vector>* codecs) { std::set uniquePayloadTypes; for (auto& codec : *codecs) { codec->EnsureNoDuplicatePayloadTypes(uniquePayloadTypes); } } void JsepTrack::EnsureSsrcs(SsrcGenerator& ssrcGenerator, size_t aNumber) { while (mSsrcs.size() < aNumber) { uint32_t ssrc, rtxSsrc; if (!ssrcGenerator.GenerateSsrc(&ssrc) || !ssrcGenerator.GenerateSsrc(&rtxSsrc)) { return; } mSsrcs.push_back(ssrc); mSsrcToRtxSsrc[ssrc] = rtxSsrc; MOZ_ASSERT(mSsrcs.size() == mSsrcToRtxSsrc.size()); } } void JsepTrack::PopulateCodecs( const std::vector>& prototype) { mPrototypeCodecs.clear(); for (const auto& prototypeCodec : prototype) { if (prototypeCodec->Type() == mType) { mPrototypeCodecs.emplace_back(prototypeCodec->Clone()); mPrototypeCodecs.back()->mDirection = mDirection; } } EnsureNoDuplicatePayloadTypes(&mPrototypeCodecs); } void JsepTrack::AddToOffer(SsrcGenerator& ssrcGenerator, SdpMediaSection* offer) { AddToMsection(mPrototypeCodecs, offer); if (mDirection == sdp::kSend) { std::vector rids; if (offer->IsSending()) { rids = mRids; } AddToMsection(rids, sdp::kSend, ssrcGenerator, IsRtxEnabled(mPrototypeCodecs), offer); } } void JsepTrack::AddToAnswer(const SdpMediaSection& offer, SsrcGenerator& ssrcGenerator, SdpMediaSection* answer) { // We do not modify mPrototypeCodecs here, since we're only creating an // answer. Once offer/answer concludes, we will update mPrototypeCodecs. std::vector> codecs = NegotiateCodecs(offer, true, Nothing()); if (codecs.empty()) { return; } AddToMsection(codecs, answer); if (mDirection == sdp::kSend) { AddToMsection(mRids, sdp::kSend, ssrcGenerator, IsRtxEnabled(codecs), answer); } } void JsepTrack::SetRids(const std::vector& aRids) { MOZ_ASSERT(!aRids.empty()); if (!mRids.empty()) { return; } mRids = aRids; } void JsepTrack::SetMaxEncodings(size_t aMax) { mMaxEncodings = aMax; if (mRids.size() > mMaxEncodings) { mRids.resize(mMaxEncodings); } } void JsepTrack::RecvTrackSetRemote(const Sdp& aSdp, const SdpMediaSection& aMsection) { mInHaveRemote = true; MOZ_ASSERT(mDirection == sdp::kRecv); MOZ_ASSERT(aMsection.GetMediaType() != SdpMediaSection::MediaType::kApplication); std::string error; SdpHelper helper(&error); mRemoteSetSendBit = aMsection.IsSending(); if (!mRemoteSetSendBit) { mReceptive = false; } if (aMsection.IsSending()) { (void)helper.GetIdsFromMsid(aSdp, aMsection, &mStreamIds); } else { mStreamIds.clear(); } // We do this whether or not the track is active SetCNAME(helper.GetCNAME(aMsection)); mSsrcs.clear(); if (aMsection.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) { for (const auto& ssrcAttr : aMsection.GetAttributeList().GetSsrc().mSsrcs) { mSsrcs.push_back(ssrcAttr.ssrc); } } // Use FID ssrc-group to associate rtx ssrcs with "regular" ssrcs. Despite // not being part of RFC 4588, this is how rtx is negotiated by libwebrtc // and jitsi. mSsrcToRtxSsrc.clear(); if (aMsection.GetAttributeList().HasAttribute( SdpAttribute::kSsrcGroupAttribute)) { for (const auto& group : aMsection.GetAttributeList().GetSsrcGroup().mSsrcGroups) { if (group.semantics == SdpSsrcGroupAttributeList::kFid && group.ssrcs.size() == 2) { // Ensure we have a "regular" ssrc for each rtx ssrc. if (std::find(mSsrcs.begin(), mSsrcs.end(), group.ssrcs[0]) != mSsrcs.end()) { mSsrcToRtxSsrc[group.ssrcs[0]] = group.ssrcs[1]; // Remove rtx ssrcs from mSsrcs auto res = std::remove_if( mSsrcs.begin(), mSsrcs.end(), [group](uint32_t ssrc) { return ssrc == group.ssrcs[1]; }); mSsrcs.erase(res, mSsrcs.end()); } } } } } void JsepTrack::RecvTrackSetLocal(const SdpMediaSection& aMsection) { MOZ_ASSERT(mDirection == sdp::kRecv); // TODO: Should more stuff live in here? Anything that needs to happen when we // decide we're ready to receive packets should probably go in here. mReceptive = aMsection.IsReceiving(); } void JsepTrack::SendTrackSetRemote(SsrcGenerator& aSsrcGenerator, const SdpMediaSection& aRemoteMsection) { mInHaveRemote = true; if (mType == SdpMediaSection::kApplication) { return; } std::vector rids; // TODO: Current language in webrtc-pc is completely broken, and so I will // not be quoting it here. if ((mType == SdpMediaSection::kVideo) && aRemoteMsection.GetAttributeList().HasAttribute( SdpAttribute::kSimulcastAttribute)) { // Note: webrtc-pc does not appear to support the full IETF simulcast // spec. In particular, the IETF simulcast spec supports requesting // multiple different sets of encodings. For example, "a=simulcast:send // 1,2;3,4;5,6" means that there are three simulcast streams, the first of // which can use either rid 1 or 2 (but not both), the second of which can // use rid 3 or 4 (but not both), and the third of which can use rid 5 or // 6 (but not both). webrtc-pc does not support this either/or stuff for // rid; each simulcast stream gets exactly one rid. // Also, webrtc-pc does not support the '~' pause syntax at all // See https://github.com/w3c/webrtc-pc/issues/2769 GetRids(aRemoteMsection, sdp::kRecv, &rids); } if (mRids.empty()) { // Initial configuration for (const auto& ridAttr : rids) { // TODO: Spec might change, making a length > 16 invalid SDP. std::string dummy; if (SdpRidAttributeList::CheckRidValidity(ridAttr.id, &dummy) && ridAttr.id.size() <= SdpRidAttributeList::kMaxRidLength) { mRids.push_back(ridAttr.id); } } if (mRids.size() > mMaxEncodings) { mRids.resize(mMaxEncodings); } } else { // JSEP is allowed to remove or reorder rids. RTCRtpSender won't pay // attention to reordering. std::vector newRids; for (const auto& ridAttr : rids) { for (const auto& oldRid : mRids) { if (oldRid == ridAttr.id) { newRids.push_back(oldRid); break; } } } mRids = std::move(newRids); } if (mRids.empty()) { mRids.push_back(""); } UpdateSsrcs(aSsrcGenerator, mRids.size()); } void JsepTrack::AddToMsection( const std::vector>& codecs, SdpMediaSection* msection) const { MOZ_ASSERT(msection->GetMediaType() == mType); MOZ_ASSERT(!codecs.empty()); for (const auto& codec : codecs) { codec->AddToMediaSection(*msection); } if ((mDirection == sdp::kSend) && (mType != SdpMediaSection::kApplication) && msection->IsSending()) { if (mStreamIds.empty()) { msection->AddMsid("-", mTrackId); } else { for (const std::string& streamId : mStreamIds) { msection->AddMsid(streamId, mTrackId); } } } } void JsepTrack::UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings) { MOZ_ASSERT(mDirection == sdp::kSend); MOZ_ASSERT(mType != SdpMediaSection::kApplication); size_t numSsrcs = std::max(encodings, 1U); EnsureSsrcs(ssrcGenerator, numSsrcs); PruneSsrcs(numSsrcs); if (mNegotiatedDetails && mNegotiatedDetails->GetEncodingCount() > numSsrcs) { mNegotiatedDetails->TruncateEncodings(numSsrcs); } MOZ_ASSERT(!mSsrcs.empty()); } void JsepTrack::PruneSsrcs(size_t aNumSsrcs) { mSsrcs.resize(aNumSsrcs); // We might have duplicate entries in mSsrcs, so we need to resize first and // then remove ummatched rtx ssrcs. auto itor = mSsrcToRtxSsrc.begin(); while (itor != mSsrcToRtxSsrc.end()) { if (std::find(mSsrcs.begin(), mSsrcs.end(), itor->first) == mSsrcs.end()) { itor = mSsrcToRtxSsrc.erase(itor); } else { ++itor; } } } bool JsepTrack::IsRtxEnabled( const std::vector>& codecs) const { for (const auto& codec : codecs) { if (codec->Type() == SdpMediaSection::kVideo && static_cast(codec.get()) ->mRtxEnabled) { return true; } } return false; } void JsepTrack::AddToMsection(const std::vector& aRids, sdp::Direction direction, SsrcGenerator& ssrcGenerator, bool rtxEnabled, SdpMediaSection* msection) { if (aRids.size() > 1) { UniquePtr simulcast(new SdpSimulcastAttribute); UniquePtr ridAttrs(new SdpRidAttributeList); for (const std::string& rid : aRids) { SdpRidAttributeList::Rid ridAttr; ridAttr.id = rid; ridAttr.direction = direction; ridAttrs->mRids.push_back(ridAttr); SdpSimulcastAttribute::Version version; version.choices.push_back(SdpSimulcastAttribute::Encoding(rid, false)); if (direction == sdp::kSend) { simulcast->sendVersions.push_back(version); } else { simulcast->recvVersions.push_back(version); } } msection->GetAttributeList().SetAttribute(simulcast.release()); msection->GetAttributeList().SetAttribute(ridAttrs.release()); } bool requireRtxSsrcs = rtxEnabled && msection->IsSending(); if (mType != SdpMediaSection::kApplication && mDirection == sdp::kSend) { UpdateSsrcs(ssrcGenerator, aRids.size()); if (requireRtxSsrcs) { MOZ_ASSERT(mSsrcs.size() == mSsrcToRtxSsrc.size()); std::vector allSsrcs; UniquePtr group(new SdpSsrcGroupAttributeList); for (const auto& ssrc : mSsrcs) { const auto rtxSsrc = mSsrcToRtxSsrc[ssrc]; allSsrcs.push_back(ssrc); allSsrcs.push_back(rtxSsrc); group->PushEntry(SdpSsrcGroupAttributeList::kFid, {ssrc, rtxSsrc}); } msection->SetSsrcs(allSsrcs, mCNAME); msection->GetAttributeList().SetAttribute(group.release()); } else { msection->SetSsrcs(mSsrcs, mCNAME); } } } void JsepTrack::GetRids(const SdpMediaSection& msection, sdp::Direction direction, std::vector* rids) const { rids->clear(); if (!msection.GetAttributeList().HasAttribute( SdpAttribute::kSimulcastAttribute)) { return; } const SdpSimulcastAttribute& simulcast( msection.GetAttributeList().GetSimulcast()); const SdpSimulcastAttribute::Versions* versions = nullptr; switch (direction) { case sdp::kSend: versions = &simulcast.sendVersions; break; case sdp::kRecv: versions = &simulcast.recvVersions; break; } if (!versions->IsSet()) { return; } // RFC 8853 does not seem to forbid duplicate rids in a simulcast attribute. // So, while this is obviously silly, we should be prepared for it and // ignore those duplicate rids. std::set uniqueRids; for (const SdpSimulcastAttribute::Version& version : *versions) { if (!version.choices.empty() && !uniqueRids.count(version.choices[0].rid)) { // We validate that rids are present (and sane) elsewhere. rids->push_back(*msection.FindRid(version.choices[0].rid)); uniqueRids.insert(version.choices[0].rid); } } } void JsepTrack::CreateEncodings( const SdpMediaSection& remote, const std::vector>& negotiatedCodecs, JsepTrackNegotiatedDetails* negotiatedDetails) { negotiatedDetails->mTias = remote.GetBandwidth("TIAS"); webrtc::RtcpMode rtcpMode = webrtc::RtcpMode::kCompound; // rtcp-rsize (video only) if (remote.GetMediaType() == SdpMediaSection::kVideo && remote.GetAttributeList().HasAttribute( SdpAttribute::kRtcpRsizeAttribute)) { rtcpMode = webrtc::RtcpMode::kReducedSize; } negotiatedDetails->mRtpRtcpConf = RtpRtcpConfig(rtcpMode); // TODO add support for b=AS if TIAS is not set (bug 976521) if (mRids.empty()) { mRids.push_back(""); } size_t numEncodings = mRids.size(); // Drop SSRCs if fewer RIDs were offered than we have encodings if (mSsrcs.size() > numEncodings) { PruneSsrcs(numEncodings); } // For each stream make sure we have an encoding, and configure // that encoding appropriately. for (size_t i = 0; i < numEncodings; ++i) { UniquePtr encoding(new JsepTrackEncoding); if (mRids.size() > i) { encoding->mRid = mRids[i]; } for (const auto& codec : negotiatedCodecs) { encoding->AddCodec(*codec); } negotiatedDetails->mEncodings.push_back(std::move(encoding)); } } std::vector> JsepTrack::GetCodecClones() const { std::vector> clones; for (const auto& codec : mPrototypeCodecs) { clones.emplace_back(codec->Clone()); } return clones; } static bool CompareCodec(const UniquePtr& lhs, const UniquePtr& rhs) { return lhs->mStronglyPreferred && !rhs->mStronglyPreferred; } std::vector> JsepTrack::NegotiateCodecs( const SdpMediaSection& remote, bool remoteIsOffer, Maybe local) { std::vector> negotiatedCodecs; std::vector> newPrototypeCodecs; // Outer loop establishes the remote side's preference for (const std::string& fmt : remote.GetFormats()) { for (auto& codec : mPrototypeCodecs) { if (!codec || !codec->mEnabled || !codec->Matches(fmt, remote)) { continue; } // First codec of ours that matches. See if we can negotiate it. UniquePtr clone(codec->Clone()); if (clone->Negotiate(fmt, remote, remoteIsOffer, local)) { // If negotiation succeeded, remember the payload type the other side // used for reoffers. codec->mDefaultPt = fmt; // Remember whether we negotiated rtx and the associated pt for later. if (codec->Type() == SdpMediaSection::kVideo) { JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); JsepVideoCodecDescription* cloneVideoCodec = static_cast(clone.get()); bool useRtx = mRtxIsAllowed && Preferences::GetBool("media.peerconnection.video.use_rtx", false); videoCodec->mRtxEnabled = useRtx && cloneVideoCodec->mRtxEnabled; videoCodec->mRtxPayloadType = cloneVideoCodec->mRtxPayloadType; } // Moves the codec out of mPrototypeCodecs, leaving an empty // UniquePtr, so we don't use it again. Also causes successfully // negotiated codecs to be placed up front in the future. newPrototypeCodecs.emplace_back(std::move(codec)); negotiatedCodecs.emplace_back(std::move(clone)); break; } } } // newPrototypeCodecs contains just the negotiated stuff so far. Add the rest. for (auto& codec : mPrototypeCodecs) { if (codec) { newPrototypeCodecs.emplace_back(std::move(codec)); } } // Negotiated stuff is up front, so it will take precedence when ensuring // there are no duplicate payload types. EnsureNoDuplicatePayloadTypes(&newPrototypeCodecs); std::swap(newPrototypeCodecs, mPrototypeCodecs); // Find the (potential) red codec and ulpfec codec or telephone-event JsepVideoCodecDescription* red = nullptr; JsepVideoCodecDescription* ulpfec = nullptr; JsepAudioCodecDescription* dtmf = nullptr; // We can safely cast here since JsepTrack has a MediaType and only codecs // that match that MediaType (kAudio or kVideo) are added. for (auto& codec : negotiatedCodecs) { if (codec->mName == "red") { red = static_cast(codec.get()); } else if (codec->mName == "ulpfec") { ulpfec = static_cast(codec.get()); } else if (codec->mName == "telephone-event") { dtmf = static_cast(codec.get()); } } // if we have a red codec remove redundant encodings that don't exist if (red) { // Since we could have an externally specified redundant endcodings // list, we shouldn't simply rebuild the redundant encodings list // based on the current list of codecs. std::vector unnegotiatedEncodings; std::swap(unnegotiatedEncodings, red->mRedundantEncodings); for (auto redundantPt : unnegotiatedEncodings) { std::string pt = std::to_string(redundantPt); for (const auto& codec : negotiatedCodecs) { if (pt == codec->mDefaultPt) { red->mRedundantEncodings.push_back(redundantPt); break; } } } } // Video FEC is indicated by the existence of the red and ulpfec // codecs and not an attribute on the particular video codec (like in // a rtcpfb attr). If we see both red and ulpfec codecs, we enable FEC // on all the other codecs. if (red && ulpfec) { for (auto& codec : negotiatedCodecs) { if (codec->mName != "red" && codec->mName != "ulpfec") { JsepVideoCodecDescription* videoCodec = static_cast(codec.get()); videoCodec->EnableFec(red->mDefaultPt, ulpfec->mDefaultPt); } } } // Dtmf support is indicated by the existence of the telephone-event // codec, and not an attribute on the particular audio codec (like in a // rtcpfb attr). If we see the telephone-event codec, we enabled dtmf // support on all the other audio codecs. if (dtmf) { for (auto& codec : negotiatedCodecs) { JsepAudioCodecDescription* audioCodec = static_cast(codec.get()); audioCodec->mDtmfEnabled = true; } } // Make sure strongly preferred codecs are up front, overriding the remote // side's preference. std::stable_sort(negotiatedCodecs.begin(), negotiatedCodecs.end(), CompareCodec); if (!red) { // No red, remove ulpfec negotiatedCodecs.erase( std::remove_if(negotiatedCodecs.begin(), negotiatedCodecs.end(), [ulpfec](const UniquePtr& codec) { return codec.get() == ulpfec; }), negotiatedCodecs.end()); // Make sure there's no dangling ptr here ulpfec = nullptr; } return negotiatedCodecs; } nsresult JsepTrack::Negotiate(const SdpMediaSection& answer, const SdpMediaSection& remote, const SdpMediaSection& local) { std::vector> negotiatedCodecs = NegotiateCodecs(remote, &answer != &remote, SomeRef(local)); if (negotiatedCodecs.empty()) { return NS_ERROR_FAILURE; } UniquePtr negotiatedDetails = MakeUnique(); CreateEncodings(remote, negotiatedCodecs, negotiatedDetails.get()); if (answer.GetAttributeList().HasAttribute(SdpAttribute::kExtmapAttribute)) { for (auto& extmapAttr : answer.GetAttributeList().GetExtmap().mExtmaps) { SdpDirectionAttribute::Direction direction = extmapAttr.direction; if (&remote == &answer) { // Answer is remote, we need to flip this. direction = reverse(direction); } if (direction & mDirection) { negotiatedDetails->mExtmap[extmapAttr.extensionname] = extmapAttr; } } } mInHaveRemote = false; mNegotiatedDetails = std::move(negotiatedDetails); return NS_OK; } // When doing bundle, if all else fails we can try to figure out which m-line a // given RTP packet belongs to by looking at the payload type field. This only // works, however, if that payload type appeared in only one m-section. // We figure that out here. /* static */ void JsepTrack::SetUniquePayloadTypes(std::vector& tracks) { // Maps to track details if no other track contains the payload type, // otherwise maps to nullptr. std::map payloadTypeToDetailsMap; for (JsepTrack* track : tracks) { if (track->GetMediaType() == SdpMediaSection::kApplication) { continue; } auto* details = track->GetNegotiatedDetails(); if (!details) { // Can happen if negotiation fails on a track continue; } std::vector payloadTypesForTrack; track->GetNegotiatedPayloadTypes(&payloadTypesForTrack); for (uint16_t pt : payloadTypesForTrack) { if (payloadTypeToDetailsMap.count(pt)) { // Found in more than one track, not unique payloadTypeToDetailsMap[pt] = nullptr; } else { payloadTypeToDetailsMap[pt] = details; } } } for (auto ptAndDetails : payloadTypeToDetailsMap) { uint16_t uniquePt = ptAndDetails.first; MOZ_ASSERT(uniquePt <= UINT8_MAX); auto trackDetails = ptAndDetails.second; if (trackDetails) { trackDetails->mUniquePayloadTypes.push_back( static_cast(uniquePt)); } } } } // namespace mozilla