网络知识 娱乐 WebRTC iOS Native SDK 接入

WebRTC iOS Native SDK 接入

借助于 OpenRTCClient 项目,我们可以非常方便地编译出 WebRTC iOS native SDK,通过 OpenRTCClient 项目提供的 webrtc_pack 工具,我们可以很方便地创建包含了 arm64、和 x64 两种还在广泛使用的 CPU 架构二进制代码的 webrtc 静态库文件。这里说明为 iOS 应用接入 webrtc 静态库文件的过程。(WebRTC 构建系统默认也提供了构建 Framework 的 target,具体的构建 target 为 framework_objc。)

在 iOS 应用程序中使用 WebRTC iOS native API 的一种比较简单的接入方式是,在 WebRTC 的构建系统中为 iOS 应用程序创建一个 GN/ninja 的构建 target,统一用 WebRTC 的构建系统来编译 WebRTC SDK 和 iOS 应用程序。但这种 WebRTC native SDK 的接入方式,对于我们一般的 iOS 应用开发来说,不是很友好。一般来说,我们更希望可以拿到一个 SDK 包,里面包含必须的二进制库文件和头文件,然后修改 iOS 应用的 Xcode 工程配置来引入库,如向头文件搜索路径中添加库的头文件路径等。

要为 WebRTC native SDK 创建一个开发包的话,获得编译生成的二进制库文件比较容易,但获得完整的 API 头文件比较麻烦。

WebRTC iOS native SDK API 说明

WebRTC C++ 核心库提供的 API 可以认为由三个部分组成:

  1. PeerConnectionInterface 为中心的核心框架 API。WebRTC 中 PeerConnection 定义 RTC 的 pipeline,它把各模块组件有机地连接起来,构造完整的 RTC 场景。核心框架 API 既包括 PeerConnection 的 API,也包括 PeerConnection 的 pipeline 中各个模块组件的部分接口。相关头文件位于 webrtc/api 目录下。

  2. 模块组件 API。PeerConnection 的 pipeline 中各个模块组件提供的 API,如 audio processing 提供的 AudioProcessingAudioProcessingBuilder,位于 webrtc/modules/audio_processing/include,用于桌面共享的 API 位于 webrtc/modules/desktop_capture

  3. 基础的实用程序 API。如在 webrtc/rtc_base 目录下包含了大量的辅助工具 API,包括日志,线程,任务队列,同步原语等等等。

WebRTC 的开发者一定是非常不希望其它开发者直接使用 WebRTC 的 C++ API 的,否则 WebRTC 的 C++ API 也不至于设计的如此混乱:

  1. 核心框架 API 的大量头文件包含了各个模块组件的头文件以及基础的实用程序头文件。

  2. 核心框架 API 的大量组件依赖于其它部分的接口。

  3. 其他开发者几乎无法捋出来 WebRTC SDK 的 C++ API 头文件,因为他们广泛分布于整个代码库的各个位置。

WebRTC C++ API 的混乱设计导致它们完全无法脱离 WebRTC 的源码库来使用。

WebRTC 项目倒是很友好地为 Android 和 iOS 开发者提供了非常干净的 Java 和 Objective-C API,这些 API 分别位于 webrtc/sdk/androidwebrtc/sdk/objc。WebRTC 的 Objective-C API 主要是对底层 C++ API 的封装,但也通过定义 RTCVideoEncoderFactoryRTCVideoDecoderFactory 将系统提供的视频硬编硬解的能力接入 PeerConnection 定义 RTC 的 pipeline。WebRTC 的 Objective-C API 完整头文件可以通过 webrtc/sdk/BUILD.gn 中定义的构建 target “framework_objc” 来了解。

这里我们使用一些 WebRTC 的 C++ API,使用一些 WebRTC 的 Objective-C API。后面我们还会看到 WebRTC 的 C++ API 其它一些十分坑爹的地方。

通过 Xcode 创建一个工程

  1. 打开 Xcode。

Xcode

  1. 选择 “Create a new Xcode project”。

Xcode 001

  1. 选择 “iOS” 标签下的 “App”。

Xcode 002

  1. 输入产品名称 "Product Name: ",选择 "Team: ",输入组织标识符 “Organization Identifier:”,"Interface: " 选择 “Storyboard”,语言 "Language: " 选择 “Objective-C”,然后点击 “Next”。为 iOS 工程选择一个目录来创建工程目录,结束 Xcode 工程创建过程。

Xcode 003

