系列文章目录
Webrtc从理论到实践一:初识
Webrtc从理论到实践二: 架构
Webrtc从理论到实践三: 角色
Webrtc从理论到实践四: 通信
Webrtc从理论到实践五: 编译webrtc源码
Webrtc从理论到实践六: Webrtc官方demo运行
Webrtc从理论到实践七: 官方demo源码走读(peerconnection_server)
文章目录
前言
本文源码基于webrtc m89版本,分析peerconnection_client 源码。相较于peerconnection_server,client的处理逻辑要更复杂的多,它除了要处理信令之外,还要进行控制界面,渲染视频,协商媒体信息等一系列工作。
一、peerconnection_client目录结构
简单介绍一下各个文件所承担的功能:
- main.cc,完成一些初始化以及主流程的控制工作
- main_wnd.cc,负责Windows操作界面的控制以及视频的渲染工作
- peer_connection_client.cc,负责信令的收发工作
- conductor.cc,利用Webrtc库实现音视频通信。媒体协商,音视频数据的采集以及从track中读取视频数据进行渲染等。
- defaults.h中保存了诸如GetPeerConnectionString(),GetDefaultServerName()和GetPeerName()等几个常用函数。
二、PeerConnection_client类图
接着我们再从类的角度来介绍一下client几个主要类的功能和关系。
- MainWindow接口,主要定义了两类接口,一类用于操作界面的切换,如SwitchToConnectUI(),另一类用于视频渲染,比如:StartLocalRenderer(webrtc::VideoTrackInterface* local_video);
- MainWnd类,继承了MainWindow类并且实现了MainWindow的所有接口,此外MainWnd也承担了视频渲染的工作,所以内部包含了VideoRenderer对象。
- VideoRender类继承自VideoSink类,可以获取到视频帧,并且转化成BITMAP图像交给MainWnd渲染。
- PeerConnectionClient类,用于信令处理与收发,如sign_in,sign_out,message和wait信令。
- Conductor类继承了MainWndCallback和PeerConnectionObserver类,可以接收来自MainWnd和PeerConnectionClient类的通知消息。
三、时序图
接下来,我们对照上面的时序图结合源码来介绍一下client的实现原理.
MainWnd对象创建
在wWinMain函数中首先先去解析命令行参数获取服务端的Ip和Port,如果没有指定,则传递默认值“localhost”和8888。
WindowsCommandLineArguments win_args;
int argc = win_args.argc();
char** argv = win_args.argv();
absl::ParseCommandLine(argc, argv);
// InitFieldTrialsFromString stores the char*, so the char array must outlive
// the application.
const std::string forced_field_trials =
absl::GetFlag(FLAGS_force_fieldtrials);
webrtc::field_trial::InitFieldTrialsFromString(forced_field_trials.c_str());
// Abort if the user specifies a port that is outside the allowed
// range [1, 65535].
if ((absl::GetFlag(FLAGS_port) < 1) || (absl::GetFlag(FLAGS_port) > 65535)) {
printf("Error: %i is not a valid port.\n", absl::GetFlag(FLAGS_port));
return -1;
}
const std::string server = absl::GetFlag(FLAGS_server);
接着创建MainWnd对象,然后调用其Create()方法创建一个Windows窗口,
MainWnd wnd(server.c_str(), absl::GetFlag(FLAGS_port),
absl::GetFlag(FLAGS_autoconnect), absl::GetFlag(FLAGS_autocall));
if (!wnd.Create()) {
RTC_NOTREACHED();
return -1;
}
我们可以进入到Create()函数中来具体看一下进行了哪些操作,除了常规的注册窗体类,创建窗体之外还包含了CreateChildWindows()创建各种子控件,比如标签,编辑框,按钮等。最后是切换到待连接的界面。
bool MainWnd::Create() {
RTC_DCHECK(wnd_ == NULL);
if (!RegisterWindowClass())
return false;
ui_thread_id_ = ::GetCurrentThreadId();
wnd_ =
::CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, kClassName, L"WebRTC",
WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, GetModuleHandle(NULL), this);
::SendMessage(wnd_, WM_SETFONT, reinterpret_cast<WPARAM>(GetDefaultFont()),
TRUE);
CreateChildWindows();
SwitchToConnectUI();
return wnd_ != NULL;
}
PeerConnectionClient和Conductor对象创建
当主界面创建好之后,就会创建PeerConnectionClient对象为连接信令服务器做准备。最后创建Conductor对象,并且将MainWnd和PeerConnectionClient对象注册其中。
PeerConnectionClient client;
rtc::scoped_refptr<Conductor> conductor(
new rtc::RefCountedObject<Conductor>(&client, &wnd));
Conductor对象是整个程序的核心,它既需要从Webrtc库中获取视频帧交给MainWnd对象渲染,又需要将自己产生的SDP信息,Candidate信息等交给PeerConnectionClient发给服务器。它还是这两个对象的观察者,这两个对象产生的事件都能及时通知它以便做进一步的处理。
开启Windows事件循环
当所有的对象都创建好之后,就会开启一个Windows事件循环,不断地从消息队列中取事件然后分发到接收消息的窗口。
MSG msg;
BOOL gm;
while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) {
if (!wnd.PreTranslateMessage(&msg)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
//clear msgs
if (conductor->connection_active() || client.is_connected()) {
while ((conductor->connection_active() || client.is_connected()) &&
(gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) {
if (!wnd.PreTranslateMessage(&msg)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
}
不过,这里要稍微提一下的是,可以看到程序里面是需要做了一层消息过滤,处理逻辑是在Mainwnd的PreTranslateMessage()函数中,我们可以来看一下具体做了哪些过滤。主要包含了对一些键盘事件的处理,此外就是对于自定义事件UI_THREAD_CALLBACK的处理。
bool MainWnd::PreTranslateMessage(MSG* msg) {
bool ret = false;
if (msg->message == WM_CHAR) {
if (msg->wParam == VK_TAB) {
HandleTabbing();
ret = true;
} else if (msg->wParam == VK_RETURN) {
OnDefaultAction();
ret = true;
} else if (msg->wParam == VK_ESCAPE) {
if (callback_) {
if (ui_ == STREAMING) {
callback_->DisconnectFromCurrentPeer();
} else {
callback_->DisconnectFromServer();
}
}
}
} else if (msg->hwnd == NULL && msg->message == UI_THREAD_CALLBACK) {
callback_->UIThreadCallback(static_cast<int>(msg->wParam),
reinterpret_cast<void*>(msg->lParam));
ret = true;
}
return ret;
}
点击Connect按钮
当用户点击Connect按钮之后,MainWnd就会接收到该消息,然后在消息处理函数OnMessage()函数回调Conductor的StartLogin()方法。
bool MainWnd::OnMessage(UINT msg,WPARAM wp,LPARAM lp,LRESULT* result){
switch (msg) {
case XXX:
//do something
break;
case WM_COMMAND:
if (button_ == reinterpret_cast<HWND>(lp)) {
if (BN_CLICKED == HIWORD(wp))
OnDefaultAction();
} else if (listbox_ == reinterpret_cast<HWND>(lp)) {
if (LBN_DBLCLK == HIWORD(wp)) {
OnDefaultAction();
}
}
return true;
}
}
void MainWnd::OnDefaultAction() {
if (!callback_)
return;
if (ui_ == CONNECT_TO_SERVER) {
std::string server(GetWindowText(edit1_));
std::string port_str(GetWindowText(edit2_));
int port = port_str.length() ? atoi(port_str.c_str()) : 0;
callback_->StartLogin(server, port);
} else if (ui_ == LIST_PEERS) {
LRESULT sel = ::SendMessage(listbox_, LB_GETCURSEL, 0, 0);
if (sel != LB_ERR) {
LRESULT peer_id = ::SendMessage(listbox_, LB_GETITEMDATA, sel, 0);
if (peer_id != -1 && callback_) {
callback_->ConnectToPeer(peer_id);
}
}
} else {
::MessageBoxA(wnd_, "OK!", "Yeah", MB_OK);
}
}
StartLogin()内部调用的是PeerConnectionClient的Connect()方法,最终调用的是DoConnect()方法。
void PeerConnectionClient::Connect(const std::string& server,
int port,
const std::string& client_name) {
//do some pre-check
...
server_address_.SetIP(server);
server_address_.SetPort(port);
client_name_ = client_name;
//if server_address_ is not a ip,parse to ip async
if (server_address_.IsUnresolvedIP()) {
state_ = RESOLVING;
resolver_ = new rtc::AsyncResolver();
resolver_->SignalDone.connect(this, &PeerConnectionClient::OnResolveResult);
resolver_->Start(server_address_);
} else {
//otherwise connect directly
DoConnect();
}
}
DoConnect()方法中使用了AsyncSocket发送一个Get请求“/sign_in”来登录服务器。
void PeerConnectionClient::DoConnect() {
control_socket_.reset(CreateClientSocket(server_address_.ipaddr().family()));
hanging_get_.reset(CreateClientSocket(server_address_.ipaddr().family()));
InitSocketSignals();
char buffer[1024];
snprintf(buffer, sizeof(buffer), "GET /sign_in?%s HTTP/1.0\r\n\r\n",
client_name_.c_str());
onconnect_data_ = buffer;
bool ret = ConnectControlSocket();
if (ret)
state_ = SIGNING_IN;
if (!ret) {
callback_->OnServerConnectionFailure();
}
}
当客户端连接成功之后就会切换到下面这个界面,因为此时只有一个客户端连接了,所以list中没有对端信息显示。
这里讲一下客户端处理服务端响应的代码。首先,先要说一下异步socket的信号与槽机制,在前面DoConnect()中调用了InitSocketSignals()这个函数,在函数内部初始化了两个socket,control_socket_负责收发client发起的信令,而hanging_get_负责对端client通过server转发的信令,如果只用一个socket来处理的话,那么如果两种信令需要在同一时间处理,只用一个的话可能出现处理异常。然后将关闭,连接,可读事件与相对应的槽函数的,所以当服务端返回登录响应之后,会触发control_socket连接的OnRead()函数
void PeerConnectionClient::InitSocketSignals() {
RTC_DCHECK(control_socket_.get() != NULL);
RTC_DCHECK(hanging_get_.get() != NULL);
control_socket_->SignalCloseEvent.connect(this,
&PeerConnectionClient::OnClose);
hanging_get_->SignalCloseEvent.connect(this, &PeerConnectionClient::OnClose);
control_socket_->SignalConnectEvent.connect(this,
&PeerConnectionClient::OnConnect);
hanging_get_->SignalConnectEvent.connect(
this, &PeerConnectionClient::OnHangingGetConnect);
control_socket_->SignalReadEvent.connect(this, &PeerConnectionClient::OnRead);
hanging_get_->SignalReadEvent.connect(
this, &PeerConnectionClient::OnHangingGetRead);
}
OnRead()函数中主要做了以下几件事:1.保存服务端分配给自己的peer_id 2.在onSignedIn()中将UI切换到展示对端列表界面。3.使用hanging_get_ 连接服务器,并且发送“GET /wait?peer_id=%i HTTP/1.0\r\n\r\n"请求并等待新的对端加入。
void PeerConnectionClient::OnRead(rtc::AsyncSocket* socket) {
size_t content_length = 0;
if (ReadIntoBuffer(socket, &control_data_, &content_length)) {
size_t peer_id = 0, eoh = 0;
//解析服务端响应结果是不是200
bool ok =
ParseServerResponse(control_data_, content_length, &peer_id, &eoh);
if (ok) {
if (my_id_ == -1) {
//do some check
...
//1. 保存服务端分配的peer_id,这是一个递增的整数
my_id_ = static_cast<int>(peer_id);
// The body of the response will be a list of already connected peers.
if (content_length) {
size_t pos = eoh + 4;
while (pos < control_data_.size()) {
size_t eol = control_data_.find('\n', pos);
if (eol == std::string::npos)
break;
int id = 0;
std::string name;
bool connected;
//从control_data中解析出name,id和connected的值,
//如果id和自己的my_id不相同则表示对端已经连接,这个时候会保存id和name到peers_id中.
//然后会在OnPeerConnected()中将一个新的item添加到listbox中
if (ParseEntry(control_data_.substr(pos, eol - pos), &name, &id,
&connected) &&
id != my_id_) {
peers_[id] = name;
callback_->OnPeerConnected(id, name);
}
pos = eol + 1;
}
}
//do some check
...
//2.切换到peerlist界面
callback_->OnSignedIn();
}
control_data_.clear();
//3.使用hanging_get_ 连接服务器
if (state_ == SIGNING_IN) {
RTC_DCHECK(hanging_get_->GetState() == rtc::Socket::CS_CLOSED);
state_ = CONNECTED;
hanging_get_->Connect(server_address_);
}
}
}
}
void Conductor::OnSignedIn() {
RTC_LOG(INFO) << __FUNCTION__;
main_wnd_->SwitchToPeerList(client_->peers());
}
当收到对端加入的信令后就会触发hanging_get_的OnHangingGetRead()函数,并且更新peerList界面,如下图:
void PeerConnectionClient::OnHangingGetRead(rtc::AsyncSocket* socket) {
size_t content_length = 0;
if (ReadIntoBuffer(socket, ¬ification_data_, &content_length)) {
size_t peer_id = 0, eoh = 0;
bool ok =
ParseServerResponse(notification_data_, content_length, &peer_id, &eoh);
if (ok) {
// Store the position where the body begins.
size_t pos = eoh + 4;
if (my_id_ == static_cast<int>(peer_id)) {
// A notification about a new member or a member that just
// disconnected.
int id = 0;
std::string name;
bool connected = false;
if (ParseEntry(notification_data_.substr(pos), &name, &id,
&connected)) {
if (connected) {
peers_[id] = name;
callback_->OnPeerConnected(id, name);
} else {
peers_.erase(id);
callback_->OnPeerDisconnected(id);
}
}
} else {
OnMessageFromPeer(static_cast<int>(peer_id),
notification_data_.substr(pos));
}
}
notification_data_.clear();
}
if (hanging_get_->GetState() == rtc::Socket::CS_CLOSED &&
state_ == CONNECTED) {
hanging_get_->Connect(server_address_);
}
}
总结
上篇暂时先讲到这里,下篇会对于双击对端之后所触发的事件结合源码带大家继续熟悉通信流程。喜欢我内容的,可以关注我或者收藏这篇文章谢谢。