【音视频】WebRTC 一对一通话 peerconnection_client 分析

一、概述

我们在编译好WebRTC Native 的C++库的时候,生成了all.sln的中有包含peerconnection_client这个解决方案,这个可以作为我们学习WebRTC的开始,主要看学习它的整个调用流程与API的接口使用,至于原理和更底层的代码,这个可以放到后面再去细看

二、信令整体实现流程

  • 使用过Web端的WebRTC的JS接口大概就知道了这个流程,首先我们需要自己实现一个信令服务器,至于这个信令服务器如何实现,有多种方法,比如使用WebSocket、HTTP,其中选择添加SSL/TLS都可以

  • 不过需要说明的是,WebRTC使用的是谷歌开源的boringSSL库,这个库是基于OpenSSL分支进行的扩展,因此许多头文件函数符号与OpenSSL一样,如果我们使用OpenSSL就会冲突,此时要么选择使用OpenSSL重新编译WebRTC,要么就是使用boringSSL编译的webrtc.lib,并且将OpenSSL编译为动态库,此时其余boringSSL未定义的符号就会链接到OpenSSL的库了

我们来看信令服务器需要实现的逻辑:

  1. 实现媒体协商的SDP交换:媒体协商主要是客户端收集自己的编解码能力,然后保存为SDP格式存储到本地,并且发送给对方,也就是两端的offeranswer都需要通过信令服务器转发到对端,当然,如果两端的网络环境可以直接通讯,也不需要这个信令服务器

  2. 实现网络协商的Candidate交换:确定了双方的编解码能力之后,后续需要收集双方的网络情况,这里的ICE Candidate逻辑简单来说就是看双方是否可以直接P2P通讯,否则进行relay中继转发,至于打洞原理,这里不做讲解

  3. 实现业务逻辑:客户端加入,需要通过信令服务器传达给对端,以至于让对端做好存储信息的准备,好回复信令服务器,自己的offeranswercandidate需要转发给谁;客户端离开,需要传达给对端,以至于让对端结束渲染,释放资源

信令的流程可以参考下面这幅图

三、PeerConnection_clinet 具体实现

来到peerconnection_client这个项目,可以看到有几个文件:

  1. conductor.h/conductor.cc,这个文件内部主要实现WebRTC的核心功能,使用的是WebRTC的核心API,也是我们后续参考的主要来源

  2. peer_connection_client.h/peer_connection_client.cc,这个是一个信令服务器,主要采用HTTP协议实现,我们简单看看,可以采用自己的方法,主要是看看消息什么时候发送,接收的消息如何解析和回复

  3. main_wnd.h/main_wnd.cc,这个是WIN32的窗口类,我们一般不使用WIN32UI,因此就就看看就好,主要是了解渲染源什么时候发送到窗口

  4. defaults.h/defaults.ccflag_defs.h,这个主要是定义一些默认的参数与配置,比如一些UI文字、端口号、以及谷歌默认的stun服务器

3.1 main.h/main.cc 初始化客户端

这个文件主要是整体客户端的初始工作,包括启动UI窗口、HTTP信令服务器、解析命令行参数、初始化SSL、同时启动消息循环,监听窗口事件(这个是WIN32特有的)

初始化系统资源

c 复制代码
rtc::WinsockInitializer winsock_init;  // 初始化 Winsock 网络库(Windows 网络基础)
rtc::Win32SocketServer w32_ss;  // WebRTC 适配 Windows 的 Socket 服务器
rtc::Win32Thread w32_thread(&w32_ss);  // WebRTC 线程(绑定 Socket 服务器)
rtc::ThreadManager::Instance()->SetCurrentThread(&w32_thread);  // 设置当前线程为 WebRTC 主线程

初始化 SSL 与核心对象

c 复制代码
rtc::InitializeSSL();  // 初始化 SSL(WebRTC 通信需加密,依赖 boringSSL)
PeerConnectionClient client;  // 信令客户端(与信令服务器交互,交换 SDP/ICE 信息)
rtc::scoped_refptr<Conductor> conductor(
    new rtc::RefCountedObject<Conductor>(&client, &wnd));  // 协调器:连接 UI、信令客户端和 WebRTC 核心

