WebRTC source code analysis (3) Windows platform P2P audio and video call examples peerconnection_client detailed explanation

introduce

Environment: webrtc m98, Windows

peerconnection_clientIt is a sample program provided by WebRTC, which mainly demonstrates how to use the WebRTC library to realize point-to-point real-time audio and video calls on the Windows platform. It is a client application program that works with a peerconnection_serversignaling server to exchange signaling through the signaling server to establish and maintain a P2P connection between two or more clients. This example is very helpful for us to understand the overall architecture and operation process of WebRTC.

Program entry and main frame

Find the compiled webrtc sample VS program, open the all.sln program through VS, and then set peerconnection_client as the startup item, as shown in the figure below

img_v2_67fbd162-8eeb-4f1b-9c18-e1b61936d83g

After starting the call, the effect is as follows:

img_v2_77dfb6a9-6c95-4ef0-9983-18b6e03692cg

If you open two client-side debugging locally, you can achieve the above effect by opening the virtual camera of OBS.

peerconnection_client is mainly composed of the following parts

peerconnection_client UML (1)

  1. main.cc: This is the entry point of the program, it creates and runs the application's message loop, initializes and runs the main window. It creates PeerConnectionClientand Conductorobjects, and links them so that they work together.
  2. main_wnd.cc: It is the implementation of the main window class. This class is responsible for all UI operations such as button clicks, video display windows, status updates, etc. It also notifies events of user actions to Conductorthe object .
  3. peer_connection_client.cc: This class is a client that connects to the PeerConnectionServersignaling server, registers with the server, and processes signaling messages from and to the server.
  4. conductor.cc: It is the core of the whole program, responsible for managing PeerConnectionClientobjects and MainWndobjects. It also creates and manages WebRTC PeerConnectionobjects , and handles all WebRTC events. For example, when the user clicks on the "user list item", MainWndthe object will notify the user of this event Conductor, Conductorand will order to send a signaling message PeerConnectionClientto the signaling server to start a new call.

Let's take a look at the core code in main.cc, which is the entry function:

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.\n", 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; 

The role of the entry function is to initialize and start the WebRTC peerconnection, process command line parameters, set the window interface, and start receiving and processing Windows messages until the peer connection is closed and the program ends.

window management

The work of window management is mainly in the main_wnd.cc create function, let's see how it creates the WebRTC window,

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;
}

The purpose of this function is to create a main window and configure it according to the needs of the program. In this window, some sub-windows (ip edit box, connection button, user list, etc.) will be created, and the UI state of the window is first set to "connection" state.

Finally, display the created series of windows by calling the 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);
}

The final window will look like this:img_v2_bba77859-bbe6-4299-b571-1f26366f7b3g

When we click Connect in the above figure, the system will send a message to the callback function of receiving the message in the WndProc window. If the connection is successful, it will switch to the user list UI. The core code is as follows:

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_));
    }
  }
}


SwitchToPeerListThe function first closes the connection interface, then resets the contents of the list, adds a title to the list, and adds all currently online peers to the list. Next, it switches the state of the UI to show the list of peers and sets the focus of the list. Finally, if automatic calling is set up and a peer is online, it selects the last item in the list and simulates a double click event.

After this code is executed, the corresponding user list can be displayed, for example, as shown below:

img_v2_1986c4b8-2db5-4bff-bc5e-813f0ea8b9dg

当双击用户名称时,双方就会发起 SDP 媒体协商,网络协商等,如果都协商成功就可以传输并显示音视频画面了,这个后面会详细说到。

如果用户主动关闭窗口,窗口会收到退出的消息并关闭 peerconnection 连接。

到这里窗口整个的创建->更新->关闭都分析完了,接下来会分析 peerconnection_client 与 server 的信令交互

信令处理

image-20230618154102953

下载 pcapng 包链接: pan.baidu.com/s/1wGyyLSxd… 提取码: frrr

通过抓包我们得到了如下几个信令:

GET sign_in: 用户登录消息

**GET sign_out:**用户退出消息

POST message: 协商交互消息

GET wait: 用户等待消息

这里绘制了一张简要的时序图

PeerConnection_Client_p2p

当用户点击 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\n\r\n",
           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\n
 
HTTP/1.1 200 Added\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 22\r\n
Pragma: 12\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n

devyk@devyk-mwin,12,1\n

当第二个人连接进来的时候,收到的消息

    HTTP/1.1 200 Added\r\n
    Server: PeerConnectionTestServer/0.1\r\n
    Cache-Control: no-cache\r\n
    Connection: close\r\n
    Content-Type: text/plain\r\n
    Content-Length: 44\r\n
    Pragma: 13\r\n
    Access-Control-Allow-Origin: *\r\n
    Access-Control-Allow-Credentials: true\r\n
    Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
    Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
    Access-Control-Expose-Headers: Content-Length\r\n
    \r\n

    devyk@devyk-mwin,13,1\n
    devyk@devyk-mwin,12,1\n

