目录

  • 1. 引言
  • 2. 音频轨创建和添加
    • 2.1 音频源AudioSource的创建
      • 2.1.1 音频源继承树
      • 2.1.2 近端音频源LocalAudioSource
      • 2.1.3 远端音频源RemoteAudioSource
    • 2.2 创建音频轨AudioTrack
      • 2.2.1 音频轨继承树
    • 2.3 添加音频轨AudioTrack到PeerConnection
      • 2.3.1 PeerConnection::FindSenderForTrack
      • 2.3.2 PeerConnection::AddTrackUnifiedPlan
      • 2.3.3 PeerConnection::UpdateNegotiationNeeded
        • 2.3.3.1 PC的信令状态——SignalingState
        • 2.3.3.2 是否需要协商?——CheckIfNegotiationIsNeeded()
      • 2.3.4 StatsCollector::AddTrack
  • 3. 总结

1. 引言

创建完PeerConnectionFactory 和 PeerConnection这两个API层的操盘对象之后,紧接着需要初始化本地的媒体,也即创建本地的音频轨、视频轨、数据通道,并将这些本地的媒体轨道添加到PeerConnection对象中。如图中红色标注所示。

本文将详细描述上述轨道的创建细节 以及 轨道被添加到PeerConnection中的存储情况。

2. 音频轨创建和添加

WebRTC的示例工程中使用如下几行代码实现AudioTrack的创建 && 添加AudioTrack到PeerConnection中。

  rtc::scoped_refptr<webrtc::AudioTrackInterface> audio_track(peer_connection_factory_->CreateAudioTrack(kAudioLabel, peer_connection_factory_->CreateAudioSource(cricket::AudioOptions())));auto result_or_error = peer_connection_->AddTrack(audio_track, {kStreamId});
  • 视频轨对象肯定实现了rtc::RefCountInterface接口,因为最后被智能指针对象audio_track所持有
  • PeerConnectionFactory.CreateAudioTrack方法用来创建音频轨,音频轨实现了AudioTrackInterface接口;
  • PeerConnectionFactory.CreateAudioSource方法用来创建音频源,并作为创建音频轨的参数,传递给音频轨对象。音频源对象实现了AudioSourceInterface接口

2.1 音频源AudioSource的创建

rtc::scoped_refptr<AudioSourceInterface>
PeerConnectionFactory::CreateAudioSource(const cricket::AudioOptions& options) {RTC_DCHECK(signaling_thread_->IsCurrent());rtc::scoped_refptr<LocalAudioSource> source(LocalAudioSource::Create(&options));return source;
}rtc::scoped_refptr<LocalAudioSource> LocalAudioSource::Create(const cricket::AudioOptions* audio_options) {rtc::scoped_refptr<LocalAudioSource> source(new rtc::RefCountedObject<LocalAudioSource>());source->Initialize(audio_options);return source;
}void LocalAudioSource::Initialize(const cricket::AudioOptions* audio_options) {if (!audio_options)return;options_ = *audio_options;
}

上述是本地音频源创建过程,由源码可知,创建的本地音频轨实体对象是LocalAudioSource,并且创建该对象后进行了初始化——>将音频选项对象传递给了LocalAudioSource进行存储。

2.1.1 音频源继承树


由上面的继承树,从上至下分析,我们可以获知如下几点信息:

  • 继承接口RefCountInterface:表明音频源是一个引用计数对象。使用时将配合智能指针scoped_ptr 和 模板RefCountedObject<>一起使用,如同上面源码所示。详细分析见:WebRTC源码分析——引用计数系统
  • 继承接口NotifierInterface:表明音频源是一个通知者NotifierInterface对象,通过继承该接口,实现注册关注音频源的观察者ObserverInterface对象。那么,观察者观察什么?音频源作为通知者,向观察者通知什么呢?——>通知音频源的状态改变。
  • 继承接口MediaSourceInterface:表明音频源首先是一个媒体源,像视频源也是一个媒体源。提供媒体源两个基本属性:媒体源的状态——SourceState(kInitializing, kLive, kEnded, kMuted);本地源还是远端源?
  • 继承接口AudioSourceInterface:音频源基础接口。提供了设置音量,获取音频选项AudioOptions的能力;提供了注册/注销音频观察者的接口,可以获知音量变化;提供了注册/注销音频轨Sink的接口,让音频数据可以从Source流向Sink;
  • 实体类Notifier<AudioSourceInterface>:提供了NotifierInterface接口的实现,维护需要观察音频源状态改变的观察者列表,并在源状态改变时,挨个通知观察者列表中的观察者
  • 最终的实体类有LocalAudioSource 和 RemoteAudioSource两类,分别代表本地音频源、远端的音频源。