解析命令行参数

c 复制代码
WindowsCommandLineArguments win_args;  // 转换命令行参数为 UTF-8
absl::ParseCommandLine(win_args.argc(), win_args.argv());  // 解析参数(如 --server、--port)

参数校验与窗口创建

c 复制代码
// 校验端口有效性(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;
}

// 创建主窗口(UI)
MainWnd wnd(server.c_str(), absl::GetFlag(FLAGS_port), ...);
if (!wnd.Create()) { ... }  // 创建窗口失败则退出

消息循环(程序主循环)

cpp 复制代码
MSG msg;
BOOL gm;
while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) {
  if (!wnd.PreTranslateMessage(&msg)) {  // 窗口预处理消息(如快捷键)
    ::TranslateMessage(&msg);  // 转换消息(如键盘消息)
    ::DispatchMessage(&msg);  // 分发消息到窗口回调(如点击按钮)
  }
}

3.2 conductor.h/conductor.cc WebRTC 核心功能

这个文件实现的都是WebRTC的核心功能,包括媒体协商、网络协商、视频流添加、音频流添加等等,下面详细介绍

UI窗口回调

这些枚举定义是回调函数的类型,主要是UI线程的回调事件,作用是在多线程环境下(WebRTC 内部逻辑多在工作线程执行,而 UI 操作需在主线程执行),通过统一的枚举值区分不同事件,确保 Conductor 类能在 UI 线程(如 Windows 窗口消息循环线程)中正确处理对应的业务逻辑。

c 复制代码
enum CallbackID {
  MEDIA_CHANNELS_INITIALIZED = 1,  // 媒体通道初始化完成
  PEER_CONNECTION_CLOSED,          // 对等连接已关闭
  SEND_MESSAGE_TO_PEER,            // 需要向对端发送消息
  NEW_TRACK_ADDED,                 // 新的媒体轨道(音视频轨)被添加
  TRACK_REMOVED                    // 媒体轨道被移除
};```

- `MEDIA_CHANNELS_INITIALIZED`:当 WebRTC 的媒体通道(音频 / 视频传输通道)初始化完成后,触发该回调,通知 UI 可以开始渲染媒体流。
- `PEER_CONNECTION_CLOSED`:对等连接(`PeerConnection`)关闭后,触发该回调,通知 UI 更新连接状态(如显示 "已断开连接")。
- `SEND_MESSAGE_TO_PEER`:需要向对端发送消息时(如信令消息),触发该回调,由 UI 线程协调消息发送逻辑。
- `NEW_TRACK_ADDED`:新的媒体轨道(如对端发送的视频轨)被添加到连接中时,触发该回调,通知 UI 创建对应的渲染窗口(如显示远程视频)。
- `TRACK_REMOVED`:媒体轨道(如对端停止发送视频)被移除时,触发该回调,通知 UI 销毁对应的渲染窗口(如关闭远程视频显示)。

在`main_wnd.h`中可以看到有`MainWndCallback`类,可以看作是一个观察者类,内部是纯虚函数的回调接口

```c
class MainWndCallback {
 public:
  virtual void StartLogin(const std::string& server, int port) = 0;
  virtual void DisconnectFromServer() = 0;
  virtual void ConnectToPeer(int peer_id) = 0;
  virtual void DisconnectFromCurrentPeer() = 0;
  virtual void UIThreadCallback(int msg_id, void* data) = 0;
  virtual void Close() = 0;

 protected:
  virtual ~MainWndCallback() {}
};

与上述定义的回调参数定义匹配,我们需要继承这个类,然后实现对应的虚函数,以至于在WebRTC触发对应事件时调用UI的回调

实现具体的纯虚函数,作为一个观察者模式使用

c 复制代码
  void StartLogin(const std::string& server, int port) override;
  void DisconnectFromServer() override;
  void ConnectToPeer(int peer_id) override;
  void DisconnectFromCurrentPeer() override;
  void UIThreadCallback(int msg_id, void* data) override;

