跳到主要内容

移动原生libwebrtc

移动原生 libwebrtc

H5 + WKWebView 解决了 80% 的"能跑"问题,但要拼性能、稳定性、长后台保持、自定义音频处理,就得回到原生 libwebrtc。Google 的 webrtc/src 仓库是事实标准,国内厂商(腾讯 TRTC、声网、即构)的 SDK 几乎都基于它再加封装。本篇讲编译、集成、与系统 API 的对接、上架要点。

1. libwebrtc 是什么

  • Google 维护的 C++ 库,覆盖采集、编解码、QoS、SRTP/DTLS、ICE、SDP。
  • 仓库:https://webrtc.googlesource.com/src,按 milestone 打 branch(M120、M125 等),约每 6 周一版。
  • 提供平台壳:iOS 的 WebRTC.framework、Android 的 libwebrtc.aar。Java/Swift/Objective-C 接口都在壳里。
  • 浏览器里跑的就是这套源码的子集(裁掉 desktop capturer 等模块)。

什么时候适用:自研 RTC SDK;要做 codec 实验(自定义 H.264 编码器、外接 H.265);对 3A、降噪、回声消除要做深度定制。 什么时候不适用:业务期内不准备养一个 5 人以上的音视频底层团队;只需要在两三个平台上做标准实时通话——直接接 TRTC / 声网更省事。

2. 版本同步:一个被反复忽视的问题

iOS / Android / Web(Chromium)三端的 libwebrtc 版本几乎从不一致:

版本来源你能控制吗
Chrome 桌面跟随 Chrome 主干不能,用户的 Chrome 版本
iOS Safari跟随 WebKit / iOS不能
Android WebView跟随系统 WebView不能
Electron跟随 Electron 内嵌 Chromium锁定 Electron 版本
iOS 原生 SDK你打包的 WebRTC.framework能锁
Android 原生 SDK你打包的 aar能锁

结论:互通问题往往不是 SDP 不兼容,而是某一端是 M110,另一端是 M125,对 RTX/RED 的字节顺序处理或者 simulcast SSRC 分配有差异。生产里要把所有原生端锁同一个 milestone,并把它写在 release note 里。

3. 编译 iOS framework

3.1 准备 depot_tools

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

mkdir webrtc_ios && cd webrtc_ios
fetch --nohooks webrtc_ios
gclient sync
git -C src checkout branch-heads/m125
gclient sync -D

3.2 构建脚本

# 在 src/ 目录下
python3 tools_webrtc/ios/build_ios_libs.py \
--arch arm64 x64 \
--bitcode false \
--extra-gn-args 'rtc_include_tests=false rtc_enable_symbol_export=true is_component_build=false enable_dsyms=true' \
--output-dir out_ios

产物在 src/out_ios/WebRTC.xcframework

编译期常见坑

  • Xcode 15+ 默认 -fno-objc-msgsend-selector-stubs 会和老 milestone 不兼容,必须升 milestone 或加 --extra-gn-args 'use_xcode_clang=true'
  • bitcode 自 Xcode 14 起 Apple 已不再要求,关掉
  • arm64e 目前不要打开,App Store 不支持。

3.3 集成到 App

# Podfile
target 'MyApp' do
use_frameworks!
pod 'WebRTC', :path => './third_party/WebRTC.xcframework'
end

或者直接拖进 Xcode 项目,Embed & Sign

3.4 最小 Objective-C 接入

#import <WebRTC/WebRTC.h>

@interface RtcManager () <RTCPeerConnectionDelegate>
@property (nonatomic, strong) RTCPeerConnectionFactory *factory;
@property (nonatomic, strong) RTCPeerConnection *pc;
@end

@implementation RtcManager

- (instancetype)init {
if (self = [super init]) {
[RTCInitializeSSL];
RTCDefaultVideoEncoderFactory *enc =
[[RTCDefaultVideoEncoderFactory alloc] init];
RTCDefaultVideoDecoderFactory *dec =
[[RTCDefaultVideoDecoderFactory alloc] init];
_factory = [[RTCPeerConnectionFactory alloc]
initWithEncoderFactory:enc
decoderFactory:dec];
}
return self;
}

