介绍
环境: webrtc m98 、Windows
peerconnection_client 是一个WebRTC提供的示例程序,主要在Windows平台上演示如何使用WebRTC库来实现点对点的实时音频和视频通话。它是一个客户端应用程序,配合 peerconnection_server 信令服务器使用,通过信令服务器进行信令交换,建立并维护两个或者多个客户端之间的P2P连接。通过该示例对于我们去了解WebRTC的整体架构和运行流程有非常大帮助。
程序入口和主体框架
找到编译好的 webrtc 示例 VS 程序,通过 VS 打开 all.sln 程序,然后将 peerconnection_client 设置为启动项,如下图所示
启动通话后,效果如下:
如果你是在本地开 2 个客户端调试,那么可以通过开启 OBS 的虚拟摄像头达到上面的效果。
peerconnection_client 主要是由以下几个部分构成
main.cc: 这是程序的入口点,它创建并运行应用程序的消息循环,初始化并运行主窗口。它会创建 PeerConnectionClient 和 Conductor 对象,并且链接他们,使得它们能一起协作。
main_wnd.cc: 它是主窗口类的实现。这个类负责所有的用户界面操作,如按钮点击、视频显示窗口、状态更新等。它还将用户操作的事件通知到 Conductor 对象。
peer_connection_client.cc: 这个类是一个客户端,它会连接到 PeerConnectionServer 信令服务器,然后向服务器注册,并处理来自服务器的信令消息,以及发送到服务器的信令消息。
conductor.cc: 它是整个程序的核心,负责管理 PeerConnectionClient 对象和 MainWnd 对象。它还创建并管理WebRTC的 PeerConnection 对象,以及处理所有的WebRTC事件。例如,当用户点击"用户列表 item"时,MainWnd 对象会将此事件通知给 Conductor,Conductor 会命令 PeerConnectionClient 向信令服务器发送一个信令消息,以便开始一个新的呼叫。
我们来看下 main.cc 中的核心代码,也就是入口函数:
int PASCAL wWinMain(HINSTANCE instance, HINSTANCE prev_instance, wchar_t* cmd_line, int cmd_show) { rtc::WinsockInitializer winsock_init; // 初始化 Winsock CustomSocketServer ss; // 自定义 Socket 服务器 rtc::AutoSocketServerThread main_thread(&ss); // 使用自定义 Socket 服务器创建主线程 WindowsCommandLineArguments win_args; // 处理命令行参数 int argc = win_args.argc(); char** argv = win_args.argv(); absl::ParseCommandLine(argc, argv); // 解析命令行参数 // InitFieldTrialsFromString 会存储 char*,所以这个字符数组必须比应用程序的生命周期更长 const std::string forced_field_trials = absl::GetFlag(FLAGS_force_fieldtrials); webrtc::field_trial::InitFieldTrialsFromString(forced_field_trials.c_str()); // 如果用户指定的端口超出了允许的范围 [1, 65535],则中止程序 if ((absl::GetFlag(FLAGS_port) < 1) || (absl::GetFlag(FLAGS_port) > 65535)) { printf("Error: %i is not a valid port.", absl::GetFlag(FLAGS_port)); return -1; } 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_DCHECK_NOTREACHED(); // 如果窗口创建失败,则终止程序 return -1; } rtc::InitializeSSL(); // 初始化 SSL PeerConnectionClient client; // 创建 PeerConnectionClient 对象 rtc::scoped_refptr<Conductor> conductor( new rtc::RefCountedObject<Conductor>(&client, &wnd)); // 创建 Conductor 对象 // 主循环 MSG msg; BOOL gm; while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) { // 获取并处理消息,如果获取失败或者程序接收到退出消息,则退出循环 if (!wnd.PreTranslateMessage(&msg)) { // 如果消息没有被预处理 ::TranslateMessage(&msg); // 翻译消息 ::DispatchMessage(&msg); // 分发消息 } } 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); // 分发消息 } } } rtc::CleanupSSL(); // 清理 SSL return 0;
入口函数的作用,就是初始化并启动WebRTC peerconnection,处理命令行参数,设置窗口界面,并开始接收和处理Windows消息,直到peer connection关闭和程序结束。
窗口管理
窗口管理的工作主要在 main_wnd.cc create 函数,我们看一下它是如何创建 WebRTC 这个窗口的,
bool MainWnd::Create() { RTC_DCHECK(wnd_ == NULL); // 检查窗口句柄是否为NULL,以确保窗口尚未创建。 if (!RegisterWindowClass()) // 注册窗口类。如果注册失败,返回false。 return false; ui_thread_id_ = ::GetCurrentThreadId(); // 获取当前线程ID并存储,这将用于后续的UI操作。 // 创建一个新的窗口实例。这个窗口是一个具有内置子窗口的主窗口,标题为"WebRTC"。 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; // 如果窗口句柄不为NULL,说明窗口创建成功,返回true;否则返回false。}bool MainWnd::RegisterWindowClass() { if (wnd_class_) // 如果窗口类已经注册,直接返回true 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; // 设置窗口类名 // 调用RegisterClassExW函数注册窗口类,注册成功会返回一个窗口类的原子类名,失败返回0 wnd_class_ = ::RegisterClassExW(&wcex); RTC_DCHECK(wnd_class_ != 0); // 检查窗口类是否注册成功 return wnd_class_ != 0; // 如果窗口类注册成功,返回true;否则返回false。}void MainWnd::CreateChildWindow(HWND* wnd, MainWnd::ChildWindowID id, const wchar_t* class_name, DWORD control_style, DWORD ex_style) { if (::IsWindow(*wnd)) // 如果窗口已存在,直接返回,避免重复创建 return; // 子窗口初始为隐藏状态,在调整大小后显示 DWORD style = WS_CHILD | control_style; // 创建子窗口,窗口位置和尺寸初始为100*100,实际会在后续调整 *wnd = ::CreateWindowExW(ex_style, class_name, L"", style, 100, 100, 100, 100, wnd_, reinterpret_cast<HMENU>(id), GetModuleHandle(NULL), NULL); RTC_DCHECK(::IsWindow(*wnd) != FALSE); // 检查窗口是否创建成功 // 发送消息给窗口,设置默认字体 ::SendMessage(*wnd, WM_SETFONT, reinterpret_cast<WPARAM>(GetDefaultFont()), TRUE);}void MainWnd::CreateChildWindows() { // 按照 tab 顺序创建子窗口 CreateChildWindow(&label1_, LABEL1_ID, L"Static", ES_CENTER | ES_READONLY, 0); CreateChildWindow(&edit1_, EDIT_ID, L"Edit", ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE); CreateChildWindow(&label2_, LABEL2_ID, L"Static", ES_CENTER | ES_READONLY, 0); CreateChildWindow(&edit2_, EDIT_ID, L"Edit", ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE); CreateChildWindow(&button_, BUTTON_ID, L"Button", BS_CENTER | WS_TABSTOP, 0); CreateChildWindow(&listbox_, LISTBOX_ID, L"ListBox", LBS_HASSTRINGS | LBS_NOTIFY, WS_EX_CLIENTEDGE); // 初始化 edit1_ 和 edit2_ 的文本内容 ::SetWindowTextA(edit1_, server_.c_str()); ::SetWindowTextA(edit2_, port_.c_str());}//接收系统发送给窗口的消息LRESULT CALLBACK MainWnd::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { ... return result;}
这个函数的目的是创建一个主窗口,并根据程序的需要进行配置。在这个窗口中,会创建一些子窗口(ip编辑框,连接按钮,用户列表等),并先设置窗口的UI状态为 "连接" 状态。
最后通过调用 windows api ShowWindow 将创建好的一系列窗口显示
void MainWnd::LayoutConnectUI(bool show) { // 定义窗口布局和属性的结构体 struct Windows { HWND wnd; const wchar_t* text; size_t width; size_t height; } windows[] = { // 初始化窗口数组 {label1_, L"Server"}, {edit1_, L"XXXyyyYYYgggXXXyyyYYYggg"}, {label2_, L":"}, {edit2_, L"XyXyX"}, {button_, L"Connect"}, }; if (show) { // 如果要显示连接界面 const size_t kSeparator = 5; // 控件之间的间隔 size_t total_width = (ARRAYSIZE(windows) - 1) * kSeparator; // 计算所有窗口的总宽度 // 计算每个窗口的尺寸并更新总宽度 for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { CalculateWindowSizeForText(windows[i].wnd, windows[i].text, &windows[i].width, &windows[i].height); total_width += windows[i].width; } RECT rc; ::GetClientRect(wnd_, &rc); // 获取主窗口的客户区大小 size_t x = (rc.right / 2) - (total_width / 2); // 计算第一个窗口的水平位置 size_t y = rc.bottom / 2; // 计算窗口的垂直位置 // 依次设置每个窗口的位置并显示 for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { size_t top = y - (windows[i].height / 2); ::MoveWindow(windows[i].wnd, static_cast<int>(x), static_cast<int>(top), static_cast<int>(windows[i].width), static_cast<int>(windows[i].height), TRUE); x += kSeparator + windows[i].width; // 更新下一个窗口的水平位置 if (windows[i].text[0] != 'X') // 设置窗口的文本内容 ::SetWindowTextW(windows[i].wnd, windows[i].text); ::ShowWindow(windows[i].wnd, SW_SHOWNA); // 显示窗口 } } else { // 如果不显示连接界面,则隐藏所有窗口 for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { ::ShowWindow(windows[i].wnd, SW_HIDE); } }}void MainWnd::SwitchToConnectUI() { RTC_DCHECK(IsWindow()); // 确保主窗口存在 LayoutPeerListUI(false); // 隐藏用户列表界面 ui_ = CONNECT_TO_SERVER; // 更新到连接状态界面 LayoutConnectUI(true); // 显示连接服务器界面 ::SetFocus(edit1_); // 将焦点设置到第一个输入框 if (auto_connect_) // 如果设置了自动连接,则模拟点击连接按钮 ::PostMessage(button_, BM_CLICK, 0, 0);}
最后窗口会这样显示:
当我们点击上图中的 Connect 后,系统会发送消息给 WndProc 窗口的接收消息的回调函数上,如果连接成功,就会切换到 用户list UI,核心代码如下:
void MainWnd::SwitchToPeerList(const Peers& peers) { // 关闭连接界面 LayoutConnectUI(false); // 重置列表内容 ::SendMessage(listbox_, LB_RESETCONTENT, 0, 0); // 向列表中添加一行标题 AddListBoxItem(listbox_, "List of currently connected peers:", -1); // 循环遍历对等端列表,将每个对等端添加到列表中 Peers::const_iterator i = peers.begin(); for (; i != peers.end(); ++i) AddListBoxItem(listbox_, i->second.c_str(), i->first); // 设置当前用户界面状态为 LIST_PEERS ui_ = LIST_PEERS; // 显示对等端列表界面 LayoutPeerListUI(true); // 将焦点设置到列表上 ::SetFocus(listbox_); // 如果 auto_call_ 为 true,并且对等端列表不为空 if (auto_call_ && peers.begin() != peers.end()) { // 获取列表中的项目数量 LRESULT count = ::SendMessage(listbox_, LB_GETCOUNT, 0, 0); if (count != LB_ERR) { // 选中列表中的最后一个项目 LRESULT selection = ::SendMessage(listbox_, LB_SETCURSEL, count - 1, 0); // 如果选中成功,发送一个 WM_COMMAND 消息,模拟双击事件 if (selection != LB_ERR) ::PostMessage(wnd_, WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(listbox_), LBN_DBLCLK), reinterpret_cast<LPARAM>(listbox_)); } }}
SwitchToPeerList函数首先关闭了连接界面,然后将列表的内容进行了重置,添加了一个标题到列表中,并添加了所有当前在线的对等端到列表中。接着,它将用户界面的状态切换到了显示对等端列表,并设置了列表的焦点。最后,如果设置了自动呼叫并且有对等端在线,它就会选中列表中的最后一个项目,并模拟一次双击事件。
这段代码执行后,对应的用户列表就可以显示出来,比如, 如下所示:
当双击用户名称时,双方就会发起 SDP 媒体协商,网络协商等,如果都协商成功就可以传输并显示音视频画面了,这个后面会详细说到。
如果用户主动关闭窗口,窗口会收到退出的消息并关闭 peerconnection 连接。
到这里窗口整个的创建->更新->关闭都分析完了,接下来会分析 peerconnection_client 与 server 的信令交互
信令处理
下载 pcapng 包链接: https://pan.baidu.com/s/1wGyyLSxd7_X2p7T8O1nPdg?pwd=frrr 提取码: frrr
通过抓包我们得到了如下几个信令:
GET sign_in: 用户登录消息
**GET sign_out:**用户退出消息
POST message: 协商交互消息
GET wait: 用户等待消息
这里绘制了一张简要的时序图
当用户点击 Connect 时,会发起登录信息:
void Conductor::StartLogin(const std::string& server, int port) { if (client_->is_connected()) return; server_ = server; //在 PeerConnectionClient 中与 server 发起信令登录连接 client_->Connect(server, port, GetPeerName());}
调用这行代码后,会执行到 PeerConnectionClient::Connect 函数:
void PeerConnectionClient::Connect(const std::string& server, int port, const std::string& client_name) { RTC_DCHECK(!server.empty()); RTC_DCHECK(!client_name.empty()); //判断当前的状态是否处于连接 if (state_ != NOT_CONNECTED) { RTC_LOG(LS_WARNING) << "The client must not be connected before you can call Connect()"; callback_->OnServerConnectionFailure(); return; } //判断ip和名称是否为空 if (server.empty() || client_name.empty()) { callback_->OnServerConnectionFailure(); return; } //如果端口小于 0 使用默认的 if (port <= 0) port = kDefaultServerPort; //设置信令服务器 IP 和端口 server_address_.SetIP(server); server_address_.SetPort(port); client_name_ = client_name; /** *if (server_address_.IsUnresolvedIP()): 检查 server_address_ 是否是一个未解析的 IP 地址 (也就是说,它实际上是一个域名)。如果是, 那么需要进行 DNS 解析。在这种情况下,代码会创建一个 rtc::AsyncResolver 对象来进行异步的 DNS 解析,并设置一个回调函数 PeerConnectionClient::OnResolveResult,当解析完成时这个函数会被调用。然后,代码调用 resolver_->Start(server_address_) 来开始解析过程。 */ if (server_address_.IsUnresolvedIP()) { state_ = RESOLVING; resolver_ = new rtc::AsyncResolver(); resolver_->SignalDone.connect(this, &PeerConnectionClient::OnResolveResult); resolver_->Start(server_address_); } else { DoConnect();//如果域名不需要解析,则直接发起连接 }}
这里由于我们填的是本机地址,所以不需要 DNS 解析,直接看 DoConnect
void PeerConnectionClient::DoConnect() { //创建一个控制连接(发送和接收命令) control_socket_.reset(CreateClientSocket(server_address_.ipaddr().family())); //用于 hanging GET 操作(长轮询,用于接收服务器的实时更新) hanging_get_.reset(CreateClientSocket(server_address_.ipaddr().family())); //初始化套接字信号,包括连接、数据接收等事件的回调处理。 InitSocketSignals(); char buffer[1024]; //准备一个 HTTP GET 请求,用于登录到服务器。这个请求的路径是 "/sign_in",并且包含一个查询参数,即客户端的名字。 snprintf(buffer, sizeof(buffer), "GET /sign_in?%s HTTP/1.0\r\r", client_name_.c_str()); onconnect_data_ = buffer; //尝试连接到控制套接字。如果连接成功,ConnectControlSocket() 将返回 true,否则返回 false。 bool ret = ConnectControlSocket(); if (ret) //如果连接成功,将状态设置为 SIGNING_IN,表示正在进行登录操作 state_ = SIGNING_IN; if (!ret) {//如果连接失败,调用回调函数 OnServerConnectionFailure(),通知其他部分连接失败 callback_->OnServerConnectionFailure(); } //启动当前线程 rtc::Thread::Current()->Start();}
PeerConnectionClient 与服务器交互的协议是 http 短连接,此处是创建了 2 个 异步的 socket, control_socket_ 主要是主动发起一些信令的操作,比如登录,退出,offer,candide 消息等;而 hanging_get_ 它主要是向信令服务器请求对方的信令消息,比如 answer,candidate,用户列表等,每次是先发一个 wait 信令,等待信令服务器的响应,当信令服务器有响应时,就会执行这些注入的回调,代码如下:
void PeerConnectionClient::InitSocketSignals() { RTC_DCHECK(control_socket_.get() != NULL); RTC_DCHECK(hanging_get_.get() != NULL); /** close 事件**/ control_socket_->SignalCloseEvent.connect(this, &PeerConnectionClient::OnClose); hanging_get_->SignalCloseEvent.connect(this, &PeerConnectionClient::OnClose); /** connect 事件**/ control_socket_->SignalConnectEvent.connect(this, &PeerConnectionClient::OnConnect); hanging_get_->SignalConnectEvent.connect( this, &PeerConnectionClient::OnHangingGetConnect); /** read 事件**/ control_socket_->SignalReadEvent.connect(this, &PeerConnectionClient::OnRead); hanging_get_->SignalReadEvent.connect( this, &PeerConnectionClient::OnHangingGetRead);}
发起登录连接
bool PeerConnectionClient::ConnectControlSocket() { //检查当前的连接状态 RTC_DCHECK(control_socket_->GetState() == rtc::Socket::CS_CLOSED); //向信令服务器发起连接请求 int err = control_socket_->Connect(server_address_); if (err == SOCKET_ERROR) { Close(); return false; } return true;}
当连接成功
void PeerConnectionClient::OnConnect(rtc::Socket* 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();}
向信令服务器发送的消息及响应
GET /sign_in?devyk@devyk-mwin HTTP/1.0\r HTTP/1.1 200 Added\rServer: PeerConnectionTestServer/0.1\rCache-Control: no-cache\rConnection: close\rContent-Type: text/plain\rContent-Length: 22\rPragma: 12\rAccess-Control-Allow-Origin: *\rAccess-Control-Allow-Credentials: true\rAccess-Control-Allow-Methods: POST, GET, OPTIONS\rAccess-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\rAccess-Control-Expose-Headers: Content-Length\r\rdevyk@devyk-mwin,12,1
当第二个人连接进来的时候,收到的消息
HTTP/1.1 200 Added\r Server: PeerConnectionTestServer/0.1\r Cache-Control: no-cache\r Connection: close\r Content-Type: text/plain\r Content-Length: 44\r Pragma: 13\r Access-Control-Allow-Origin: *\r Access-Control-Allow-Credentials: true\r Access-Control-Allow-Methods: POST, GET, OPTIONS\r Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r Access-Control-Expose-Headers: Content-Length\r \r devyk@devyk-mwin,13,1 devyk@devyk-mwin,12,1
解析 Socket 收到的协议
void PeerConnectionClient::OnRead(rtc::Socket* socket) { size_t content_length = 0; // 读取服务器发送的数据到 control_data_ 缓冲区,并获取内容长度 if (ReadIntoBuffer(socket, &control_data_, &content_length)) { size_t peer_id = 0, eoh = 0; // 解析服务器的响应,获取 peer_id 和 eoh(头部结束的位置) bool ok = ParseServerResponse(control_data_, content_length, &peer_id, &eoh); if (ok) { if (my_id_ == -1) { // 如果是第一次响应,存储服务器分配的 ID RTC_DCHECK(state_ == SIGNING_IN); my_id_ = static_cast<int>(peer_id); RTC_DCHECK(my_id_ != -1); // 如果响应的主体部分存在内容,则将已经连接的对等方信息添加到 peers_ 列表中 if (content_length) { size_t pos = eoh + 4; while (pos < control_data_.size()) { size_t eol = control_data_.find('', pos); if (eol == std::string::npos) break; int id = 0; std::string name; bool connected; // 解析对等方条目,获取名字、ID以及连接状态 if (ParseEntry(control_data_.substr(pos, eol - pos), &name, &id, &connected) && id != my_id_) { // 如果对等方不是自己,将其添加到对等方列表中,并触发连接事件 peers_[id] = name; callback_->OnPeerConnected(id, name); } pos = eol + 1; } } RTC_DCHECK(is_connected()); // 触发已登录事件 callback_->OnSignedIn(); } else if (state_ == SIGNING_OUT) { // 如果当前状态是正在退出,则关闭连接并触发断开连接事件 Close(); callback_->OnDisconnected(); } else if (state_ == SIGNING_OUT_WAITING) { // 如果当前状态是等待退出,则退出 SignOut(); } } // 清空 control_data_ 缓冲区 control_data_.clear(); if (state_ == SIGNING_IN) { // 如果当前状态是正在登录,则切换到已连接状态,并连接到服务器 RTC_DCHECK(hanging_get_->GetState() == rtc::Socket::CS_CLOSED); state_ = CONNECTED; hanging_get_->Connect(server_address_); } }}
当信令服务器发送登录响应时,会触发 PeerConnectionClient::OnRead() 函数。 首先从 socket 读取响应信息至 control_data_ 中,如果是短连接则需要关闭socket。接着验证响应中的状态码,获取信令服务器分配的peer id。 登录信令的响应中会包含其他登录客户端的信息,这些客户端的信令会显示到peer list界面上。
解析其他客户端的信息后,会触发Conductor::OnPeerConnected函数,在这个函数中会将客户端的信息显示到peer list界面上。
devyk@devyk-mwin,13,1 devyk@devyk-mwin,12,1
响应信息的格式是:peer的name,信令服务器分配的peer id,是否处于登录状态,1表示处于登录状态,0表示登出状态。
成功登录信令服务器后,hanging_get socket 也开始登录信令服务器,用于接收信令服务器发送给客户端的信息。
当连接成功后,发送等待消息,如果有新的信令消息,服务端就转发过来
void PeerConnectionClient::OnHangingGetConnect(rtc::Socket* socket) { char buffer[1024]; snprintf(buffer, sizeof(buffer), "GET /wait?peer_id=%i HTTP/1.0\r\r", my_id_); int len = static_cast<int>(strlen(buffer)); int sent = socket->Send(buffer, len); RTC_DCHECK(sent == len);}
//发送的数据GET /wait?peer_id=12 HTTP/1.0\r
当有新的信令消息产生时,会以 wait 的响应回来
HTTP/1.1 200 OK\r Server: PeerConnectionTestServer/0.1\r Cache-Control: no-cache\r Connection: close\r Content-Type: text/plain\r Content-Length: 22\r Pragma: 12\r Access-Control-Allow-Origin: *\r Access-Control-Allow-Credentials: true\r Access-Control-Allow-Methods: POST, GET, OPTIONS\r Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r Access-Control-Expose-Headers: Content-Length\r \r devyk@devyk-mwin,13,1
然后就会触发下面的函数:
void PeerConnectionClient::OnHangingGetRead(rtc::Socket* socket) { RTC_LOG(LS_INFO) << __FUNCTION__; size_t content_length = 0; //从指定的socket读取响应信息,并做适当的处理。如果从响应中得知使用的是http短连接,那么需要关闭socket。 if (ReadIntoBuffer(socket, ¬ification_data_, &content_length)) { size_t peer_id = 0, eoh = 0; //解析响应码,并读取信令服务器分配的 peer id bool ok = ParseServerResponse(notification_data_, content_length, &peer_id, &eoh); if (ok) { // Store the position where the body begins. size_t pos = eoh + 4; //检查是否是自己的ID,如果是,那么这个通知可能是有新的成员加入或者有成员断开连接。 // 然后,它尝试解析主体内容,获取 peer 的 id,名称和连接状态。如果解析成功, // 并且 peer 是已连接的,那么就将这个 peer 添加到 peers 列表中, //并通知回调有 peer 连接;如果 peer 是断开的,那么就从 peers 列表中移除,并通知回调有 peer 断开连接 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 { //用于处理offer、answer、candidate信令 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_); }}
当信令服务器需要主动发送消息给客户端时,会包装成wait信令的响应信息。有其他客户端登录或登出信令服务器时,会通知本端,本端会根据信令服务器反馈的信息更新peer list界面的用户列表。 当收到信令服务器转发的其他客户端的offer、answer、candidate信息时,会进入OnMessageFromPeer()函数处理。
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 { /*收到的是offer、answer、candidate信令*/ callback_->OnMessageFromPeer(peer_id, message); }}
分析完读取和解析 http 协议后,我们看下如何进行 CreateOffer 的,
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; } //初始化 peer ,成功就创建 CreateOffer if (InitializePeerConnection()) { peer_id_ = peer_id; peer_connection_->CreateOffer( this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions()); } else { main_wnd_->MessageBox("Error", "Failed to initialize PeerConnection", true); }}
当点击 peer list 用户中任意一个,会执行到此处,如果 InitializePeerConnection 为 true ,那么就可以 CreateOffer.
//第一步:bool Conductor::InitializePeerConnection() { // 检查是否已存在 peer_connection_factory_ 或 // peer_connection_,都应该是空的,否则报错 RTC_DCHECK(!peer_connection_factory_); RTC_DCHECK(!peer_connection_); // 没有 signaling_thread_ 的话就创建一个新的 if (!signaling_thread_.get()) { signaling_thread_ = rtc::Thread::CreateWithSocketServer(); signaling_thread_->Start(); } // 使用 signaling_thread_ 创建 PeerConnectionFactory // PeerConnectionFactory 是用于生成 PeerConnections, MediaStreams 和 MediaTracks 的工厂类 peer_connection_factory_ = webrtc::CreatePeerConnectionFactory( nullptr /* network_thread */, nullptr /* worker_thread */, signaling_thread_.get(), /* signaling_thread */ nullptr /* default_adm */, webrtc::CreateBuiltinAudioEncoderFactory(), webrtc::CreateBuiltinAudioDecoderFactory(), webrtc::CreateBuiltinVideoEncoderFactory(), webrtc::CreateBuiltinVideoDecoderFactory(), nullptr /* audio_mixer */, nullptr /* audio_processing */); // 如果 PeerConnectionFactory 初始化失败,清理资源并返回错误 if (!peer_connection_factory_) { main_wnd_->MessageBox("Error", "Failed to initialize PeerConnectionFactory", true); DeletePeerConnection(); return false; } // 创建 PeerConnection,如果失败,清理资源并返回错误 if (!CreatePeerConnection()) { main_wnd_->MessageBox("Error", "CreatePeerConnection failed", true); DeletePeerConnection(); } // 添加音频和视频轨道 AddTracks(); // 返回 peer_connection_ 是否已初始化 return peer_connection_ != nullptr;}
第一步: InitializePeerConnection():这个方法的目标是初始化一个PeerConnectionFactory,并创建一个PeerConnection。首先,它确保PeerConnectionFactory和PeerConnection不存在。如果还没有创建信令线程,就创建一个新的。然后,使用这个信令线程创建一个新的PeerConnectionFactory,用于后续生成PeerConnections, MediaStreams和MediaTracks。如果PeerConnectionFactory创建失败,它将清理资源并返回错误。最后,创建一个PeerConnection,添加音频和视频轨道,并返回是否成功初始化PeerConnection。
//第二步:bool Conductor::CreatePeerConnection() { // 检查 peer_connection_factory_ 是否存在且 peer_connection_ // 是否为空,否则报错 RTC_DCHECK(peer_connection_factory_); RTC_DCHECK(!peer_connection_); // 创建一个新的 PeerConnection 配置 webrtc::PeerConnectionInterface::RTCConfiguration config; config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan; webrtc::PeerConnectionInterface::IceServer server; server.uri = GetPeerConnectionString(); config.servers.push_back(server); // 使用 PeerConnectionFactory 和配置创建新的 PeerConnection peer_connection_ = peer_connection_factory_->CreatePeerConnection( config, nullptr, nullptr, this); return peer_connection_ != nullptr;}
第二步: CreatePeerConnection():这个方法用于创建一个新的PeerConnection。首先,它会检查PeerConnectionFactory是否存在,且PeerConnection是否为空。然后,创建一个新的PeerConnection配置,设置SDP协议的语义为统一计划,并添加ICE服务器。最后,使用PeerConnectionFactory和刚刚创建的配置来创建一个新的PeerConnection,并返回创建是否成功。
//第三步:void Conductor::AddTracks() { // 如果已经添加了轨道,则不再添加 if (!peer_connection_->GetSenders().empty()) { return; // Already added tracks. } // 创建音频轨道并添加到 PeerConnection rtc::scoped_refptr<webrtc::AudioTrackInterface> audio_track( peer_connection_factory_->CreateAudioTrack( kAudioLabel, peer_connection_factory_->CreateAudioSource( cricket::AudioOptions()))); auto result_or_error = peer_connection_->AddTrack(audio_track, {kStreamId}); if (!result_or_error.ok()) { RTC_LOG(LS_ERROR) << "Failed to add audio track to PeerConnection: " << result_or_error.error().message(); } // 创建视频源和视频轨道并添加到 PeerConnection rtc::scoped_refptr<CapturerTrackSource> video_device = CapturerTrackSource::Create(); if (video_device) { rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_( peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device)); main_wnd_->StartLocalRenderer(video_track_); result_or_error = peer_connection_->AddTrack(video_track_, {kStreamId}); if (!result_or_error.ok()) { RTC_LOG(LS_ERROR) << "Failed to add video track to PeerConnection: " << result_or_error.error().message(); } } else { RTC_LOG(LS_ERROR) << "OpenVideoCaptureDevice failed"; } // 将界面切换到流媒体 UI main_wnd_->SwitchToStreamingUI();}
第三步: AddTracks():这个方法的目标是向PeerConnection添加音频和视频轨道。首先,它会检查是否已经添加了轨道。如果已经添加了,则不再添加。然后,创建一个音频轨道并添加到PeerConnection。之后,创建一个视频源和一个视频轨道,并添加到PeerConnection。如果添加轨道失败,会记录错误信息。最后,将用户界面切换到流媒体UI。
这些步骤(任意平台)是设置WebRTC通信的关键步骤。在创建并初始化PeerConnectionFactory之后,我们可以创建PeerConnection,然后在PeerConnection上添加音频和视频轨道,这样我们就可以开始进行实时的音视频通信了。
如果这三步执行都没有问题,那么就是发起 offer 了,当 CreateOffer 成功时,会有成功回调
/** SDP 设置成功回调*/void Conductor::OnSuccess(webrtc::SessionDescriptionInterface* desc) { peer_connection_->SetLocalDescription( DummySetSessionDescriptionObserver::Create(), desc); std::string sdp; desc->ToString(&sdp); // For loopback test. To save some connecting delay. if (loopback_) { // Replace message type from "offer" to "answer" std::unique_ptr<webrtc::SessionDescriptionInterface> session_description = webrtc::CreateSessionDescription(webrtc::SdpType::kAnswer, sdp); peer_connection_->SetRemoteDescription( DummySetSessionDescriptionObserver::Create(), session_description.release()); return; } Json::StyledWriter writer; Json::Value jmessage; jmessage[kSessionDescriptionTypeName] = webrtc::SdpTypeToString(desc->GetType()); jmessage[kSessionDescriptionSdpName] = sdp; SendMessage(writer.write(jmessage));}void Conductor::SendMessage(const std::string& json_object) { std::string* msg = new std::string(json_object); main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER, msg);}
当 CreateOffer 成功时,首先调用 webrtc SetLocalDescription API 设置当前的 SDP,
然后会将 offer sdp 发送给信令服务器,通过抓包,我们拿到了具体的 sdp 信息
POST /message?peer_id=13&to=12 HTTP/1.0\rContent-Length: 5608\rContent-Type: text/plain\r\r{ "sdp" : "v=0\ro=- 6269511735434714595 2 IN IP4 127.0.0.1\rs=-\rt=0 0\ra=group:BUNDLE 0 1\ra=extmap-allow-mixed\ra=msid-semantic: WMS stream_id\rm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1 "type" : "offer"}
这是 peer_id=13 发送给 12 的 offer 信令,对应的响应如下:
HTTP/1.1 200 OK\r Server: PeerConnectionTestServer/0.1\r Cache-Control: no-cache\r Connection: close\r Content-Type: text/plain\r Content-Length: 0\r Access-Control-Allow-Origin: *\r Access-Control-Allow-Credentials: true\r Access-Control-Allow-Methods: POST, GET, OPTIONS\r Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r Access-Control-Expose-Headers: Content-Length\r \r
服务端通过转发给另一个 peer wait 的 offer 响应
HTTP/1.1 200 OK\r Server: PeerConnectionTestServer/0.1\r Cache-Control: no-cache\r Connection: close\r Content-Type: text/plain\r Content-Length: 5608\r Pragma: 13\r Access-Control-Allow-Origin: *\r Access-Control-Allow-Credentials: true\r Access-Control-Allow-Methods: POST, GET, OPTIONS\r Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r Access-Control-Expose-Headers: Content-Length\r \r { "sdp" : "v=0\ro=- 6269511735434714595 2 IN IP4 127.0.0.1\rs=-\rt=0 0\ra=group:BUNDLE 0 1\ra=extmap-allow-mixed\ra=msid-semantic: WMS stream_id\rm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1 "type" : "offer" }
另一方收到 offer 响应后,会执行刚刚我们分析的 OnMessageFromPeer 函数
void Conductor::OnMessageFromPeer(int peer_id, const std::string& message) { RTC_DCHECK(peer_id_ == peer_id || peer_id_ == -1); RTC_DCHECK(!message.empty()); /*此时被动peer还没有创建PeerConnection对象*/ if (!peer_connection_.get()) { RTC_DCHECK(peer_id_ == -1); peer_id_ = peer_id; /*创建PeerConnection对象*/ if (!InitializePeerConnection()) { RTC_LOG(LS_ERROR) << "Failed to initialize our PeerConnection instance"; client_->SignOut(); return; } } else if (peer_id != peer_id_) { RTC_DCHECK(peer_id_ != -1); RTC_LOG(LS_WARNING) << "Received a message from unknown peer while already in a " "conversation with a different peer."; return; } /*将收到的消息解析成json对象*/ Json::Reader reader; Json::Value jmessage; if (!reader.parse(message, jmessage)) { RTC_LOG(LS_WARNING) << "Received unknown message. " << message; return; } std::string type_str; std::string json_object; /*从json消息中解析出消息的类型*/ rtc::GetStringFromJsonObject(jmessage, kSessionDescriptionTypeName, &type_str); if (!type_str.empty()) { if (type_str == "offer-loopback") { // This is a loopback call. // Recreate the peerconnection with DTLS disabled. if (!ReinitializePeerConnectionForLoopback()) { RTC_LOG(LS_ERROR) << "Failed to initialize our PeerConnection instance"; DeletePeerConnection(); client_->SignOut(); } return; } /*获取消息的类型*/ absl::optional<webrtc::SdpType> type_maybe = webrtc::SdpTypeFromString(type_str); if (!type_maybe) { RTC_LOG(LS_ERROR) << "Unknown SDP type: " << type_str; return; } /*从json消息中获取sdp,此处为offer。*/ webrtc::SdpType type = *type_maybe; std::string sdp; if (!rtc::GetStringFromJsonObject(jmessage, kSessionDescriptionSdpName, &sdp)) { RTC_LOG(LS_WARNING) << "Can't parse received session description message."; return; } /*将offer转成webrtc可以理解的对象*/ webrtc::SdpParseError error; std::unique_ptr<webrtc::SessionDescriptionInterface> session_description = webrtc::CreateSessionDescription(type, sdp, &error); if (!session_description) { RTC_LOG(LS_WARNING) << "Can't parse received session description message. " "SdpParseError was: " << error.description; return; } RTC_LOG(LS_INFO) << " Received session description :" << message; /*将offer通过SetRemoteDescription设置到PeerConnection中*/ peer_connection_->SetRemoteDescription( DummySetSessionDescriptionObserver::Create(), session_description.release()); /*收到了对端的offer,本端需要产生answer。*/ if (type == webrtc::SdpType::kOffer) { peer_connection_->CreateAnswer( this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions()); } } else { //处理 candidate 消息 std::string sdp_mid; int sdp_mlineindex = 0; std::string sdp; if (!rtc::GetStringFromJsonObject(jmessage, kCandidateSdpMidName, &sdp_mid) || !rtc::GetIntFromJsonObject(jmessage, kCandidateSdpMlineIndexName, &sdp_mlineindex) || !rtc::GetStringFromJsonObject(jmessage, kCandidateSdpName, &sdp)) { RTC_LOG(LS_WARNING) << "Can't parse received message."; return; } webrtc::SdpParseError error; std::unique_ptr<webrtc::IceCandidateInterface> candidate( webrtc::CreateIceCandidate(sdp_mid, sdp_mlineindex, sdp, &error)); if (!candidate.get()) { RTC_LOG(LS_WARNING) << "Can't parse received candidate message. " "SdpParseError was: " << error.description; return; } if (!peer_connection_->AddIceCandidate(candidate.get())) { RTC_LOG(LS_WARNING) << "Failed to apply the received candidate"; return; } RTC_LOG(LS_INFO) << " Received candidate :" << message; }}
这一段代码较长,其实就3个意思
实例化 PeerConnectionFactoy 和 PeerConnectionClient
设置远端的 SDP,并 CreateAnswer
收到对方发来的 candidate 消息,并添加到 PeerConnectionClient 中
上面第二点中的 CreateAnswer 创建成功后,也会想 CreateOffer 一样,有成功的回调,然后再发送给对方,这里就不再过多描述了。后面我们再看一下 candidate 消息
当 CreateOffer 、CreateAnswer 后,WebRTC 会通过 OnIceCandidate 回调信息将一些候选者的信息通知给我们
void Conductor::OnIceCandidate(const webrtc::IceCandidateInterface* candidate) { RTC_LOG(LS_INFO) << __FUNCTION__ << " " << candidate->sdp_mline_index(); // For loopback test. To save some connecting delay. if (loopback_) { if (!peer_connection_->AddIceCandidate(candidate)) { RTC_LOG(LS_WARNING) << "Failed to apply the received candidate"; } return; } Json::StyledWriter writer; Json::Value jmessage; jmessage[kCandidateSdpMidName] = candidate->sdp_mid(); jmessage[kCandidateSdpMlineIndexName] = candidate->sdp_mline_index(); std::string sdp; if (!candidate->ToString(&sdp)) { RTC_LOG(LS_ERROR) << "Failed to serialize candidate"; return; } jmessage[kCandidateSdpName] = sdp; SendMessage(writer.write(jmessage));}
这里主要是将 webrtc ice 中收集到的 candidate 组装成 json 然后发送给信令服务器,服务器再转发给另一端
POST /message?peer_id=13&to=12 HTTP/1.0\r Content-Length: 186\r Content-Type: text/plain\r \r { "candidate" : "candidate:1019731727 1 udp 2122260223 192.168.1.104 53072 typ host generation 0 ufrag IEDW network-id 3 network-cost 10", "sdpMLineIndex" : 0, "sdpMid" : "0" }
通过抓包得到了如上 candidate 消息,注意 candidate 会存在多个消息,双方收到后并添加到 PeerConnectionClient 中,如果网络协商成功,那么就可以进行采集->编码->传输了。
最后一个信令是 退出信令 ,当关闭窗口时,发送如下格式的信令
request:GET /sign_out?peer_id=13 HTTP/1.0\rresponse:HTTP/1.1 200 OK\rServer: PeerConnectionTestServer/0.1\rCache-Control: no-cache\rConnection: close\rContent-Type: text/plain\rContent-Length: 0\rAccess-Control-Allow-Origin: *\rAccess-Control-Allow-Credentials: true\rAccess-Control-Allow-Methods: POST, GET, OPTIONS\rAccess-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\rAccess-Control-Expose-Headers: Content-Length\r\r
到此,所有信令就分析完了,建议大家可以通过抓包去分析对应的流程。
媒体流处理
当媒体协商,网络协商完成后,就能进行等待收对方发过来的音视频流了,当有新轨道产生,会执行 OnAddTrack 回调
void Conductor::OnAddTrack( rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver, const std::vector<rtc::scoped_refptr<webrtc::MediaStreamInterface>>& streams) { RTC_LOG(LS_INFO) << __FUNCTION__ << " " << receiver->id(); main_wnd_->QueueUIThreadCallback(NEW_TRACK_ADDED, receiver->track().release());}
经过一系列的线程切换,最后会执行到如下代码:
void Conductor::UIThreadCallback(int msg_id, void* data) {... case NEW_TRACK_ADDED: { auto* track = reinterpret_cast<webrtc::MediaStreamTrackInterface*>(data); if (track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) { /*获取远端video track*/ auto* video_track = static_cast<webrtc::VideoTrackInterface*>(track); /*送至MainWnd处理*/ main_wnd_->StartRemoteRenderer(video_track); } track->Release(); break; }...}void MainWnd::StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video) { /*生成远端视频渲染器,同时将远端视频渲染器注册到webrtc中。*/ remote_renderer_.reset(new VideoRenderer(handle(), 1, 1, remote_video));}
最后,当有视频帧产生时,会通过 OnFrame 回调给 ViewRenderer (其实 WebRTC 的接口设计在各平台上基本上一致的。前面我们分析 Android 视频渲染或者采集,也都是通过 OnFrame 虚函数给回调的)
// OnFrame方法,当接收到新的视频帧时被调用void MainWnd::VideoRenderer::OnFrame(const webrtc::VideoFrame& video_frame) { // 用AutoLock确保同一时刻只有一个线程可以访问此方法 { AutoLock<VideoRenderer> lock(this); // 获取视频帧的I420格式的缓冲区 rtc::scoped_refptr<webrtc::I420BufferInterface> buffer( video_frame.video_frame_buffer()->ToI420()); // 如果视频帧的旋转角度不为0,则将视频帧旋转至指定角度 if (video_frame.rotation() != webrtc::kVideoRotation_0) { buffer = webrtc::I420Buffer::Rotate(*buffer, video_frame.rotation()); } // 设置视频帧的宽度和高度 SetSize(buffer->width(), buffer->height()); // 确保image_已经被初始化 RTC_DCHECK(image_.get() != NULL); // 将I420格式的图像数据转换为ARGB格式,然后存储到image_中 libyuv::I420ToARGB(buffer->DataY(), buffer->StrideY(), buffer->DataU(), buffer->StrideU(), buffer->DataV(), buffer->StrideV(), image_.get(), bmi_.bmiHeader.biWidth * bmi_.bmiHeader.biBitCount / 8, buffer->width(), buffer->height()); } // 使窗口重绘 InvalidateRect(wnd_, NULL, TRUE);}
这个方法是WebRTC在接收到新的视频帧时的处理过程。它首先获取视频帧的I420格式的缓冲区,然后检查视频帧是否需要旋转,如果需要就进行旋转。接着设置视频帧的宽度和高度,然后将I420格式的图像数据转换为ARGB格式,并存储在image_中。最后,通过调用InvalidateRect函数使窗口无效,这会触发窗口的重绘事件,即显示新的视频帧。
接下来窗口会收到 WM_PAINT 消息,标识即需要重新绘制窗口
// OnPaint方法,当窗口需要重绘时被调用void MainWnd::OnPaint() { PAINTSTRUCT ps; // 开始绘制 ::BeginPaint(handle(), &ps); RECT rc; // 获取窗口客户区的大小 ::GetClientRect(handle(), &rc); VideoRenderer* local_renderer = local_renderer_.get(); VideoRenderer* remote_renderer = remote_renderer_.get(); // 如果正在进行流媒体播放并且本地和远程渲染器都存在 if (ui_ == STREAMING && remote_renderer && local_renderer) { // 使用AutoLock确保同一时刻只有一个线程可以访问这些渲染器 AutoLock<VideoRenderer> local_lock(local_renderer); AutoLock<VideoRenderer> remote_lock(remote_renderer); // 获取远程渲染器的视频信息 const BITMAPINFO& bmi = remote_renderer->bmi(); int height = abs(bmi.bmiHeader.biHeight); int width = bmi.bmiHeader.biWidth; // 获取远程渲染器的视频图像 const uint8_t* image = remote_renderer->image(); // 如果图像存在,开始进行绘制 if (image != NULL) { // 创建一个设备上下文与ps.hdc兼容的内存设备上下文 HDC dc_mem = ::CreateCompatibleDC(ps.hdc); // 设置位图拉伸模式为HALFTONE ::SetStretchBltMode(dc_mem, HALFTONE); // 设置映射模式以保持宽高比 HDC all_dc[] = {ps.hdc, dc_mem}; for (size_t i = 0; i < arraysize(all_dc); ++i) { SetMapMode(all_dc[i], MM_ISOTROPIC); SetWindowExtEx(all_dc[i], width, height, NULL); SetViewportExtEx(all_dc[i], rc.right, rc.bottom, NULL); } // 创建一个与ps.hdc兼容的位图 HBITMAP bmp_mem = ::CreateCompatibleBitmap(ps.hdc, rc.right, rc.bottom); // 将新位图选入内存设备上下文,同时保留旧的位图 HGDIOBJ bmp_old = ::SelectObject(dc_mem, bmp_mem); // 将设备上下文坐标转换为逻辑坐标 POINT logical_area = {rc.right, rc.bottom}; DPtoLP(ps.hdc, &logical_area, 1); // 创建一个黑色的画刷并填充矩形 HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0)); RECT logical_rect = {0, 0, logical_area.x, logical_area.y}; ::FillRect(dc_mem, &logical_rect, brush); // 删除创建的画刷 ::DeleteObject(brush); // 计算绘制图像的起始位置,以使图 // 计算绘制图像的起始位置,以使图像位于中心 int x = (logical_area.x / 2) - (width / 2); int y = (logical_area.y / 2) - (height / 2); // 使用StretchDIBits函数将视频帧图像画到内存设备上下文 StretchDIBits(dc_mem, x, y, width, height, 0, 0, width, height, image, &bmi, DIB_RGB_COLORS, SRCCOPY); // 如果窗口足够大,就在右下角画一个本地视频流的缩略图 if ((rc.right - rc.left) > 200 && (rc.bottom - rc.top) > 200) { const BITMAPINFO& bmi = local_renderer->bmi(); image = local_renderer->image(); int thumb_width = bmi.bmiHeader.biWidth / 4; int thumb_height = abs(bmi.bmiHeader.biHeight) / 4; StretchDIBits(dc_mem, logical_area.x - thumb_width - 10, logical_area.y - thumb_height - 10, thumb_width, thumb_height, 0, 0, bmi.bmiHeader.biWidth, -bmi.bmiHeader.biHeight, image, &bmi, DIB_RGB_COLORS, SRCCOPY); } // 使用BitBlt函数将内存设备上下文的内容复制到屏幕设备上下文 BitBlt(ps.hdc, 0, 0, logical_area.x, logical_area.y, dc_mem, 0, 0, SRCCOPY); // 清理创建的对象 ::SelectObject(dc_mem, bmp_old); ::DeleteObject(bmp_mem); ::DeleteDC(dc_mem); } else { // 如果还没有接收到视频流,就填充黑色背景,并绘制提示文本 HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0)); ::FillRect(ps.hdc, &rc, brush); ::DeleteObject(brush); // 设置字体、文本颜色和背景模式,然后绘制提示文本 HGDIOBJ old_font = ::SelectObject(ps.hdc, GetDefaultFont()); ::SetTextColor(ps.hdc, RGB(0xff, 0xff, 0xff)); ::SetBkMode(ps.hdc, TRANSPARENT); std::string text(kConnecting); if (!local_renderer->image()) { text += kNoVideoStreams; } else { text += kNoIncomingStream; } ::DrawTextA(ps.hdc, text.c_str(), -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); ::SelectObject(ps.hdc, old_font); } } else { // 如果不在流媒体播放状态,就填充白色背景 HBRUSH brush = ::CreateSolidBrush(::GetSysColor(COLOR_WINDOW)); ::FillRect(ps.hdc, &rc, brush); ::DeleteObject(brush); } // 结束绘制 ::EndPaint(handle(), &ps);}
代码有点长,这里做一下总结:
如果正在播放流媒体且本地和远程渲染器都存在,则绘制远程视频流。如果窗口足够大,就在右下角绘制本地视频流的缩略图。
如果还没有接收到视频流,则在黑色背景上显示提示信息。
如果不在播放流媒体的状态,则只填充窗口的背景。
到此,对端视频可以正常的显示出来了。
总结
该篇文章详细的分析了 peerconnection_client 客户端的窗口交互、信令交互、和视频渲染等处理,篇幅较长,建议自己先 debug peerconnection_client demo, 如流程上有不懂的再来看该篇对应的处理讲解。下一篇文章会进行 Windows P2P 的实战开发,与之前的 Web 和 Android 可以进行音视频通话。