PeerConnectionClientObserver 回调

  • PeerConnectionClientObserver是一个观察者接口,用于监听与信令服务器的交互事件(如连接状态、对等端状态变化、消息收发等)。通过实现该接口的回调方法,客户端可以响应服务器和对等端的各类事件。

  • 在我们的peer_connection_client.h中有这个类,内部定义了一些虚函数接口

c 复制代码
struct PeerConnectionClientObserver {
  virtual void OnSignedIn() = 0;  // Called when we're logged on.
  virtual void OnDisconnected() = 0;
  virtual void OnPeerConnected(int id, const std::string& name) = 0;
  virtual void OnPeerDisconnected(int peer_id) = 0;
  virtual void OnMessageFromPeer(int peer_id, const std::string& message) = 0;
  virtual void OnMessageSent(int err) = 0;
  virtual void OnServerConnectionFailure() = 0;

 protected:
  virtual ~PeerConnectionClientObserver() {}
};
1. OnSignedIn()
  • 触发时机:当客户端成功登录到信令服务器时调用。
  • 作用:通知客户端已完成与服务器的认证 / 连接,此时可以开始发现其他在线对等端(peer)。
2. OnDisconnected()
  • 触发时机:当客户端与信令服务器断开连接时调用(可能是主动断开或网络异常)。
  • 作用:通知客户端当前已与服务器失去连接。
3. OnPeerConnected(int id, const std::string& name)
  • 触发时机:当有新的对等端(peer)登录到信令服务器时,当前客户端会收到此回调。
  • 参数
    • id:新对等端的唯一标识 ID(由服务器分配)。
    • name:新对等端的名称
  • 作用:通知客户端有新的对等端上线,可用于建立 P2P 连接。
4. OnPeerDisconnected(int id)
  • 触发时机:当某个已在线的对等端从信令服务器断开连接时调用。
  • 参数id:断开连接的对等端 ID。
  • 作用:通知客户端某个对等端已下线。
5. OnMessageFromPeer(int peer_id, const std::string& message)
  • 触发时机:当客户端收到来自其他对等端的消息(通过信令服务器转发)时调用。
  • 参数
    • peer_id:发送消息的对等端 ID。
    • message:消息内容(通常是 WebRTC 信令信息,如 SDP offer/answer、ICE 候选等)。
  • 作用:接收并处理对等端的信令消息,驱动 P2P 连接建立过程。

6. OnMessageSent(int err)

  • 触发时机:当客户端向对等端发送消息后,收到服务器的发送结果通知时调用。
  • 参数err:错误码(0 通常表示成功,非 0 表示发送失败)。
  • 作用:通知消息发送的结果(成功或失败)。

7. OnServerConnectionFailure()

  • 触发时机:当客户端尝试连接信令服务器但失败时调用(如服务器地址错误、网络不通等)。
  • 作用:通知客户端与服务器的连接尝试失败。

webrtc::PeerConnectionObserver 回调

  • 这个类是 Conductor 类对 webrtc::PeerConnectionObserver 接口 的实现,该接口是 WebRTC 中监听 PeerConnection(对等连接)状态变化的核心接口,用于响应 P2P 连接过程中的各类事件(如媒体轨道增减、ICE 候选生成、连接状态变化等)

  • 要实现WebRTC通话,这些回调函数必须被实现,在回调函数中收集信息,收集到自己的媒体信息、网络信息并发送到对端

1. OnSignalingChange
cpp 复制代码
void OnSignalingChange(webrtc::PeerConnectionInterface::SignalingState new_state) override {}
  • 作用 :监听信令状态变化 。信令状态反映 SDP 协商的阶段(如是否有本地 / 远程 SDP 未处理),SignalingState 包含以下状态:
    • kStable:无未处理的 SDP,协商稳定;
    • kHaveLocalOffer:已生成本地 SDP offer 但未收到远程 answer;
    • kHaveRemoteOffer:已收到远程 SDP offer 但未生成本地 answer;
    • kHaveLocalPrAnswer/kHaveRemotePrAnswer:存在临时 answer(用于中间协商)。
