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) 开启渲染。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
WebRTC PeerConnectionWebRTC技术框架中重要的一环,它提供了实时音视频通信的能力。如果你想在你的应用程序中使用PeerConnection,你需要经过以下步骤将它引入进来: 1. 首先,你需要在你的应用程序中引入WebRTC的JavaScript库。你可以通过在你的HTML文件中引入以下代码来实现: ```<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>``` 2. 接下来,你需要在你的应用程序中创建PeerConnection实例。创建实例时,你需要指定ICE服务器的URL。此外,你还可以将本地音视频流添加到PeerConnection中。 ``` var pc = new RTCPeerConnection({ 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }); navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(stream) { var videoTrack = stream.getVideoTracks()[0]; var audioTrack = stream.getAudioTracks()[0]; pc.addTrack(videoTrack, stream); pc.addTrack(audioTrack, stream); }); ``` 3. 现在,你已经成功地创建了PeerConnection实例,并添加了本地音视频流。下一步是启动ICE候选项协商,以建立远程音视频流。这可以通过创建一个SDP交换会话,以与远程对等方交换SDP来实现。 ``` pc.createOffer().then(function(offer) { return pc.setLocalDescription(offer); }).then(function() { // send offer to remote peer }); ``` 以上就是引入WebRTC PeerConnection的方法。当然,以上代码只是基础代码,并不能实现完整的音视频通信。在实际应用中,还需要处理候选项协商、SDP交换、ICE连接状态等。但不管怎样,PeerConnection作为WebRTC技术的核心,对于实现实时音视频通信至关重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椛茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值