Introduction

WebRTC (Web Real-Time Communication) is a powerful open-source technology that enables real-time communication capabilities in web and mobile applications. While WebRTC is well-supported in web browsers, incorporating it into iOS applications often requires (re)building a binary framework to ensure seamless integration and improved performance. In this article, we will provide a comprehensive step-by-step guide on how to integrate WebRTC into any iOS/macOS application and building it from source entirely in Xcode.

Wait! What? Why?

But one can build this easily as a framework with Google’s depot tools, you might say. That is entirely correct. It might even be more convenient to run someone’s clever build script, grab your favorite (non)cafeinated drink and voila. But what if maybe there’s another way?

Motivation

The main motiviation for this endeavor is to create the ability for developers to not only live-debug into the framework with familiar tools (Xcode, Instruments) but to also build a greater variety of targets. This then opens up a lot more opportunities to optimize and modernize some parts of WebRTC for iOS. With Swift-C++ interopability coming with the Swift 5.9 toolchain, some very interesting engineering might happen that could yield benefits not only for Apple platforms. Let’s get started.

Creating an Xcode project for WebRTC

A basic familiarity with Xcode and how to create projects, targets and their basic configuration is assumed.

The final product is available in this github respsitory

Obtaining the sources

Refer to Google’s instructions for fetching the WebRTC sources. At the time of writing this, the full sync would take around 20 GB of disk space. In contrast, the “Built-With-Xcode” variant occupies 788 MB.

Creating the targets

While Google’s build configuration defines several dozens of individual targets that are very neatly organized into an efficient dependency graph, this is not something that is going to be replicated for the Xcode build. The build graph is something that Xcode manages internally. Rather than taking on those responsibilities manually, Xcode is entrusted with this. Please refer to the paragraph about build performance for a breakdown of build times of Xcode and Ninja.

The targets that are being created are the following:

  • WebRTC (framework)
  • libWebRTC (static library)
  • CoreRTC (static library)

CoreRTC

The CoreRTC static library will contain all objects (minus the unit tests and mock objects) from the following folders:

  • api
  • audio
  • call
  • common_audio
  • common_video
  • logging
  • media
  • modules
  • net
  • p2p
  • pc
  • resources
  • rtc_base
  • rtc_tools
  • stats
  • system_wrappers
  • test
  • testing
  • third_party*
  • video

All Source Files

Google engineers follow the pattern of storing header files near the location of the source files and refer to them by thir relative path to the source root. There should be a name for this 🤷‍♂️.

Setting the User Header Search Paths to $(SRCROOT) in Xcode’s Build Settings.

USER_HEADER_SEARCH_PATHS = $(SRCROOT)

libWebRTC

The static WebRTC library links the CoreRTC static library and builds the following sources.

All Source Files

Additionally, the public headers need to be made available for other libraries to link this one. This is achieved with a Copy Files Phase where all relevent headers are copied to the Products Directory at subpath include/WebRTC.

All Header Files

THe following SDK classes / protocols / objects were moved into the modules/audio_device/ios/components/audio location because the audio_device_ios.h header is included in modules/audio_device/audio_device_impl.cc. The change was introduced with this commit. While this probably made sense when using Blaze/Bazel. In this configuration here, the SDK libraries are linking the core library and this is a unidirectional path. In iOS SDK terms, this would be something like QuartzCore linking UIKit.

So this approach has to be reversed but without making code changes. This can easily be achieved by re-exporting the symbols:

  • RTCAudioDevice
  • RTCAudioSession
  • RTCAudioSessionDelegate
  • RTCAudioSessionActivationDelegate
  • RTCAudioSessionConfiguration

from CoreRTC in the libWebRTC and the WebRTC (framework) targets. One minor issue is that the implementation of those references SDK includes, such as RTCMacros.h and RTCLogging.h.

A new header is created and added to the rtc_base directory: rtc_export_bridge.h. The content borrows from those defines that are declared in the SDK.

10    #ifndef rtc_export_bridge_h
11    #define rtc_export_bridge_h
12
13    #include "rtc_base/system/rtc_export.h"
14
15    #ifndef RTC_OBJC_TYPE
16    #define RTC_OBJC_TYPE(t) t
17    #endif
18
19    #ifndef RTC_OBJC_EXPORT
20    #define RTC_OBJC_EXPORT RTC_EXPORT
21    #endif
22
23    #ifndef RTC_EXTERN
24    #if defined(__cplusplus)
25    #define RTC_EXTERN extern "C" RTC_OBJC_EXPORT
26    #else
27    #define RTC_EXTERN extern RTC_OBJC_EXPORT
28    #endif
29    #endif
30
31    #ifndef RTC_FWD_DECL_OBJC_CLASS
32    #ifdef __OBJC__
33    #define RTC_FWD_DECL_OBJC_CLASS(classname) @class classname
34    #else
35    #define RTC_FWD_DECL_OBJC_CLASS(classname) typedef struct objc_object classname
36    #endif
37    #endif