2. OnAddTrack
cpp 复制代码
void OnAddTrack(
    rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver,
    const std::vector<rtc::scoped_refptr<webrtc::MediaStreamInterface>>& streams) override;
  • 触发时机:当远程端向当前连接添加媒体轨道(音频 / 视频)时触发(如远程开启摄像头 / 麦克风)。
  • 参数
    • receiver:用于接收该轨道数据的 RtpReceiver(包含解码后的媒体数据);
    • streams:该轨道关联的媒体流(一个轨道可属于多个流)。
  • 核心作用:处理远程新增的媒体轨道,通常用于初始化渲染逻辑(如创建视频渲染窗口、绑定音频输出设备)。
3. OnRemoveTrack
cpp 复制代码
void OnRemoveTrack(rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver) override;
  • 触发时机:当远程端从连接中移除媒体轨道时触发(如远程关闭摄像头 / 麦克风)。
  • 参数receiver:与被移除轨道关联的 RtpReceiver
  • 核心作用:清理被移除轨道的资源,如关闭对应的视频渲染窗口、停止音频输出。

4. OnDataChannel

cpp 复制代码
void OnDataChannel(rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override {}
  • 作用:监听远程端创建的数据通道(用于传输非媒体数据,如文本消息)。
  • 参数channel:远程创建的数据通道接口。

5. OnRenegotiationNeeded

cpp 复制代码
void OnRenegotiationNeeded() override {}
  • 触发时机:当连接需要重新协商 SDP 时触发(如媒体轨道增减、网络状态剧变)。
  • 作用:通知应用需重新生成 SDP 并交换(如重新创建 offer/answer)。

6. OnIceConnectionChange

cpp 复制代码
void OnIceConnectionChange(webrtc::PeerConnectionInterface::IceConnectionState new_state) override {}
  • 作用 :监听ICE 连接状态变化 ,反映 P2P 连接的可用性,IceConnectionState 包含:

    • kNew/kChecking:初始状态 / 正在检查 ICE 候选;
    • kConnected:已找到可用的 P2P 路径;
    • kCompleted:ICE 协商完成;
    • kFailed/kDisconnected:连接失败 / 断开。
  • 示例实现:空实现(示例未额外处理连接状态的 UI 反馈或重试逻辑)。

7. OnIceGatheringChange

cpp 复制代码
void OnIceGatheringChange(webrtc::PeerConnectionInterface::IceGatheringState new_state) override {}
  • 作用 :监听ICE 候选者收集状态IceGatheringState 包含:
    • kNew:未开始收集;
    • kGathering:正在收集候选者(如本地 IP、端口);
    • kComplete:候选者收集完成。

8. OnIceCandidate

cpp 复制代码
void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) override;
  • 触发时机:当本地生成一个 ICE 候选者(用于 P2P 连接的网络路径信息,如 IP、端口、传输协议)时触发。
  • 参数candidate:包含候选者信息的对象(如 sdp_mline_indexsdp 内容)。
  • 核心作用:必须实现的关键方法,需将本地 ICE 候选者通过信令服务器发送给远程端,以完成 P2P 路径协商。

9. OnIceConnectionReceivingChange

cpp 复制代码
void OnIceConnectionReceivingChange(bool receiving) override {}
  • 作用 :监听 ICE 连接是否正在接收数据(receivingtrue 表示正在接收)。
  • 示例实现:空实现(示例无需处理接收状态的额外逻辑)。

初始化 PeerConnectionFactory

InitializePeerConnection函数内实现了对peerconnectionpeerconnection_factory

  • webrtc::CreatePeerConnectionFactory创建了线程对象:

    • 网络线程network_thread,主要用于WebRTC中的网络操作
    • 工作线程worker_thread,主要用于音视频编解码等媒体数据操作
    • 信令线程signaling_thread,主要调用WebRTC中的信令,比如创建offer,保存SDP
  • webrtc::CreatePeerConnectionFactory还可以指定编解码工厂,如果编译时未启动H264编解码,需要自己注入外部的H264编解码工厂

  • 其他参数如audio_mixerdefault_adm,我们直接传入nullptr,使用默认的就好

c 复制代码
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;
}

