通过webrtc 点对点会话建立过程分析可以知道 CreateOffer 的具体实现位置在 src\third_party\webrtc\pc\mediasession.cc ,但是 CreateOffer 执行过程中具体经历了什么,还没有进行介绍,接下来将介绍 CreateOffer 究竟创建了什么内容。
在 CreateOffer 中,会获取本地所支持的音视频编码格式,以及传输相关参数信息。函数原型如下:
SessionDescription* MediaSessionDescriptionFactory::CreateOffer(
const MediaSessionOptions& session_options,
const SessionDescription* current_description) const;
参数 session_options
是上层传入的约束,譬如,是否需要音频描述,以及是否加密等。current_description
是当前的会话描述内容,如果是第一次 CreateOffer ,这个值为 nullptr,如果中途因为某些原因需要再次协商会话描述信息,这个值就是有意义的。
在 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(all_audio_codecs_, audio_codecs, &used_pltypes);
MergeCodecs(video_codecs_, video_codecs, &used_pltypes);
MergeCodecs(data_codecs_, data_codecs, &used_pltypes);
}
第一步,执行 clear()
的动作,避免指针指向了无效数据;
第二步,如果 current_description
不为空,也就是不是第一次执行 CreateOffer ,那么执行 MergeCodecsFromDescription
,将current_description
中记录的编码信息存入 offer_xxx_codecs;
第三步,执行 MergeCodecs
,将本地支持的编码格式存入 offer_xxx_codecs。
上面提到本地支持的编码格式,那么这些信息是如何获取的呢?下面会按照调用堆栈的思路来介绍,一直到获取编码格式的源头。
(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 cert_generator,
const rtc::scoped_refptr& 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(
std::move(media_engine_), rtc::MakeUnique(),
worker_thread_, network_thread_);
到此为止,我们已经看到了 ChannelManager
的构造位置,接下来分析其获取编码参数的代码。
(3)音频、视频和数据获取编码参数的方法类似,因此以音频为例:
void ChannelManager::GetSupportedAudioSendCodecs(
std::vector* 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(
std::forward_as_tuple(adm, audio_encoder_factory, audio_decoder_factory,
audio_mixer, audio_processing),
std::move(video_args));
CompositeMediaEngine
是一个类模板,需要传入音频引擎和视频引擎才能成为成为可以构造对象的类,std::pair
分别记录了对应的音频和视频引擎。
virtual const std::vector& 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* 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 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);
}
}
到这里,本端所有的音频发送编码信息就收集完毕了。
到这一步,就完成了本地支持的音频发送编码的收集工作。收集完后,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 的编码。
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 端会话描述 的一部分。
接下来将所有与音频、视频和数据相关的描述信息添加到 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_content
和 supported_audio_codecs
来对 audio_codecs
进行过滤,将 audio_codecs
中同时匹配前面两个条件的音频编码存入 filtered_codecs
;
(3)获取支持的音频会话描述加密套件名字,GetSupportedAudioSdesCryptoSuiteNames()
;
void GetSupportedAudioSdesCryptoSuites(const rtc::CryptoOptions& crypto_options,
std::vector* 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 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 端的会话描述信息里了。
如果 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 端的会话描述信息就已经全部创建完毕。
会话描述信息的收集过程比较繁杂,前面介绍了发送端会话描述信息创建的大致过程,其中有些细节被忽略了,有疏漏的地方以后再完善。