PS1:有三套注册/注销接口,分别是:

  • RegisterObserver/UnregisterObserver:注册源状态变化的观察者,当源的SourceState发生改变时,将调用观察者的OnChanged()方法,来通知观察者;
  • RegisterAudioObserver/UnregisterAudioObserver:注册音量大小变化的的观察者,当音频源音量大小改变时,将通过调用观察者OnSetVolume(double volume)方法,来通知观察者;
  • AddSink/RemoveSink:注册音频数据的接收者,当源产生音频数据时,将通过调用Sink的OnData(const void* audio_data, int bits_per_sample, int sample_rate, size_t number_of_channels, size_t number_of_frames)方法,让音频数据从源流入Sink。

2.1.2 近端音频源LocalAudioSource

WebRTC中,近端原始音视频数据总是要经过 “采集->音视频源->音频轨” 这样一条路径,至少视频数据是严格按照该条路径输出的。我们来看看近端音频的情况——LocalAudioSource

class LocalAudioSource : public Notifier<AudioSourceInterface> {public:// Creates an instance of LocalAudioSource.static rtc::scoped_refptr<LocalAudioSource> Create(const cricket::AudioOptions* audio_options);SourceState state() const override { return kLive; }bool remote() const override { return false; }const cricket::AudioOptions options() const override { return options_; }void AddSink(AudioTrackSinkInterface* sink) override {}void RemoveSink(AudioTrackSinkInterface* sink) override {}protected:LocalAudioSource() {}~LocalAudioSource() override {}private:void Initialize(const cricket::AudioOptions* audio_options);cricket::AudioOptions options_;
};

源码如上所示,在本地创建轨道时所使用的音频源对象是LocalAudioSource。仔细查看LocalAudioSource类的代码,可以知道该音频源实质上什么也没有做:既没有与音频设备建立联系,从音频设备处获取采集的音频数据,也没有实质的提供注册Sink的方法,更没有向注册的Sink推送数据。由此可知,本地的音频数据的流转跟LocalAudioSource其实没什么关系,这个是我非常纳闷的一点,因为LocalAudioSource看起来像是一个没有完成、或者说是废弃的类,但示例中正常使用了,并且近端音频数据还是正常流转的。那么肯定是走了别的路径。

当前,音频设备模块ADM是被音频引擎VoiceEngine所持有的,因此,音频数据采集开始,最初的位置可能就是VoiceEngine。后续将专门出一篇文章来分析介绍近端音频流转。

2.1.3 远端音频源RemoteAudioSource

与LocalAudioSource不一样,代表远端音频源的RemoteAudioSource类是真实有效的类,提供了继承树上所有接口和功能的实现。

远端音频源从哪儿获取数据?又将数据推向何处?
RemoteAudioSource::AudioDataProxy类的对象可以携带RemoteAudioSource对象被注册到VoiceEngine中,从那得到从远端收到的音频数据;RemoteAudioSource又可以向注册到其中的Sink列表进一步扇出音频数据。具体看如下源码:

class RemoteAudioSource::AudioDataProxy : public AudioSinkInterface {public:explicit AudioDataProxy(RemoteAudioSource* source) : source_(source) {RTC_DCHECK(source);}~AudioDataProxy() override { source_->OnAudioChannelGone(); }// AudioSinkInterface implementation.void OnData(const AudioSinkInterface::Data& audio) override {source_->OnData(audio);}private:const rtc::scoped_refptr<RemoteAudioSource> source_;RTC_DISALLOW_IMPLICIT_CONSTRUCTORS(AudioDataProxy);
};void RemoteAudioSource::OnData(const AudioSinkInterface::Data& audio) {// Called on the externally-owned audio callback thread, via/from webrtc.rtc::CritScope lock(&sink_lock_);for (auto* sink : sinks_) {sink->OnData(audio.data, 16, audio.sample_rate, audio.channels,audio.samples_per_channel);}
}

2.2 创建音频轨AudioTrack

rtc::scoped_refptr<AudioTrackInterface> PeerConnectionFactory::CreateAudioTrack(const std::string& id,AudioSourceInterface* source) {RTC_DCHECK(signaling_thread_->IsCurrent());rtc::scoped_refptr<AudioTrackInterface> track(AudioTrack::Create(id, source));return AudioTrackProxy::Create(signaling_thread_, track);
}rtc::scoped_refptr<AudioTrack> AudioTrack::Create(const std::string& id,const rtc::scoped_refptr<AudioSourceInterface>& source) {return new rtc::RefCountedObject<AudioTrack>(id, source);
}AudioTrack::AudioTrack(const std::string& label,const rtc::scoped_refptr<AudioSourceInterface>& source): MediaStreamTrack<AudioTrackInterface>(label), audio_source_(source) {if (audio_source_) {audio_source_->RegisterObserver(this);OnChanged();}
}void AudioTrack::OnChanged() {RTC_DCHECK(thread_checker_.IsCurrent());if (audio_source_->state() == MediaSourceInterface::kEnded) {set_state(kEnded);} else {set_state(kLive);}
}
  • PeerConnectionFactory::CreateAudioTrack、AudioTrack::Create、AudioTrack::AudioTrack三步创建了AudioTrack类的实体对象,这是音频轨的实体类对象。
  • 向应用层返回的是AudioTrack的代理对象AudioTrackProxy,这是为了WebRTC中防止线程乱入所作的常规操作,正如PeerConnectionFactory 和 PeerConnection那样——WebRTC源码分析-线程安全之Proxy,防止线程乱入
  • AudioTrack对象注册为它相关音频源的观察者,从而获取相关音频源的状态通知,在通知中查看音频源的状态,从而同步更新轨道的状态。