初始化 PeerConnection

CreatePeerConnection函数中,我们初始化了peerconnection,这里可以指定配置,比如添加dtls加密,选择SDP的版本,以及配置ICE服务器,用于进行打洞和中继,这里直接使用的是谷歌默认的服务器

c 复制代码
// 创建WebRTC对等连接(PeerConnection)实例
// 参数dtls:是否启用DTLS-SRTP加密媒体流(确保音视频传输的安全性)
bool Conductor::CreatePeerConnection(bool dtls) {
  // 检查PeerConnection工厂是否已初始化(工厂是创建PeerConnection的前提)
  RTC_DCHECK(peer_connection_factory_);
  // 检查当前是否已存在PeerConnection实例(避免重复创建)
  RTC_DCHECK(!peer_connection_);

  // 初始化PeerConnection的配置信息
  webrtc::PeerConnectionInterface::RTCConfiguration config;
  
  // 设置SDP语义为Unified Plan(WebRTC推荐的最新SDP格式,支持多轨道、灵活媒体协商)
  config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
  
  // 根据参数启用DTLS-SRTP加密(DTLS用于密钥协商,SRTP用于媒体流加密,防止窃听)
  config.enable_dtls_srtp = dtls;

  // 配置ICE服务器(用于NAT穿透,获取可通信的网络路径)
  webrtc::PeerConnectionInterface::IceServer server;
  // 从GetPeerConnectionString()获取STUN/TURN服务器地址(如"stun:stun.l.google.com:19302")
  server.uri = GetPeerConnectionString();
  // 将ICE服务器添加到配置中(支持多个服务器,此处添加一个)
  config.servers.push_back(server);

  // 通过PeerConnection工厂创建PeerConnection实例
  // 参数说明:
  // - config:上述配置的对等连接参数
  // - 第二个参数:ICEServerObserver(此处为nullptr,不监听ICE服务器状态)
  // - 第三个参数:StatsObserver(此处为nullptr,不监听统计信息)
  // - this:当前Conductor实例作为PeerConnectionObserver,监听对等连接状态变化
  peer_connection_ = peer_connection_factory_->CreatePeerConnection(
      config, nullptr, nullptr, this);

  // 返回创建结果(非空表示创建成功)
  return peer_connection_ != nullptr;
}

添加音视频Track

添加音频的track直接使用默认的即可,比较简单,因为电脑默认都只有一个麦克风

c 复制代码
void Conductor::AddTracks() {
  if (!peer_connection_->GetSenders().empty()) {
    return;  // Already added tracks.
  }

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

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

  main_wnd_->SwitchToStreamingUI();
}

添加视频的track通常需要自己自定义一个输入源,这里是需要继承webrtc::VideoTrackSource,并且需要提供source接口返回对应的源,因为在进行本地编解码能力收集的时候后调用这个函数添加发布视频源,在这里面的构造Create()接口只是创建了能打开的第一个源

cpp 复制代码
class CapturerTrackSource : public webrtc::VideoTrackSource {
 public:
  static rtc::scoped_refptr<CapturerTrackSource> Create() {
    const size_t kWidth = 640;
    const size_t kHeight = 480;
    const size_t kFps = 30;
    std::unique_ptr<webrtc::test::VcmCapturer> capturer;
    std::unique_ptr<webrtc::VideoCaptureModule::DeviceInfo> info(
        webrtc::VideoCaptureFactory::CreateDeviceInfo());
    if (!info) {
      return nullptr;
    }
    int num_devices = info->NumberOfDevices();
    for (int i = 0; i < num_devices; ++i) {
      capturer = absl::WrapUnique(
          webrtc::test::VcmCapturer::Create(kWidth, kHeight, kFps, i));
      if (capturer) {
        return new rtc::RefCountedObject<CapturerTrackSource>(
            std::move(capturer));
      }
    }

    return nullptr;
  }
   protected:
  explicit CapturerTrackSource(
      std::unique_ptr<webrtc::test::VcmCapturer> capturer)
      : VideoTrackSource(/*remote=*/false), capturer_(std::move(capturer)) {}

