☰
Mac上从源码编译WebRTC:从环境准备到自定义SDK编译指南
2026/10/3 7:15:39 网站建设 项目流程

第一次动编译WebRTC的念头,八成不是因为好奇,而是因为预编译SDK不够用了。我当时要在项目里调音频丢包隐藏的参数,翻遍了官方SDK接口文档,发现这一层API根本不向你开放——它只管把编解码和网络传输封装好,丢给你几个回调,你插不上手。于是只能走“自己编译”这条路。这是Mac OS上编译WebRTC最典型的动机:拿到带源码、能改参数、能裁剪模块的完整产物。这篇教程,就是给那些和我一样,需要真正“碰”WebRTC源码,而不仅仅是调SDK的开发者准备的。


1. 为什么非要在Mac上自己编译WebRTC:预编译SDK满足不了的定制需求

想清楚这个问题,后面才不会有中途放弃的念头。WebRTC的预编译产物,无论是Google官方维护的二进制包,还是第三方打包的framework,解决的都只是“即时可用”的问题:配置好证书,拖进工程,调用API,跑通音视频通话。但一旦你遇到下面这几种情况,预编译SDK基本就无能为力了。

1.1 算法级定制:预编译SDK不开放的内部参数

WebRTC里丢包隐藏、抖动缓冲、拥塞控制(比如GCC算法)的很多参数都写在源码里。你如果想改neteq的延迟参数,或者调整pacer的发送节奏,在二进制SDK里找不到任何入口。预编译包最多给你几个RTCPeerConnection级别的接口,粒度离底层算法隔着十万八千里。只有拿到源码自己编译,才能直接在modules/audio_coding/或modules/pacing/里改代码验证效果。

1.2 模块裁剪:把上百MB打薄到几十MB

预编译包通常把音频Codec、视频Codec、网络协议栈全部打进去,体积可能上百MB。而如果你只做一套纯音频的1v1通话,或者只需要自定义的采集/渲染通道,完全可以把用不到的模块去掉。比如只保留Opus、去掉所有视频Codec,产物体积能缩小一大截。这个只有在你掌握编译参数之后才能实现,预编译SDK从来不会给你这种选项。

1.3 调试定位:源码和符号是线上排查的底气

线上音视频通话出了诡异的问题,你要在RtpPacketizer里加日志、在VideoStreamEncoder里断点查看码率分配,没有源码和调试符号,就只能靠猜测。自己编一个is_debug=true的版本能帮多大忙,实践过的人都知道。断点下进去,变量一查,问题定位往往就在十分钟内。

此外,Mac平台本身有个特殊性:一份WebRTC源码,编译产物既可以给macOS用,也能交叉编译出iOS版framework。两边共用一套代码和构建体系,这对做跨端音视频SDK的人来说是巨大的便利,也是我坚持在Mac上折腾自编译的核心原因。所以这篇教程面向的读者,是不再满足于“能调通”,而是想在WebRTC里做实事的人。


2. 编译前的硬条件自查:磁盘、内存、Xcode、网络这四关

WebRTC编译不是“装个包就完事”的普通开源项目。它整个构建系统是为Chromium这种巨型代码库设计的,对机器环境有硬性要求。很多人编译失败,不是命令敲错,而是前面这四关没过。

2.1 磁盘空间:预留60GB是最低诚意

这是个最容易被低估的数字。我自己的项目,源码加依赖大概12GB,Debug构建目录峰值超过25GB,Release构建也要20GB上下。如果你中间再切换几次target_cpu,磁盘分分钟爆掉。建议在开始前用df -h看一下根目录剩余空间,60GB以上是起步线,低于40GB我建议先清理完再动手。其实有个取巧的办法:只要保留构建产物和用到的源码目录,depot_tools里的.git历史可以适当精简,不过这个属于进阶操作,新手别乱来。

一定要在开始前留足空间,而不是等报错再去想办法。

2.2 内存和CPU:16GB以上才谈得上体验

编译过程基本是“CPU全核拉满、内存吃到80%”的画风。我最初在8GB内存的MacBook上试过,开编译之后整个系统基本处于半瘫痪状态,而且并行度太高会让内存耗尽,编译进程被系统直接杀掉。16GB内存、四核以上CPU是比较稳妥的起步配置。如果你和我一样是8GB内存的老机器,至少编译的时候把浏览器、模拟器全关掉,再用ninja -j4限制并行任务数,能熬过去,但会比较痛苦。

2.3 Xcode环境:Command Line Tools不够用