解析 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('\n', 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\n
    devyk@devyk-mwin,12,1\n

响应信息的格式是: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\n\r\n",
           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\n

当有新的信令消息产生时,会以 wait 的响应回来

    HTTP/1.1 200 OK\r\n
    Server: PeerConnectionTestServer/0.1\r\n
    Cache-Control: no-cache\r\n
    Connection: close\r\n
    Content-Type: text/plain\r\n
    Content-Length: 22\r\n
    Pragma: 12\r\n
    Access-Control-Allow-Origin: *\r\n
    Access-Control-Allow-Credentials: true\r\n
    Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
    Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
    Access-Control-Expose-Headers: Content-Length\r\n
    \r\n


    devyk@devyk-mwin,13,1\n

然后就会触发下面的函数:

void PeerConnectionClient::OnHangingGetRead(rtc::Socket* socket) {
  RTC_LOG(LS_INFO) << __FUNCTION__;
  size_t content_length = 0;
  //从指定的socket读取响应信息,并做适当的处理。如果从响应中得知使用的是http短连接,那么需要关闭socket。
  if (ReadIntoBuffer(socket, &notification_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\r\n
Content-Length: 5608\r\n
Content-Type: text/plain\r\n
\r\n

{\n
     "sdp" : "v=0\r\no=- 6269511735434714595 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream_id\r\nm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1
       "type" : "offer"\n
}\n

这是 peer_id=13 发送给 12 的 offer 信令,对应的响应如下:

    HTTP/1.1 200 OK\r\n
    Server: PeerConnectionTestServer/0.1\r\n
    Cache-Control: no-cache\r\n
    Connection: close\r\n
    Content-Type: text/plain\r\n
    Content-Length: 0\r\n
    Access-Control-Allow-Origin: *\r\n
    Access-Control-Allow-Credentials: true\r\n
    Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
    Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
    Access-Control-Expose-Headers: Content-Length\r\n
    \r\n


服务端通过转发给另一个 peer wait 的 offer 响应

    HTTP/1.1 200 OK\r\n
    Server: PeerConnectionTestServer/0.1\r\n
    Cache-Control: no-cache\r\n
    Connection: close\r\n
    Content-Type: text/plain\r\n
    Content-Length: 5608\r\n
    Pragma: 13\r\n
    Access-Control-Allow-Origin: *\r\n
    Access-Control-Allow-Credentials: true\r\n
    Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
    Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
    Access-Control-Expose-Headers: Content-Length\r\n
    \r\n

    {\n
        "sdp" : "v=0\r\no=- 6269511735434714595 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream_id\r\nm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1
       "type" : "offer"\n
    }\n

另一方收到 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个意思

  1. 实例化 PeerConnectionFactoy 和 PeerConnectionClient
  2. 设置远端的 SDP,并 CreateAnswer
  3. 收到对方发来的 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\n
    Content-Length: 186\r\n
    Content-Type: text/plain\r\n
    \r\n

    {\n
       "candidate" : "candidate:1019731727 1 udp 2122260223 192.168.1.104 53072 typ host generation 0 ufrag IEDW network-id 3 network-cost 10",\n
       "sdpMLineIndex" : 0,\n
       "sdpMid" : "0"\n
    }\n

通过抓包得到了如上 candidate 消息,注意 candidate 会存在多个消息,双方收到后并添加到 PeerConnectionClient 中,如果网络协商成功,那么就可以进行采集->编码->传输了。

最后一个信令是 退出信令 ,当关闭窗口时,发送如下格式的信令

request:
GET /sign_out?peer_id=13 HTTP/1.0\r\n

response:
HTTP/1.1 200 OK\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 0\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n


到此,所有信令就分析完了,建议大家可以通过抓包去分析对应的流程。

媒体流处理

当媒体协商,网络协商完成后,就能进行等待收对方发过来的音视频流了,当有新轨道产生,会执行 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);
}

代码有点长,这里做一下总结:

  1. 如果正在播放流媒体且本地和远程渲染器都存在,则绘制远程视频流。如果窗口足够大,就在右下角绘制本地视频流的缩略图。
  2. 如果还没有接收到视频流,则在黑色背景上显示提示信息。
  3. 如果不在播放流媒体的状态,则只填充窗口的背景。

到此,对端视频可以正常的显示出来了。

总结

This article analyzes the window interaction, signaling interaction, and video rendering of the peerconnection_client client in detail. It is quite long. It is recommended to debug the peerconnection_client demo first. If you do not understand the process, then read the corresponding processing explanation. The next article will carry out the actual development of Windows P2P, which can make audio and video calls with the previous Web and Android.

Guess you like

Origin juejin.im/post/7245922875181334588