 private:
  rtc::VideoSourceInterface<webrtc::VideoFrame>* source() override {
    return capturer_.get();
  }
  std::unique_ptr<webrtc::test::VcmCapturer> capturer_;
};

创建本地Offer

当对端连接到我们的时候,此时我们需要创建自己的本地offer,也就是收集自己的编解码能力,因此,我们需要先初始化peerconnection,然后调用CreateOffer这个接口,开始在信令线程中收集我们的媒体信息。

c 复制代码
// 连接到指定ID的对等端(peer),启动WebRTC连接建立流程
// 参数peer_id:目标对等端的唯一标识(由信令服务器分配)
void Conductor::ConnectToPeer(int peer_id) {
  // 断言检查:当前未连接到任何对等端(peer_id_为-1表示无连接)
  RTC_DCHECK(peer_id_ == -1);
  // 断言检查:目标对等端ID有效(不为-1)
  RTC_DCHECK(peer_id != -1);

  // 如果已存在PeerConnection实例(表示已有连接),提示仅支持单连接
  if (peer_connection_.get()) {
    main_wnd_->MessageBox(
        "Error", "We only support connecting to one peer at a time", true);
    return;
  }

  // 初始化PeerConnection(创建对等连接实例,配置ICE服务器等)
  if (InitializePeerConnection()) {
    // 记录当前连接的对等端ID
    peer_id_ = peer_id;
    // 创建SDP Offer(发起方主动生成的会话描述,包含本地媒体能力等信息)
    // 参数说明:
    // - this:当前Conductor作为CreateSessionDescriptionObserver,监听offer创建结果
    // - RTCOfferAnswerOptions:创建offer的选项(默认配置,如是否需要重传等)
    peer_connection_->CreateOffer(
        this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
  } else {
    // 初始化PeerConnection失败时,提示错误
    main_wnd_->MessageBox("Error", "Failed to initialize PeerConnection", true);
  }
}

保存本地offer

收集媒体信息结束之后,会调用 webrtc::CreateSessionDescriptionObserver类的回调函数,告知我们SDP是否创建成功,这个也是一个观察者类,因此我们一开始也继承了它:

那么,很自然我们就要实现这两个接口,分别是创建SDP成功和失败:

c 复制代码
  void OnSuccess(webrtc::SessionDescriptionInterface* desc) override;
  void OnFailure(webrtc::RTCError error) override;

OnSuccess中,我们要保存我们创建的SDP,同时把我们创建的offer发送到对端,这里使用JSON来包装,主要是方便和Web端的通讯

c 复制代码
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));
}
  • 保存SDP到本地,还需要继承一个观察者类:webrtc::SetSessionDescriptionObserver,这个类同样有创建成功和创建失败的两个回调接口:

  • 创建好了本地的offer之后,我们只需要等待对端的回复,然后再将对端的answer保存到本地,因此保存成功后什么都不需要做,因此直接输出一条日志即可

