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

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的信令状态机不能是closed
  if (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 查找是否存在可复用的RtpTransceiver
  auto 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到对应的sender
    transceiver->sender()->SetTrack(track);
    // 2.3 设置RtpSender的流id
    transceiver->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,传入track
    auto sender = CreateSender(media_type, sender_id, track, stream_ids, {});
    // 3.4 创建新的RtpReceiver,无track
    auto receiver = CreateReceiver(media_type, rtc::CreateRandomUuid());
    // 3.5 根据新的RtpSender,RtpReceiver创建新的RtpTransceiver
    transceiver = 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内容描述结构体ContentInfo
    const ContentInfo* current_local_msection =
        FindTransceiverMSection(transceiver.get(), description);
    // 获取Rtptranceiver在remote sdp中的mline内容描述结构体ContentInfo
    const 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对象中存储的数据与信息源相一致。
  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
WebRTC(Web Real-Time Communication)是一个开源项目,它提供了在浏览器中实现实时音视频通信的技术。下面是对WebRTC源码的简要分析WebRTC源码主要分为以下几个模块: 1. 信令(Signaling)模块:负责建立和维护通信的连接。它使用WebSocket或者其他协议进行通信,包括传输SDP(Session Description Protocol)和ICE(Interactive Connectivity Establishment)信息。 2. 媒体(Media)模块:处理音视频数据的采集、编码、解码和传输。媒体模块使用WebRTC提供的API,通过WebRTCPeerConnection建立点对点的媒体通信。 3. 网络(Networking)模块:处理网络传输相关的功能,例如NAT穿越、ICE候选地址的收集和选择、STUN和TURN服务器的使用等。 4. 安全(Security)模块:处理加密和身份验证相关的功能,确保通信过程的安全性和隐私性。 5. SDP解析(SDP Parsing)模块:解析和生成SDP信息,SDP包含了关于媒体会话的描述和参数。 6. ICE代理(ICE Agent)模块:负责管理ICE协议的运行,处理候选地址的收集和选择,以及NAT穿越等功能。 7. RTP/RTCP模块:处理音视频的实时传输协议(RTP)和实时传输控制协议(RTCP),包括数据包的发送和接收、丢包恢复、拥塞控制等。 8. 编解码器(Codec)模块:负责音视频数据的编码和解码,WebRTC支持一系列开源编解码器,如VP8、VP9、H.264等。 这些模块之间相互协作,实现了基于浏览器的实时音视频通信。WebRTC源码使用C++语言编写,涉及到了底层的网络和媒体处理,同时也提供了一系列的API供开发者使用。 请注意,由于WebRTC源码较为庞大,这里只是简要地介绍了主要模块,实际的源码分析需要深入研究和阅读源码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值