这个坑非常隐蔽。很多教程让你安装Xcode,有人觉得“反正只是命令行编译,装Command Line Tools就行了吧”——不行。WebRTC的构建脚本会调用xcode-select查找完整Xcode路径,还需要系统的iOS SDK和macOS SDK来做交叉编译配置。就算你只编macOS版本,也建议装完整的Xcode。

装完之后检查一下路径:

xcode-select -p

正常情况下输出是/Applications/Xcode.app/Contents/Developer。如果不是这个路径,执行:

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

另外第一次安装完Xcode,记得启动一次,让它跑完License协议,否则后续clang调用会直接报错。

2.4 网络环境与下载策略

fetch源码和gclient同步依赖时,需要在多个代码托管节点之间来回下载,数据量很大。这个过程对网络稳定性非常敏感。如果你的网络本身不太稳定,不要用“一次到底”的心态跑gclient sync,它其实支持断点续传,中途失败后重跑就能接着下载。我的建议是选择一个网络相对空闲的时段跑,sync过程中不要去动网络。如果反复在同一个文件上报错,可以用gclient sync --force强制重新拉取,但不要手动去删目录,容易越修越乱。


3. 拉源码的正确姿势:depot_tools、fetch和gclient sync

WebRTC的源码管理跟一般Git项目不太一样,它依赖一套Chromium体系下的工具链。这套工具初看很反直觉,但理解了它的逻辑之后,你会觉得整个过程其实很顺。

3.1 安装depot_tools

depot_tools是Google为Chromium系项目准备的一套开发工具集,里面包含了gclient、gn、ninja等我们今天全都要用到的东西。安装方式非常简单:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH="$PWD/depot_tools:$PATH"

注意这两行命令最后要落到~/.zshrc或~/.bash_profile里,否则新终端窗口又找不到命令了。加完之后执行echo $PATH确认。

3.2 用fetch拉取WebRTC源码

fetch是depot_tools里的一层包装脚本,它会根据你传入的项目名,自动创建.gclient文件并同步初始代码。创建工程目录:

mkdir ~/webrtc-checkout cd ~/webrtc-checkout fetch --nohooks webrtc_ios

这里我用的是webrtc_ios而不是webrtc,区别在于.gclient文件里会额外写入target_os = ["ios", "mac"],这样后续既能编macOS库,也能交叉编译iOS库。如果你确定只做macOS桌面端,用fetch webrtc也可以。但我个人建议直接webrtc_ios,因为绝大多数在Mac上折腾WebRTC的人,最终都逃不过“顺手编个iOS版本”的需求。

注意这里我故意加了--nohooks,意思是先只拉代码,不执行后面的初始化钩子。等代码下来之后,再单独执行同步:

gclient sync

gclient sync会补齐所有子模块依赖,并生成构建所需的各类脚本。这一步消耗的时间和网络带宽都很可观,第一次跑建议做好准备,别开着流量套餐硬冲。

3.3 sync过程中的常见状况

最常遇到的状况就是“卡住”。比如屏幕停在一行Syncing projects: 45% (54/120)不动了。我的经验是:不要慌,也不要直接Ctrl+C然后删目录。如果超过20分钟没有变化,才考虑中断重试。很多节点下载速度慢,它其实是在传输,只是进度显示不够实时而已。中断后重新执行gclient sync,已经下载的部分会保留,不会从头再来。

还有一种是反复报Unable to connect或者Timeout。这种基本是网络质量问题。换个时间再跑,或者用手机热点换一个网络出口,一般能解决。网上流传的各种网络加速方案,我个人的态度是不建议在下载阶段引入额外变量,先重试再说。真实情况是,WebRTC依赖的下载节点不止一个,大部分时候多跑两次就能通过。


4. GN参数表才是编译的“灵魂”:关键配置项逐一拆解

源码拉下来之后,下一步不是直接编译,而是先配置构建参数。WebRTC现在用的构建系统是GN + Ninja,这跟很多开源项目用CMake完全不是一回事。很多初学者第一次接触gn gen会一脸懵,这里先花点篇幅讲清楚它的逻辑。

4.1 GN到底是什么,为什么不是CMake

GN(Generate Ninja)是Chromium团队自己开发的元构建系统,它的作用是“根据配置生成Ninja能直接使用的构建文件”。Ninja本身是一个极快的构建执行器,但配置起来非常不友好,于是GN就负责把人对配置的“意图”翻译成Ninja能执行的具体指令。

