/* 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 "RTCRtpSender.h" #include "transport/logging.h" #include "mozilla/dom/MediaStreamTrack.h" #include "mozilla/dom/Promise.h" #include "mozilla/glean/GleanMetrics.h" #include "nsPIDOMWindow.h" #include "nsString.h" #include "mozilla/dom/VideoStreamTrack.h" #include "jsep/JsepTransceiver.h" #include "mozilla/dom/RTCRtpSenderBinding.h" #include "RTCStatsReport.h" #include "mozilla/Preferences.h" #include "RTCRtpTransceiver.h" #include "PeerConnectionImpl.h" #include "libwebrtcglue/AudioConduit.h" #include #include "call/call.h" namespace mozilla::dom { LazyLogModule gSenderLog("RTCRtpSender"); NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpSender) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(RTCRtpSender) // We do not do anything here, we wait for BreakCycles to be called NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(RTCRtpSender) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mPc, mSenderTrack, mTransceiver, mStreams, mDtmf) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCRtpSender) NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCRtpSender) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCRtpSender) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END #define INIT_CANONICAL(name, val) \ name(AbstractThread::MainThread(), val, "RTCRtpSender::" #name " (Canonical)") RTCRtpSender::RTCRtpSender(nsPIDOMWindowInner* aWindow, PeerConnectionImpl* aPc, MediaTransportHandler* aTransportHandler, AbstractThread* aCallThread, nsISerialEventTarget* aStsThread, MediaSessionConduit* aConduit, dom::MediaStreamTrack* aTrack, const Sequence& aEncodings, RTCRtpTransceiver* aTransceiver) : mWatchManager(this, AbstractThread::MainThread()), mWindow(aWindow), mPc(aPc), mSenderTrack(aTrack), mTransportHandler(aTransportHandler), mTransceiver(aTransceiver), INIT_CANONICAL(mSsrcs, Ssrcs()), INIT_CANONICAL(mVideoRtxSsrcs, Ssrcs()), INIT_CANONICAL(mLocalRtpExtensions, RtpExtList()), INIT_CANONICAL(mAudioCodec, Nothing()), INIT_CANONICAL(mVideoCodec, Nothing()), INIT_CANONICAL(mVideoRtpRtcpConfig, Nothing()), INIT_CANONICAL(mVideoCodecMode, webrtc::VideoCodecMode::kRealtimeVideo), INIT_CANONICAL(mCname, std::string()), INIT_CANONICAL(mTransmitting, false) { mPipeline = new MediaPipelineTransmit( mPc->GetHandle(), aTransportHandler, aCallThread, aStsThread, aConduit->type() == MediaSessionConduit::VIDEO, aConduit); mPipeline->InitControl(this); if (aConduit->type() == MediaSessionConduit::AUDIO) { mDtmf = new RTCDTMFSender(aWindow, mTransceiver); } mPipeline->SetTrack(mSenderTrack); mozilla::glean::rtcrtpsender::count.Add(1); if (mPc->ShouldAllowOldSetParameters()) { mAllowOldSetParameters = true; mozilla::glean::rtcrtpsender::count_setparameters_compat.Add(1); } if (aEncodings.Length()) { // This sender was created by addTransceiver with sendEncodings. mParameters.mEncodings = aEncodings; mSimulcastEnvelopeSet = true; mozilla::glean::rtcrtpsender::used_sendencodings.AddToNumerator(1); } else { // This sender was created by addTrack, sRD(offer), or addTransceiver // without sendEncodings. RTCRtpEncodingParameters defaultEncoding; defaultEncoding.mActive = true; if (aConduit->type() == MediaSessionConduit::VIDEO) { defaultEncoding.mScaleResolutionDownBy.Construct(1.0f); } Unused << mParameters.mEncodings.AppendElement(defaultEncoding, fallible); UpdateRestorableEncodings(mParameters.mEncodings); MaybeGetJsepRids(); } if (mDtmf) { mWatchManager.Watch(mTransmitting, &RTCRtpSender::UpdateDtmfSender); } } #undef INIT_CANONICAL RTCRtpSender::~RTCRtpSender() = default; JSObject* RTCRtpSender::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return RTCRtpSender_Binding::Wrap(aCx, this, aGivenProto); } RTCDtlsTransport* RTCRtpSender::GetTransport() const { if (!mTransceiver) { return nullptr; } return mTransceiver->GetDtlsTransport(); } RTCDTMFSender* RTCRtpSender::GetDtmf() const { return mDtmf; } already_AddRefed RTCRtpSender::GetStats(ErrorResult& aError) { RefPtr promise = MakePromise(aError); if (aError.Failed()) { return nullptr; } if (NS_WARN_IF(!mPipeline)) { // TODO(bug 1056433): When we stop nulling this out when the PC is closed // (or when the transceiver is stopped), we can remove this code. We // resolve instead of reject in order to make this eventual change in // behavior a little smaller. promise->MaybeResolve(new RTCStatsReport(mWindow)); return promise.forget(); } if (!mSenderTrack) { promise->MaybeResolve(new RTCStatsReport(mWindow)); return promise.forget(); } mTransceiver->ChainToDomPromiseWithCodecStats(GetStatsInternal(), promise); return promise.forget(); } nsTArray> RTCRtpSender::GetStatsInternal( bool aSkipIceStats) { MOZ_ASSERT(NS_IsMainThread()); nsTArray> promises(2); if (!mSenderTrack || !mPipeline) { return promises; } nsAutoString trackName; if (auto track = mPipeline->GetTrack()) { track->GetId(trackName); } { // Add bandwidth estimation stats promises.AppendElement(InvokeAsync( mPipeline->mCallThread, __func__, [conduit = mPipeline->mConduit, trackName]() mutable { auto report = MakeUnique(); Maybe stats = conduit->GetCallStats(); stats.apply([&](const auto aStats) { dom::RTCBandwidthEstimationInternal bw; bw.mTrackIdentifier = trackName; bw.mSendBandwidthBps.Construct(aStats.send_bandwidth_bps / 8); bw.mMaxPaddingBps.Construct(aStats.max_padding_bitrate_bps / 8); bw.mReceiveBandwidthBps.Construct(aStats.recv_bandwidth_bps / 8); bw.mPacerDelayMs.Construct(aStats.pacer_delay_ms); if (aStats.rtt_ms >= 0) { bw.mRttMs.Construct(aStats.rtt_ms); } if (!report->mBandwidthEstimations.AppendElement(std::move(bw), fallible)) { mozalloc_handle_oom(0); } }); return RTCStatsPromise::CreateAndResolve(std::move(report), __func__); })); } promises.AppendElement(InvokeAsync( mPipeline->mCallThread, __func__, [pipeline = mPipeline, trackName] { auto report = MakeUnique(); auto asAudio = pipeline->mConduit->AsAudioSessionConduit(); auto asVideo = pipeline->mConduit->AsVideoSessionConduit(); nsString kind = asVideo.isNothing() ? u"audio"_ns : u"video"_ns; nsString idstr = kind + u"_"_ns; idstr.AppendInt(static_cast(pipeline->Level())); for (uint32_t ssrc : pipeline->mConduit->GetLocalSSRCs()) { nsString localId = u"outbound_rtp_"_ns + idstr + u"_"_ns; localId.AppendInt(ssrc); nsString remoteId; Maybe base_seq = pipeline->mConduit->RtpSendBaseSeqFor(ssrc); auto constructCommonRemoteInboundRtpStats = [&](RTCRemoteInboundRtpStreamStats& aRemote, const webrtc::ReportBlockData& aRtcpData) { remoteId = u"outbound_rtcp_"_ns + idstr + u"_"_ns; remoteId.AppendInt(ssrc); aRemote.mTimestamp.Construct( RTCStatsTimestamp::FromNtp( pipeline->GetTimestampMaker(), webrtc::Timestamp::Micros( aRtcpData.report_block_timestamp_utc_us()) + webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970)) .ToDom()); aRemote.mId.Construct(remoteId); aRemote.mType.Construct(RTCStatsType::Remote_inbound_rtp); aRemote.mSsrc = ssrc; aRemote.mKind = kind; aRemote.mMediaType.Construct( kind); // mediaType is the old name for kind. aRemote.mLocalId.Construct(localId); if (base_seq) { if (aRtcpData.report_block() .extended_highest_sequence_number < *base_seq) { aRemote.mPacketsReceived.Construct(0); } else { aRemote.mPacketsReceived.Construct( aRtcpData.report_block() .extended_highest_sequence_number - aRtcpData.report_block().packets_lost - *base_seq + 1); } } }; auto constructCommonOutboundRtpStats = [&](RTCOutboundRtpStreamStats& aLocal) { aLocal.mSsrc = ssrc; aLocal.mTimestamp.Construct( pipeline->GetTimestampMaker().GetNow().ToDom()); aLocal.mId.Construct(localId); aLocal.mType.Construct(RTCStatsType::Outbound_rtp); aLocal.mKind = kind; aLocal.mMediaType.Construct( kind); // mediaType is the old name for kind. if (remoteId.Length()) { aLocal.mRemoteId.Construct(remoteId); } }; asAudio.apply([&](auto& aConduit) { Maybe audioStats = aConduit->GetSenderStats(); if (audioStats.isNothing()) { return; } if (audioStats->packets_sent == 0) { // By spec: "The lifetime of all RTP monitored objects starts // when the RTP stream is first used: When the first RTP packet // is sent or received on the SSRC it represents" return; } // First, fill in remote stat with rtcp receiver data, if present. // ReceiverReports have less information than SenderReports, so fill // in what we can. Maybe reportBlockData; { if (const auto remoteSsrc = aConduit->GetRemoteSSRC(); remoteSsrc) { for (auto& data : audioStats->report_block_datas) { if (data.report_block().source_ssrc == ssrc && data.report_block().sender_ssrc == *remoteSsrc) { reportBlockData.emplace(data); break; } } } } reportBlockData.apply([&](auto& aReportBlockData) { RTCRemoteInboundRtpStreamStats remote; constructCommonRemoteInboundRtpStats(remote, aReportBlockData); if (audioStats->jitter_ms >= 0) { remote.mJitter.Construct(audioStats->jitter_ms / 1000.0); } if (audioStats->packets_lost >= 0) { remote.mPacketsLost.Construct(audioStats->packets_lost); } if (audioStats->rtt_ms >= 0) { remote.mRoundTripTime.Construct( static_cast(audioStats->rtt_ms) / 1000.0); } remote.mFractionLost.Construct(audioStats->fraction_lost); remote.mTotalRoundTripTime.Construct( double(aReportBlockData.sum_rtt_ms()) / 1000); remote.mRoundTripTimeMeasurements.Construct( aReportBlockData.num_rtts()); if (!report->mRemoteInboundRtpStreamStats.AppendElement( std::move(remote), fallible)) { mozalloc_handle_oom(0); } }); // Then, fill in local side (with cross-link to remote only if // present) RTCOutboundRtpStreamStats local; constructCommonOutboundRtpStats(local); local.mPacketsSent.Construct(audioStats->packets_sent); local.mBytesSent.Construct(audioStats->payload_bytes_sent); local.mNackCount.Construct( audioStats->rtcp_packet_type_counts.nack_packets); local.mHeaderBytesSent.Construct( audioStats->header_and_padding_bytes_sent); local.mRetransmittedPacketsSent.Construct( audioStats->retransmitted_packets_sent); local.mRetransmittedBytesSent.Construct( audioStats->retransmitted_bytes_sent); /* * Potential new stats that are now available upstream. * Note: when we last tried exposing this we were getting * targetBitrate for audio was ending up as 0. We did not * investigate why. local.mTargetBitrate.Construct(audioStats->target_bitrate_bps); */ if (!report->mOutboundRtpStreamStats.AppendElement(std::move(local), fallible)) { mozalloc_handle_oom(0); } }); asVideo.apply([&](auto& aConduit) { Maybe videoStats = aConduit->GetSenderStats(); if (videoStats.isNothing()) { return; } Maybe streamStats; auto kv = videoStats->substreams.find(ssrc); if (kv != videoStats->substreams.end()) { streamStats = Some(kv->second); } if (!streamStats) { // By spec: "The lifetime of all RTP monitored objects starts // when the RTP stream is first used: When the first RTP packet // is sent or received on the SSRC it represents" return; } aConduit->GetAssociatedLocalRtxSSRC(ssrc).apply( [&](const auto rtxSsrc) { auto kv = videoStats->substreams.find(rtxSsrc); if (kv != videoStats->substreams.end()) { streamStats->rtp_stats.Add(kv->second.rtp_stats); } }); if (streamStats->rtp_stats.first_packet_time_ms == -1) { return; } // First, fill in remote stat with rtcp receiver data, if present. // ReceiverReports have less information than SenderReports, so fill // in what we can. if (streamStats->report_block_data) { const webrtc::ReportBlockData& rtcpReportData = *streamStats->report_block_data; RTCRemoteInboundRtpStreamStats remote; remote.mJitter.Construct( static_cast(rtcpReportData.report_block().jitter) / webrtc::kVideoPayloadTypeFrequency); remote.mPacketsLost.Construct( rtcpReportData.report_block().packets_lost); if (rtcpReportData.has_rtt()) { remote.mRoundTripTime.Construct( static_cast(rtcpReportData.last_rtt_ms()) / 1000.0); } constructCommonRemoteInboundRtpStats(remote, rtcpReportData); remote.mTotalRoundTripTime.Construct( streamStats->report_block_data->sum_rtt_ms() / 1000.0); remote.mFractionLost.Construct( static_cast( rtcpReportData.report_block().fraction_lost) / (1 << 8)); remote.mRoundTripTimeMeasurements.Construct( streamStats->report_block_data->num_rtts()); if (!report->mRemoteInboundRtpStreamStats.AppendElement( std::move(remote), fallible)) { mozalloc_handle_oom(0); } } // Then, fill in local side (with cross-link to remote only if // present) RTCOutboundRtpStreamStats local; constructCommonOutboundRtpStats(local); local.mPacketsSent.Construct( streamStats->rtp_stats.transmitted.packets); local.mBytesSent.Construct( streamStats->rtp_stats.transmitted.payload_bytes); local.mNackCount.Construct( streamStats->rtcp_packet_type_counts.nack_packets); local.mFirCount.Construct( streamStats->rtcp_packet_type_counts.fir_packets); local.mPliCount.Construct( streamStats->rtcp_packet_type_counts.pli_packets); local.mFramesEncoded.Construct(streamStats->frames_encoded); if (streamStats->qp_sum) { local.mQpSum.Construct(*streamStats->qp_sum); } local.mHeaderBytesSent.Construct( streamStats->rtp_stats.transmitted.header_bytes + streamStats->rtp_stats.transmitted.padding_bytes); local.mRetransmittedPacketsSent.Construct( streamStats->rtp_stats.retransmitted.packets); local.mRetransmittedBytesSent.Construct( streamStats->rtp_stats.retransmitted.payload_bytes); local.mTotalEncodedBytesTarget.Construct( videoStats->total_encoded_bytes_target); local.mFrameWidth.Construct(streamStats->width); local.mFrameHeight.Construct(streamStats->height); local.mFramesSent.Construct(streamStats->frames_encoded); local.mHugeFramesSent.Construct(streamStats->huge_frames_sent); local.mTotalEncodeTime.Construct( double(streamStats->total_encode_time_ms) / 1000.); /* * Potential new stats that are now available upstream. local.mTargetBitrate.Construct(videoStats->target_media_bitrate_bps); */ if (!report->mOutboundRtpStreamStats.AppendElement(std::move(local), fallible)) { mozalloc_handle_oom(0); } }); } auto constructCommonMediaSourceStats = [&](RTCMediaSourceStats& aStats) { nsString id = u"mediasource_"_ns + idstr + trackName; aStats.mTimestamp.Construct( pipeline->GetTimestampMaker().GetNow().ToDom()); aStats.mId.Construct(id); aStats.mType.Construct(RTCStatsType::Media_source); aStats.mTrackIdentifier = trackName; aStats.mKind = kind; }; // TODO(bug 1804678): Use RTCAudioSourceStats/RTCVideoSourceStats RTCMediaSourceStats mediaSourceStats; constructCommonMediaSourceStats(mediaSourceStats); if (!report->mMediaSourceStats.AppendElement( std::move(mediaSourceStats), fallible)) { mozalloc_handle_oom(0); } return RTCStatsPromise::CreateAndResolve(std::move(report), __func__); })); if (!aSkipIceStats && GetJsepTransceiver().mTransport.mComponents) { promises.AppendElement(mTransportHandler->GetIceStats( GetJsepTransceiver().mTransport.mTransportId, mPipeline->GetTimestampMaker().GetNow().ToDom())); } return promises; } void RTCRtpSender::GetCapabilities(const GlobalObject&, const nsAString& aKind, Nullable& aResult) { PeerConnectionImpl::GetCapabilities(aKind, aResult, sdp::Direction::kSend); } void RTCRtpSender::WarnAboutBadSetParameters(const nsCString& aError) { nsCString warning( "WARNING! Invalid setParameters call detected! The good news? Firefox " "supports sendEncodings in addTransceiver now, so we ask that you switch " "over to using the parameters code you use for other browsers. Thank you " "for your patience and support. The specific error was: "); warning += aError; mPc->SendWarningToConsole(warning); } nsCString RTCRtpSender::GetEffectiveTLDPlus1() const { return mPc->GetEffectiveTLDPlus1(); } already_AddRefed RTCRtpSender::SetParameters( const dom::RTCRtpSendParameters& aParameters, ErrorResult& aError) { dom::RTCRtpSendParameters paramsCopy(aParameters); // When the setParameters method is called, the user agent MUST run the // following steps: // Let parameters be the method's first argument. // Let sender be the RTCRtpSender object on which setParameters is invoked. // Let transceiver be the RTCRtpTransceiver object associated with sender // (i.e.sender is transceiver.[[Sender]]). RefPtr p = MakePromise(aError); if (aError.Failed()) { return nullptr; } if (mPc->IsClosed()) { p->MaybeRejectWithInvalidStateError("Peer connection is closed"); return p.forget(); } // If transceiver.[[Stopped]] is true, return a promise rejected with a newly // created InvalidStateError. if (mTransceiver->Stopped()) { p->MaybeRejectWithInvalidStateError("This sender's transceiver is stopped"); return p.forget(); } // If sender.[[LastReturnedParameters]] is null, return a promise rejected // with a newly created InvalidStateError. if (!mLastReturnedParameters.isSome()) { nsCString error( "Cannot call setParameters without first calling getParameters"); if (mAllowOldSetParameters) { if (!mHaveWarnedBecauseNoGetParameters) { mHaveWarnedBecauseNoGetParameters = true; mozilla::glean::rtcrtpsender_setparameters::warn_no_getparameters .AddToNumerator(1); #ifdef EARLY_BETA_OR_EARLIER mozilla::glean::rtcrtpsender_setparameters::blame_no_getparameters .Get(GetEffectiveTLDPlus1()) .Add(1); #endif } WarnAboutBadSetParameters(error); } else { if (!mHaveFailedBecauseNoGetParameters) { mHaveFailedBecauseNoGetParameters = true; mozilla::glean::rtcrtpsender_setparameters::fail_no_getparameters .AddToNumerator(1); } p->MaybeRejectWithInvalidStateError(error); return p.forget(); } } // According to the spec, our consistency checking is based on // [[LastReturnedParameters]], but if we're letting // [[LastReturnedParameters]]==null slide, we still want to do // consistency checking on _something_ so we can warn implementers if they // are messing that up also. Just find something, _anything_, to do that // checking with. // TODO(bug 1803388): Remove this stuff once it is no longer needed. // TODO(bug 1803389): Remove the glean errors once they are no longer needed. Maybe oldParams; if (mAllowOldSetParameters) { if (mLastReturnedParameters.isSome()) { oldParams = mLastReturnedParameters; } else if (mPendingParameters.isSome()) { oldParams = mPendingParameters; } else { oldParams = Some(mParameters); } MOZ_ASSERT(oldParams.isSome()); } else { oldParams = mLastReturnedParameters; } MOZ_ASSERT(oldParams.isSome()); // Validate parameters by running the following steps: // Let encodings be parameters.encodings. // Let codecs be parameters.codecs. // Let N be the number of RTCRtpEncodingParameters stored in // sender.[[SendEncodings]]. // If any of the following conditions are met, // return a promise rejected with a newly created InvalidModificationError: bool pendingRidChangeFromCompatMode = false; // encodings.length is different from N. if (paramsCopy.mEncodings.Length() != oldParams->mEncodings.Length()) { nsCString error("Cannot change the number of encodings with setParameters"); if (!mAllowOldSetParameters) { if (!mHaveFailedBecauseEncodingCountChange) { mHaveFailedBecauseEncodingCountChange = true; mozilla::glean::rtcrtpsender_setparameters::fail_length_changed .AddToNumerator(1); } p->MaybeRejectWithInvalidModificationError(error); return p.forget(); } // Make sure we don't use the old rids in SyncToJsep while we wait for the // queued task below to update mParameters. pendingRidChangeFromCompatMode = true; mSimulcastEnvelopeSet = true; if (!mHaveWarnedBecauseEncodingCountChange) { mHaveWarnedBecauseEncodingCountChange = true; mozilla::glean::rtcrtpsender_setparameters::warn_length_changed .AddToNumerator(1); #ifdef EARLY_BETA_OR_EARLIER mozilla::glean::rtcrtpsender_setparameters::blame_length_changed .Get(GetEffectiveTLDPlus1()) .Add(1); #endif } WarnAboutBadSetParameters(error); } else { // encodings has been re-ordered. for (size_t i = 0; i < paramsCopy.mEncodings.Length(); ++i) { const auto& oldEncoding = oldParams->mEncodings[i]; const auto& newEncoding = paramsCopy.mEncodings[i]; if (oldEncoding.mRid != newEncoding.mRid) { nsCString error("Cannot change rid, or reorder encodings"); if (!mHaveFailedBecauseRidChange) { mHaveFailedBecauseRidChange = true; mozilla::glean::rtcrtpsender_setparameters::fail_rid_changed .AddToNumerator(1); } p->MaybeRejectWithInvalidModificationError(error); return p.forget(); } } } // TODO(bug 1803388): Handle this in webidl, once we stop allowing the old // setParameters style. if (!paramsCopy.mTransactionId.WasPassed()) { nsCString error("transactionId is not set!"); if (!mAllowOldSetParameters) { if (!mHaveFailedBecauseNoTransactionId) { mHaveFailedBecauseNoTransactionId = true; mozilla::glean::rtcrtpsender_setparameters::fail_no_transactionid .AddToNumerator(1); } p->MaybeRejectWithTypeError(error); return p.forget(); } if (!mHaveWarnedBecauseNoTransactionId) { mHaveWarnedBecauseNoTransactionId = true; mozilla::glean::rtcrtpsender_setparameters::warn_no_transactionid .AddToNumerator(1); #ifdef EARLY_BETA_OR_EARLIER mozilla::glean::rtcrtpsender_setparameters::blame_no_transactionid .Get(GetEffectiveTLDPlus1()) .Add(1); #endif } WarnAboutBadSetParameters(error); } else if (oldParams->mTransactionId != paramsCopy.mTransactionId) { // Any parameter in parameters is marked as a Read-only parameter (such as // RID) and has a value that is different from the corresponding parameter // value in sender.[[LastReturnedParameters]]. Note that this also applies // to transactionId. nsCString error( "Cannot change transaction id: call getParameters, modify the result, " "and then call setParameters"); if (!mAllowOldSetParameters) { if (!mHaveFailedBecauseStaleTransactionId) { mHaveFailedBecauseStaleTransactionId = true; mozilla::glean::rtcrtpsender_setparameters::fail_stale_transactionid .AddToNumerator(1); } p->MaybeRejectWithInvalidModificationError(error); return p.forget(); } if (!mHaveWarnedBecauseStaleTransactionId) { mHaveWarnedBecauseStaleTransactionId = true; mozilla::glean::rtcrtpsender_setparameters::warn_stale_transactionid .AddToNumerator(1); #ifdef EARLY_BETA_OR_EARLIER mozilla::glean::rtcrtpsender_setparameters::blame_stale_transactionid .Get(GetEffectiveTLDPlus1()) .Add(1); #endif } WarnAboutBadSetParameters(error); } // This could conceivably happen if we are allowing the old setParameters // behavior. if (!paramsCopy.mEncodings.Length()) { nsCString error("Cannot set an empty encodings array"); if (!mAllowOldSetParameters) { if (!mHaveFailedBecauseNoEncodings) { mHaveFailedBecauseNoEncodings = true; mozilla::glean::rtcrtpsender_setparameters::fail_no_encodings .AddToNumerator(1); } p->MaybeRejectWithInvalidModificationError(error); return p.forget(); } // TODO: Add some warning telemetry here WarnAboutBadSetParameters(error); // Just don't do this; it's stupid. paramsCopy.mEncodings = oldParams->mEncodings; } // TODO: Verify remaining read-only parameters // headerExtensions (bug 1765851) // rtcp (bug 1765852) // codecs (bug 1534687) // CheckAndRectifyEncodings handles the following steps: // If transceiver kind is "audio", remove the scaleResolutionDownBy member // from all encodings that contain one. // // If transceiver kind is "video", and any encoding in encodings contains a // scaleResolutionDownBy member whose value is less than 1.0, return a // promise rejected with a newly created RangeError. // // Verify that each encoding in encodings has a maxFramerate member whose // value is greater than or equal to 0.0. If one of the maxFramerate values // does not meet this requirement, return a promise rejected with a newly // created RangeError. ErrorResult rv; CheckAndRectifyEncodings(paramsCopy.mEncodings, mTransceiver->IsVideo(), rv); if (rv.Failed()) { if (!mHaveFailedBecauseOtherError) { mHaveFailedBecauseOtherError = true; mozilla::glean::rtcrtpsender_setparameters::fail_other.AddToNumerator(1); } p->MaybeReject(std::move(rv)); return p.forget(); } // If transceiver kind is "video", then for each encoding in encodings that // doesn't contain a scaleResolutionDownBy member, add a // scaleResolutionDownBy member with the value 1.0. if (mTransceiver->IsVideo()) { for (auto& encoding : paramsCopy.mEncodings) { if (!encoding.mScaleResolutionDownBy.WasPassed()) { encoding.mScaleResolutionDownBy.Construct(1.0); } } } // Let p be a new promise. (see above) // In parallel, configure the media stack to use parameters to transmit // sender.[[SenderTrack]]. // Right now this is infallible. That may change someday. // We need to put this in a member variable, since MaybeUpdateConduit needs it // This also allows PeerConnectionImpl to detect when there is a pending // setParameters, which has implcations for the handling of // setRemoteDescription. mPendingRidChangeFromCompatMode = pendingRidChangeFromCompatMode; mPendingParameters = Some(paramsCopy); uint32_t serialNumber = ++mNumSetParametersCalls; MaybeUpdateConduit(); // If the media stack is successfully configured with parameters, // queue a task to run the following steps: GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( __func__, [this, self = RefPtr(this), p, paramsCopy, serialNumber] { // Set sender.[[LastReturnedParameters]] to null. mLastReturnedParameters = Nothing(); // Set sender.[[SendEncodings]] to parameters.encodings. mParameters = paramsCopy; UpdateRestorableEncodings(mParameters.mEncodings); // Only clear mPendingParameters if it matches; there could have been // back-to-back calls to setParameters, and we only want to clear this // if no subsequent setParameters is pending. if (serialNumber == mNumSetParametersCalls) { mPendingParameters = Nothing(); // Ok, nothing has called SyncToJsep while this async task was // pending. No need for special handling anymore. mPendingRidChangeFromCompatMode = false; } MOZ_ASSERT(mParameters.mEncodings.Length()); // Resolve p with undefined. p->MaybeResolveWithUndefined(); })); // Return p. return p.forget(); } // static void RTCRtpSender::CheckAndRectifyEncodings( Sequence& aEncodings, bool aVideo, ErrorResult& aRv) { // If any encoding contains a rid member whose value does not conform to the // grammar requirements specified in Section 10 of [RFC8851], throw a // TypeError. for (const auto& encoding : aEncodings) { if (encoding.mRid.WasPassed()) { std::string utf8Rid = NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get(); std::string error; if (!SdpRidAttributeList::CheckRidValidity(utf8Rid, &error)) { aRv.ThrowTypeError(nsCString(error)); return; } if (utf8Rid.size() > SdpRidAttributeList::kMaxRidLength) { std::ostringstream ss; ss << "Rid can be at most " << SdpRidAttributeList::kMaxRidLength << " characters long (due to internal limitations)"; aRv.ThrowTypeError(nsCString(ss.str())); return; } } } if (aEncodings.Length() > 1) { // If some but not all encodings contain a rid member, throw a TypeError. // rid must be set if there is more than one encoding // NOTE: Since rid is read-only, and the number of encodings cannot grow, // this should never happen in setParameters. for (const auto& encoding : aEncodings) { if (!encoding.mRid.WasPassed()) { aRv.ThrowTypeError("Missing rid"); return; } } // If any encoding contains a rid member whose value is the same as that of // a rid contained in another encoding in sendEncodings, throw a TypeError. // NOTE: Since rid is read-only, and the number of encodings cannot grow, // this should never happen in setParameters. std::set uniqueRids; for (const auto& encoding : aEncodings) { if (uniqueRids.count(encoding.mRid.Value())) { aRv.ThrowTypeError("Duplicate rid"); return; } uniqueRids.insert(encoding.mRid.Value()); } } // TODO: ptime/adaptivePtime validation (bug 1733647) // If kind is "audio", remove the scaleResolutionDownBy member from all // encodings that contain one. if (!aVideo) { for (auto& encoding : aEncodings) { if (encoding.mScaleResolutionDownBy.WasPassed()) { encoding.mScaleResolutionDownBy.Reset(); } if (encoding.mMaxFramerate.WasPassed()) { encoding.mMaxFramerate.Reset(); } } } // If any encoding contains a scaleResolutionDownBy member whose value is // less than 1.0, throw a RangeError. for (const auto& encoding : aEncodings) { if (encoding.mScaleResolutionDownBy.WasPassed()) { if (encoding.mScaleResolutionDownBy.Value() < 1.0f) { aRv.ThrowRangeError("scaleResolutionDownBy must be >= 1.0"); return; } } } // Verify that the value of each maxFramerate member in sendEncodings that is // defined is greater than 0.0. If one of the maxFramerate values does not // meet this requirement, throw a RangeError. for (const auto& encoding : aEncodings) { if (encoding.mMaxFramerate.WasPassed()) { if (encoding.mMaxFramerate.Value() < 0.0f) { aRv.ThrowRangeError("maxFramerate must be non-negative"); return; } } } } void RTCRtpSender::GetParameters(RTCRtpSendParameters& aParameters) { MOZ_ASSERT(mParameters.mEncodings.Length()); // If sender.[[LastReturnedParameters]] is not null, return // sender.[[LastReturnedParameters]], and abort these steps. if (mLastReturnedParameters.isSome()) { aParameters = *mLastReturnedParameters; return; } // Let result be a new RTCRtpSendParameters dictionary constructed as follows: // transactionId is set to a new unique identifier aParameters.mTransactionId.Construct(mPc->GenerateUUID()); // encodings is set to the value of the [[SendEncodings]] internal slot. aParameters.mEncodings = mParameters.mEncodings; // The headerExtensions sequence is populated based on the header extensions // that have been negotiated for sending // TODO(bug 1765851): We do not support this yet // aParameters.mHeaderExtensions.Construct(); // codecs is set to the value of the [[SendCodecs]] internal slot // TODO(bug 1534687): We do not support this yet // rtcp.cname is set to the CNAME of the associated RTCPeerConnection. // rtcp.reducedSize is set to true if reduced-size RTCP has been negotiated // for sending, and false otherwise. // TODO(bug 1765852): We do not support this yet aParameters.mRtcp.Construct(); aParameters.mRtcp.Value().mCname.Construct(); aParameters.mRtcp.Value().mReducedSize.Construct(false); aParameters.mHeaderExtensions.Construct(); aParameters.mCodecs.Construct(); // Set sender.[[LastReturnedParameters]] to result. mLastReturnedParameters = Some(aParameters); // Queue a task that sets sender.[[LastReturnedParameters]] to null. GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( __func__, [this, self = RefPtr(this)] { mLastReturnedParameters = Nothing(); })); } bool operator==(const RTCRtpEncodingParameters& a1, const RTCRtpEncodingParameters& a2) { // webidl does not generate types that are equality comparable return a1.mActive == a2.mActive && a1.mFec == a2.mFec && a1.mMaxBitrate == a2.mMaxBitrate && a1.mMaxFramerate == a2.mMaxFramerate && a1.mPriority == a2.mPriority && a1.mRid == a2.mRid && a1.mRtx == a2.mRtx && a1.mScaleResolutionDownBy == a2.mScaleResolutionDownBy && a1.mSsrc == a2.mSsrc; } // static void RTCRtpSender::ApplyJsEncodingToConduitEncoding( const RTCRtpEncodingParameters& aJsEncoding, VideoCodecConfig::Encoding* aConduitEncoding) { aConduitEncoding->active = aJsEncoding.mActive; if (aJsEncoding.mMaxBitrate.WasPassed()) { aConduitEncoding->constraints.maxBr = aJsEncoding.mMaxBitrate.Value(); } if (aJsEncoding.mMaxFramerate.WasPassed()) { aConduitEncoding->constraints.maxFps = Some(aJsEncoding.mMaxFramerate.Value()); } if (aJsEncoding.mScaleResolutionDownBy.WasPassed()) { // Optional does not have a valueOr, despite being based on Maybe // :( aConduitEncoding->constraints.scaleDownBy = aJsEncoding.mScaleResolutionDownBy.Value(); } else { aConduitEncoding->constraints.scaleDownBy = 1.0f; } } void RTCRtpSender::UpdateRestorableEncodings( const Sequence& aEncodings) { MOZ_ASSERT(aEncodings.Length()); if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()) { // Once initial negotiation completes, we are no longer allowed to restore // the unicast encoding. mUnicastEncoding.reset(); } else if (mParameters.mEncodings.Length() == 1 && !mParameters.mEncodings[0].mRid.WasPassed()) { // If we have not completed the initial negotiation, and we currently are // ridless unicast, we need to save our unicast encoding in case a // rollback occurs. mUnicastEncoding = Some(mParameters.mEncodings[0]); } } Sequence RTCRtpSender::ToSendEncodings( const std::vector& aRids) const { MOZ_ASSERT(!aRids.empty()); Sequence result; // If sendEncodings is given as input to this algorithm, and is non-empty, // set the [[SendEncodings]] slot to sendEncodings. for (const auto& rid : aRids) { MOZ_ASSERT(!rid.empty()); RTCRtpEncodingParameters encoding; encoding.mActive = true; encoding.mRid.Construct(NS_ConvertUTF8toUTF16(rid.c_str())); Unused << result.AppendElement(encoding, fallible); } // If sendEncodings is non-empty, set each encoding's scaleResolutionDownBy // to 2^(length of sendEncodings - encoding index - 1). if (mTransceiver->IsVideo()) { double scale = 1.0f; for (auto it = result.rbegin(); it != result.rend(); ++it) { it->mScaleResolutionDownBy.Construct(scale); scale *= 2; } } return result; } void RTCRtpSender::MaybeGetJsepRids() { MOZ_ASSERT(!mSimulcastEnvelopeSet); MOZ_ASSERT(mParameters.mEncodings.Length()); auto jsepRids = GetJsepTransceiver().mSendTrack.GetRids(); if (!jsepRids.empty()) { UpdateRestorableEncodings(mParameters.mEncodings); if (jsepRids.size() != 1 || !jsepRids[0].empty()) { // JSEP is using at least one rid. Stomp our single ridless encoding mParameters.mEncodings = ToSendEncodings(jsepRids); } mSimulcastEnvelopeSet = true; mSimulcastEnvelopeSetByJSEP = true; } } Sequence RTCRtpSender::GetMatchingEncodings( const std::vector& aRids) const { Sequence result; if (!aRids.empty() && !aRids[0].empty()) { // Simulcast, or unicast with rid for (const auto& encoding : mParameters.mEncodings) { for (const auto& rid : aRids) { auto utf16Rid = NS_ConvertUTF8toUTF16(rid.c_str()); if (!encoding.mRid.WasPassed() || (utf16Rid == encoding.mRid.Value())) { auto encodingCopy(encoding); if (!encodingCopy.mRid.WasPassed()) { encodingCopy.mRid.Construct(NS_ConvertUTF8toUTF16(rid.c_str())); } Unused << result.AppendElement(encodingCopy, fallible); break; } } } } // If we're allowing the old setParameters behavior, we _might_ be able to // get into this situation even if there were rids above. Be extra careful. // Under normal circumstances, this just handles the ridless case. if (!result.Length()) { // Unicast with no specified rid. Restore mUnicastEncoding, if // it exists, otherwise pick the first encoding. if (mUnicastEncoding.isSome()) { Unused << result.AppendElement(*mUnicastEncoding, fallible); } else { Unused << result.AppendElement(mParameters.mEncodings[0], fallible); } } return result; } void RTCRtpSender::SetStreams( const Sequence>& aStreams, ErrorResult& aRv) { if (mPc->IsClosed()) { aRv.ThrowInvalidStateError( "Cannot call setStreams if the peer connection is closed"); return; } SetStreamsImpl(aStreams); mPc->UpdateNegotiationNeeded(); } void RTCRtpSender::SetStreamsImpl( const Sequence>& aStreams) { mStreams.Clear(); std::set ids; for (const auto& stream : aStreams) { nsString id; stream->GetId(id); if (!ids.count(id)) { ids.insert(id); mStreams.AppendElement(stream); } } } void RTCRtpSender::GetStreams(nsTArray>& aStreams) { aStreams = mStreams.Clone(); } class ReplaceTrackOperation final : public PeerConnectionImpl::Operation { public: ReplaceTrackOperation(PeerConnectionImpl* aPc, const RefPtr& aTransceiver, const RefPtr& aTrack, ErrorResult& aError); NS_DECL_ISUPPORTS_INHERITED NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation) private: MOZ_CAN_RUN_SCRIPT RefPtr CallImpl(ErrorResult& aError) override; ~ReplaceTrackOperation() = default; RefPtr mTransceiver; RefPtr mNewTrack; }; NS_IMPL_CYCLE_COLLECTION_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation, mTransceiver, mNewTrack) NS_IMPL_ADDREF_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation) NS_IMPL_RELEASE_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReplaceTrackOperation) NS_INTERFACE_MAP_END_INHERITING(PeerConnectionImpl::Operation) ReplaceTrackOperation::ReplaceTrackOperation( PeerConnectionImpl* aPc, const RefPtr& aTransceiver, const RefPtr& aTrack, ErrorResult& aError) : PeerConnectionImpl::Operation(aPc, aError), mTransceiver(aTransceiver), mNewTrack(aTrack) {} RefPtr ReplaceTrackOperation::CallImpl(ErrorResult& aError) { RefPtr sender = mTransceiver->Sender(); // If transceiver.[[Stopped]] is true, return a promise rejected with a newly // created InvalidStateError. if (mTransceiver->Stopped()) { RefPtr error = sender->MakePromise(aError); if (aError.Failed()) { return nullptr; } MOZ_LOG(gSenderLog, LogLevel::Debug, ("%s Cannot call replaceTrack when transceiver is stopped", __FUNCTION__)); error->MaybeRejectWithInvalidStateError( "Cannot call replaceTrack when transceiver is stopped"); return error; } // Let p be a new promise. RefPtr p = sender->MakePromise(aError); if (aError.Failed()) { return nullptr; } if (!sender->SeamlessTrackSwitch(mNewTrack)) { MOZ_LOG(gSenderLog, LogLevel::Info, ("%s Could not seamlessly replace track", __FUNCTION__)); p->MaybeRejectWithInvalidModificationError( "Could not seamlessly replace track"); return p; } // Queue a task that runs the following steps: GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( __func__, [p, sender, track = mNewTrack]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { // If connection.[[IsClosed]] is true, abort these steps. // Set sender.[[SenderTrack]] to withTrack. if (sender->SetSenderTrackWithClosedCheck(track)) { // Resolve p with undefined. p->MaybeResolveWithUndefined(); } })); // Return p. return p; } already_AddRefed RTCRtpSender::ReplaceTrack( dom::MediaStreamTrack* aWithTrack, ErrorResult& aError) { // If withTrack is non-null and withTrack.kind differs from the transceiver // kind of transceiver, return a promise rejected with a newly created // TypeError. if (aWithTrack) { nsString newKind; aWithTrack->GetKind(newKind); nsString oldKind; mTransceiver->GetKind(oldKind); if (newKind != oldKind) { RefPtr error = MakePromise(aError); if (aError.Failed()) { return nullptr; } error->MaybeRejectWithTypeError( "Cannot replaceTrack with a different kind!"); return error.forget(); } } MOZ_LOG(gSenderLog, LogLevel::Debug, ("%s[%s]: %s (%p to %p)", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__, mSenderTrack.get(), aWithTrack)); // Return the result of chaining the following steps to connection's // operations chain: RefPtr op = new ReplaceTrackOperation(mPc, mTransceiver, aWithTrack, aError); if (aError.Failed()) { return nullptr; } // Static analysis forces us to use a temporary. auto pc = mPc; return pc->Chain(op, aError); } nsPIDOMWindowInner* RTCRtpSender::GetParentObject() const { return mWindow; } already_AddRefed RTCRtpSender::MakePromise( ErrorResult& aError) const { return mPc->MakePromise(aError); } bool RTCRtpSender::SeamlessTrackSwitch( const RefPtr& aWithTrack) { // We do not actually update mSenderTrack here! Spec says that happens in a // queued task after this is done (this happens in // SetSenderTrackWithClosedCheck). mPipeline->SetTrack(aWithTrack); MaybeUpdateConduit(); // There may eventually be cases where a renegotiation is necessary to switch. return true; } void RTCRtpSender::SetTrack(const RefPtr& aTrack) { // Used for RTCPeerConnection.removeTrack and RTCPeerConnection.addTrack mSenderTrack = aTrack; SeamlessTrackSwitch(aTrack); if (aTrack) { // RFC says (in the section on remote rollback): // However, an RtpTransceiver MUST NOT be removed if a track was attached // to the RtpTransceiver via the addTrack method. mAddTrackCalled = true; } } bool RTCRtpSender::SetSenderTrackWithClosedCheck( const RefPtr& aTrack) { if (!mPc->IsClosed()) { mSenderTrack = aTrack; return true; } return false; } void RTCRtpSender::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); mWatchManager.Shutdown(); mPipeline->Shutdown(); mPipeline = nullptr; } void RTCRtpSender::BreakCycles() { mWindow = nullptr; mPc = nullptr; mSenderTrack = nullptr; mTransceiver = nullptr; mStreams.Clear(); mDtmf = nullptr; } void RTCRtpSender::UpdateTransport() { MOZ_ASSERT(NS_IsMainThread()); if (!mHaveSetupTransport) { mPipeline->SetLevel(GetJsepTransceiver().GetLevel()); mHaveSetupTransport = true; } mPipeline->UpdateTransport_m(GetJsepTransceiver().mTransport.mTransportId, nullptr); } void RTCRtpSender::MaybeUpdateConduit() { // NOTE(pkerr) - the Call API requires the both local_ssrc and remote_ssrc be // set to a non-zero value or the CreateVideo...Stream call will fail. if (NS_WARN_IF(GetJsepTransceiver().mSendTrack.GetSsrcs().empty())) { MOZ_ASSERT( false, "No local ssrcs! This is a bug in the jsep engine, and should never " "happen!"); return; } if (!mPipeline) { return; } bool wasTransmitting = mTransmitting; if (mPipeline->mConduit->type() == MediaSessionConduit::VIDEO) { Maybe newConfig = GetNewVideoConfig(); if (newConfig.isSome()) { ApplyVideoConfig(*newConfig); } } else { Maybe newConfig = GetNewAudioConfig(); if (newConfig.isSome()) { ApplyAudioConfig(*newConfig); } } if (!mSenderTrack && !wasTransmitting && mTransmitting) { MOZ_LOG(gSenderLog, LogLevel::Debug, ("%s[%s]: %s Starting transmit conduit without send track!", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__)); } } void RTCRtpSender::SyncFromJsep(const JsepTransceiver& aJsepTransceiver) { if (!mSimulcastEnvelopeSet) { // JSEP is establishing the simulcast envelope for the first time, right now // This is the addTrack (or addTransceiver without sendEncodings) case. MaybeGetJsepRids(); } else if (!aJsepTransceiver.mSendTrack.GetNegotiatedDetails() || !aJsepTransceiver.mSendTrack.IsInHaveRemote()) { // Spec says that we do not update our encodings until we're in stable, // _unless_ this is the first negotiation. std::vector rids = aJsepTransceiver.mSendTrack.GetRids(); if (mSimulcastEnvelopeSetByJSEP && rids.empty()) { // JSEP previously set the simulcast envelope, but now it has no opinion // regarding unicast/simulcast. This can only happen on rollback of the // initial remote offer. mParameters.mEncodings = GetMatchingEncodings(rids); MOZ_ASSERT(mParameters.mEncodings.Length()); mSimulcastEnvelopeSetByJSEP = false; mSimulcastEnvelopeSet = false; } else if (!rids.empty()) { // JSEP has an opinion on the simulcast envelope, which trumps anything // we have already. mParameters.mEncodings = GetMatchingEncodings(rids); MOZ_ASSERT(mParameters.mEncodings.Length()); } } MaybeUpdateConduit(); } void RTCRtpSender::SyncToJsep(JsepTransceiver& aJsepTransceiver) const { std::vector streamIds; for (const auto& stream : mStreams) { nsString wideStreamId; stream->GetId(wideStreamId); std::string streamId = NS_ConvertUTF16toUTF8(wideStreamId).get(); MOZ_ASSERT(!streamId.empty()); streamIds.push_back(streamId); } aJsepTransceiver.mSendTrack.UpdateStreamIds(streamIds); if (mSimulcastEnvelopeSet) { std::vector rids; Maybe parameters; if (mPendingRidChangeFromCompatMode) { // *sigh* If we have just let a setParameters change our rids, but we have // not yet updated mParameters because the queued task hasn't run yet, // we want to set the _new_ rids on the JsepTrack. So, we are forced to // grab them from mPendingParameters. parameters = mPendingParameters; } else { parameters = Some(mParameters); } for (const auto& encoding : parameters->mEncodings) { if (encoding.mRid.WasPassed()) { rids.push_back(NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get()); } else { rids.push_back(""); } } aJsepTransceiver.mSendTrack.SetRids(rids); } if (mTransceiver->IsVideo()) { aJsepTransceiver.mSendTrack.SetMaxEncodings(webrtc::kMaxSimulcastStreams); } else { aJsepTransceiver.mSendTrack.SetMaxEncodings(1); } if (mAddTrackCalled) { aJsepTransceiver.SetOnlyExistsBecauseOfSetRemote(false); } } Maybe RTCRtpSender::GetNewVideoConfig() { // It is possible for SDP to signal that there is a send track, but there not // actually be a send track, according to the specification; all that needs to // happen is for the transceiver to be configured to send... if (!GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()) { return Nothing(); } VideoConfig oldConfig; oldConfig.mSsrcs = mSsrcs; oldConfig.mLocalRtpExtensions = mLocalRtpExtensions; oldConfig.mCname = mCname; oldConfig.mTransmitting = mTransmitting; oldConfig.mVideoRtxSsrcs = mVideoRtxSsrcs; oldConfig.mVideoCodec = mVideoCodec; oldConfig.mVideoRtpRtcpConfig = mVideoRtpRtcpConfig; oldConfig.mVideoCodecMode = mVideoCodecMode; VideoConfig newConfig(oldConfig); UpdateBaseConfig(&newConfig); newConfig.mVideoRtxSsrcs = GetJsepTransceiver().mSendTrack.GetRtxSsrcs(); const JsepTrackNegotiatedDetails details( *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()); if (mSenderTrack) { RefPtr videotrack = mSenderTrack->AsVideoStreamTrack(); if (!videotrack) { MOZ_CRASH( "In ConfigureVideoCodecMode, mSenderTrack is not video! This should " "never happen!"); } dom::MediaSourceEnum source = videotrack->GetSource().GetMediaSource(); switch (source) { case dom::MediaSourceEnum::Browser: case dom::MediaSourceEnum::Screen: case dom::MediaSourceEnum::Window: case dom::MediaSourceEnum::Application: newConfig.mVideoCodecMode = webrtc::VideoCodecMode::kScreensharing; break; case dom::MediaSourceEnum::Camera: case dom::MediaSourceEnum::Other: // Other is used by canvas capture, which we treat as realtime video. // This seems debatable, but we've been doing it this way for a long // time, so this is likely fine. newConfig.mVideoCodecMode = webrtc::VideoCodecMode::kRealtimeVideo; break; case dom::MediaSourceEnum::Microphone: case dom::MediaSourceEnum::AudioCapture: case dom::MediaSourceEnum::EndGuard_: MOZ_ASSERT(false); break; } } std::vector configs; RTCRtpTransceiver::NegotiatedDetailsToVideoCodecConfigs(details, &configs); if (configs.empty()) { // TODO: Are we supposed to plumb this error back to JS? This does not // seem like a failure to set an answer, it just means that codec // negotiation failed. For now, we're just doing the same thing we do // if negotiation as a whole failed. MOZ_LOG(gSenderLog, LogLevel::Error, ("%s[%s]: %s No video codecs were negotiated (send).", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__)); return Nothing(); } newConfig.mVideoCodec = Some(configs[0]); // Spec says that we start using new parameters right away, _before_ we // update the parameters that are visible to JS (ie; mParameters). const RTCRtpSendParameters& parameters = mPendingParameters.isSome() ? *mPendingParameters : mParameters; for (VideoCodecConfig::Encoding& conduitEncoding : newConfig.mVideoCodec->mEncodings) { for (const RTCRtpEncodingParameters& jsEncoding : parameters.mEncodings) { std::string rid; if (jsEncoding.mRid.WasPassed()) { rid = NS_ConvertUTF16toUTF8(jsEncoding.mRid.Value()).get(); } if (conduitEncoding.rid == rid) { ApplyJsEncodingToConduitEncoding(jsEncoding, &conduitEncoding); break; } } } newConfig.mVideoRtpRtcpConfig = Some(details.GetRtpRtcpConfig()); if (newConfig == oldConfig) { MOZ_LOG(gSenderLog, LogLevel::Debug, ("%s[%s]: %s No change in video config", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__)); return Nothing(); } if (newConfig.mVideoCodec.isSome()) { MOZ_ASSERT(newConfig.mSsrcs.size() == newConfig.mVideoCodec->mEncodings.size()); } return Some(newConfig); } Maybe RTCRtpSender::GetNewAudioConfig() { AudioConfig oldConfig; oldConfig.mSsrcs = mSsrcs; oldConfig.mLocalRtpExtensions = mLocalRtpExtensions; oldConfig.mCname = mCname; oldConfig.mTransmitting = mTransmitting; oldConfig.mAudioCodec = mAudioCodec; AudioConfig newConfig(oldConfig); UpdateBaseConfig(&newConfig); if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails() && GetJsepTransceiver().mSendTrack.GetActive()) { const auto& details( *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()); std::vector configs; RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(details, &configs); if (configs.empty()) { // TODO: Are we supposed to plumb this error back to JS? This does not // seem like a failure to set an answer, it just means that codec // negotiation failed. For now, we're just doing the same thing we do // if negotiation as a whole failed. MOZ_LOG(gSenderLog, LogLevel::Error, ("%s[%s]: %s No audio codecs were negotiated (send)", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__)); return Nothing(); } std::vector dtmfConfigs; std::copy_if( configs.begin(), configs.end(), std::back_inserter(dtmfConfigs), [](const auto& value) { return value.mName == "telephone-event"; }); const AudioCodecConfig& sendCodec = configs[0]; if (!dtmfConfigs.empty()) { // There is at least one telephone-event codec. // We primarily choose the codec whose frequency matches the send codec. // Secondarily we choose the one with the lowest frequency. auto dtmfIterator = std::find_if(dtmfConfigs.begin(), dtmfConfigs.end(), [&sendCodec](const auto& dtmfCodec) { return dtmfCodec.mFreq == sendCodec.mFreq; }); if (dtmfIterator == dtmfConfigs.end()) { dtmfIterator = std::min_element( dtmfConfigs.begin(), dtmfConfigs.end(), [](const auto& a, const auto& b) { return a.mFreq < b.mFreq; }); } MOZ_ASSERT(dtmfIterator != dtmfConfigs.end()); newConfig.mDtmfPt = dtmfIterator->mType; newConfig.mDtmfFreq = dtmfIterator->mFreq; } newConfig.mAudioCodec = Some(sendCodec); } if (newConfig == oldConfig) { MOZ_LOG(gSenderLog, LogLevel::Debug, ("%s[%s]: %s No change in audio config", mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__)); return Nothing(); } return Some(newConfig); } void RTCRtpSender::UpdateBaseConfig(BaseConfig* aConfig) { aConfig->mSsrcs = GetJsepTransceiver().mSendTrack.GetSsrcs(); aConfig->mCname = GetJsepTransceiver().mSendTrack.GetCNAME(); if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails() && GetJsepTransceiver().mSendTrack.GetActive()) { const auto& details( *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()); { std::vector extmaps; // @@NG read extmap from track details.ForEachRTPHeaderExtension( [&extmaps](const SdpExtmapAttributeList::Extmap& extmap) { extmaps.emplace_back(extmap.extensionname, extmap.entry); }); aConfig->mLocalRtpExtensions = extmaps; } } // RTCRtpTransceiver::IsSending is updated after negotiation completes, in a // queued task (which we may be in right now). Don't use // JsepTrack::GetActive, because that updates before the queued task, which // is too early for some of the things we interact with here (eg; // RTCDTMFSender). aConfig->mTransmitting = mTransceiver->IsSending(); } void RTCRtpSender::ApplyVideoConfig(const VideoConfig& aConfig) { if (aConfig.mVideoCodec.isSome()) { MOZ_ASSERT(aConfig.mSsrcs.size() == aConfig.mVideoCodec->mEncodings.size()); } mSsrcs = aConfig.mSsrcs; mCname = aConfig.mCname; mLocalRtpExtensions = aConfig.mLocalRtpExtensions; mVideoRtxSsrcs = aConfig.mVideoRtxSsrcs; mVideoCodec = aConfig.mVideoCodec; mVideoRtpRtcpConfig = aConfig.mVideoRtpRtcpConfig; mVideoCodecMode = aConfig.mVideoCodecMode; mTransmitting = aConfig.mTransmitting; } void RTCRtpSender::ApplyAudioConfig(const AudioConfig& aConfig) { mTransmitting = false; mSsrcs = aConfig.mSsrcs; mCname = aConfig.mCname; mLocalRtpExtensions = aConfig.mLocalRtpExtensions; mAudioCodec = aConfig.mAudioCodec; if (aConfig.mDtmfPt >= 0) { mDtmf->SetPayloadType(aConfig.mDtmfPt, aConfig.mDtmfFreq); } mTransmitting = aConfig.mTransmitting; } void RTCRtpSender::Stop() { MOZ_ASSERT(mTransceiver->Stopped()); mTransmitting = false; } bool RTCRtpSender::HasTrack(const dom::MediaStreamTrack* aTrack) const { if (!mSenderTrack) { return false; } if (!aTrack) { return true; } return mSenderTrack.get() == aTrack; } RefPtr RTCRtpSender::GetPipeline() const { return mPipeline; } std::string RTCRtpSender::GetMid() const { return mTransceiver->GetMidAscii(); } JsepTransceiver& RTCRtpSender::GetJsepTransceiver() { return mTransceiver->GetJsepTransceiver(); } void RTCRtpSender::UpdateDtmfSender() { if (!mDtmf) { return; } if (mTransmitting) { return; } mDtmf->StopPlayout(); } } // namespace mozilla::dom #undef LOGTAG