本文主要是介绍WebRTC example peerconnection剖析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
一. 前言
二. peerconnection example使用说明
三. peerconnection_server
四. peerconnection_client
1. 类图关系介绍
2. 程序时序图
a. 创建MainWnd,PeerConnectionClient,Conductor类对象
b. 点击Connect连接信令服务器
c. 媒体协商
d. PeerConnection说明
e. ICE
f. 接收对端媒体流
一. 前言
WebRTC 提供了许多 example,本文介绍 peerconnection example 的使用与代码流程分析,通过该示例可以帮助理解 WebRTC 信令交互,媒体协商,音视频数据交互等流程。
该示例涉及两个角色:peerconnection_server,peerconnection_client。peerconnection_server 是一个信令服务器,它负责连接成员的管理以及信令交互与转发。peerconnection_client 是一个用于通话的客户端程序,它负责与信令服务器交互以及音视频的采集,编码,打包发送,接收,渲染。
二. peerconnection example使用说明
1. 运行 peerconnection_server.exe,启动后界面如下。
2. 在机器 1 运行 peerconnection_client.exe,输入服务器 IP 和端口后点击 Connect,完成后页面如下,List Of currently connected peers 会显示其他已加入的 peer(此时还没有其他 peer 加入)。
3. 在机器 2 同样运行 peerconnection_client.exe,输入服务器 IP 和端口后点击 Connect,此时机器 1 和机器 2 上可以看到对端 peer。
4. 双击 peer,即可开启音视频通话。
三. peerconnection_server
peerconnection_server 是一个 select 模型的高并发服务器,它负责连接管理和信令消息中转,信令是使用 HTTP 短连接的方式,主要有 sign_in,message,wait,sign_out 信令,信令时序和作用如下所示。
信令 | 方法 | 含义 | 示例 |
sign_in | GET | 登入 | GET /sign_in?%s HTTP/1.0\r\n\r\n |
message | POST | peerid发送消息给to | POST /message?peer_id=%i&to=%i HTTP/1.0\r\n Content-Length: %zu\r\n Content-Type: text/plain\r\n\r\n |
wait | GET | 等待消息 | GET /wait?peer_id=%i HTTP/1.0\r\n\r\n |
sign_out | GET | 退出登录 | GET /sign_out?peer_id=%i HTTP/1.0\r\n\r\n |
四. peerconnection_client
1. 类图关系介绍
peerconnection_client 是一个用于通话的客户端,核心类为 Conductor,它主要包含了 MainWindow 和 PeerConnectionClient 两个核心类对象(实际上它是一个中转调用的对象),前者是处理 Windows 窗口和消息的类,后者是与信令服务器交互的类,类关系图如下。
2. 程序时序图
a. 创建MainWnd,PeerConnectionClient,Conductor类对象
peerconnection_client 程序时序图如上所示,首先创建 MainWnd 对象,调用 MainWnd::Create 函数注册窗口类,然后创建窗口,显示窗口,接下来创建 PeerConnectionClient 对象,并使用 MainWnd 和 PeerConnectionClient 对象初始化 Conductor。
int PASCAL wWinMain(HINSTANCE instance,HINSTANCE prev_instance,wchar_t* cmd_line,int cmd_show) {// 省略...const std::string server = absl::GetFlag(FLAGS_server);MainWnd wnd(server.c_str(), absl::GetFlag(FLAGS_port),absl::GetFlag(FLAGS_autoconnect), absl::GetFlag(FLAGS_autocall));if (!wnd.Create()) {RTC_NOTREACHED();return -1;}rtc::InitializeSSL();PeerConnectionClient client;rtc::scoped_refptr<Conductor> conductor(new rtc::RefCountedObject<Conductor>(&client, &wnd));// Main loop.MSG msg;BOOL gm;while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) {if (!wnd.PreTranslateMessage(&msg)) {::TranslateMessage(&msg);::DispatchMessage(&msg);}}// 省略...return 0;
}
MainWnd 窗口创建的主要逻辑:RegisterWindowClass 注册窗口类和消息处理函数,CreateWindowExW 创建窗口,CreateChildWindows 创建子窗口,SwitchToConnectUI 切换到进行连接服务器的窗口页面。
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;
}
bool MainWnd::RegisterWindowClass() {if (wnd_class_)return true;WNDCLASSEXW wcex = {sizeof(WNDCLASSEX)};wcex.style = CS_DBLCLKS;wcex.hInstance = GetModuleHandle(NULL);wcex.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW);wcex.lpfnWndProc = &WndProc;wcex.lpszClassName = kClassName;wnd_class_ = ::RegisterClassExW(&wcex);RTC_DCHECK(wnd_class_ != 0);return wnd_class_ != 0;
}
GetMessage,wnd.PreTranslateMessage,TranslateMessage,DispatchMessage 是窗口消息获取,消息转换,消息分发的框架代码,有了它我们可以获取窗口的消息事件,并进行相应的回调操作。
// Main loop.
MSG msg;
BOOL gm;
while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) {if (!wnd.PreTranslateMessage(&msg)) {::TranslateMessage(&msg);::DispatchMessage(&msg);}
}
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;
}
b. 点击Connect连接信令服务器
当窗口捕捉到用户点击 Connect 按键的事件后调用 Conductor->StartLogin 函数,然后调用 PeerConnectionClient->Connect 函数连接信令服务器,Connect 函数是通过 DoConnect 创建 Socket 连接到信令服务器,并注册了 Socket 的回调处理函数。
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();}
}
成功连接后会给 peerconnection_server 发送 sign_in 信令请求登入,并且发送 wait 信令等待 peer_id 的消息。
void PeerConnectionClient::OnConnect(rtc::AsyncSocket* socket) {RTC_DCHECK(!onconnect_data_.empty());size_t sent = socket->Send(onconnect_data_.c_str(), onconnect_data_.length());RTC_DCHECK(sent == onconnect_data_.length());onconnect_data_.clear();
}
void PeerConnectionClient::OnHangingGetConnect(rtc::AsyncSocket* socket) {char buffer[1024];snprintf(buffer, sizeof(buffer), "GET /wait?peer_id=%i HTTP/1.0\r\n\r\n",my_id_);int len = static_cast<int>(strlen(buffer));int sent = socket->Send(buffer, len);RTC_DCHECK(sent == len);
}
c. 媒体协商
媒体协商步骤如上图所示,对于发起端用户首先需要初始化 PeerConnection 连接,然后 createOffer 将其设置到 localDescription 中并发送给被叫端,被叫端用户收到 offer sdp 后也初始化 PeerConnection 连接并将 offer sdp 设置成 remoteDescription,再 createAnswer 创建 answer sdp 并设置到 localDescription 并发送给发起端,发起端收到 answer sdp 后设置到 remoteDescription,至此媒体协商就完成了,下面说明 peerconnection example 中协商协商步骤的实现。
当有其他用户连接到 peerconnection_server 并且成功 sign_in,已加入的用户会收到新用户加入的状态通知(BroadcastChangedState)。
bool PeerChannel::AddMember(DataSocket* ds) {assert(IsPeerConnection(ds));ChannelMember* new_guy = new ChannelMember(ds);Members failures;BroadcastChangedState(*new_guy, &failures);HandleDeliveryFailures(&failures);members_.push_back(new_guy);printf("New member added (total=%s): %s\n",size_t2str(members_.size()).c_str(), new_guy->name().c_str());// Let the newly connected peer know about other members of the channel.std::string content_type;std::string response = BuildResponseForNewMember(*new_guy, &content_type);ds->Send("200 Added", true, content_type, new_guy->GetPeerIdHeader(),response);return true;
}
已加入的用户收到状态通知后会回调 Conductor::OnPeerConnected,执行该函数后会切换到连接用户列表,显示已连接用户。
void Conductor::OnPeerConnected(int id, const std::string& name) {RTC_LOG(INFO) << __FUNCTION__;// Refresh the list if we're showing it.if (main_wnd_->current_ui() == MainWindow::LIST_PEERS)main_wnd_->SwitchToPeerList(client_->peers());
}
当用户在连接列表双击对端用户时会回调 Conductor::ConnectToPeer,该函数执行 InitializePeerConnection 初始化 PeerConnection,然后 createOffer 创建 offer SDP 添加到 PeerConnection 的 localDescription,再把 offer sdp 信息发送给被叫方。(我们把发起呼叫的用户称为主叫方,另外一个用户称为被叫方)
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);}
}
void Conductor::ConnectToPeer(int peer_id) {RTC_DCHECK(peer_id_ == -1);RTC_DCHECK(peer_id != -1);if (peer_connection_.get()) {main_wnd_->MessageBox("Error", "We only support connecting to one peer at a time", true);return;}if (InitializePeerConnection()) {peer_id_ = peer_id;peer_connection_->CreateOffer(this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());} else {main_wnd_->MessageBox("Error", "Failed to initialize PeerConnection", true);}
}
被叫方用户接收 message 消息后执行 InitializePeerConnection 初始化 PeerConnection 连接和添加音视频轨的动作,并把 offer sdp 添加到 remoteDescription,再创建 answer sdp 添加到 PeerConnection localDescription 并把 answer sdp 发送给主叫方,主叫方收到 answer sdp 后将其添加到 PeerConnection localDescription 中。
void PeerConnectionClient::OnMessageFromPeer(int peer_id,const std::string& message) {if (message.length() == (sizeof(kByeMessage) - 1) &&message.compare(kByeMessage) == 0) {callback_->OnPeerDisconnected(peer_id);} else {callback_->OnMessageFromPeer(peer_id, message);}
}
d. PeerConnection说明
如下是创建 PeerConnection 的说明,首先通过 CreatePeerConnectionFactory 创建 PeerConnectionFactoryInterface 工厂对象,通过该对象可以创建 PeerConnectionInterface,LocalMediaStreamInterface,LocalAudio/VideoTrackInterface 等对象。
bool Conductor::InitializePeerConnection() {RTC_DCHECK(!peer_connection_factory_);RTC_DCHECK(!peer_connection_);peer_connection_factory_ = webrtc::CreatePeerConnectionFactory(nullptr /* network_thread */, nullptr /* worker_thread */,nullptr /* signaling_thread */, nullptr /* default_adm */,webrtc::CreateBuiltinAudioEncoderFactory(),webrtc::CreateBuiltinAudioDecoderFactory(),webrtc::CreateBuiltinVideoEncoderFactory(),webrtc::CreateBuiltinVideoDecoderFactory(), nullptr /* audio_mixer */,nullptr /* audio_processing */);if (!peer_connection_factory_) {main_wnd_->MessageBox("Error", "Failed to initialize PeerConnectionFactory",true);DeletePeerConnection();return false;}if (!CreatePeerConnection(/*dtls=*/true)) {main_wnd_->MessageBox("Error", "CreatePeerConnection failed", true);DeletePeerConnection();}AddTracks();return peer_connection_ != nullptr;
}
CreatePeerConnection 对象时传入 SDP 类型,配置是否使用 SRTP 以及 STUN 服务器地址等,PeerConnection 创建完成后需要把音视频轨添加到 PeerConnection 中,通过 CreateAudioTrack 创建音频轨,CreateVideoTrack 创建视频轨,通过 AddTrack 往 PeerConnection 添加轨,本地视频画面需要在本地窗口渲染显示,调用的是 main_wnd_->StartLocalRenderer。
e. ICE
ICE(Interactive Connectivity Establishment)称为交互式连接建立。
主叫和被叫想要通信首先需要知道对端的地址,如果主叫和被叫在同一内网环境,则可以使用内网地址进行通信,如果主叫和被叫不在同一内网环境,则需要通过 STUN 服务器获取自己 NAT 映射后的公网地址,将公网地址发送给对端,对端再进行连通性检查,因此主叫和被叫都需要收集自己的候选地址 candidate 并告知对端,如下是 ICE 候选者收集完成的回调函数。
本端收到对端候选者地址后通过 AddIceCandidate 添加到 PeerConnection,对端收到本端的候选者也是执行同样的流程,之后会再进行连通性检查。
f. 接收对端媒体流
媒体连通性检查完成后就开始发送媒体数据,当收到音视频数据后就回调 OnAddTrack 添加音视频轨,对于视频会调用 main_wnd_->StartRemoteRenderer(video_track) 开启渲染。
这篇关于WebRTC example peerconnection剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!