21、移动端WebRTC:React Native与WebRTC、Android原生WebRTC集成、iOS原生WebRTC集成

移动端做WebRTC,说实话,坑比PC端多不少。

我最早在React Native上集成WebRTC时,以为就是装个npm包的事。结果一跑起来,视频黑屏、音频没声、连接断断续续……折腾了整整一周。后来我才明白,移动端WebRTC不是“装个库”就完事的,你得理解底层怎么跑,才能用好它。

这一章,咱们就聊聊三个方向:React Native、Android原生、iOS原生。我会把我在项目中踩过的坑、总结的经验,都摊开来讲。

核心要点:移动端WebRTC的难点不在API调用,而在权限管理、生命周期、硬件适配和原生桥接。这三个平台各有各的脾气,但底层逻辑是相通的。

21.1 React Native + WebRTC:跨平台的最优解?

React Native做WebRTC,说白了就是“一套代码,两端跑”。但你要知道,WebRTC本身是C++写的,RN只是给你包了一层JS桥。所以性能瓶颈不在JS,而在桥接层。

我个人习惯用 react-native-webrtc 这个库。它封装了大部分原生逻辑,但有些细节你得自己处理。

安装与基础配置

npm install react-native-webrtc
cd ios && pod install

嗯,这里要注意:Android端不需要额外配置,但iOS端必须在 Info.plist 里添加摄像头和麦克风权限描述。不然一调用就闪退,我当初就栽在这上面。

平台 所需权限 配置位置
iOS NSCameraUsageDescription, NSMicrophoneUsageDescription Info.plist
Android CAMERA, RECORD_AUDIO AndroidManifest.xml

核心代码示例

import { RTCPeerConnection, mediaDevices } from 'react-native-webrtc';

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

// 获取本地流
const stream = await mediaDevices.getUserMedia({
  audio: true,
  video: { facingMode: 'user' }
});

stream.getTracks().forEach(track => pc.addTrack(track, stream));

// 创建offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

这段代码看着简单,但有个坑:RN的媒体流生命周期和组件生命周期是分离的。如果你在组件卸载时没有释放流,摄像头会一直亮着。我曾经有个用户反馈说“为什么我的手机发烫?”——就是因为流没释放。

避坑指南:useEffect 的清理函数中,一定要调用 stream.getTracks().forEach(track => track.stop())。同时把 pc.close() 也加上。

21.2 Android原生集成:从JNI到SurfaceView

Android原生集成WebRTC,我建议直接用Google官方的 org.webrtc:google-webrtc 库。它封装了底层JNI调用,你只需要关注Java层的逻辑。

为什么会这样?因为WebRTC的C++层在Android上是通过JNI桥接的,如果你自己写JNI,那工作量太大了。官方库已经帮你做好了。

Gradle依赖

implementation 'org.webrtc:google-webrtc:1.0.32006'

核心流程

// 初始化PeerConnectionFactory
PeerConnectionFactory.InitializationOptions initOptions =
    PeerConnectionFactory.InitializationOptions.builder(context)
        .setFieldTrials("")
        .createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);

PeerConnectionFactory factory = PeerConnectionFactory.builder()
    .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBaseContext))
    .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBaseContext, true, true))
    .createPeerConnectionFactory();

// 创建PeerConnection
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
PeerConnection pc = factory.createPeerConnection(config, observer);

这里有个关键点:EGLContext。Android的硬件编码解码依赖OpenGL ES,你必须先创建一个EGL上下文。我刚开始没注意,结果视频一直黑屏,查了两天才发现是EGL没初始化。

警告:Android 12以上,后台应用无法访问摄像头。如果你的App在后台时还想保持视频通话,必须使用前台服务(Foreground Service)。否则系统会直接杀掉你的摄像头。

SurfaceView渲染

Android端渲染视频,我推荐用 SurfaceViewRenderer。它比TextureView性能更好,尤其是在低端机上。

SurfaceViewRenderer localRenderer = findViewById(R.id.local_renderer);
localRenderer.init(eglBaseContext, null);

VideoSource videoSource = factory.createVideoSource(false);
VideoTrack localVideoTrack = factory.createVideoTrack("local_track", videoSource);

// 将摄像头采集绑定到VideoSource
CameraVideoCapturer capturer = Camera2Enumerator.createCapturer();
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
capturer.startCapture(1280, 720, 30);

嗯,这里要注意:startCapture 的分辨率不要设太高。720p在大多数手机上够用了,1080p反而会增加编码延迟。我测试过,720p下端到端延迟能控制在200ms以内,1080p就飙到400ms了。

21.3 iOS原生集成:Swift与Objective-C的抉择