为 iOS 应用工程引入 WebRTC 静态库

  1. WebRTC native SDK 提供的是 C++ 的 API,为了在 iOS 应用工程中使用 WebRTC 的 native API,需要将引用 WebRTC native API 的源文件后缀名由 .m 修改为 .mm,这里修改 ViewController.mmain.m 这两个源文件的后缀名为 .mm

  2. 引入 WebRTC 静态库

在这一步中,我们在 main.mm 中包含一些 WebRTC 的头文件,调用一些基本的 API,并使应用程序编译链接成功,并能在 iPhone 模拟器中运行起来。

我门在 main.mm 中调用创建 webrtc::PeerConnectionFactoryInterfacewebrtc::CreatePeerConnectionFactory() 接口,并包含相关的头文件,这样 main.mm 的完整代码如下:

#import 
#import "AppDelegate.h"

#include "api/audio_codecs/builtin_audio_decoder_factory.h"
#include "api/audio_codecs/builtin_audio_encoder_factory.h"
#include "api/create_peerconnection_factory.h"
#include "api/peer_connection_interface.h"
#include "api/video_codecs/builtin_video_decoder_factory.h"
#include "api/video_codecs/builtin_video_encoder_factory.h"
#include "rtc_base/thread.h"

int main(int argc, char * argv[]) {
    std::unique_ptr network_thread_ = rtc::Thread::CreateWithSocketServer();
    network_thread_->SetName("network_thread", nullptr);
    if (!network_thread_->Start()) {
      fprintf(stderr, "Failed to start network threadn");
    }

    std::unique_ptr worker_thread_ = rtc::Thread::Create();
    worker_thread_->SetName("worker_thread", nullptr);
    if (!worker_thread_->Start()) {
      fprintf(stderr, "Failed to start worker threadn");
    }

    std::unique_ptr signaling_thread_ = rtc::Thread::Create();
    signaling_thread_->SetName("signaling_thread", nullptr);
    if (!signaling_thread_->Start()) {
      fprintf(stderr, "Failed to start worker signaling threadn");
    }

    rtc::scoped_refptr pcf_ = webrtc::CreatePeerConnectionFactory(
        network_thread_.get() /* network_thread */,
        worker_thread_.get() /* worker_thread */,
        signaling_thread_.get() /* signaling_thread */, nullptr /* default_adm */,
        webrtc::CreateBuiltinAudioEncoderFactory(),
        webrtc::CreateBuiltinAudioDecoderFactory(),
        webrtc::CreateBuiltinVideoEncoderFactory(),
        webrtc::CreateBuiltinVideoDecoderFactory(), nullptr /* audio_mixer */,
        nullptr /* audio_processing */);

    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

毫无疑问,我们应该将 WebRTC 的源码根目录加入 iOS app 工程的头文件搜素路径中去,以便于 Xcode 的构建系统可以找到我们包含的那些头文件,应该将 WebRTC 库文件的路径加进库的搜索路径,同时应该配置工程,增加对 webrtc 库的依赖。

在 Xcode 的左侧,选中项目,点击 Build Settings 标签,找到 Search Paths,然后在 Header Search Paths 中加入 WebRTC 的源码根目录为头文件搜索路径增加WebRTC 的源码根目录。在 Search PathsLibrary Search Paths 中加入 webrtc 库文件的路径以将 webrtc 库文件的路径添加库文件搜索路径中。如:

Xcode 003

选中 TARGETS 下我们 iOS 应用的 loop_connect,选中 Build Phases,点开 Link Binary With Libraries,然后将 webrtc 的静态库文件拖进来,如:

Xcode 004

  1. 添加 abseil-cpp 的路径为头文件搜索路径

完成上面的配置之后,我们通过 Product -> Build 第一次构建我们的工程。此时报出如下的编译错误:

Xcode 005

错误提示说找不到头文件 absl/types/optional.h。这是由于在 WebRTC 的 API 头文件中,包含了第三方库 abseil-cpp 的头文件。这是 WebRTC C++ API 的一个非常坑爹的地方 —— 开发者只是想使用 WebRTC 的 API,但却不得不把第三方库 abseil-cpp 也引进自己的工程。

这个问题可以通过把 webrtc/third_party/abseil-cpp 的路径添加进工程的头文件搜索路径来解决。

  1. 定义平台宏

完成上面的配置之后,再次构建我们的工程。这次报出如下的编译错误:

Xcode 006

这次是有几十个对于用来做网络字节序和本地字节序之间的转换的符号的引用找不到。这是 WebRTC C++ API 最坑爹的地方了。这需要我们仔细了解 WebRTC 库的构建过程和一些头文件的代码。这个错误主要发生在 webrtc/rtc_base/ip_address.h 头文件中,这个头文件相关的代码如下:

#if defined(WEBRTC_POSIX)
#include 
#include 
#include 
#include 
#endif
#if defined(WEBRTC_WIN)
#include 
#include 
#endif
#include 

#include 

#include "rtc_base/byte_order.h"
#if defined(WEBRTC_WIN)
#include "rtc_base/win32.h"
#endif
#include "rtc_base/system/rtc_export.h"

webrtc/rtc_base/ip_address.h 头文件中,它会根据不同的平台宏的定义情况来包含不同的头文件,以使得访问用于执行网络字节序和本地字节序之间的转换的符号成为可能。但这里的平台宏不是编译器定义的,而是 WebRTC 的构建系统定义的,如 WEBRTC_POSIXWEBRTC_WIN 等。坑爹的地方在于,WEBRTC_POSIXWEBRTC_WIN 这些平台宏,不是根据编译器定义的平台宏在一个基础的头文件中定义,并包含在其它头文件中的,而是在 BUILD.gn.gni 这样的构建配置文件中定义的。这使得使用了 WebRTC C++ API 的其它项目根本无法了解到需要去定义这样的宏,于是常常会导致编译 WebRTC 时,WebRTC 的源文件看到的这些头文件的内容和引用这些头文件的其它项目的代码看到的这些头文件的内容有巨大的差异。

对于 iOS,WebRTC 中与它相关的平台宏有这样三个,WEBRTC_POSIX WEBRTC_MACWEBRTC_IOS。对于这个问题,我们需要通过配置工程定义这几个预处理器宏来解决。选择 Build Settings --> Apple Clang - Preprocessing --> Preprocessor Macros,添加这几个宏,如下图:

Xcode 007

这里遇到的编译错误,属于 WebRTC 在 API 中对于自定义平台宏的依赖所造成的相对比较小的问题。这种做法实际上还有着极大的隐患。如 WebRTC 的 webrtc/modules/audio_device/audio_device_generic.h 这个头文件中的一段代码:

  // Play underrun count.
  virtual int32_t GetPlayoutUnderrunCount() const;

// iOS only.
// TODO(henrika): add Android support.
#if defined(WEBRTC_IOS)
  virtual int GetPlayoutAudioParameters(AudioParameters* params) const;
  virtual int GetRecordAudioParameters(AudioParameters* params) const;
#endif  // WEBRTC_IOS

  virtual void AttachAudioBuffer(AudioDeviceBuffer* audioBuffer) = 0;

  virtual ~AudioDeviceGeneric() {}
};

