WebRTC example peerconnection剖析

2023-10-11 01:10

本文主要是介绍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

messagePOSTpeerid发送消息给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

waitGET等待消息

GET /wait?peer_id=%i HTTP/1.0\r\n\r\n

sign_outGET退出登录

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剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/184440

相关文章

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

雷动WEBRTC产品

http://www.rtcpower.com/html/leidongwebrtc.html ; 1.前言      WebRTC是一项在浏览器内部进行实时视频和音频通信的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得一项技术。WebRTC实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以

Apple quietly slips WebRTC audio, video into Safari's WebKit spec

转自:http://www.zdnet.com/article/apple-quietly-slips-webrtc-audio-video-into-safaris-webkit-spec/?from=timeline&isappinstalled=0 http://www.zdnet.com/article/apple-quietly-slips-webrtc-audio-video-

WebRTC-nack机制详解

1.NACK的含义 丢包重传(NACK)是抵抗网络错误的重要手段。NACK在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端,发送缓冲区的支持。 WebRTC中支持音频和视频的NACK重传。我们这里只分析nack机制,不分析jitterbuffer或者neteq的更多实现。 2.WebRTC

深度剖析AI情感陪伴类产品及典型应用 Character.ai

前段时间AI圈内C.AI的受够风波可谓是让大家都丈二摸不着头脑,连C.AI这种行业top应用都要找谋生方法了!投资人摸不着头脑,用户们更摸不着头脑。在这之前断断续续玩了一下这款产品,这次也是乘着这个风波,除了了解一下为什么这么厉害的创始人 Noam Shazeer 也要另寻他路,以及产品本身的发展阶段和情况! 什么是Character.ai? Character.ai官网:https://

最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)

文章目录 一、自动配置概念二、半自动配置(误~🙏🙏)三、源码分析1、验证DispatcherServlet的自动配置2、源码分析入口@SpringBootApplication3、@SpringBootConfiguration的@Configuration4、@EnableAutoConfiguration的@AutoConfigurationPackage和@Import5、Auto

C语言深度剖析--不定期更新的第四弹

哈哈哈哈哈哈,今天一天两更! void关键字 void关键字不能用来定义变量,原因是void本身就被编译器解释为空类型,编译器强制地不允许定义变量 定义变量的本质是:开辟空间 而void 作为空类型,理论上不应该开辟空间(针对编译器而言),即使开辟了空间,也只是作为一个占位符看待(针对Linux来说) 所以,既然无法开辟空间,也无法作为正常变量使用,既然无法使用,干脆编译器不让它编译变

Java CAS 原理剖析

在Java并发中,我们最初接触的应该就是synchronized关键字了,但是synchronized属于重量级锁,很多时候会引起性能问题,volatile也是个不错的选择,但是volatile不能保证原子性,只能在某些场合下使用。   像synchronized这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正

STL源码剖析之【二分查找】

ForwardIter lower_bound(ForwardIter first, ForwardIter last,const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置。      ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val

深入剖析 Redis 基础及其在 Java 应用中的实战演练

引言 在现代分布式系统和高并发应用中,缓存系统是不可或缺的一环,而 Redis 作为一种高性能的内存数据存储以其丰富的数据结构和快速的读写性能,成为了众多开发者的首选。本篇博客将详细介绍 Redis 的基础知识,并通过 Java 代码演示其在实际项目中的应用。 目录 什么是 Redis?Redis 数据结构详解Redis 与其他 NoSQL 数据库的对比在 Java 中使用 Redis 使用