你可以简单理解为:CMake的产物是Makefile,而GN的产物是ninja.build文件。WebRTC选GN不选CMake,本质是因为源码规模太庞大,模块依赖关系极其复杂,CMake在这种体量下无论是配置速度还是增量编译效率都跟不上。

这个背景了解之后,再看接下来的命令就不会觉得奇怪了。

4.2 核心参数一览

GN参数的设置方式有两种,一种是在命令行里直接用--args传:

gn gen out/mac-arm64-release --args='target_os="mac" target_cpu="arm64" is_debug=false'

另一种是先生成默认配置,再打开编辑器调整:

gn args out/mac-arm64-release

两种等价,推荐第二种,因为每次改动时GN会自动重新生成Ninja文件,省得记一长串命令行。下面这张表,是我认为在Mac编译场景下优先级最高的参数:

参数可选值作用说明
target_os"mac" / "ios" / "android"目标操作系统类型
target_cpu"x64" / "arm64" / "x86"目标CPU架构
is_debugtrue / false是否Debug构建
is_component_buildtrue / false动态库/静态库模式
rtc_include_teststrue / false是否包含单元测试代码
rtc_build_frameworktrue / false是否生成WebRTC.framework
symbol_level0 / 1 / 2调试符号级别
rtc_use_h264true / false是否集成H.264编解码器
mac_deployment_target"10.14" / "11.0"最低支持的macOS版本

每个参数单独说明一下:

  • target_os和target_cpu决定产物运行在什么平台上。Apple Silicon机器默认生成arm64,Intel机器默认x64,但你可以手动指定交叉编译。
  • is_debug决定是否带完整调试信息。Debug版本编译更慢、产物更大,但调试体验好。线上分发给第三方用,一定要关掉。
  • is_component_build尽量设置成 false。true 会编译出几十个dylib,开发迭代快,但最后交付给他人集成时相当麻烦;false 会把所有代码打进一个静态库或framework,部署简单。
  • rtc_include_tests设置为 false,能省掉大量测试相关源码的编译时间,是个很划算的参数。
  • rtc_build_framework设为 true,编译完成后直接得到WebRTC.framework,省去自己折腾头文件和库路径的时间。
  • symbol_level设为0,编译速度和产物体积都会有明显改善。线上发布版本建议0,Debug用默认值即可。
  • rtc_use_h264默认可能是false,涉及H.264的专利问题。如果不需要H.264硬编硬解,保持默认即可。
  • mac_deployment_target就是要支持的最低macOS版本,按你的目标用户群体设置,不设的话默认给到比较新的系统。

4.3 我常用的三种参数组合

前面铺垫完了,直接给可以直接抄作业的模板。

第一种,Apple Silicon Mac上构建macOS arm64 Release版framework:

target_os = "mac" target_cpu = "arm64" is_debug = false is_component_build = false rtc_include_tests = false rtc_build_framework = true symbol_level = 0

第二种,Intel Mac上构建macOS x64 Release版静态库:

target_os = "mac" target_cpu = "x64" is_debug = false is_component_build = false rtc_include_tests = false symbol_level = 0

注意这种不会生成framework,后面需要手动找静态库和头文件。

第三种,交叉编译iOS arm64 Release版framework:

target_os = "ios" target_cpu = "arm64" is_debug = false is_component_build = false rtc_include_tests = false rtc_build_framework = true symbol_level = 0

这里能编iOS版本,正得益于前面用了fetch webrtc_ios,把iOS的依赖也拉全了。

这些参数都不是一次定死的,后续想改随时用gn args out/xxx重新改,GN会智能地只重编受影响的部分,不用全部重来。


5. 正式编译全流程:从gn gen到拿到第一个WebRTC.framework

参数定好之后,正式编译反而没什么技术含量了。因为前面已经把最难的配置和源码准备做完了。这一章我把完整命令串起来,并告诉你每一步大概要等多久。

5.1 生成构建目录

进入src目录:

cd ~/webrtc-checkout/src

然后创建构建目录。我习惯用out/平台-架构-模式这样的命名方式,便于多配置共存:

gn gen out/mac-arm64-release --args='target_os="mac" target_cpu="arm64" is_debug=false is_component_build=false rtc_include_tests=false rtc_build_framework=true symbol_level=0'

如果参数很多,我更推荐用gn args交互式编辑。执行后GN会检查环境依赖,这一步通常几秒到十几秒。如果看到类似ERROR at //build/config/mac/BUILD.gn的报错,多半是Xcode路径或SDK版本不对,回到第2章检查。

生成成功后,out目录下会看到一个args.gn文件,里面就是刚才的配置,下次可以直接复用。

5.2 执行ninja编译