iOS端集成WebRTC,我建议用CocoaPods引入 GoogleWebRTC 库。它包含了编译好的framework,省去了自己编译的麻烦。

我个人习惯用Swift写新项目,但WebRTC的iOS API是Objective-C风格的。所以你得在Swift里做桥接,或者直接用OC写。

Podfile配置

pod 'GoogleWebRTC'

Swift集成示例

import GoogleWebRTC

// 初始化工厂
let factory = RTCPeerConnectionFactory()

// 配置ICE
let config = RTCConfiguration()
config.iceServers = [RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"])]

// 创建连接
let pc = factory.peerConnection(with: config, constraints: constraints, delegate: self)

// 获取本地流
let mediaConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = factory.audioSource(with: mediaConstraints)
let audioTrack = factory.audioTrack(with: audioSource, trackId: "audio0")

let videoSource = factory.videoSource()
let videoTrack = factory.videoTrack(with: videoSource, trackId: "video0")

// 绑定摄像头
let capturer = RTCCameraVideoCapturer(delegate: videoSource)
if let device = RTCCameraVideoCapturer.captureDevices().first {
    try? capturer.startCapture(with: device,
                                format: device.supportedFormats.last!,
                                fps: 30)
}

pc.add(audioTrack, streamIds: ["stream0"])
pc.add(videoTrack, streamIds: ["stream0"])

这段代码看着挺长,但核心逻辑就三步:建工厂、配连接、绑设备。我刚开始写的时候,总忘记设置 RTCMediaConstraints,结果音频一直没声音。后来才发现,iOS默认不开启音频,你得显式声明。

避坑指南:iOS端如果遇到“摄像头黑屏”,先检查 AVCaptureDevice.authorizationStatus。用户拒绝权限后,WebRTC不会报错,只会返回空流。我曾经因为这个被测试骂了一下午。

21.4 三平台对比与选型建议

你想想看,三个平台各有优劣。我整理了一张对比表,方便你决策:

维度 React Native Android原生 iOS原生
开发效率 高(一套代码) 中(需写Java/Kotlin) 中(需写Swift/OC)
性能 中(桥接开销) 高(直接调用JNI) 高(直接调用C++)
硬件适配 依赖原生库 完全可控 完全可控
调试难度 中(JS层+原生层) 低(Android Studio) 低(Xcode)
推荐场景 快速原型、中小型项目 大型App、定制化需求 iOS独占、高性能要求

我个人建议:如果你团队有前端背景,优先选React Native。但如果你要做音视频通话这种核心功能,还是原生更稳。我在一个项目中用RN做UI层,原生做WebRTC层,通过Native Modules桥接,效果还不错。

21.5 移动端WebRTC的核心架构图

下面这张图,是我根据项目经验总结的移动端WebRTC分层架构。你看一眼,就能明白各层之间的关系:

移动端WebRTC分层架构 应用层 React Native / Android Activity / iOS ViewController 桥接层 RN Native Modules / Android JNI / iOS ObjC-Swift Bridge WebRTC核心层 PeerConnection / MediaStream / DataChannel / ICE / DTLS 硬件抽象层 Camera / Microphone / Speaker / EGL / VideoToolbox 数据流向

这张图你看懂了吗?从上到下,应用层只管UI和用户交互,桥接层负责把JS/Java/Swift的调用转成C++,核心层处理信令、连接、媒体流,硬件层管摄像头和麦克风。每一层各司其职,出了问题也好定位。

21.6 实战中的避坑总结

最后,我把我在移动端WebRTC项目中遇到最多的坑,列成清单给你:

  • 权限问题:iOS的Info.plist、Android的Manifest,少一个就崩。我建议在App启动时就申请权限,不要等到调用getUserMedia时才弹窗。
  • 生命周期:Android的onPause/onResume、iOS的viewDidDisappear,都要对应暂停/恢复摄像头。否则用户切到后台再回来,视频就卡死了。
  • ICE失败:移动端网络切换频繁(WiFi切4G),ICE重连机制必须做好。我习惯在onIceConnectionStateChange里监听状态,如果disconnected就主动重启ICE。
  • 内存泄漏:PeerConnection、MediaStream、VideoTrack都要在退出时释放。RN里尤其要注意,JS的垃圾回收不会帮你释放原生对象。
  • 编解码兼容:Android碎片化严重,有些低端机不支持H264硬件编码。我建议优先用VP8,兼容性最好。

一句话总结:移动端WebRTC,七分在原生,三分在业务。把底层搞扎实了,上层怎么玩都行。

公众号:蓝海资料掘金营,微信deep3321