- (void)startWithSignaling:(id<Signaling>)sig {
RTCConfiguration *cfg = [[RTCConfiguration alloc] init];
cfg.sdpSemantics = RTCSdpSemanticsUnifiedPlan;
cfg.bundlePolicy = RTCBundlePolicyMaxBundle;
cfg.iceServers = @[[[RTCIceServer alloc]
initWithURLStrings:@[@"stun:stun.l.google.com:19302"]]];
RTCMediaConstraints *cons =
[[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil
optionalConstraints:nil];
self.pc = [self.factory peerConnectionWithConfiguration:cfg
constraints:cons
delegate:self];
}
@end

3.5 AVAudioSession 配置

RTCAudioSession *session = [RTCAudioSession sharedInstance];
[session lockForConfiguration];
NSError *err;
[session setCategory:AVAudioSessionCategoryPlayAndRecord
mode:AVAudioSessionModeVoiceChat
options:AVAudioSessionCategoryOptionAllowBluetooth |
AVAudioSessionCategoryOptionDefaultToSpeaker
error:&err];
[session setActive:YES error:&err];
[session unlockForConfiguration];

不调这套,AGC/AEC 不会启用,AirPods 切换路由乱跳。

4. 编译 Android aar

4.1 拉源码

fetch --nohooks webrtc_android
gclient sync
git -C src checkout branch-heads/m125
gclient sync -D

4.2 构建

cd src
./tools_webrtc/android/build_aar.py \
--build-dir out_android \
--arch arm64-v8a armeabi-v7a x86_64 \
--extra-gn-args 'is_debug=false rtc_include_tests=false'

产物 libwebrtc.aar 大约 25~30 MB(含 4 个 ABI),实际接入只保留 arm64-v8aarmeabi-v7a

4.3 集成

// build.gradle.kts
dependencies {
implementation(files("libs/libwebrtc.aar"))
}

android {
packagingOptions {
jniLibs.useLegacyPackaging = false
jniLibs.pickFirsts.add("**/libjingle_peerconnection_so.so")
}
}

4.4 最小 Kotlin 接入

class RtcManager(ctx: Context) {
private val factory: PeerConnectionFactory

init {
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(ctx)
.setEnableInternalTracer(false)
.createInitializationOptions()
)
val eglBase = EglBase.create()
factory = PeerConnectionFactory.builder()
.setVideoEncoderFactory(
DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true)
)
.setVideoDecoderFactory(
DefaultVideoDecoderFactory(eglBase.eglBaseContext)
)
.createPeerConnectionFactory()
}

fun createPc(observer: PeerConnection.Observer): PeerConnection? {
val cfg = PeerConnection.RTCConfiguration(
listOf(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer())
).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
}
return factory.createPeerConnection(cfg, observer)
}
}

4.5 与 AudioManager / FocusRequest 对接

val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.mode = AudioManager.MODE_IN_COMMUNICATION
am.isSpeakerphoneOn = true

val req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setOnAudioFocusChangeListener { /* 处理来电中断 */ }
.build()
am.requestAudioFocus(req)

通话结束必须 abandonAudioFocusRequest(req),否则系统媒体(音乐、视频)会继续被压低。

5. 与系统能力的集成点

系统能力iOSAndroid注意
来电中断CallKit + CXProviderTelephonyManager.PhoneStateListener中断结束后必须重启 AudioSession
后台保活Background Modes: audio/voipForeground Service + microphone typeAndroid 14+ 强制 foregroundServiceType="microphone"
蓝牙路由AVAudioSession 选项AudioManager.setBluetoothScoOn蓝牙 SCO 抢占要重新协商
摄像头预览RTCCameraVideoCapturerCamera2 / CameraXAndroid 上推荐 CameraX 自定义采集
屏幕共享ReplayKit + Broadcast ExtensionMediaProjection需独立进程 + IPC
锁屏接听CallKitConnectionService推送侧用 PushKit / FCM 高优先

6. 跨端 Codec 协商策略

场景推荐 codec理由
移动 ↔ 移动同厂商 SDKVP8 / H.264大厂 SDK 内部 VP8 调过得很好
移动 ↔ Web 浏览器H.264 (Baseline 42e01f)Safari 兼容性最稳
移动 ↔ 小程序H.264 onlyTRTC/声网小程序仅 H.264
移动 ↔ 推流到直播H.264RTMP/HLS 链路要求
1080p+ 高清会议VP9 / AV1(端能力允许时)同码率画质更好

代码层面把 codec 选择做成可配置,不要硬编码

data class CodecPreference(
val video: List<String> = listOf("H264", "VP8"),
val audio: List<String> = listOf("OPUS")
)

7. 上架 App Store / Play Store 注意点