2.2.1 音频轨继承树


从上面的继承图上,分析出以下要点:

  • 继承接口ObserverInterface: 让音频轨成为一个观察者。正如前文所述,在音频轨创建时,音频轨会注册为相关的音频源的观察者。
  • 继承接口NotifierInterface: 让音频轨同时又成为一个通知者。当音频轨的状态改变时,将通知关注音频轨状态的观察者,这些观察者通过继承树上的MediaStreamTrack<AudioTrackInterface>所提供的注册/注销/通知 接口来实现音频轨道的状态跟踪。
  • 继承接口RefCountInterface: 让音频轨成为引用计数对象。
  • 继承接口MediaStreamTrackInterface: 音频轨首先必须是媒体轨,视频轨也继承该接口。该接口提供媒体轨道的基本属性接口:媒体类别,使能,轨道状态等。
  • 继承接口AudioTrackInterface: 提供注册/注销音频轨Sink的接口,让音频数据能进一步的从音频轨流向外部对象。
  • 中间对象MediaStreamTrack<AudioTrackInterface> && MediaStreamTrack<AudioTrackInterface>:分别提供底层接口的具体实现。
  • 实体类AudioTrack:进一步提供底层接口的一些实现,是创建的音频轨实体对象。

2.3 添加音频轨AudioTrack到PeerConnection

PeerConnection::AddTrack方法提供两个入参:媒体轨道Track以及媒体流id向量。暗示了WebRTC中一个概念:一个媒体轨道MediaTrack逻辑上可以归属多个媒体流MediaStream。入参stream_ids向量在SDP中会以msid参数出现,一个msid表示逻辑上的一个媒体流。往后,我们可以看到媒体Track会被添加到一个RtpSender中,stream_ids也会存储在RtpSender中。

在继续往下分析前,需要先略微分析下SDP,对SDP中的msid、mid做一个简单的介绍

a=group:BUNDLE audio video data
a=msid-semantic: WMS h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
a=mid:audio
a=ssrc:18509423 msid:h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C 15598a91-caf9-4fff-a28f-3082310b2b7a
m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98
a=mid:video
a=ssrc:3463951252 msid:h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C ead4b4e9-b650-4ed5-86f8-6f5f5806346d
m=application 9 DTLS/SCTP 5000
a=mid:data

上述是一个被简化的SDP数据,从中简要的总结如下几点知识,可以辅助我们去理解后续的内容(我们总是基于Unified Plan这种SDP格式进行讨论,因为Plan B这种格式大多数情况下已经被弃用):

  • mid: 一个mLine(即m=* section)在WebRTC中与一个RtpTranceiver对象对应(track会存储于RtpTranceiver的RtpSender中),RtpTranceiver对象的mid属性与SDP的mLine的属性a=mid:xxx对应。当然RtpTranceiver只能存储音频、视频轨道。应用数据通道得另算。
  • a=group:BUNDLE audio video data 是一个全局性的描述,表示mid为audio video data的这三个mLine所表征的媒体数据要绑定传输,也即对应网络传输层的一个连接来收发包。
  • msid: a=msid-semantic: WMS h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C 也是一个全局性的描述,告知了这个会话中存在几个逻辑上的媒体流,WMS表示WebRTC Media Stream。此处只有一个,流id为h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C。
  • mLine的a=msid=xxx的属性表示了该mline所对应地媒体轨道逻辑上所属的流,可以同时属于多个流。示例上,音频轨和视频轨均属于流h1aZ20mbQB0GSsq0YxLfJmiYWE9CBfGch97C。其后跟随的是轨道的id。

PeerConnection::AddTrack源码如下:

RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>> PeerConnection::AddTrack(rtc::scoped_refptr<MediaStreamTrackInterface> track,const std::vector<std::string>& stream_ids) {// 1 进行一些条件判断// 1.1 必须在信令线程RTC_DCHECK_RUN_ON(signaling_thread());TRACE_EVENT0("webrtc", "PeerConnection::AddTrack");// 1.2 轨道不能为空,必须是音频 or 视频轨if (!track) {LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, "Track is null.");}if (!(track->kind() == MediaStreamTrackInterface::kAudioKind ||track->kind() == MediaStreamTrackInterface::kVideoKind)) {LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,"Track has invalid kind: " + track->kind());}// 1.3 PeerConnection的信令状态机不能是closedif (IsClosed()) {LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_STATE,"PeerConnection is closed.");}// 2 遍历PeerConnection.RtpTransceiver列表.RtpSender列表,//   查看当前的track是否在某个RtpSender中,存在则不能重复添加if (FindSenderForTrack(track)) {LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,"Sender already exists for track " + track->id() + ".");}// 3 根据SDP采用UnifiedPlan还是Plan B决定如何添加Track到PeerConnection的成员中auto sender_or_error =(IsUnifiedPlan() ? AddTrackUnifiedPlan(track, stream_ids): AddTrackPlanB(track, stream_ids));if (sender_or_error.ok()) {// 4 是否需要进行重新协商UpdateNegotiationNeeded();// 5 添加轨道到统计数据收集器stats_->AddTrack(track);}return sender_or_error;
}

