webrtc 的 CreateOffer 过程分析

通过webrtc 点对点会话建立过程分析可以知道 CreateOffer 的具体实现位置在 src\third_party\webrtc\pc\mediasession.cc ,但是 CreateOffer 执行过程中具体经历了什么,还没有进行介绍,接下来将介绍 CreateOffer 究竟创建了什么内容。

1. 总体介绍

在 CreateOffer 中,会获取本地所支持的音视频编码格式,以及传输相关参数信息。函数原型如下:

SessionDescription* MediaSessionDescriptionFactory::CreateOffer(
    const MediaSessionOptions& session_options,
    const SessionDescription* current_description) const;

参数 session_options 是上层传入的约束,譬如,是否需要音频描述,以及是否加密等。current_description 是当前的会话描述内容,如果是第一次 CreateOffer ,这个值为 nullptr,如果中途因为某些原因需要再次协商会话描述信息,这个值就是有意义的。

2.获取编码参数

2.1 获取编码的总流程

在 CreateOffer 的过程中,我们需要获取本地支持的编码格式,以传递给对端进行协商。获取编码格式的代码如下:

  AudioCodecs offer_audio_codecs;
  VideoCodecs offer_video_codecs;
  DataCodecs offer_data_codecs;
  GetCodecsForOffer(current_description, &offer_audio_codecs,
                    &offer_video_codecs, &offer_data_codecs);

GetCodecsForOffer 的具体实现如下:

void MediaSessionDescriptionFactory::GetCodecsForOffer(
    const SessionDescription* current_description,
    AudioCodecs* audio_codecs,
    VideoCodecs* video_codecs,
    DataCodecs* data_codecs) const {
  UsedPayloadTypes used_pltypes;
  audio_codecs->clear();
  video_codecs->clear();
  data_codecs->clear();

  // First - get all codecs from the current description if the media type
  // is used. Add them to |used_pltypes| so the payload type is not reused if a
  // new media type is added.
  if (current_description) {
    MergeCodecsFromDescription(current_description, audio_codecs, video_codecs,
                               data_codecs, &used_pltypes);
  }

  // Add our codecs that are not in |current_description|.
  MergeCodecs<AudioCodec>(all_audio_codecs_, audio_codecs, &used_pltypes);
  MergeCodecs<VideoCodec>(video_codecs_, video_codecs, &used_pltypes);
  MergeCodecs<DataCodec>(data_codecs_, data_codecs, &used_pltypes);
}

第一步,执行 clear() 的动作,避免指针指向了无效数据;
第二步,如果 current_description 不为空,也就是不是第一次执行 CreateOffer ,那么执行 MergeCodecsFromDescription ,将current_description 中记录的编码信息存入 offer_xxx_codecs;
第三步,执行 MergeCodecs,将本地支持的编码格式存入 offer_xxx_codecs。

2.2 获取本地支持的编码格式

上面提到本地支持的编码格式,那么这些信息是如何获取的呢?下面会按照调用堆栈的思路来介绍,一直到获取编码格式的源头。
(1)在构建 MediaSessionDescriptionFactory 对象时,会获取本地支持的编码格式:

MediaSessionDescriptionFactory::MediaSessionDescriptionFactory(
    ChannelManager* channel_manager,
    const TransportDescriptionFactory* transport_desc_factory)
    : transport_desc_factory_(transport_desc_factory) {
  channel_manager->GetSupportedAudioSendCodecs(&audio_send_codecs_);
  channel_manager->GetSupportedAudioReceiveCodecs(&audio_recv_codecs_);
  channel_manager->GetSupportedAudioRtpHeaderExtensions(&audio_rtp_extensions_);
  channel_manager->GetSupportedVideoCodecs(&video_codecs_);
  channel_manager->GetSupportedVideoRtpHeaderExtensions(&video_rtp_extensions_);
  channel_manager->GetSupportedDataCodecs(&data_codecs_);
  ComputeAudioCodecsIntersectionAndUnion();
}

这里获取了 audio_send_codecs_audio_recv_codecs_video_codecs_data_codecs_,然而并没有 all_audio_codecs_,这个参数是 audio_send_codecs_audio_recv_codecs_ 的并集,通过函数 MediaSessionDescriptionFactory::ComputeAudioCodecsIntersectionAndUnion() 求取的,这个函数用于求取 audio_send_codecs_ 和 audio_recv_codecs_ 的交集和并集。
(2)MediaSessionDescriptionFactory 的 channel_manager 参数来自哪里呢