This allows the symbols that are already annotated with RTC_OBJC_EXPORT do be re-exported. Another small caveat… the RTCAudioDevice.h header must include the newly defined rtc_export_bridge.h header. This include needs to be hidden from the public interface however. For that purpose, the reexported folder was created, containing the original header files that are part of the public API definition. The actual headers are hidden from the public interfaces of libWebRTC and WebRTC.

WebRTC

The framework target builds a similar set of sources as the static library, the public and internal headers however are organized using Xcode’s Build Phases configuration.

Defines

Looking at the BUILD.gn and webrtc.gni file(s), there are a lot of preprocessor macro definitions that seem extremely relevant. There are several options to provide those to the build system. A config.h file seems to be generally used for provisioning relevant macros.

Default configuration values for iOS builds

Adding a new header file to the rtc_base directory: rtc_base/rtc_defines.h.

Content of rtc_defines.h

In order to take advantage of this configuration, the header needs to be included in a lot of source files. As a rule of thumb, if a #define is checked, the include statement #include "rtc_base/rtc_defines.h" should be added.

10#ifndef COMMON_AUDIO_FIR_FILTER_AVX2_H_
11#define COMMON_AUDIO_FIR_FILTER_AVX2_H_
12
13#include "rtc_base/rtc_defines.h"
14
15#if defined(WEBRTC_ARCH_X86_FAMILY) && defined(WEBRTC_HAS_AVX2)
16
17#include <stddef.h>
18
19#include <memory>
20
21#include "common_audio/fir_filter.h"
22#include "rtc_base/memory/aligned_malloc.h"
23
24namespace webrtc {
25
26class FIRFilterAVX2 : public FIRFilter {...}
27
28}  // namespace webrtc
29
30#endif // defined(WEBRTC_ARCH_X86_FAMILY) && defined(WEBRTC_HAS_AVX2)
31#endif  // COMMON_AUDIO_FIR_FILTER_AVX2_H_

On a platform without AVX2 support, this would be compiled into an (empty) .o file, which adds a small amount of overhead but should be stripped out of any optimized binary that is being distributed.

Generated Files

Protobufs

Use your favorite protoc build, or try out this one that is part of WebRTC’s dependency list. Executable protoc binaries can be built with SwiftPM for iOS/macOS on arm64/x86_64.

Registered Field Trials

The registered_field_trials.h file is generated by a script during Google’s build process. Depending on the user’s needs, the individual variants could be added manually to Xcode.

10// This file was automatically generated. Do not edit.
11
12#ifndef GEN_REGISTERED_FIELD_TRIALS_H_
13#define GEN_REGISTERED_FIELD_TRIALS_H_
14
15#include "absl/strings/string_view.h"
16
17namespace webrtc {
18
19inline constexpr absl::string_view kRegisteredFieldTrials[] = {
20    "",
21};
22
23}  // namespace webrtc
24
25#endif  // GEN_REGISTERED_FIELD_TRIALS_H_

For that purpose, a dedicated generated folder is created and relevant headers / sources are copied into it.

Third Party Dependencies

The following repositories were forked from their origin and a Package.swift file was added to allow building static and dynamic libraries with Xcode / SwiftPM. The branch release/webrtc has been created where the package definiton is available and also potential (required/useful) changes were made.

Build issues

Not considering implicit int32 to int64 conversions, Xcode has revealed a small list of warnings

Build performance

All measurements were taken on a 2021 16" MacBook Pro M1-Max running macOS Ventura 13.4.1 (c), build: 22F770820d

The WebRTC framework build with Ninja:

10time ninja -C out/ios_64 framework_objc
11ninja: Entering directory `out/ios_64'
12[3640/3640] STAMP obj/sdk/framework_objc.stamp
13ninja -C out/ios_64 framework_objc  892.88s user 189.84s system 623% cpu 2:53.54 total

Three Xcode builds using the Product > Perform Action > Build with Timing Summary were done with the following results:

1: Build succeeded 7/26/23, 10:40 AM 171.6 seconds

2: Build succeeded 7/26/23, 10:43 AM 192.5 seconds

3: Build succeeded 7/26/23, 11:00 AM 187.9 seconds

Conclusion

Judging from those preliminary (and unarguably unverified) results, the following allegations are made:

  • The Xcode build of the WebRTC iOS framework (arm64 target) using the methods described in this article is performed in similar time compared to the Ninja build, described by Google.
  • The build graph created by xcodebuild is NOT less efficient than the (allegedly) manually created and highly optimized build graph produced by Google’s Bazel/Blaze toolchain.
  • The complexity of managing all files in the build targets is significantly lower (lower is better) with Xcode.
  • There are no performance concerns managing several thousand source and header files with Xcode, when run on an APFS file system on a current generation SSD drive. It is reasonable to assume that the same statement would not apply when the Xcode project file is located in a user space mounted network file system.