添加轨道到PeerConnection过程如上源码分为5个步骤:

  1. 检查状态和入参;
  2. 遍历PeerConnection的存储轨道的成员,确定当前轨道是否已在PeerConnection中,防止重复添加同一个track;
  3. 根据SDP采用UnifiedPlan还是Plan B决定如何添加Track到PeerConnection的成员中;
  4. 判断是否需要进行重新协商;
  5. 添加轨道到统计数据收集器。

接下来将对2~5这4个步骤都进行详细的梳理。

2.3.1 PeerConnection::FindSenderForTrack

防止重复添加同一个track,从PeerConnection保存track的字段中查找当前的track是否已存在,存在则返回对应的RtpSender,否则返回空指针。

rtc::scoped_refptr<RtpSenderProxyWithInternal<RtpSenderInternal>>
PeerConnection::FindSenderForTrack(MediaStreamTrackInterface* track) const {for (const auto& transceiver : transceivers_) {for (auto sender : transceiver->internal()->senders()) {if (sender->track() == track) {return sender;}}}return nullptr;
}

PeerConnection对象有个成员 std::vector<rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>transceivers_; 该成员是一个RtpTransceiver向量。每个RtpTransceiver代表sdp中同一个mLine的所包含的a=ssrc的音频轨 or 视频轨 or 数据(因为,一个mLine只能表示一种媒体类型)。由于sdp中可能会有多个mLine,因此,PeerConnection中需要包含多个RtpTransceiver,以上述向量成员transceivers_来保存

由于SDP的Plan B格式下,本地要发送的多个相同媒体类型的轨道(a=ssrc不同)可能会属于同一个mLine,因此,RtpTransceiver包含一个RtpSender的向量 ,每个RtpSender会存储其中一个轨道。当SDP采用Unified Plan时,RtpTransceiver的RtpSender向量实质上只会存在一个RtpSender,为了兼容Plan B的格式才会存在多个RtpSender。

RtpTransceiver即可以代表本地轨道数据的发送器,又能代表接收远端轨道数据的接收器。 因此,RtpTransceiver还包含一个RtpReceiver的向量。远端的轨道会被存储在RtpTransceiver的某个RtpReceiver中。

上述查找本地Track的过程就遍历了每个RtpTransceiver对象的每个RtpSender中的track的地址,看是否是同一个。 关于RtpTransceiver类的阐述可见——WebRTC源码分析——RtpTransceiver类

2.3.2 PeerConnection::AddTrackUnifiedPlan

添加track到PeerConnection,根据SDP采用Unified Plan还是Plan B。由于Plab B大概率要被遗弃,因此,当前只分析Unified Plan。

RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>>
PeerConnection::AddTrackUnifiedPlan(rtc::scoped_refptr<MediaStreamTrackInterface> track,const std::vector<std::string>& stream_ids) {// 1 查找是否存在可复用的RtpTransceiverauto transceiver = FindFirstTransceiverForAddedTrack(track);// 2 存在,添加track到该复用的RtpTransceiver,并修改必要的属性if (transceiver) {RTC_LOG(LS_INFO) << "Reusing an existing "<< cricket::MediaTypeToString(transceiver->media_type())<< " transceiver for AddTrack.";// 2.1 设置RtpTransceiver的方向,注意不能将接收方向覆盖掉,即如果接收方向是存在的,//     则必须保留,因此有如下的判断。if (transceiver->direction() == RtpTransceiverDirection::kRecvOnly) {transceiver->internal()->set_direction(RtpTransceiverDirection::kSendRecv);} else if (transceiver->direction() == RtpTransceiverDirection::kInactive) {transceiver->internal()->set_direction(RtpTransceiverDirection::kSendOnly);}// 2.2 添加track到对应的sendertransceiver->sender()->SetTrack(track);// 2.3 设置RtpSender的流idtransceiver->internal()->sender_internal()->set_stream_ids(stream_ids);// 2.4 设置重用标识transceiver->internal()->set_reused_for_addtrack(true);// 3 不存在,创建新的RtpTransceiver,添加track到新的RtpTransceiver} else {// 3.1 得到与轨道一致的媒体类型 cricket::MediaType media_type =(track->kind() == MediaStreamTrackInterface::kAudioKind? cricket::MEDIA_TYPE_AUDIO: cricket::MEDIA_TYPE_VIDEO);RTC_LOG(LS_INFO) << "Adding " << cricket::MediaTypeToString(media_type)<< " transceiver in response to a call to AddTrack.";// 3.2 得到或新建RtpSender的id,可以与track的id相同,但是不能与其他//     RtpSender的id重复,否则创建一个新的UUID作为RtpSender的唯一标识。             std::string sender_id = track->id();// Avoid creating a sender with an existing ID by generating a random ID.// This can happen if this is the second time AddTrack has created a sender// for this track.if (FindSenderById(sender_id)) {sender_id = rtc::CreateRandomUuid();}// 3.3 创建新的RtpSender,传入trackauto sender = CreateSender(media_type, sender_id, track, stream_ids, {});// 3.4 创建新的RtpReceiver,无trackauto receiver = CreateReceiver(media_type, rtc::CreateRandomUuid());// 3.5 根据新的RtpSender,RtpReceiver创建新的RtpTransceivertransceiver = CreateAndAddTransceiver(sender, receiver);// 3.6 设置RtpTransceiver创建标识transceiver->internal()->set_created_by_addtrack(true);// 3.7 新创建的RtpTransceiver的方向设置为既接收又发送transceiver->internal()->set_direction(RtpTransceiverDirection::kSendRecv);}return transceiver->sender();
}