webrtc::AudioDeviceGeneric 这个类接口的定义对于不同平台来说是不同的。如果编译 WebRTC 的时候定义了宏,使用 WebRTC 的其它项目在包含这个头文件的时候没有定义宏,代码中调用这个类的接口,则编译将安安静静地成功完成,但在运行期,调用这个类的接口函数时,轻则应用程序莫名其妙地崩溃,重则将发现实际执行的函数是牛头不对马嘴,明明在代码中调用了函数 A,实际执行的确实函数 B。

  1. 添加对 iOS 系统 framework 的依赖

解决了上面的问题之后,再次编译我们的工程,此时将报出大量符号找不到的链接错误,如下图:

Xcode 008

在 WebRTC 中使用了大量的 iOS 系统库的符号,还需要添加对于相关系统库的依赖。报错中的这些符号需要添加对 AudioToolbox.frameworkAVFoundation.framework 这两个 framework 的依赖。类似于前面添加 webrtc 静态库那样,添加对这两个 framework 的依赖。

  1. 引入更多对 iOS 系统 framework 的依赖

经过了前面的操作,再次编译我们的工程,此时 iOS App 工程可以成功编译。我们在模拟器上运行我们的应用。此时很快就报出了 unrecognized selector sent to . . . 的错误,如下图:

Xcode 008

这与 Objective-C 本身的机制有关。webrtc 静态库的某些符号没有链接进来。这个问题可以通过把 “-ObjC” 添加到 “Build Settings” -> “Linking” -> “Other Linker Flags” 来解决。这个链接选项会促使链接器加载 webrtc 静态库中的所有 Objective-C class 或 category。由于这个链接选项会使链接器加载更多 Objective-C class 或 category,我们再次编译时,报出了更多的符号找不到的链接错误,如下图:

Xcode 009

这需要我们添加更多对 iOS 系统 framework 的依赖。关于这些 iOS 系统 framework 的线索,可以在 webrtc/sdk/BUILD.gn 找到一部分。具体来说,这些需要依赖的 iOS 系统 framework 还包括这些:Network.frameworkCoreVideo.frameworkVideoToolbox.frameworkCoreMedia.frameworkGLKit.frameworkOpenGLES.frameworkQuartzCore.framework

  1. 引用 Objective-C 接口

经过上面的配置,我们的集成了 WebRTC iOS native SDK 的 iOS App 已经可以正常编译运行起来了。

要想在我们的 iOS App 中引用 WebRTC 的 Objective-C 接口,如 RTCDefaultVideoEncoderFactoryRTCDefaultVideoDecoderFactory,还需要给我们的 iOS App 工程的头文件搜索路径加上 webrtc/sdk/objcwebrtc/sdk/objc/base 这两个目录。

  1. 实现同一台设备上两个连接之间的音频数据收发

这里使用 WebRTC 的 API 实现一个简单的示例:在同一台设备上创建两个连接,一个连接采集发送音频,另一个连接接收播放音频。

首先是 main.mm,它恢复到 Xcode 刚刚创建工程时的样子:

#import 
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
  NSString * appDelegateClassName;
  @autoreleasepool {
    // Setup code that might create autoreleased objects goes here.
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
  }

  return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

然后是 ViewController.mm 文件,它定义我们示例 App 的 UI 界面,并控制我们示例的流程的初始化、启动和停止,它的代码如下:

#import "ViewController.h"

#include 

#import 

#import "sdk/objc/base/RTCVideoRenderer.h"
#if defined(RTC_SUPPORTS_METAL)
#import "sdk/objc/components/renderer/metal/RTCMTLVideoView.h"  // nogncheck
#endif
#import "sdk/objc/components/renderer/opengl/RTCEAGLVideoView.h"
#import "sdk/objc/helpers/RTCCameraPreviewView.h"

#import "CallClientWithNoVideo.h"

@interface ViewController ()

@property(nonatomic) __kindof UIView *remoteVideoView;
@property(nonatomic) UIButton * button_init;
@property(nonatomic) UIButton * button;
@property(nonatomic) BOOL started;
@property(nonatomic) std::shared_ptr call_client;

@end

@implementation ViewController {
  UIView *_view;
}

@synthesize button_init = _button_init;
@synthesize button = _button;
@synthesize started = _started;
@synthesize call_client = _call_client;