WebRtcSessionDescriptionFactory::WebRtcSessionDescriptionFactory(
    rtc::Thread* signaling_thread,
    cricket::ChannelManager* channel_manager,
    PeerConnection* pc,
    const std::string& session_id,
    std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator,
    const rtc::scoped_refptr<rtc::RTCCertificate>& certificate)
    : signaling_thread_(signaling_thread),
      session_desc_factory_(channel_manager, &transport_desc_factory_),

session_desc_factory_MediaSessionDescriptionFactory 的实例,其构造用的 channel_manager 来自 WebRtcSessionDescriptionFactory 的构造函数参数。在 PeerConnection::Initialize() 函数中,执行了以下代码:

  webrtc_session_desc_factory_.reset(new WebRtcSessionDescriptionFactory(
      signaling_thread(), channel_manager(), this, session_id(),
      std::move(cert_generator), certificate));

channel_manager() 函数的原型如下:

cricket::ChannelManager* PeerConnection::channel_manager() const {
  return factory_->channel_manager();
}

factory_PeerConnectionFactory 的类实例,在 PeerConnectionFactory::Initialize() 函数内部,有如下代码:

  channel_manager_ = rtc::MakeUnique<cricket::ChannelManager>(
      std::move(media_engine_), rtc::MakeUnique<cricket::RtpDataEngine>(),
      worker_thread_, network_thread_);

到此为止,我们已经看到了 ChannelManager 的构造位置,接下来分析其获取编码参数的代码。
(3)音频、视频和数据获取编码参数的方法类似,因此以音频为例:

void ChannelManager::GetSupportedAudioSendCodecs(
    std::vector<AudioCodec>* codecs) const {
  if (!media_engine_) {
    return;
  }
  *codecs = media_engine_->audio_send_codecs();
}

可以看到是通过 media_engine_ 这个变量来获取编码参数的,接下来分析这个变量。media_engine_ 是 MediaEngineInterface 类型的智能指针,MediaEngineInterface 是一个纯虚类,根据代码搜索只有 CompositeMediaEngine 继承了这个类,所以 media_engine_ 的真实类型必然是 CompositeMediaEngine

MediaEngineInterface* CreateWebRtcMediaEngine(...
...
return new CompositeMediaEngine<WebRtcVoiceEngine, VideoEngine>(
      std::forward_as_tuple(adm, audio_encoder_factory, audio_decoder_factory,
                            audio_mixer, audio_processing),
      std::move(video_args));

CompositeMediaEngine是一个类模板,需要传入音频引擎和视频引擎才能成为成为可以构造对象的类,std::pair<VOICE, VIDEO> 分别记录了对应的音频和视频引擎。

  virtual const std::vector<AudioCodec>& audio_send_codecs() {
    return voice().send_codecs();
  }
  VOICE& voice() { return engines_.first; }

所以最终是通过音频引擎来执行 send_codecs() 来获取对应的编码参数。
(4)音频引擎
从上面给出的代码得知,音频引擎是 WebRtcVoiceEngine ,该类的 send_codecs()函数返回的 send_codecs_ 获取如下:

send_codecs_ = CollectCodecs(encoder_factory_->GetSupportedEncoders());

其中,encoder_factory_->GetSupportedEncoders() 的调用堆栈如图:
在这里插入图片描述
encoder_factory_ 依次获取每一个音频编码格式的编码器,其中获取 ISAC 编码器参数的代码如下:

void AudioEncoderIsacFloat::AppendSupportedEncoders(
    std::vector<AudioCodecSpec>* specs) {
  for (int sample_rate_hz : {16000, 32000}) {
    const SdpAudioFormat fmt = {"ISAC", sample_rate_hz, 1};
    const AudioCodecInfo info = QueryAudioEncoder(*SdpToConfig(fmt));
    specs->push_back({fmt, info});
  }
}
AudioCodecInfo AudioEncoderIsacFloat::QueryAudioEncoder(
    const AudioEncoderIsacFloat::Config& config) {
  RTC_DCHECK(config.IsOk());
  constexpr int min_bitrate = 10000;
  const int max_bitrate = config.sample_rate_hz == 16000 ? 32000 : 56000;
  const int default_bitrate = max_bitrate;
  return {config.sample_rate_hz, 1, default_bitrate, min_bitrate, max_bitrate};
}

(5)CollectCodecs
encoder_factory_->GetSupportedEncoders()获取得到的 AudioCodecSpec 信息,转换成对应的音频编码器,然后会根据音频 AudioCodecSpec 信息,对编码器做一些设置,然后将编码器保存起来。

for (const auto& spec : specs) {
    // We need to do some extra stuff before adding the main codecs to out.
    rtc::Optional<AudioCodec> opt_codec = map_format(spec.format, nullptr);
    ...
      out.push_back(codec);
    }
  }