大致过程就是判断是否存在可复用的RtpTransceiver,存在则添加track到可复用的RtpTransceiver,否则新建一个RtpTransceiver,添加进去。同时注意,需要修改RtpTransceiver的一些属性。

如何判断该RtpTransceiver是可复用的?判断依据是什么?

rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
PeerConnection::FindFirstTransceiverForAddedTrack(rtc::scoped_refptr<MediaStreamTrackInterface> track) {RTC_DCHECK(track);for (auto transceiver : transceivers_) {if (!transceiver->sender()->track() &&cricket::MediaTypeToString(transceiver->media_type()) ==track->kind() &&!transceiver->internal()->has_ever_been_used_to_send() &&!transceiver->stopped()) {return transceiver;}}return nullptr;
}

如上源码所示,给本地track可复用的RtpTransceiver必须满足以下几个条件

  1. 由于是Unified Plan,那么通过RtpTransceiver的sender()来获取RtpSender,sender()方法将断言当前是否是Unified Plan,是否RtpTransceiver的RtpSender向量size为1,取该唯一的RtpSender,并判断存储的track是否为空,不为空,表示sender已经有track存在,不可复用。
  2. 判断track的媒体类型是否跟该RtpTransceiver的媒体类型一致(音频、视频、数据三类),RtpTransceiver只可存储媒体类型一致的轨道,不相同则不可复用。
  3. 判断RtpTransceiver之前的方向是不是被设置为包含发送(即kSendRecv or kSendOnly),若设置过,则不可复用。
  4. 判断RtpTransceiver是否调用过Stop,如果RtpTransceiver已经停止过,则不复用。

2.3.3 PeerConnection::UpdateNegotiationNeeded

UpdateNegotiationNeeded() 方法在往PC中添加/移除轨道、添加/移除流、添加/移除RtpTransceiver、应用local/remote sdp、状态需要进行回滚到KStable时都会被调用,经过各种条件检测后,更新PC的内部成员is_negotiation_needed_。更新后若is_negotiation_needed_为真,那么表示需要重新协商。

void PeerConnection::UpdateNegotiationNeeded() {RTC_DCHECK_RUN_ON(signaling_thread());// 1 如果是Plan B则需要协商,直接通知外部的观察者需要重新协商,不需要关注本方法的功能://   检查有无重新协商的必要,更新字段is_negotiation_needed_。 后续分析将忽略Plan B//   时如何处理,因为Plan B将被遗弃。if (!IsUnifiedPlan()) {Observer()->OnRenegotiationNeeded();return;}// 2 对PC的信令状态机的状态进行判断。// 2.1 PC的信令状态为kClosed,表示会话已经被关闭了,无协商的必要了if (IsClosed())return;// 2.2 PC的信令状态没有处于kStable(初始化状态),也不需判断是否需要进行协商if (signaling_state() != kStable)return;// 3. 使用CheckIfNegotiationIsNeeded()判断是否需要重新协商// NOTE// The negotiation-needed flag will be updated once the state transitions to// "stable", as part of the steps for setting an RTCSessionDescription.bool is_negotiation_needed = CheckIfNegotiationIsNeeded();// 4. 根据之前是否需要协商的状态,以及当前是否需要协商的结论,进行不同的响应//    只有当false——>true的状态时,直接通知观察者进行重新协商。// 4.1 如果当前结论不需要协商,则is_negotiation_needed_更新为false,返回if (!is_negotiation_needed) {is_negotiation_needed_ = false;return;}// 4.2 如果当前需要协商,之前也是需要协商的状态,那就不必进行状态更新了if (is_negotiation_needed_)return;// 4.3 如果当前需要协商,之前时不需要协商的状态,那么更新为需要进行协商,同时通知观察者//     进行协商——即,调用观察者的OnRenegotiationNeeded()方法。is_negotiation_needed_ = true;Observer()->OnRenegotiationNeeded();
}