7.1 App Store

  • 隐私清单(Privacy Manifest):自 2024 起强制要求声明 API 使用原因。WebRTC 涉及的:
    • NSPrivacyAccessedAPICategoryUserDefaults
    • NSPrivacyAccessedAPICategoryFileTimestamp
    • NSPrivacyAccessedAPICategorySystemBootTime
    • NSPrivacyAccessedAPICategoryDiskSpace 自己写 PrivacyInfo.xcprivacy,把 libwebrtc 用到的几项加进去。
  • 出口管控(ITSAppUsesNonExemptEncryption):WebRTC 用了 SRTP/DTLS,按"使用标准加密"声明,需在 Info.plist 加 ITSAppUsesNonExemptEncryption = NO 或走豁免流程。
  • 通话场景应使用 CallKit,否则 5.5.4 条款有可能被打回(语聊房豁免,但要在审核备注说明)。
  • ReplayKit 屏幕共享的 Broadcast Extension 是独立 target,签名和主 App 同 Team,App Group 共享数据。

7.2 Google Play

  • Android 14+ Foreground Service 类型:通话必须声明 foregroundServiceType="microphone"(音频)+ "camera"(视频),并在 manifest 加权限。
  • 麦克风权限属于敏感权限,需要在 Play Console 上传 declaration form。
  • Target API 必须 ≥ 当年要求(2026 年起 ≥ API 34)。
  • 64 位强制:libwebrtc 必须包含 arm64-v8a
  • 大小:libwebrtc 单 ABI 约 6~8 MB,全 ABI 25+ MB,注意 App Bundle 分发。

8. 自定义采集 / 自定义渲染

libwebrtc 默认采集是系统摄像头,业务侧常常要做美颜、虚拟背景、画板叠加,必须接管采集。

8.1 iOS 自定义采集

RTCVideoSource *source = [factory videoSource];
RTCVideoTrack *videoTrack = [factory videoTrackWithSource:source trackId:@"video0"];

// 把 CVPixelBufferRef 喂给 source
- (void)pushFrame:(CVPixelBufferRef)pixelBuffer timestamp:(int64_t)ns {
RTCCVPixelBuffer *rtcBuf = [[RTCCVPixelBuffer alloc] initWithPixelBuffer:pixelBuffer];
RTCVideoFrame *frame = [[RTCVideoFrame alloc] initWithBuffer:rtcBuf
rotation:RTCVideoRotation_0
timeStampNs:ns];
[source capturer:nil didCaptureVideoFrame:frame];
}

8.2 Android 自定义采集

val source = factory.createVideoSource(false)
val track = factory.createVideoTrack("video0", source)

// 自定义采集器
class MyCapturer : VideoCapturer { /* ... */ }
val capturer = MyCapturer()
capturer.initialize(surfaceTextureHelper, ctx, source.capturerObserver)
capturer.startCapture(1280, 720, 30)

业务通常的链路:Camera → GPU pipeline(OpenGL/Metal)做美颜 → 转回 NV12/I420 → 喂给 capturerObserver.onFrameCaptured(frame)

9. 失败时的样子

现象原因处理
iOS 链接报 _OBJC_CLASS_$_RTCPeerConnectionFactory 缺失xcframework 未 Embed改 Embed & Sign
Android 启动 crash UnsatisfiedLinkError libjingle_peerconnection_soABI 不全或被混淆保留 arm64-v8a,proguard 加 keep
上架被拒(4.5 / 隐私清单)没声明 API 用途补 PrivacyInfo.xcprivacy
切后台 30s 通话断没声明 audio background mode加 UIBackgroundModes
Android 14 通话进程被杀没启 Foreground Service启 microphone type FGS
互通方画面绿屏自定义采集 YUV stride 错用 RTCCVPixelBuffer / I420Buffer.Builder
蓝牙耳机切换后 AEC 失效AudioSession 路由变了未重配监听 routeChange / AudioDeviceCallback

10. 反模式

反模式后果
自己 fork libwebrtc 不跟主干半年后被 codec / SDP 变更打死
iOS、Android、Web 用三个不同 milestone互通诡异 bug 频发
关掉 AEC/AGC 自己写 3A99% 比官方差
直接抓 webrtc::PeerConnection C++ 接口iOS 上崩在 ABI 不一致
不接 CallKit / ConnectionService锁屏体验差,审核风险
Android 用 Service 而非 ForegroundServiceAndroid 14 直接被系统 kill
上架时不写隐私清单提交被打回,反复浪费一周

11. 何时该用厂商 SDK 代替

决策因素自研 libwebrtc接 TRTC / 声网 / 即构
团队规模 < 5 人不推荐推荐
需要 7×24 SLA投入巨大厂商兜底
全球节点自建昂贵厂商现成
需要深度定制 codec/3A必须自研受限
与小程序互通还要再搭一层厂商一站式
数据合规要求私有部署自研合理看厂商支持

详见 微信小程序方案.mdReactNative与Flutter.md

12. 权威资料