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
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.
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
.
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
.
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’sBazel/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.