c 复制代码
class DummySetSessionDescriptionObserver
    : public webrtc::SetSessionDescriptionObserver {
 public:
  static DummySetSessionDescriptionObserver* Create() {
    return new rtc::RefCountedObject<DummySetSessionDescriptionObserver>();
  }
  virtual void OnSuccess() { RTC_LOG(INFO) << __FUNCTION__; }
  virtual void OnFailure(webrtc::RTCError error) {
    RTC_LOG(INFO) << __FUNCTION__ << " " << ToString(error.type()) << ": "
                  << error.message();
  }

保存对端answer

收到对端的消息,解析后,如果是回复我们的answer,那么我们需要触发自身继承的webrtc::CreateSessionDescriptionObserver回调函数,我们需要调用SetRemoteDescription,保存来自对端的answer,保存成功后,如果调用OnSurcess就说明媒体协商此时结束了

c 复制代码
void Conductor::OnMessageFromPeer(int peer_id, const std::string& message) {
//省略...

    if (!session_description) {
      RTC_LOG(WARNING) << "Can't parse received session description message. "
                          "SdpParseError was: "
                       << error.description;
      return;
    }
    RTC_LOG(INFO) << " Received session description :" << message;
    peer_connection_->SetRemoteDescription(
        DummySetSessionDescriptionObserver::Create(),
        session_description.release());
    
    
    //省略...
}

开始ICE Candidate

媒体协商结束的标志是双方交换offeranswer的SDP结束,此时就会开始收集自身的ICE Candidate,如果收集完毕,就会调用webrtc::PeerConnectionObserver类的回调函数OnIceCandidate,传入收集好的candidate,我们此时需要将这个candidate封装为JSON格式发送给对端

c 复制代码
void Conductor::OnIceCandidate(const webrtc::IceCandidateInterface* candidate) {
  RTC_LOG(INFO) << __FUNCTION__ << " " << candidate->sdp_mline_index();
  // For loopback test. To save some connecting delay.
  if (loopback_) {
    if (!peer_connection_->AddIceCandidate(candidate)) {
      RTC_LOG(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));
}

保存对端Candidate

对端同样会开始收集自身的candidate,然后发送给我们,我们解析消息如果发现是对端的candidate信息,那么我们会保存到本地,也就是调用AddIceCandidate接口

c 复制代码
void Conductor::OnMessageFromPeer(int peer_id, const std::string& message) {
	//省略...

    if (!peer_connection_->AddIceCandidate(candidate.get())) {
      RTC_LOG(WARNING) << "Failed to apply the received candidate";
      return;
    }
    RTC_LOG(INFO) << " Received candidate :" << message;
  }
	//省略...
}

收集对端Track

  • 如果ICE Candidate也成功了,那么之后无论是通过P2P还是Relay的方式,我们都会收到来自对端的视频流/音频流,音频流默认不需要额外处理,我们需要处理视频流,收集到的Track会在OnTrack回调函数中传入

  • 我们需要将我们的窗口或其他渲染器,绑定对应的视频轨道,视频轨道中的源可以被我订阅,调用AddOrUpdateSink注册回调,然后我们就会在OnFrame回调函数中收到解码后的视频帧

cpp 复制代码
void Conductor::OnAddTrack(
    rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver,
    const std::vector<rtc::scoped_refptr<webrtc::MediaStreamInterface>>&
        streams) {
  RTC_LOG(INFO) << __FUNCTION__ << " " << receiver->id();
  main_wnd_->QueueUIThreadCallback(NEW_TRACK_ADDED,
                                   receiver->track().release());
}

渲染视频

对于渲染视频,不同的渲染器有不同的方式,但是核心都是在OnFrame回调函数中得到视频帧,然后重采样为rgb显示到我们的窗口中,这里采样Win32的方式,现在用的不多,也不需要看太多了

更多资料:https://github.com/0voice

相关推荐
音视频牛哥10 分钟前
如何计算 PCM 音频与 YUV/RGB 原始视频文件大小?
音视频·pcm·大牛直播sdk·rtsp播放器·rtmp播放器·yuv rgb计算大小·pcm计算大小
音视频牛哥2 小时前
从H.264到AV1:音视频技术演进与模块化SDK架构全解析
人工智能·音视频·大牛直播sdk·rtsp h.265·h.264 h.265 av1·h.265和h.266·enhenced rtmp
恒拓高科WorkPlus14 小时前
局域网视频软件BeeWorks,内网顺畅沟通
音视频
关键帧-Keyframe14 小时前
音视频面试题集锦第 26 期
面试·音视频
liefyuan14 小时前
【音视频】ISP能力
音视频·接口隔离原则
STC_USB_CAN_805118 小时前
实战 AI8051U 音视频播放:USART-SPI→DMA-P2P→SPI+I2S 例程详解
单片机·嵌入式硬件·音视频
阿赵3D1 天前
Unity引擎播放HLS自适应码率流媒体视频
unity·游戏引擎·音视频·流媒体·hls
sukalot1 天前
window显示驱动开发—在混合系统中使用跨适配器资源
数据库·驱动开发·音视频
眠りたいです1 天前
Qt音频播放器项目实践:文件过滤、元数据提取与动态歌词显示实现
c++·qt·ui·音视频·媒体·qt5·mime