将舒适噪声(Comfort Noise) 编码添加到普通音频编码之后。

// Add CN codecs after "proper" audio codecs.
for (const auto& cn : generate_cn) {
    if (cn.second) {
      map_format({kCnCodecName, cn.first, 1}, &out);
    }
  }

将电话事件的编码放在音频编码的最后。

// Add telephone-event codecs last.
  for (const auto& dtmf : generate_dtmf) {
    if (dtmf.second) {
      map_format({kDtmfCodecName, dtmf.first, 1}, &out);
    }
  }

到这里,本端所有的音频发送编码信息就收集完毕了。

2.3 编码信息的后续处理

到这一步,就完成了本地支持的音频发送编码的收集工作。收集完后,CreateOffer 还有对编码的进一步处理。

  if (!session_options.vad_enabled) {
    // If application doesn't want CN codecs in offer.
    StripCNCodecs(&offer_audio_codecs);
  }
  FilterDataCodecs(&offer_data_codecs, session_options.data_channel_type == DCT_SCTP);

上面的代码在应用不需要舒适噪声编码时,会将其从 offer_audio_codecs 中删除。
如果 session_options.data_channel_type 的类型为 DCT_SCTP 则从 offer_data_codecs 中过滤删除 名字为kGoogleRtpDataCodecName 的编码,否则删除编码名字为 kGoogleSctpDataCodecName 的编码。

3.获取 RTP 包头的扩展选项

RTP 头扩展信息也是在 MediaSessionDescriptionFactory 对象的构造过程中获取的,具体过程类似音频编码的获取,这里就不再赘述了。音频的 RTP 头扩展信息构造如下:

RtpCapabilities WebRtcVoiceEngine::GetCapabilities() const {
  RTC_DCHECK(signal_thread_checker_.CalledOnValidThread());
  RtpCapabilities capabilities;
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kAudioLevelUri, webrtc::RtpExtension::kAudioLevelDefaultId));
  if (webrtc::field_trial::IsEnabled("WebRTC-Audio-SendSideBwe")) {
    capabilities.header_extensions.push_back(webrtc::RtpExtension(
        webrtc::RtpExtension::kTransportSequenceNumberUri, webrtc::RtpExtension::kTransportSequenceNumberDefaultId));
  }
  return capabilities;
}
const char RtpExtension::kAudioLevelUri[] = "urn:ietf:params:rtp-hdrext:ssrc-audio-level";
const int RtpExtension::kAudioLevelDefaultId = 1;
const char RtpExtension::kTransportSequenceNumberUri[] = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01";
const int RtpExtension::kTransportSequenceNumberDefaultId = 5;

视频部分的 RTP 头扩展信息构造如下:

RtpCapabilities WebRtcVideoEngine::GetCapabilities() const {
  RtpCapabilities capabilities;
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kTimestampOffsetUri,  webrtc::RtpExtension::kTimestampOffsetDefaultId));
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kAbsSendTimeUri, webrtc::RtpExtension::kAbsSendTimeDefaultId));
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kVideoRotationUri, webrtc::RtpExtension::kVideoRotationDefaultId));
  capabilities.header_extensions.push_back(webrtc::RtpExtension(
      webrtc::RtpExtension::kTransportSequenceNumberUri, webrtc::RtpExtension::kTransportSequenceNumberDefaultId));
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kPlayoutDelayUri, webrtc::RtpExtension::kPlayoutDelayDefaultId));
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kVideoContentTypeUri, webrtc::RtpExtension::kVideoContentTypeDefaultId));
  capabilities.header_extensions.push_back(
        webrtc::RtpExtension(webrtc::RtpExtension::kVideoTimingUri, webrtc::RtpExtension::kVideoTimingDefaultId));
  return capabilities;
}

在 MediaSessionDescriptionFactory::CreateOffer() 函数中,GetRtpHdrExtsToOffer() 获取音频和视频 RTP 头扩展信息,作为 offer 端会话描述 的一部分。

4.为offer sdp 添加音视频内容

接下来将所有与音频、视频和数据相关的描述信息添加到 offer 端会话描述中。下面以 AddAudioContentForOffer() 为例分析:

bool MediaSessionDescriptionFactory::AddAudioContentForOffer(
    const MediaDescriptionOptions& media_description_options,
    const MediaSessionOptions& session_options,
    const ContentInfo* current_content,
    const SessionDescription* current_description,
    const RtpHeaderExtensions& audio_rtp_extensions,
    const AudioCodecs& audio_codecs,
    StreamParamsVec* current_streams,
    SessionDescription* desc) const;

函数执行步骤大致如下:
(1)GetAudioCodecsForOffer() 函数根据接收还是发送的方向信息,返回音频发送编码或是音频接收编码,或者两者的并集;
(2)根据 current_contentsupported_audio_codecs 来对 audio_codecs 进行过滤,将 audio_codecs 中同时匹配前面两个条件的音频编码存入 filtered_codecs
(3)获取支持的音频会话描述加密套件名字,GetSupportedAudioSdesCryptoSuiteNames();

void GetSupportedAudioSdesCryptoSuites(const rtc::CryptoOptions& crypto_options,
                                       std::vector<int>* crypto_suites) {
  if (crypto_options.enable_gcm_crypto_suites) {
    crypto_suites->push_back(rtc::SRTP_AEAD_AES_256_GCM);
    crypto_suites->push_back(rtc::SRTP_AEAD_AES_128_GCM);
  }
  crypto_suites->push_back(rtc::SRTP_AES128_CM_SHA1_32);
  crypto_suites->push_back(rtc::SRTP_AES128_CM_SHA1_80);
}

(4)将前面过滤得到的 filtered_codecs 、加密套件名字以及其他信息,通过 CreateMediaContentOffer() 组装到 AudioContentDescription 对象中;
(5)通过 desc->AddContent()AudioContentDescription 对象添加到 SessionDescription 对象 desc 中;
(6)通过 AddTransportOffer() 将传输相关的描述信息加入到 offer 端的描述信息中,其中核心函数是TransportDescriptionFactory::CreateOffer(),具体代码如下:

TransportDescription* TransportDescriptionFactory::CreateOffer(
    const TransportOptions& options,
    const TransportDescription* current_description) const {
  std::unique_ptr<TransportDescription> desc(new TransportDescription());

  // Generate the ICE credentials if we don't already have them.
  if (!current_description || options.ice_restart) {
    desc->ice_ufrag = rtc::CreateRandomString(ICE_UFRAG_LENGTH);
    desc->ice_pwd = rtc::CreateRandomString(ICE_PWD_LENGTH);
  } else {
    desc->ice_ufrag = current_description->ice_ufrag;
    desc->ice_pwd = current_description->ice_pwd;
  }
  desc->AddOption(ICE_OPTION_TRICKLE);
  if (options.enable_ice_renomination) {
    desc->AddOption(ICE_OPTION_RENOMINATION);
  }

  // If we are trying to establish a secure transport, add a fingerprint.
  if (secure_ == SEC_ENABLED || secure_ == SEC_REQUIRED) {
    // Fail if we can't create the fingerprint.
    // If we are the initiator set role to "actpass".
    if (!SetSecurityInfo(desc.get(), CONNECTIONROLE_ACTPASS)) {
      return NULL;
    }
  }

  return desc.release();
}

到这里就把所有音频相关的会话描述信息添加到 offer 端的会话描述信息里了。

5.更新传输参数

如果 session_options.bundle_enabled 为 true,通过 UpdateTransportInfoForBundle()UpdateCryptoParamsForBundle()更新传输描述信息。所谓 bundle 就是将多条信息捆绑起来,也可以理解成合并。

static bool UpdateTransportInfoForBundle(const ContentGroup& bundle_group, SessionDescription* sdesc);

这个函数所做的工作就是,根据 bundle_group 更新 sdesc 中的 transport infos 。更新规则是:如果 transport info 的 content name 属于 bundle_group,那么这个 transport info 的 ufrag, pwd and DTLS role 信息需要被修改为 bundle_group 第一个元素对应的sdesc 中的 transport info 的对应值。
简单来说,就是将多组 ufrag, pwd and DTLS role 信息根据 bundle_group 合并到一组,部分 transport info 的 ufrag, pwd and DTLS role 信息会被选定 transport info 的信息覆盖。

至此,offer 端的会话描述信息就已经全部创建完毕。

6.小结

会话描述信息的收集过程比较繁杂,前面介绍了发送端会话描述信息创建的大致过程,其中有些细节被忽略了,有疏漏的地方以后再完善。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值