本文所指的 webrtc 代码位于 chromium(64) 的第三方仓库中,webrtc 更新比较频繁,所以不同的版本代码可能改动较大。
1. 会话建立标准流程(需要 NAT 穿透)
在通过 webrtc 实现点对点的连接时,需要遵循如下流程,
图片来自
1. Client A 创建一个 PeerConnection 对象,然后打开本地音视频设备,将音视频数据封装成 MediaStream 添加到 PeerConnection 中;
2. Client A 调用 PeerConnection 的CreateOffer 方法创建一个 offer 的 SDP 对象,SDP 对象中保存当前音视频的相关参数;
3. Client A 调用 SetLocalDescription 方法将 SDP 保存起来;
4. Client A 通过信令机制将自己的 SDP 发送给 Client B;
5. Client B 接收到 Client A 发送过来的 offer SDP对象,通过自己的 PeerConnection 的 SetRemoteDescription 方法将其保存起来;
6. Client B 调用 PeerConnection 的 CreateAnswer 方法创建一个应答的 SDP 对象;
7. Client B 调用 SetLocalDescription 方法保存 answer SDP 对象;
8. Client B 通过信令机制将自己的 SDP 发送给 Client A;
9. Client A 调用 PeerConnection 对象的 SetRemoteDescription 方法保存 Client B 发来的 SDP;
10. 在 SDP 信息的 offer/answer 流程中,Client A 和Client B 已经根据 SDP 信息创建好相应的音频Channel和视频Channel 并开启 Candidate 数据的收集,Candidate 数据可以简单地理解成 Client 端的 IP 地址信息(本地 IP 地址、公网IP地址、Relay 服务端分配的地址);
11. Client A 收集到自己的 Candidate 信息后,PeerConnection 会通过 OnIceCandidate 接口给 Client A 发送通知;
12. Client A 将收集到的 Candidate 信息通过信令机制发送给 Client B;
13. Client B 通过 PeerConnection 的 AddIceCandidate 方法将 Client A 的 Candidate 保存起来;
14. Client B 收集自己的 Candidate 信息,PeerConnection 通过 OnIceCandidate 接口给 Client B 发送通知;
15. Client B 将收集到的 Candidate 信息通过信令机制发送给 Client A;
16. Client A 通过 PeerConnection 的 AddIceCandidate 方法将 Client B Candidate 保存起来。
至此,Client A 和 Client B 就建立了点对点的连接。
备注:通信双方都必须有自己独立的 PeerConnection 对象。
2.接收端会话建立相关代码
Client B 设置远端会话描述信息
void PeerConnection::SetRemoteDescription(
std::unique_ptr<SessionDescriptionInterface> desc,
rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer) {
...
// Find all audio rtp streams and create corresponding remote AudioTracks
// and MediaStreams.
if (audio_content) {
if (audio_content->rejected) {
RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
} else {
bool default_audio_track_needed =
!remote_peer_supports_msid_ &&
MediaContentDirectionHasSend(audio_desc->direction());
UpdateRemoteSendersList(GetActiveStreams(audio_desc),
default_audio_track_needed, audio_desc->type(),
new_streams);
}
}
...
}
其中,UpdateRemoteSendersList 函数用于添加远端媒体流的信息。
rtc::scoped_refptr<MediaStreamInterface> stream =
remote_streams_->find(stream_label);
if (!stream) {
// This is a new MediaStream. Create a new remote MediaStream.
stream = MediaStreamProxy::Create(rtc::Thread::Current(),
MediaStream::Create(stream_label));
remote_streams_->AddStream(stream);
new_streams->AddStream(stream);
}
如果在 remote_streams_ 变量里没有找到 stream_label 标记的流,那么说明新到来的 会话描述信息尚未记录,需要根据 stream_label 创建一个新的流,并添加到 remote_streams_ 里。
Client B 创建自己的会话描述信息
Client B 作为接受连接的一端,通过 PeerConnection 的 CreateAnswer 方法开始创建自己的会话描述信息,代码位置src\third_party\webrtc\pc\peerconnection.cc ,代码如下:
void PeerConnection::CreateAnswer(CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options) {
...
webrtc_session_desc_factory_->CreateAnswer(observer, session_options);
}
函数内部 CreateAnswer 调用 WebRtcSessionDescriptionFactory 类的 CreateAnswer 方法,跟踪下去,最终进入 MediaSessionDescriptionFactory 的 CreateAnswer 方法完成最终的 answer SDP 创建。
Client B 发送自己的 answer SDP
在完成 answer SDP 创建后,会通过信令机制将 answer SDP 发送给 Client A,代码位置:src\third_party\webrtc\pc\webrtcsessiondescriptionfactory.cc,发送的代码如下:
void WebRtcSessionDescriptionFactory::InternalCreateAnswer(
CreateSessionDescriptionRequest request) {
...
cricket::SessionDescription* desc(session_desc_factory_.CreateAnswer(
pc_->remote_description() ? pc_->remote_description()->description()
: nullptr,
request.options,
pc_->local_description() ? pc_->local_description()->description()
: nullptr));
...
// 完成 answer SDP 的创建,并通过信令机制发送给 Client A
PostCreateSessionDescriptionSucceeded(request.observer, answer);
}
进入 PostCreateSessionDescriptionSucceeded 函数,其实现为:
void WebRtcSessionDescriptionFactory::PostCreateSessionDescriptionSucceeded(
CreateSessionDescriptionObserver* observer,
SessionDescriptionInterface* description) {
CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer);
msg->description.reset(description);
signaling_thread_->Post(RTC_FROM_HERE, this,
MSG_CREATE_SESSIONDESCRIPTION_SUCCESS, msg);
}
Client B 将 Client A 的 candidate 信息保存起来
bool PeerConnection::AddIceCandidate(
const IceCandidateInterface* ice_candidate) {
...
// Add this candidate to the remote session description.
if (!mutable_remote_description()->AddCandidate(ice_candidate)) {
RTC_LOG(LS_ERROR) << "ProcessIceMessage: Candidate cannot be used.";
return false;
}
...
}
真正执行添加 Candidate 操作的是 JsepSessionDescription 的 AddCandidate 方法:
bool JsepSessionDescription::AddCandidate(
const IceCandidateInterface* candidate) {
...
std::unique_ptr<JsepIceCandidate> updated_candidate_wrapper(
new JsepIceCandidate(candidate->sdp_mid(),
static_cast<int>(mediasection_index),
updated_candidate));
if (!candidate_collection_[mediasection_index].HasCandidate(
updated_candidate_wrapper.get())) {
candidate_collection_[mediasection_index].add(
updated_candidate_wrapper.release());
UpdateConnectionAddress(
candidate_collection_[mediasection_index],
description_->contents()[mediasection_index].description);
}
...
}
在添加 Client A 发送过来的 Candidate 信息后,执行 UpdateConnectionAddress 方法:
// Update the connection address for the MediaContentDescription based on the
// candidates.
static void UpdateConnectionAddress(
const JsepCandidateCollection& candidate_collection,
cricket::ContentDescription* content_description) {
...
rtc::SocketAddress connection_addr;
connection_addr.SetIP(ip);
connection_addr.SetPort(port);
static_cast<cricket::MediaContentDescription*>(content_description)
->set_connection_address(connection_addr);
...
}
3. stun服务器
3.1 非同一局域网
如果两台机器处于不同的局域网中,要实现点对点的通信,必须借助于 stun 服务器,webrtc 中给出的例子(src\third_party\webrtc\examples\peerconnection\client\defaults.cc)中给出了一个 stun 服务器的网址,在可以连接外网的情况下能够使用。
stun:stun.l.google.com:19302
网上有网友整理了可用的
stun 服务器
地址,粘贴如下:
{url:'stun:stun01.sipphone.com'},
{url:'stun:stun.ekiga.net'},
{url:'stun:stun.fwdnet.net'},
{url:'stun:stun.ideasip.com'},
{url:'stun:stun.iptel.org'},
{url:'stun:stun.rixtelecom.se'},
{url:'stun:stun.schlund.de'},
{url:'stun:stun.l.google.com:19302'},
{url:'stun:stun1.l.google.com:19302'},
{url:'stun:stun2.l.google.com:19302'},
{url:'stun:stun3.l.google.com:19302'},
{url:'stun:stun4.l.google.com:19302'},
{url:'stun:stunserver.org'},
{url:'stun:stun.softjoys.com'},
{url:'stun:stun.voiparound.com'},
{url:'stun:stun.voipbuster.com'},
{url:'stun:stun.voipstunt.com'},
{url:'stun:stun.voxgratia.org'},
{url:'stun:stun.xten.com'},
{
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc@live.com'
},
{
url: 'turn:192.158.29.39:3478?transport=udp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
},
{
url: 'turn:192.158.29.39:3478?transport=tcp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
}
3.2 同一局域网
当两台机器处于同一局域网中时,不需要借助 stun 服务器,信令交换后就已经可以传递媒体流了。
参考:
Android IOS WebRTC 音视频开发总结(九)– webrtc入门001
WebRTC手记之初探
P2P通信标准协议(三)之ICE
WebRTC 实现Android点到点互连(含Demo)