移动端WebRTC:Android原生集成、iOS原生集成、Flutter跨平台方案

说实话,WebRTC在移动端的集成,是很多开发者绕不过去的坎。我自己带团队做实时音视频SDK的时候,发现大部分问题都出在移动端适配这块。PC端跑得好好的,一到手机上就各种翻车。今天咱们就把Android、iOS、Flutter这三个方向掰开揉碎了讲清楚。

一、Android原生集成

Android端的WebRTC集成,我个人的习惯是直接用Google官方的webrtc-android仓库。别自己去编译源码,太折腾了。直接用Maven依赖就行。

核心依赖:

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

嗯,这里要注意版本号。我踩过坑,版本不对会导致so库冲突。建议统一用1.0.32006以上版本。

1.1 权限申请

Android 6.0以上需要动态申请权限。别偷懒,直接在代码里处理:

private static final String[] MANDATORY_PERMISSIONS = {
    Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.INTERNET
};

// 在Activity中检查权限
if (PermissionChecker.checkSelfPermission(this, MANDATORY_PERMISSIONS)) {
    // 初始化PeerConnectionFactory
} else {
    ActivityCompat.requestPermissions(this, MANDATORY_PERMISSIONS, REQUEST_CODE);
}

我曾经遇到过一个线上问题:用户拒绝麦克风权限后,App直接闪退。后来加了个权限拒绝的友好提示,才解决。

1.2 PeerConnectionFactory初始化

这个步骤很关键。初始化必须在子线程做,否则会卡UI:

PeerConnectionFactory.InitializationOptions initOptions =
    PeerConnectionFactory.InitializationOptions.builder(context)
        .setFieldTrials("")
        .setEnableInternalTracer(true)
        .createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);

PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
PeerConnectionFactory factory = PeerConnectionFactory.builder()
    .setOptions(options)
    .createPeerConnectionFactory();

为什么要在子线程?因为WebRTC内部会创建一堆线程池,主线程搞这个会ANR。你想想看,用户刚打开App就卡住,体验多差。

1.3 视频渲染

Android端渲染用SurfaceViewRenderer。我建议用TextureViewRenderer,性能更好:

<org.webrtc.SurfaceViewRenderer
    android:id="@+id/local_renderer"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

// 代码中初始化
SurfaceViewRenderer localRenderer = findViewById(R.id.local_renderer);
localRenderer.setMirror(true);
localRenderer.setEnableHardwareScaler(true);

小技巧:setMirror(true)可以让本地预览画面不镜像。很多新手忘了这个,结果视频通话时看到自己是反的。

二、iOS原生集成

iOS端相对简单一些,因为Apple的生态比较封闭。用CocoaPods集成最省事:

pod 'GoogleWebRTC'

我个人习惯用1.1.31999版本,稳定。别追新,WebRTC的iOS版本有时候会出些奇怪的bug。

2.1 权限配置

在Info.plist里加两个key:

<key>NSCameraUsageDescription</key>
<string>需要使用相机进行视频通话</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风进行语音通话</string>

嗯,这里有个坑:如果用户第一次拒绝权限,第二次再请求时系统不会弹窗。需要引导用户去设置里手动开启。我曾经因为这个被用户骂过。

2.2 RTCPeerConnectionFactory初始化

RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];

RTCPeerConnectionFactory *factory = [[RTCPeerConnectionFactory alloc]
    initWithEncoderFactory:encoderFactory
            decoderFactory:decoderFactory];

iOS端不需要像Android那样手动管理线程,WebRTC内部已经处理好了。但要注意,所有WebRTC API调用必须在主线程,否则会崩溃。

2.3 视频渲染

iOS用RTCMTLVideoRenderer,基于Metal的渲染器,性能比OpenGL好:

RTCMTLVideoRenderer *remoteRenderer = [[RTCMTLVideoRenderer alloc] init];
[remoteVideoTrack addRenderer:remoteRenderer];

// 添加到视图
[self.view addSubview:remoteRenderer];

注意:iOS 14以上,如果App没有使用相机权限,系统会在控制台打印警告。这不是bug,是Apple的隐私保护机制。

三、Flutter跨平台方案