根据源码分析可以得出以下结论:

  • 只有PC的信令状态处于稳定状态KStable时,我们认为有重新协商的必要;
  • CheckIfNegotiationIsNeeded()方法进行当前是否需要重新协商的检查;
  • 如果是否需要协商的状态由false——>true,那么直接通知观察者进行协商。

上述源码以及论述中,提到了PC信令状态,以及方法 CheckIfNegotiationIsNeeded(),接下来进行一定程度的讨论。

2.3.3.1 PC的信令状态——SignalingState

PC根据JESP会话进行程度,维护了一个信令状态机,状态迁移图如下所示:

各个状态代表的含义如下表格所示:

从呼叫和被呼端的视角分别去跟踪这个状态机会更好理解:

PS: 参阅 https://w3c.github.io/webrtc-pc/#dom-rtcsignalingstate

2.3.3.2 是否需要协商?——CheckIfNegotiationIsNeeded()

是否需要重新协商?依据是什么?问这个问题之前,我们需要搞清楚另外一个问题,即协商的内容是什么?我们知道WebRTC中协商的内容是多样的媒体信息,传输信息,具体可见文章:WebRTC56版本SDP详细解析

协商的手段是收集sdp数据进行互换来达成的,而webrtc中收集sdp数据时,pc用本地会话对象和远端会话对象来存储sdp数据,这些数据的来源就是我们的PC中的ice相关信息,rtptranceiver对象等等,当应用层添加删除轨道等操作时,相应的数据来源会发生变化,但是这个变化并不会同步到存储sdp的近端/远端会话对象中,如此带来了信息的差异。此时,我们就需要重新进行协商,让会话对象存储的信息与数据源保持一致。