- (void)loadView {
  _view = [[UIView alloc] initWithFrame:CGRectZero];
  
#if defined(RTC_SUPPORTS_METAL)
  _remoteVideoView = [[RTC_OBJC_TYPE(RTCMTLVideoView) alloc] initWithFrame:CGRectZero];
#else
  _remoteVideoView = [[RTC_OBJC_TYPE(RTCEAGLVideoView) alloc] initWithFrame:CGRectZero];
#endif
  _remoteVideoView.translatesAutoresizingMaskIntoConstraints = NO;
  [_view addSubview:_remoteVideoView];
  
  _button_init = [UIButton buttonWithType:UIButtonTypeSystem];
  _button_init.translatesAutoresizingMaskIntoConstraints = NO;
  [_button_init setTitle:@"Initialize" forState:UIControlStateNormal];
  [_button_init setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
  [_button_init addTarget:self action:@selector(loopCallInitialize) forControlEvents:UIControlEventTouchUpInside];
  [_view addSubview:_button_init];
  
  _button = [UIButton buttonWithType:UIButtonTypeSystem];
  _button.translatesAutoresizingMaskIntoConstraints = NO;
  [_button setTitle:@"Start loop call" forState:UIControlStateNormal];
  [_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
  [_button addTarget:self
              action:@selector(loopCallControl)
    forControlEvents:UIControlEventTouchUpInside];
  [_view addSubview:_button];
  
  UILayoutGuide *margin = _view.layoutMarginsGuide;
  [_remoteVideoView.leadingAnchor constraintEqualToAnchor:margin.leadingAnchor].active = YES;
  [_remoteVideoView.topAnchor constraintEqualToAnchor:margin.topAnchor].active = YES;
  [_remoteVideoView.trailingAnchor constraintEqualToAnchor:margin.trailingAnchor].active = YES;
  [_remoteVideoView.bottomAnchor constraintEqualToAnchor:margin.bottomAnchor].active = YES;
  
  [_button_init.leadingAnchor constraintEqualToAnchor:margin.leadingAnchor constant:8.0].active =
  YES;
  [_button_init.bottomAnchor constraintEqualToAnchor:margin.bottomAnchor constant:8.0].active = YES;
  [_button_init.widthAnchor constraintEqualToConstant:100].active = YES;
  [_button_init.heightAnchor constraintEqualToConstant:40].active = YES;
  
  [_button.trailingAnchor constraintEqualToAnchor:margin.trailingAnchor constant:8.0].active =
  YES;
  [_button.bottomAnchor constraintEqualToAnchor:margin.bottomAnchor constant:8.0].active =
  YES;
  [_button.widthAnchor constraintEqualToConstant:100].active = YES;
  [_button.heightAnchor constraintEqualToConstant:40].active = YES;
  
  self.view = _view;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  
  self.call_client = nullptr;
}

- (void)loopCallInitialize{
  if (self.call_client) {
    NSLog(@"Loop call client has been created");
  } else {
    self.call_client = std::make_shared();
  }
}

- (void)loopCallControl{
  if (!self.call_client) {
    NSLog(@"Loop call client has not been created");
    return;
  }
  if (self.started) {
    self.call_client->StopLoopCall();
    [self.button setTitle:@"Start loop call" forState:UIControlStateNormal];
  } else {
    self.call_client->StartLoopCall();
    [self.button setTitle:@"Stop loop call" forState:UIControlStateNormal];
  }
  self.started = !self.started;
}

@end

然后是实现了连接创建,及连接建立的 CallClientWithNoVideo.mm 文件:

CallClientWithNoVideo::CallClientWithNoVideo() : call_started_(false) {
  thread_checker_.Detach();
  CreatePeerConnectionFactory();
}

void CallClientWithNoVideo::CreatePeerConnectionFactory() {
  network_thread_ = rtc::Thread::CreateWithSocketServer();
  network_thread_->SetName("network_thread", nullptr);
  RTC_CHECK(network_thread_->Start()) <SetName("worker_thread", nullptr);
  RTC_CHECK(worker_thread_->Start()) <SetName("signaling_thread", nullptr);
  RTC_CHECK(signaling_thread_->Start()) << "Failed to start thread";

  webrtc::PeerConnectionFactoryDependencies dependencies;
    
  dependencies.network_thread = network_thread_.get();
  dependencies.worker_thread = worker_thread_.get();
  dependencies.signaling_thread = signaling_thread_.get();

  dependencies.task_queue_factory = webrtc::CreateDefaultTaskQueueFactory();

  cricket::MediaEngineDependencies media_deps;

  media_deps.task_queue_factory = dependencies.task_queue_factory.get();
  media_deps.audio_encoder_factory = webrtc::CreateBuiltinAudioEncoderFactory();
  media_deps.audio_decoder_factory = webrtc::CreateBuiltinAudioDecoderFactory();

  media_deps.video_encoder_factory = webrtc::ObjCToNativeVideoEncoderFactory(
      [[RTC_OBJC_TYPE(RTCDefaultVideoEncoderFactory) alloc] init]);
  media_deps.video_decoder_factory = webrtc::ObjCToNativeVideoDecoderFactory(
      [[RTC_OBJC_TYPE(RTCDefaultVideoDecoderFactory) alloc] init]);

  media_deps.audio_processing = webrtc::AudioProcessingBuilder().Create();

  dependencies.media_engine = cricket::CreateMediaEngine(std::move(media_deps));

  RTC_LOG(LS_INFO) << "Media engine created: " << dependencies.media_engine.get();
  dependencies.call_factory = webrtc::CreateCallFactory();
  dependencies.event_log_factory =
      std::make_unique(dependencies.task_queue_factory.get());
  pcf_ = webrtc::CreateModularPeerConnectionFactory(std::move(dependencies));
  RTC_LOG(LS_INFO) << "PeerConnectionFactory created: " << pcf_;
}

bool CallClientWithNoVideo::LoopCallStarted() {
  webrtc::MutexLock lock(&pc_mutex_);
  return call_started_;
}

void CallClientWithNoVideo::CreateConnections() {
  webrtc::MutexLock lock(&pc_mutex_);
  
  webrtc::PeerConnectionInterface::RTCConfiguration config;
  config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
  // DTLS SRTP has to be disabled for loopback to work.
  config.enable_dtls_srtp = false;

  send_observer_ = std::make_unique("SendConn");

  webrtc::PeerConnectionDependencies pc_dependencies(send_observer_.get());
  send_connection_ = pcf_->CreatePeerConnection(config, std::move(pc_dependencies));
  RTC_LOG(LS_INFO) << "Send PeerConnection created: " << send_connection_;

  recv_observer_ = std::make_unique("RecvConn");

  webrtc::PeerConnectionDependencies recv_pc_dependencies(recv_observer_.get());
  recv_connection_ = pcf_->CreatePeerConnection(config, std::move(recv_pc_dependencies));
  RTC_LOG(LS_INFO) << "Receive PeerConnection created: " <AddTrack(local_audio_track_, {kStreamId});
  RTC_LOG(LS_INFO) << "Local auido track set up: " << local_audio_track_;
}

void CallClientWithNoVideo::Connect() {
  webrtc::MutexLock lock(&pc_mutex_);
  
  std::string send_role("SendConn");
  rtc::scoped_refptr observer(new rtc::RefCountedObject(send_role));
  send_connection_->CreateOffer(observer,
                                webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
  
  std::future sdp_future = observer->GetSDP();
  std::string sdp = sdp_future.get();
  
  std::unique_ptr send_offer(
      webrtc::CreateSessionDescription(webrtc::SdpType::kOffer, sdp));
  send_connection_->SetLocalDescription(new rtc::RefCountedObject("SendConn"),
                                        send_offer.release());

  std::unique_ptr recv_send_offer(
      webrtc::CreateSessionDescription(webrtc::SdpType::kOffer, sdp));
  recv_connection_->SetRemoteDescription(std::move(recv_send_offer),
                            new rtc::RefCountedObject("RecvConn"));

  rtc::scoped_refptr recv_answer_observer(new rtc::RefCountedObject("RecvConn"));
  recv_connection_->CreateAnswer(recv_answer_observer,
                                 webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
  
  std::future recv_sdp_answer_future = recv_answer_observer->GetSDP();
  std::string recv_sdp_answer = recv_sdp_answer_future.get();
  
  std::unique_ptr recv_answer(
      webrtc::CreateSessionDescription(webrtc::SdpType::kAnswer, recv_sdp_answer));
  send_connection_->SetRemoteDescription(std::move(recv_answer),
                                         new rtc::RefCountedObject("SendConn"));

  std::unique_ptr recv_recv_answer(
      webrtc::CreateSessionDescription(webrtc::SdpType::kAnswer, recv_sdp_answer));
  recv_connection_->SetLocalDescription(new rtc::RefCountedObject("RecvConn"),
                                        recv_recv_answer.release());
  
  send_observer_->SetConnection(recv_connection_);
  recv_observer_->SetConnection(send_connection_);
}

void CallClientWithNoVideo::StartLoopCall() {
  {
    webrtc::MutexLock lock(&pc_mutex_);
    if (call_started_) {
      RTC_LOG(LS_WARNING) <CreateAudioSource(audio_options);
  
  local_audio_track_ = pcf_->CreateAudioTrack(kAudioLabel, audio_source_);

  CreateConnections();
  Connect();
}

void CallClientWithNoVideo::DestroyConnections() {
  webrtc::MutexLock lock(&pc_mutex_);
  for (const rtc::scoped_refptr& tranceiver :
       send_connection_->GetTransceivers()) {
    send_connection_->RemoveTrack(tranceiver->sender().get());
  }

  send_observer_->SetConnection(nullptr);
  recv_observer_->SetConnection(nullptr);

  send_connection_ = nullptr;
  recv_connection_ = nullptr;
  local_audio_track_ = nullptr;
  audio_source_ = nullptr;
}

void CallClientWithNoVideo::StopLoopCall() {
  DestroyConnections();

  webrtc::MutexLock lock(&pc_mutex_);
  if (!call_started_) {
    RTC_LOG(LS_WARNING) << "Call already started.";
    return;
  }
  call_started_ = false;
}

相关更完整的示例代码可以在 OpenRTCClient 项目的 examples/loop_connect_ios 下找到。

参考文档

[UIDevice deviceType]: unrecognized selector sent to class 0x23a2421b0

Technical Q&A QA1490

What does the -ObjC linker flag do?

webrtc iOS native : +[UIDevice deviceType]: unrecognized selector sent toclass 0x3aa4b420