构建配置已生成,编译就是一条命令:

ninja -C out/mac-arm64-release WebRTC.framework

如果你没有设置rtc_build_framework=true,则直接编译默认目标:

ninja -C out/mac-arm64-release

-C指定的是构建目录。ninja会自动读取该目录下GN生成的构建文件。

等待时间方面,第一次全量编译非常劝退:在Apple Silicon的MacBook Pro上,关掉测试、symbol_level=0的前提下,大概需要一个多小时;Intel Mac可能要到两三个小时。过程中风扇会保持高位运转,这是正常的,不用慌。编译期间最好不要同时开虚拟机或大型IDE,避免内存不够导致编译进程被系统杀死。

如果机器内存小,可以限制并行度:

ninja -C out/mac-arm64-release -j4

这会牺牲一定速度,但能保证编译不中途崩溃。

5.3 产物在哪

编译成功之后,按是否启用framework,产物位置不一样。

framework模式:

out/mac-arm64-release/WebRTC.framework

这个目录就是一个完整的framework包,里面包含Headers、Modules和二进制文件,之后集成到你自己的工程只需要把这个framework拖进去,并设置Header Search Path。

静态库模式,需要先搜索一下库文件:

find out/mac-arm64-release -name "libwebrtc.a"

大概率输出路径是:

out/mac-arm64-release/obj/webrtc/libwebrtc.a

对应的头文件就在源码根目录的api/、rtc_base/等子目录里,集成的时候要把这些头文件目录都加进Header Search Path。

到这里,编译流水线就走通了。但产物是不是真的能用来做集成?接下来还有几个常见的坑要专门说。


6. 编译到一半失败的完整排查链路:我实际踩过的坑

前面几章是理想路径。真实操作中,大概率会在某个环节翻车。我自己从第一次尝试到稳定复现,前前后后踩了不少坑,这里把最有代表性的五个整理出来,每个都给出完整的排查思路,方便你对照。

6.1 磁盘写满:最阴间的“No space left on device”

编译到86%左右,突然抛出一片类似clang: error: unable to open output file ... No space left on device的报错,这是磁盘爆了。ninja的报错机制在这种时候很迷惑,可能只是一两个文件编译失败,但实际上整个磁盘已经没有可写空间,后面就算修好单个文件也过不去。