bool PeerConnection::CheckIfNegotiationIsNeeded() {RTC_DCHECK_RUN_ON(signaling_thread());// 1. If any implementation-specific negotiation is required, as described at// the start of this section, return true.// 2. If connection's [[RestartIce]] internal slot is true, return true.//    如果有ICE的凭证了,则是需要协商的if (local_ice_credentials_to_replace_->HasIceCredentials()) {return true;}// 3. Let description be connection.[[CurrentLocalDescription]].//    如果还没有本地的SDP,则是需要协商的const SessionDescriptionInterface* description = current_local_description();if (!description)return true;// 4. If connection has created any RTCDataChannels, and no m= section in// description has been negotiated yet for data, return true.//    如果创建了DataChannel,但是sdp中没有对应的mLine,则需要协商。if (data_channel_controller_.HasSctpDataChannels()) {if (!cricket::GetFirstDataContent(description->description()->contents()))return true;}// 5. For each transceiver in connection's set of transceivers, perform the// following checks://    对PC中的每个Rtptranceiver进行如下判断:for (const auto& transceiver : transceivers_) {// 获取Rtptranceiver在local sdp中的mline内容描述结构体ContentInfoconst ContentInfo* current_local_msection =FindTransceiverMSection(transceiver.get(), description);// 获取Rtptranceiver在remote sdp中的mline内容描述结构体ContentInfoconst ContentInfo* current_remote_msection = FindTransceiverMSection(transceiver.get(), current_remote_description());// 5.3 If transceiver is stopped and is associated with an m= section,// but the associated m= section is not yet rejected in// connection.[[CurrentLocalDescription]] or// connection.[[CurrentRemoteDescription]], return true.//     如果Rtptranceiver已经是停止状态,但是在local sdp或者是remote sdp中//     不处于rejected状态,也即是有效的,这状况显然是不对的,因此需要进行协商。if (transceiver->stopped()) {if (current_local_msection && !current_local_msection->rejected &&((current_remote_msection && !current_remote_msection->rejected) ||!current_remote_msection)) {return true;}continue;}// 5.1 If transceiver isn't stopped and isn't yet associated with an m=// section in description, return true.//     如果Rtptranceiver没有停止,并且在本地SDP中没有相应的mline,那么肯定需要//     进行协商if (!current_local_msection)return true;const MediaContentDescription* current_local_media_description =current_local_msection->media_description();// 5.2 If transceiver isn't stopped and is associated with an m= section// in description then perform the following checks:// 如果Rtptranceiver没有停止,并且也与本地sdp的mline进行了关联,那么获取对应的// MediaContentDescription进行更细节性的排查// 5.2.1 If transceiver.[[Direction]] is "sendrecv" or "sendonly", and the// associated m= section in description either doesn't contain a single// "a=msid" line, or the number of MSIDs from the "a=msid" lines in this// m= section, or the MSID values themselves, differ from what is in// transceiver.sender.[[AssociatedMediaStreamIds]], return true.// 如果Rtptranceiver包含有效的RtpSender(即Rtptranceiver的方向包含send方向)// 但是SDP中与其关联的mline没有包含单独的a=msid行,或者mline的a=msid行的msid值与// Rtptranceiver的RtpSender的关联的媒体流id值不一致。需要进行协商if (RtpTransceiverDirectionHasSend(transceiver->direction())) {//如果mline所属流ID数量为0,即不归属于某个流,则需要进行协商if (current_local_media_description->streams().size() == 0)return true;//遍历并提取所有关联的流ID到临时向量保存std::vector<std::string> msection_msids;for (const auto& stream : current_local_media_description->streams()) {for (const std::string& msid : stream.stream_ids())msection_msids.push_back(msid);}//若sender所属的流,ID数量和ID值与sdp中抽取的不一致,则需要进行协商。std::vector<std::string> transceiver_msids =transceiver->sender()->stream_ids();if (msection_msids.size() != transceiver_msids.size())return true;absl::c_sort(transceiver_msids);absl::c_sort(msection_msids);if (transceiver_msids != msection_msids)return true;}// 5.2.2 If description is of type "offer", and the direction of the// associated m= section in neither connection.[[CurrentLocalDescription]]// nor connection.[[CurrentRemoteDescription]] matches// transceiver.[[Direction]], return true.//// 本地sdp为offer sdp(即当前pc为呼叫发起方),Rtptranceiver的mid必须// 在本地sdp中有对应的mline,在远端sdp中也应该有对应的mline。并且Rtptranceiver的// 方向必须与本地sdp mline中的方向一致,与远端sdp mline中的方向相反。if (description->GetType() == SdpType::kOffer) {if (!current_remote_description())return true;if (!current_remote_msection)return true;RtpTransceiverDirection current_local_direction =current_local_media_description->direction();RtpTransceiverDirection current_remote_direction =current_remote_msection->media_description()->direction();if (transceiver->direction() != current_local_direction &&transceiver->direction() !=RtpTransceiverDirectionReversed(current_remote_direction)) {return true;}}// 5.2.3 If description is of type "answer", and the direction of the// associated m= section in the description does not match// transceiver.[[Direction]] intersected with the offered direction (as// described in [JSEP] (section 5.3.1.)), return true.// // 本地sdp为answer sdp(即当前pc为被呼方),那么远端sdp为offer sdp// 此时,近端sdp的mline的方向如果与 “Rtptranceiver方向&&远端mline方向的交集” // 不一致,则需要进行协商(此处较难理解,应反复斟酌)if (description->GetType() == SdpType::kAnswer) {// 远端sdp不存在,则需要进行协商if (!remote_description())return true;// 获取远端sdp,也即offer sdp中对应的mline描述const ContentInfo* offered_remote_msection =FindTransceiverMSection(transceiver.get(), remote_description());// 如果远端offser sdp中mline描述存在,则获取mline中描述的方向,否则认为offer sdp// 中该mline是无效的。RtpTransceiverDirection offered_direction =offered_remote_msection? offered_remote_msection->media_description()->direction(): RtpTransceiverDirection::kInactive;// 近端sdp的mline的方向如果与 “Rtptranceiver方向&&远端mline方向的交集” // 不一致,则需要进行协商if (current_local_media_description->direction() !=// 求二者方向的交集(RtpTransceiverDirectionIntersection(transceiver->direction(),// 先对远端offer sdp的mline方向取反RtpTransceiverDirectionReversed(offered_direction)))) {return true;}}}

2.3.4 StatsCollector::AddTrack

void StatsCollector::AddTrack(MediaStreamTrackInterface* track) {if (track->kind() == MediaStreamTrackInterface::kAudioKind) {CreateTrackReport(static_cast<AudioTrackInterface*>(track), &reports_,&track_ids_);} else if (track->kind() == MediaStreamTrackInterface::kVideoKind) {CreateTrackReport(static_cast<VideoTrackInterface*>(track), &reports_,&track_ids_);} else {RTC_NOTREACHED() << "Illegal track kind";}
}

将该轨道纳入统计数据收集器,如此,可以出具关于该track的统计数据报表。详细分析可见后续WebRTC关于数据统计的分析,此处不赘述。

3. 总结

经过上述长篇论述,我们大致对WebRTC中的音频源,音频轨的继承结构,创建过程有了大致的了解;并且对PC如何添加、存储音频轨有了比较深刻的理解;同时,当音频轨被添加到PC中后,我们需要判断PC中近远端SDP会话对象 与 RtpTranceiver中保存的信息是否一致,从而决定了是否需要进行重新协商。 有一些观点需要再次强调,也有一些疑惑需要列举出来,以备往后源码分析中一一解惑。

  • WebRTC的有个重要的观念:媒体数据总是由“源”流向“轨道”,然后再从“轨道”流出。疑惑的点在于,近端的源LocalAudioSource根本没有提供这样的能力,那么近端的音频数据流转是如何实现的仍然是个谜团。
  • WebRTC中使用SDP进行数据交换,有两种格式的SDP:Unified Plan 和 Plan B。目前,已经大多转向使用Unified Plan,因此,往后的源码分析都只分析Unified Plan。
  • Unified Plan格式下,RtpTranceiver具有一个RtpSender和一个RtpReceiver,分别用来存储本地发送Track和接收对端数据的Track,RtpTranceiver以mLine的形式出现的本地SDP中,也会出现在远端SDP中,RtpTranceiver反应在近远端的mLine具有相同的mid,但RtpTranceiver方向在近远端SDP中必须有相反方向的属性,比如近端SDP中是SendOnly,则远端SDP中肯定是RecvOnly。
  • 文中也详细的论述了为什么添加轨道到PC将触发重新协商,详细分析了需要进行重新协商的条件是如何判断的——SDP对象与构建SDP对象所需要的信息源,二者之间信息不对等,不匹配时就需要重新协商,让SDP对象中存储的数据与信息源相一致。

WebRTC源码分析-呼叫建立过程之四(上)(创建并添加本地音频轨到PeerConnection)相关推荐

  1. WebRTC源码分析-呼叫建立过程之五(创建Offer,CreateOffer,上篇)

    目录 1. 引言 2 CreateOffer声明 && 两个参数 2.1 CreateOffer声明 2.2 参数CreateSessionDescriptionObserver 2. ...

  2. Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树

    Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 文章目录 Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 0x00 摘要 0x01 背景概念 1.1 词向量 ...

  3. Hyperledger Fabric从源码分析背书提案过程

    在之前的文章中 Hyperledger Fabric从源码分析链码安装过程 Hyperledger Fabric从源码分析链码实例化过程 Hyperledger Fabric从源码分析链码查询与调用 ...

  4. MyBatis 源码分析 - 配置文件解析过程

    文章目录 * 本文速览 1.简介 2.配置文件解析过程分析 2.1 配置文件解析入口 2.2 解析 properties 配置 2.3 解析 settings 配置 2.3.1 settings 节点 ...

  5. React Native 源码分析(三)——Native View创建流程

    1.React Native 源码分析(一)-- 启动流程 2.React Native 源码分析(二)-- 通信机制 3.React Native 源码分析(三)-- Native View创建流程 ...

  6. webrtc源码分析之-从视频采集到编码流程

    peer_connection中从视频采集到编码的流程 摘要:本篇文章主要讲述当我们通过peer_connection完成推流时,视频从采集到编码是如何衔接的. 既,视频采集后如何传送到编码器.重点分 ...

  7. modelandview使用过程_深入源码分析SpringMVC执行过程

    本文主要讲解 SpringMVC 执行过程,并针对相关源码进行解析. 首先,让我们从 Spring MVC 的四大组件:前端控制器(DispatcherServlet).处理器映射器(HandlerM ...

  8. SOFA 源码分析 —— 服务发布过程

    前言 SOFA 包含了 RPC 框架,底层通信框架是 bolt ,基于 Netty 4,今天将通过 SOFA-RPC 源码中的例子,看看他是如何发布一个服务的. 示例代码 下面的代码在 com.ali ...

  9. 云客Drupal源码分析之插件系统(上)

    各位<云客drupal源码分析>系列的读者: 本系列一直以每周一篇的速度进行博客原创更新,希望帮助大家理解drupal底层原理,并缩短学习时间,但自<插件系统(上)>主题开始博 ...

最新文章

  1. 建立能够持续请求的CS网络程序
  2. Spring 事务提交回滚源码解析
  3. 第9章 项目人力资源管理
  4. 推荐系统之---如何理解低秩矩阵?
  5. android - 使用Parcelable序列化
  6. git clone错误
  7. 人工与计算机解决问题的异同,1.1计算机解决问题的过程ppt课件 .ppt
  8. [数据仓库]基于大数据的数仓和传统数仓的区别
  9. win10连接win7共享打印机(win10连接win7共享打印机)
  10. 高速服务器充电桩位置,最全高速服务区充电站汇总,再也不担心过年回家趴半路啦!...
  11. 静态HTML网页设计作品——食品餐饮行业网站模板(10页) HTML+CSS+JavaScript 学生DW网页设计作业成品 美食生鲜零食网页设计
  12. linux如何显示文件后缀名,如何在win7系统中显示文件后缀名、扩展名
  13. 未来的计算机博士就业前景_恐怖博士:电视的未来
  14. 渗透测试实验_安装Windows7旗舰版
  15. 今天发一个制作课工场论坛发帖
  16. 怎么根据日志分析出 PV 和 UV?
  17. 移动应用/APP的测试流程及方法
  18. 免费送纸质书, 感谢亲们的陪伴~
  19. 七夕节其实是最古老的异地恋
  20. 上海最新住房贷款(含公积金贷款,商业贷款,组合贷款)

热门文章

  1. ionic开发——图片加载失败或不存在时显示提示图片的解决方法
  2. 1月8日服务器例行维护公告,【维护】1月8日官方维护公告(正式服)
  3. 越“抽”越“亏”,网约车的未来在哪里?
  4. 手握曹操出行,吉利为什么还要打造一个新的网约车平台?
  5. [蓝桥杯 2022 省 B] 砍竹子
  6. 老旧的钟表(数学题)
  7. windows设置网卡成100M b/s
  8. 金属标记/荧光标记/功能化改性/官能团表面包覆聚苯乙烯微球
  9. 八 常用控件 QLabel
  10. 将子窗体展示到父窗体Panel中的方法,以及调用的按钮单击事件