Flutter做WebRTC,说白了就是用一套代码跑两端。我推荐用flutter_webrtc这个插件,社区活跃,更新快。

3.1 依赖配置

dependencies:
  flutter_webrtc: ^0.9.46

记得在Android的build.gradle里加:

minSdkVersion 21
targetSdkVersion 33

iOS端不需要额外配置,CocoaPods会自动处理。

3.2 核心API使用

import 'package:flutter_webrtc/flutter_webrtc.dart';

class VideoCallPage extends StatefulWidget {
  @override
  _VideoCallPageState createState() => _VideoCallPageState();
}

class _VideoCallPageState extends State<VideoCallPage> {
  final _localRenderer = RTCVideoRenderer();
  final _remoteRenderer = RTCVideoRenderer();
  RTCPeerConnection? _peerConnection;

  @override
  void initState() {
    super.initState();
    _initRenderers();
  }

  Future<void> _initRenderers() async {
    await _localRenderer.initialize();
    await _remoteRenderer.initialize();
  }

  // 创建PeerConnection
  Future<void> createPeerConnection() async {
    final configuration = {
      'iceServers': [
        {'urls': 'stun:stun.l.google.com:19302'}
      ]
    };
    
    _peerConnection = await createPeerConnection(configuration);
    
    // 添加本地流
    final stream = await navigator.mediaDevices.getUserMedia({
      'video': true,
      'audio': true
    });
    _localRenderer.srcObject = stream;
    
    stream.getTracks().forEach((track) {
      _peerConnection?.addTrack(track, stream);
    });
  }

  @override
  void dispose() {
    _localRenderer.dispose();
    _remoteRenderer.dispose();
    _peerConnection?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 远端视频
          RTCVideoView(_remoteRenderer, mirror: true),
          // 本地小窗
          Positioned(
            right: 16,
            top: 16,
            child: Container(
              width: 120,
              height: 160,
              child: RTCVideoView(_localRenderer, mirror: true),
            ),
          ),
        ],
      ),
    );
  }
}

这段代码我实际项目里用过,基本能覆盖90%的场景。注意dispose方法一定要调用,否则内存泄漏。

3.3 平台差异处理

Flutter虽然跨平台,但有些细节还是得区分处理:

功能 Android iOS
摄像头切换 支持前后置 支持前后置
屏幕共享 需MediaProjection权限 需ReplayKit扩展
音频模式 需设置AudioManager 自动处理
后台运行 需前台服务 支持后台音频

你想想看,如果不用Flutter,这些差异你得写两套代码。用Flutter的话,一套代码搞定,省心不少。

四、知识体系总览

下面这张图是我自己总结的移动端WebRTC知识体系,你可以对照着查漏补缺:

移动端WebRTC知识体系 Android原生 iOS原生 Flutter跨平台 权限管理 (Camera/Mic) PeerConnectionFactory SurfaceViewRenderer Info.plist权限配置 RTCPeerConnectionFactory RTCMTLVideoRenderer flutter_webrtc插件 RTCVideoView组件 平台差异处理 核心:统一信令流程 + 平台特定渲染

五、避坑指南

做移动端WebRTC,有几个坑我不得不提:

  • 内存泄漏:PeerConnection不用了记得close,Renderer不用了记得dispose。我曾经有个项目因为忘了释放,内存涨到200MB被用户投诉。
  • 线程问题:Android端所有WebRTC操作必须在子线程,iOS端必须在主线程。搞反了直接崩溃。
  • 版本兼容:WebRTC版本更新很快,建议锁定一个稳定版本,不要频繁升级。我见过有人从1.0.31999升到1.0.32006,结果视频编码器不兼容。
  • STUN/TURN服务器:生产环境一定要部署自己的TURN服务器,用Google的STUN只能用于测试。我踩过这个坑,用户在不同网络环境下连不上。

我的建议:如果你刚开始做移动端WebRTC,先从Flutter入手。flutter_webrtc封装得很好,踩坑少。等业务稳定了,再考虑用原生重写性能敏感的部分。

好了,移动端WebRTC的集成方案就讲到这里。记住一点:不管用哪种方案,核心都是信令交互 + 媒体协商 + 渲染展示。把这三个环节打通,剩下的就是细节优化了。

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