排查链路:

  1. 先用df -h /确认磁盘剩余空间,如果显示100%,基本坐实。
  2. 再跑du -sh ~/webrtc-checkout/*看哪个目录最占空间。
  3. 修复方案是把旧的out目录或其他项目临时移走,给webrtc留出空间。
  4. 清理后直接重跑ninja -C out/mac-arm64-release,ninja会跳过已经编译好的部分,只继续剩下的。

不要天真地以为删几个小文件就能腾出空间,这一行至少要留出20GB余量。

6.2 Xcode路径漂移:所有工具链集体罢工

如果你机器上装了多个版本的Xcode,或者重装过系统,经常会遇到一类诡异报错:gn gen能过,但在执行ninja时大量xcrun: error: unable to find utility "clang"或SDK not found。

排查链路:

  1. 执行xcode-select -p看当前路径。
  2. 如果路径不是/Applications/Xcode.app/Contents/Developer,执行sudo xcode-select -s /Applications/Xcode.app/Contents/Developer。
  3. 再跑一次xcodebuild -version,能正常输出版本号说明Xcode本身没问题。
  4. 重跑ninja。

这类问题通常不是构建配置损坏,而是系统工具链指向错了。

6.3 Python脚本版本不匹配

WebRTC的构建脚本很长一段时间依赖Python 2。如果你的系统干净,只有Python 3,某些旧版本在跑gclient sync时会报python: command not found。不过现在官方已经切到Python 3,新拉取的代码基本不会碰到这个问题。如果你使用了很久以前的分支或旧版depot_tools,可能还需要额外安装Python 2。思路要么升级depot_tools,要么按报错路径给Python 2提供对应环境,具体看报错信息再定。

6.4 第三方依赖的符号链接损坏

gclient sync在网络中断后,偶尔会留下失效的符号链接。典型症状是ninja编译时找不到third_party/xxx下的头文件,而这种报错看不出规律,时有时无。

排查链路:

  1. 进到src/third_party下,用find . -type l -exec test ! -e {} \; -print找出坏掉的符号链接。
  2. 大概率发现某些目录是空的,或链接指向不存在的路径。
  3. 修复方式是删掉这些坏链接,然后重新执行gclient sync --force。
  4. 如果修复后仍反复出现,就把third_party目录里的异常子目录移走,在.gclient文件不变的情况下再同步一次。

注意不要单独重新clone第三方仓库,除非你非常确定自己在干什么,否则容易把版本搞乱。

6.5 arm64与x64混用:链接阶段符号找不到

Apple Silicon机器上,Xcode默认支持的编译架构可能和你手动指定的不一致。比如你用target_cpu="x64"编出了x64的库,但在测试程序里用默认arm64的clang去链接,就会报Undefined symbols或者building for macOS-arm64 but attempting to link with file built for macOS-x86_64。

排查链路:

  1. 用file out/mac-x64-release/WebRTC.framework/WebRTC查看二进制的真实架构。
  2. 确认测试程序是用相同架构编译的,比如clang++ -arch x86_64。
  3. 在Apple Silicon上跑x64程序记得终端要支持Rosetta,或者直接用arm64版重新编译。

这个坑好多人踩,踩了还不好查,因为报错会指向某个具体的WebRTC符号,看起来像代码问题,其实是架构不匹配。


7. 写一个最小C++工程,验证编译出来的WebRTC真的能跑

编译出framework之后,最担心的是“是不是真的能用”。直接扔进大工程里试,出了问题又不好定位。我的习惯是先做一个最小的C++工程,把核心的PeerConnectionFactory创建出来跑通,确认库本身没问题,再逐步集成到真实业务里。

7.1 准备一个测试源文件

新建一个目录,比如~/webrtc-demo/,在里面创建main.cc:

#include <cstdio> #include "api/peer_connection_interface.h" #include "rtc_base/thread.h" int main() { auto network_thread = rtc::Thread::CreateWithSocketServer(); auto worker_thread = rtc::Thread::Create(); auto signaling_thread = rtc::Thread::Create(); if (!network_thread->Start() || !worker_thread->Start() || !signaling_thread->Start()) { std::fprintf(stderr, "failed to start threads\n"); return 1; } auto factory = webrtc::CreatePeerConnectionFactory( network_thread.get(), worker_thread.get(), signaling_thread.get(), nullptr, nullptr, nullptr, nullptr); if (!factory) { std::fprintf(stderr, "CreatePeerConnectionFactory failed\n"); return 1; } std::printf("WebRTC factory created successfully\n"); return 0; }

这段代码不涉及任何推流和采集,只是把WebRTC最核心的工厂对象创建出来。如果连这步都能成功,说明编译产物在运行时环境上基本没问题。

7.2 头文件路径与链接配置

使用framework模式编译:

clang++ main.cc \ -std=c++17 \ -I ~/webrtc-checkout/src/out/mac-arm64-release/WebRTC.framework/Headers \ -F ~/webrtc-checkout/src/out/mac-arm64-release \ -framework WebRTC \ -o demo

关键点是-I指向framework内的Headers目录,-F指向framework所在目录,-framework WebRTC告诉链接器使用WebRTC这个framework。编译器会把api/...和rtc_base/...这两个头文件路径自动解析出来,因为它们相对Headers目录存在。

如果用的是静态库模式,编译器要额外指定一堆系统framework依赖,比如CoreAudio、CoreVideo、AudioToolbox等等。所以在条件允许的情况下,我强烈建议用framework模式来验证。

7.3 运行结果判定

链接成功后执行:

./demo

如果看到输出:

WebRTC factory created successfully

说明整个编译链路是通的。如果运行时报dyld找不到库,检查一下DYLD_LIBRARY_PATH是否需要指向framework父目录;如果崩在CreatePeerConnectionFactory内部,大概率是线程或日志模块初始化问题,可以再检查rtc::InitializeSSL()这类初始化调用是否遗漏。

我在实际项目中会在这个基础上继续封装一层,比如把AudioDeviceModule换成自采集的实现,把VideoEncoderFactory换成带硬编的自定义工厂。但作为WebRTC的“hello world”,上面这个demo已经足够验证编译成果了。


最后说点实在的。第一次完整跑通WebRTC编译的人,多少都会经历从兴奋到崩溃再到平静的过程。最痛苦的不是敲命令,而是遇到一个错误之后,搜遍全网发现每个说法都不一样。上面这七个坑,是我在Mac上实际编译时一个一个踩出来、又一条一条验证过的。如果你也是被某个编译错误卡住了,不妨按这套流程重新捋一遍,大概率能省下一天时间。我自己的经验是:编译这种事,第一次完整跑通之后,后面所有的定制、调参、裁模块就都变成了水磨工夫,再也没有想象中那么神秘了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询