在移动端使用 WebRTC (Android/iOS)

在移动端使用 WebRTC (Android/iOS)

本文是 WebRTC 系列专栏的第二十五篇,也是本系列的收官之作。我们将探讨如何在 Android 和 iOS 平台上使用 WebRTC Native SDK 构建跨平台实时应用。


目录

  1. [移动端 WebRTC 概述](#移动端 WebRTC 概述)
  2. [Android 集成](#Android 集成)
  3. [iOS 集成](#iOS 集成)
  4. 跨平台方案
  5. 性能优化
  6. 常见问题
  7. 总结

1. 移动端 WebRTC 概述

1.1 移动端特点

复制代码
移动端 WebRTC 挑战:

1. 硬件差异
   - 不同设备性能差异大
   - 摄像头/麦克风规格不同

2. 网络环境
   - WiFi/4G/5G 切换
   - 网络不稳定

3. 电量消耗
   - 编解码耗电
   - 网络传输耗电

4. 后台限制
   - iOS 后台限制严格
   - Android 后台服务限制

5. 权限管理
   - 摄像头/麦克风权限
   - 后台运行权限

1.2 SDK 选择

方案 说明 适用场景
WebRTC Native Google 官方 SDK 原生开发
WebView 浏览器内核 简单集成
Flutter WebRTC Flutter 插件 跨平台
React Native RN 插件 跨平台

1.3 架构对比

复制代码
原生 SDK vs WebView:

原生 SDK:
+------------------+
|   Application    |
+------------------+
|  WebRTC Native   |
+------------------+
|   OS (Android/iOS)|
+------------------+

优点: 性能好,功能完整
缺点: 开发成本高

WebView:
+------------------+
|   Application    |
+------------------+
|     WebView      |
+------------------+
|  WebRTC (浏览器)  |
+------------------+
|   OS             |
+------------------+

优点: 开发简单,跨平台
缺点: 性能受限,功能受限

2. Android 集成

2.1 添加依赖

groovy 复制代码
// build.gradle (app)
dependencies {
    // WebRTC 官方库
    implementation 'org.webrtc:google-webrtc:1.0.32006'
    
    // 或使用 JitsiMeet 维护的版本
    // implementation 'com.onesignal:onesignal-webrtc:1.0.0'
}

// 权限配置 AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

2.2 初始化

kotlin 复制代码
// WebRTCManager.kt
class WebRTCManager(private val context: Context) {
    
    private var peerConnectionFactory: PeerConnectionFactory? = null
    private var peerConnection: PeerConnection? = null
    private var localVideoTrack: VideoTrack? = null
    private var localAudioTrack: AudioTrack? = null
    
    private var videoCapturer: VideoCapturer? = null
    private var surfaceTextureHelper: SurfaceTextureHelper? = null
    
    private val eglBase = EglBase.create()
    
    // 初始化
    fun initialize() {
        // 初始化 PeerConnectionFactory
        val options = PeerConnectionFactory.InitializationOptions.builder(context)
            .setEnableInternalTracer(true)
            .createInitializationOptions()
        PeerConnectionFactory.initialize(options)
        
        // 创建 PeerConnectionFactory
        val encoderFactory = DefaultVideoEncoderFactory(
            eglBase.eglBaseContext,
            true,  // 启用硬件加速
            true   // 启用 H.264 高配置
        )
        val decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
        
        peerConnectionFactory = PeerConnectionFactory.builder()
            .setVideoEncoderFactory(encoderFactory)
            .setVideoDecoderFactory(decoderFactory)
            .setOptions(PeerConnectionFactory.Options())
            .createPeerConnectionFactory()
    }
    
    // 创建 PeerConnection
    fun createPeerConnection(observer: PeerConnection.Observer): PeerConnection? {
        val iceServers = listOf(
            PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
        )
        
        val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
            sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
            continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
        }
        
        peerConnection = peerConnectionFactory?.createPeerConnection(rtcConfig, observer)
        return peerConnection
    }
    
    // 创建本地媒体
    fun createLocalMedia(localView: SurfaceViewRenderer) {
        // 初始化本地视图
        localView.init(eglBase.eglBaseContext, null)
        localView.setMirror(true)
        
        // 创建视频源
        videoCapturer = createCameraCapturer()
        surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext)
        
        val videoSource = peerConnectionFactory?.createVideoSource(videoCapturer!!.isScreencast)
        videoCapturer?.initialize(surfaceTextureHelper, context, videoSource?.capturerObserver)
        videoCapturer?.startCapture(1280, 720, 30)
        
        localVideoTrack = peerConnectionFactory?.createVideoTrack("video0", videoSource)
        localVideoTrack?.addSink(localView)
        
        // 创建音频源
        val audioConstraints = MediaConstraints().apply {
            mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
        }
        val audioSource = peerConnectionFactory?.createAudioSource(audioConstraints)
        localAudioTrack = peerConnectionFactory?.createAudioTrack("audio0", audioSource)
    }
    
    private fun createCameraCapturer(): VideoCapturer? {
        val enumerator = Camera2Enumerator(context)
        
        // 优先使用前置摄像头
        for (deviceName in enumerator.deviceNames) {
            if (enumerator.isFrontFacing(deviceName)) {
                return enumerator.createCapturer(deviceName, null)
            }
        }
        
        // 使用后置摄像头
        for (deviceName in enumerator.deviceNames) {
            if (enumerator.isBackFacing(deviceName)) {
                return enumerator.createCapturer(deviceName, null)
            }
        }
        
        return null
    }
    
    // 添加轨道到 PeerConnection
    fun addTracksToConnection() {
        localVideoTrack?.let { track ->
            peerConnection?.addTrack(track, listOf("stream0"))
        }
        localAudioTrack?.let { track ->
            peerConnection?.addTrack(track, listOf("stream0"))
        }
    }
    
    // 创建 Offer
    fun createOffer(callback: (SessionDescription?) -> Unit) {
        val constraints = MediaConstraints().apply {
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
        }
        
        peerConnection?.createOffer(object : SdpObserver {
            override fun onCreateSuccess(sdp: SessionDescription?) {
                peerConnection?.setLocalDescription(object : SdpObserver {
                    override fun onSetSuccess() {
                        callback(sdp)
                    }
                    override fun onSetFailure(error: String?) {}
                    override fun onCreateSuccess(sdp: SessionDescription?) {}
                    override fun onCreateFailure(error: String?) {}
                }, sdp)
            }
            override fun onCreateFailure(error: String?) {
                callback(null)
            }
            override fun onSetSuccess() {}
            override fun onSetFailure(error: String?) {}
        }, constraints)
    }
    
    // 设置远程描述
    fun setRemoteDescription(sdp: SessionDescription, callback: () -> Unit) {
        peerConnection?.setRemoteDescription(object : SdpObserver {
            override fun onSetSuccess() {
                callback()
            }
            override fun onSetFailure(error: String?) {}
            override fun onCreateSuccess(sdp: SessionDescription?) {}
            override fun onCreateFailure(error: String?) {}
        }, sdp)
    }
    
    // 添加 ICE Candidate
    fun addIceCandidate(candidate: IceCandidate) {
        peerConnection?.addIceCandidate(candidate)
    }
    
    // 设置远程视频视图
    fun setRemoteView(remoteView: SurfaceViewRenderer) {
        remoteView.init(eglBase.eglBaseContext, null)
    }
    
    // 释放资源
    fun release() {
        videoCapturer?.stopCapture()
        videoCapturer?.dispose()
        surfaceTextureHelper?.dispose()
        localVideoTrack?.dispose()
        localAudioTrack?.dispose()
        peerConnection?.close()
        peerConnectionFactory?.dispose()
        eglBase.release()
    }
}

2.3 Activity 实现

kotlin 复制代码
// VideoCallActivity.kt
class VideoCallActivity : AppCompatActivity(), PeerConnection.Observer {
    
    private lateinit var webRTCManager: WebRTCManager
    private lateinit var signalingClient: SignalingClient
    
    private lateinit var localView: SurfaceViewRenderer
    private lateinit var remoteView: SurfaceViewRenderer
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_call)
        
        localView = findViewById(R.id.localView)
        remoteView = findViewById(R.id.remoteView)
        
        // 检查权限
        if (checkPermissions()) {
            initWebRTC()
        } else {
            requestPermissions()
        }
    }
    
    private fun initWebRTC() {
        webRTCManager = WebRTCManager(this)
        webRTCManager.initialize()
        webRTCManager.createPeerConnection(this)
        webRTCManager.createLocalMedia(localView)
        webRTCManager.setRemoteView(remoteView)
        webRTCManager.addTracksToConnection()
        
        // 初始化信令
        signalingClient = SignalingClient(this)
        signalingClient.connect()
    }
    
    // 发起通话
    fun startCall() {
        webRTCManager.createOffer { sdp ->
            sdp?.let {
                signalingClient.sendOffer(it)
            }
        }
    }
    
    // PeerConnection.Observer 实现
    override fun onIceCandidate(candidate: IceCandidate?) {
        candidate?.let {
            signalingClient.sendCandidate(it)
        }
    }
    
    override fun onTrack(transceiver: RtpTransceiver?) {
        transceiver?.receiver?.track()?.let { track ->
            if (track is VideoTrack) {
                runOnUiThread {
                    track.addSink(remoteView)
                }
            }
        }
    }
    
    override fun onIceConnectionChange(state: PeerConnection.IceConnectionState?) {
        Log.d("WebRTC", "ICE state: $state")
    }
    
    // 其他 Observer 方法...
    override fun onSignalingChange(state: PeerConnection.SignalingState?) {}
    override fun onIceConnectionReceivingChange(receiving: Boolean) {}
    override fun onIceGatheringChange(state: PeerConnection.IceGatheringState?) {}
    override fun onAddStream(stream: MediaStream?) {}
    override fun onRemoveStream(stream: MediaStream?) {}
    override fun onDataChannel(channel: DataChannel?) {}
    override fun onRenegotiationNeeded() {}
    override fun onAddTrack(receiver: RtpReceiver?, streams: Array<out MediaStream>?) {}
    
    override fun onDestroy() {
        super.onDestroy()
        webRTCManager.release()
        signalingClient.disconnect()
    }
}

3. iOS 集成

3.1 添加依赖

ruby 复制代码
# Podfile
platform :ios, '12.0'

target 'YourApp' do
  use_frameworks!
  
  # WebRTC
  pod 'GoogleWebRTC'
  
  # 或使用 Location 维护的版本
  # pod 'WebRTC-lib'
end
xml 复制代码
<!-- Info.plist 权限配置 -->
<key>NSCameraUsageDescription</key>
<string>需要访问摄像头进行视频通话</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风进行语音通话</string>
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>voip</string>
</array>

3.2 WebRTC 管理器

swift 复制代码
// WebRTCManager.swift
import WebRTC

class WebRTCManager: NSObject {
    
    private var peerConnectionFactory: RTCPeerConnectionFactory!
    private var peerConnection: RTCPeerConnection?
    private var localVideoTrack: RTCVideoTrack?
    private var localAudioTrack: RTCAudioTrack?
    
    private var videoCapturer: RTCCameraVideoCapturer?
    private var localVideoSource: RTCVideoSource?
    
    weak var delegate: WebRTCManagerDelegate?
    
    // 初始化
    func initialize() {
        RTCInitializeSSL()
        
        let encoderFactory = RTCDefaultVideoEncoderFactory()
        let decoderFactory = RTCDefaultVideoDecoderFactory()
        
        peerConnectionFactory = RTCPeerConnectionFactory(
            encoderFactory: encoderFactory,
            decoderFactory: decoderFactory
        )
    }
    
    // 创建 PeerConnection
    func createPeerConnection() {
        let config = RTCConfiguration()
        config.iceServers = [
            RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"])
        ]
        config.sdpSemantics = .unifiedPlan
        config.continualGatheringPolicy = .gatherContinually
        
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: nil,
            optionalConstraints: nil
        )
        
        peerConnection = peerConnectionFactory.peerConnection(
            with: config,
            constraints: constraints,
            delegate: self
        )
    }
    
    // 创建本地媒体
    func createLocalMedia(localView: RTCMTLVideoView) {
        // 视频
        localVideoSource = peerConnectionFactory.videoSource()
        videoCapturer = RTCCameraVideoCapturer(delegate: localVideoSource!)
        
        localVideoTrack = peerConnectionFactory.videoTrack(
            with: localVideoSource!,
            trackId: "video0"
        )
        localVideoTrack?.add(localView)
        
        // 启动摄像头
        startCapture()
        
        // 音频
        let audioConstraints = RTCMediaConstraints(
            mandatoryConstraints: nil,
            optionalConstraints: nil
        )
        let audioSource = peerConnectionFactory.audioSource(with: audioConstraints)
        localAudioTrack = peerConnectionFactory.audioTrack(
            with: audioSource,
            trackId: "audio0"
        )
    }
    
    private func startCapture() {
        guard let capturer = videoCapturer else { return }
        
        let devices = RTCCameraVideoCapturer.captureDevices()
        guard let frontCamera = devices.first(where: { $0.position == .front }) else { return }
        
        let formats = RTCCameraVideoCapturer.supportedFormats(for: frontCamera)
        guard let format = formats.first(where: {
            let dimensions = CMVideoFormatDescriptionGetDimensions($0.formatDescription)
            return dimensions.width == 1280 && dimensions.height == 720
        }) ?? formats.last else { return }
        
        let fps = format.videoSupportedFrameRateRanges.first?.maxFrameRate ?? 30
        
        capturer.startCapture(
            with: frontCamera,
            format: format,
            fps: Int(fps)
        )
    }
    
    // 添加轨道
    func addTracksToConnection() {
        if let videoTrack = localVideoTrack {
            peerConnection?.add(videoTrack, streamIds: ["stream0"])
        }
        if let audioTrack = localAudioTrack {
            peerConnection?.add(audioTrack, streamIds: ["stream0"])
        }
    }
    
    // 创建 Offer
    func createOffer(completion: @escaping (RTCSessionDescription?) -> Void) {
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: [
                "OfferToReceiveVideo": "true",
                "OfferToReceiveAudio": "true"
            ],
            optionalConstraints: nil
        )
        
        peerConnection?.offer(for: constraints) { [weak self] sdp, error in
            guard let sdp = sdp else {
                completion(nil)
                return
            }
            
            self?.peerConnection?.setLocalDescription(sdp) { error in
                completion(error == nil ? sdp : nil)
            }
        }
    }
    
    // 创建 Answer
    func createAnswer(completion: @escaping (RTCSessionDescription?) -> Void) {
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: [
                "OfferToReceiveVideo": "true",
                "OfferToReceiveAudio": "true"
            ],
            optionalConstraints: nil
        )
        
        peerConnection?.answer(for: constraints) { [weak self] sdp, error in
            guard let sdp = sdp else {
                completion(nil)
                return
            }
            
            self?.peerConnection?.setLocalDescription(sdp) { error in
                completion(error == nil ? sdp : nil)
            }
        }
    }
    
    // 设置远程描述
    func setRemoteDescription(_ sdp: RTCSessionDescription, completion: @escaping () -> Void) {
        peerConnection?.setRemoteDescription(sdp) { error in
            if error == nil {
                completion()
            }
        }
    }
    
    // 添加 ICE Candidate
    func addIceCandidate(_ candidate: RTCIceCandidate) {
        peerConnection?.add(candidate)
    }
    
    // 切换摄像头
    func switchCamera() {
        guard let capturer = videoCapturer else { return }
        
        let devices = RTCCameraVideoCapturer.captureDevices()
        let currentPosition = capturer.captureSession.inputs
            .compactMap { ($0 as? AVCaptureDeviceInput)?.device.position }
            .first ?? .front
        
        let newPosition: AVCaptureDevice.Position = currentPosition == .front ? .back : .front
        
        guard let newCamera = devices.first(where: { $0.position == newPosition }) else { return }
        
        let formats = RTCCameraVideoCapturer.supportedFormats(for: newCamera)
        guard let format = formats.last else { return }
        
        let fps = format.videoSupportedFrameRateRanges.first?.maxFrameRate ?? 30
        
        capturer.startCapture(with: newCamera, format: format, fps: Int(fps))
    }
    
    // 静音
    func setAudioEnabled(_ enabled: Bool) {
        localAudioTrack?.isEnabled = enabled
    }
    
    // 关闭视频
    func setVideoEnabled(_ enabled: Bool) {
        localVideoTrack?.isEnabled = enabled
    }
    
    // 释放资源
    func release() {
        videoCapturer?.stopCapture()
        localVideoTrack = nil
        localAudioTrack = nil
        peerConnection?.close()
        peerConnection = nil
    }
}

// MARK: - RTCPeerConnectionDelegate
extension WebRTCManager: RTCPeerConnectionDelegate {
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
        print("Signaling state: \(stateChanged)")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
        print("Stream added")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
        print("Stream removed")
    }
    
    func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
        print("Negotiation needed")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
        print("ICE connection state: \(newState)")
        delegate?.webRTCManager(self, didChangeIceConnectionState: newState)
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
        print("ICE gathering state: \(newState)")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
        delegate?.webRTCManager(self, didGenerateCandidate: candidate)
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
        print("Candidates removed")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
        print("Data channel opened")
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didAdd rtpReceiver: RTCRtpReceiver, streams mediaStreams: [RTCMediaStream]) {
        if let videoTrack = rtpReceiver.track as? RTCVideoTrack {
            delegate?.webRTCManager(self, didReceiveRemoteVideoTrack: videoTrack)
        }
    }
}

// MARK: - Delegate Protocol
protocol WebRTCManagerDelegate: AnyObject {
    func webRTCManager(_ manager: WebRTCManager, didGenerateCandidate candidate: RTCIceCandidate)
    func webRTCManager(_ manager: WebRTCManager, didChangeIceConnectionState state: RTCIceConnectionState)
    func webRTCManager(_ manager: WebRTCManager, didReceiveRemoteVideoTrack track: RTCVideoTrack)
}

4. 跨平台方案

4.1 Flutter WebRTC

yaml 复制代码
# pubspec.yaml
dependencies:
  flutter_webrtc: ^0.9.0
dart 复制代码
// video_call_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';

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

class _VideoCallPageState extends State<VideoCallPage> {
  RTCPeerConnection? _peerConnection;
  MediaStream? _localStream;
  
  final _localRenderer = RTCVideoRenderer();
  final _remoteRenderer = RTCVideoRenderer();
  
  @override
  void initState() {
    super.initState();
    _initRenderers();
    _initWebRTC();
  }
  
  Future<void> _initRenderers() async {
    await _localRenderer.initialize();
    await _remoteRenderer.initialize();
  }
  
  Future<void> _initWebRTC() async {
    // 获取本地媒体
    _localStream = await navigator.mediaDevices.getUserMedia({
      'audio': true,
      'video': {
        'facingMode': 'user',
        'width': 1280,
        'height': 720,
      }
    });
    
    _localRenderer.srcObject = _localStream;
    
    // 创建 PeerConnection
    final config = {
      'iceServers': [
        {'urls': 'stun:stun.l.google.com:19302'}
      ]
    };
    
    _peerConnection = await createPeerConnection(config);
    
    // 添加本地轨道
    _localStream?.getTracks().forEach((track) {
      _peerConnection?.addTrack(track, _localStream!);
    });
    
    // 监听远程轨道
    _peerConnection?.onTrack = (event) {
      if (event.track.kind == 'video') {
        _remoteRenderer.srcObject = event.streams[0];
        setState(() {});
      }
    };
    
    // 监听 ICE Candidate
    _peerConnection?.onIceCandidate = (candidate) {
      // 发送给对端
      _sendCandidate(candidate);
    };
  }
  
  Future<void> _createOffer() async {
    final offer = await _peerConnection?.createOffer();
    await _peerConnection?.setLocalDescription(offer!);
    // 发送 offer 给对端
    _sendOffer(offer!);
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Video Call')),
      body: Column(
        children: [
          Expanded(
            child: RTCVideoView(_remoteRenderer),
          ),
          Container(
            height: 150,
            child: RTCVideoView(
              _localRenderer,
              mirror: true,
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(
                icon: Icon(Icons.call),
                onPressed: _createOffer,
              ),
              IconButton(
                icon: Icon(Icons.call_end),
                onPressed: _hangUp,
              ),
              IconButton(
                icon: Icon(Icons.switch_camera),
                onPressed: _switchCamera,
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  void _switchCamera() {
    _localStream?.getVideoTracks().forEach((track) {
      Helper.switchCamera(track);
    });
  }
  
  void _hangUp() {
    _localStream?.dispose();
    _peerConnection?.close();
    Navigator.pop(context);
  }
  
  @override
  void dispose() {
    _localRenderer.dispose();
    _remoteRenderer.dispose();
    super.dispose();
  }
}

4.2 React Native WebRTC

javascript 复制代码
// VideoCall.js
import React, { useEffect, useRef, useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
  RTCPeerConnection,
  RTCView,
  mediaDevices,
} from 'react-native-webrtc';

const VideoCall = () => {
  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(null);
  const pc = useRef(null);
  
  useEffect(() => {
    initWebRTC();
    return () => {
      cleanup();
    };
  }, []);
  
  const initWebRTC = async () => {
    // 获取本地媒体
    const stream = await mediaDevices.getUserMedia({
      audio: true,
      video: {
        facingMode: 'user',
        width: 1280,
        height: 720,
      },
    });
    setLocalStream(stream);
    
    // 创建 PeerConnection
    const config = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
      ],
    };
    
    pc.current = new RTCPeerConnection(config);
    
    // 添加本地轨道
    stream.getTracks().forEach(track => {
      pc.current.addTrack(track, stream);
    });
    
    // 监听远程轨道
    pc.current.ontrack = (event) => {
      setRemoteStream(event.streams[0]);
    };
    
    // 监听 ICE Candidate
    pc.current.onicecandidate = (event) => {
      if (event.candidate) {
        sendCandidate(event.candidate);
      }
    };
  };
  
  const createOffer = async () => {
    const offer = await pc.current.createOffer();
    await pc.current.setLocalDescription(offer);
    sendOffer(offer);
  };
  
  const switchCamera = () => {
    localStream?.getVideoTracks().forEach(track => {
      track._switchCamera();
    });
  };
  
  const cleanup = () => {
    localStream?.getTracks().forEach(track => track.stop());
    pc.current?.close();
  };
  
  return (
    <View style={styles.container}>
      {remoteStream && (
        <RTCView
          streamURL={remoteStream.toURL()}
          style={styles.remoteVideo}
        />
      )}
      {localStream && (
        <RTCView
          streamURL={localStream.toURL()}
          style={styles.localVideo}
          mirror={true}
        />
      )}
      <View style={styles.controls}>
        <Button title="Call" onPress={createOffer} />
        <Button title="Switch Camera" onPress={switchCamera} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  remoteVideo: {
    flex: 1,
  },
  localVideo: {
    position: 'absolute',
    bottom: 100,
    right: 20,
    width: 120,
    height: 160,
  },
  controls: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    padding: 20,
  },
});

export default VideoCall;

5. 性能优化

5.1 硬件加速

kotlin 复制代码
// Android: 启用硬件编解码
val encoderFactory = DefaultVideoEncoderFactory(
    eglBase.eglBaseContext,
    true,  // enableIntelVp8Encoder
    true   // enableH264HighProfile
)

// 检查硬件编码器支持
val supportedCodecs = encoderFactory.supportedCodecs
for (codec in supportedCodecs) {
    Log.d("WebRTC", "Supported encoder: ${codec.name}")
}

5.2 分辨率自适应

kotlin 复制代码
// 根据网络状况调整分辨率
class AdaptiveVideoManager {
    private var currentWidth = 1280
    private var currentHeight = 720
    
    fun onNetworkQualityChanged(quality: NetworkQuality) {
        when (quality) {
            NetworkQuality.EXCELLENT -> setResolution(1280, 720)
            NetworkQuality.GOOD -> setResolution(960, 540)
            NetworkQuality.FAIR -> setResolution(640, 360)
            NetworkQuality.POOR -> setResolution(320, 240)
        }
    }
    
    private fun setResolution(width: Int, height: Int) {
        if (width != currentWidth || height != currentHeight) {
            currentWidth = width
            currentHeight = height
            videoCapturer?.changeCaptureFormat(width, height, 30)
        }
    }
}

5.3 电量优化

swift 复制代码
// iOS: 后台音频保持
func configureAudioSession() {
    let session = AVAudioSession.sharedInstance()
    
    do {
        try session.setCategory(
            .playAndRecord,
            mode: .voiceChat,
            options: [.allowBluetooth, .defaultToSpeaker]
        )
        try session.setActive(true)
    } catch {
        print("Audio session error: \(error)")
    }
}

// 进入后台时降低帧率
func applicationDidEnterBackground() {
    videoCapturer?.stopCapture()
    // 或降低帧率
    // videoCapturer?.changeCaptureFormat(640, 480, 15)
}

6. 常见问题

6.1 权限处理

kotlin 复制代码
// Android 权限请求
private fun checkPermissions(): Boolean {
    val permissions = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )
    
    return permissions.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }
}

private fun requestPermissions() {
    ActivityCompat.requestPermissions(
        this,
        arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
        PERMISSION_REQUEST_CODE
    )
}

6.2 网络切换

kotlin 复制代码
// 监听网络变化
class NetworkMonitor(context: Context) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    
    fun startMonitoring(callback: (Boolean) -> Unit) {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                callback(true)
            }
            
            override fun onLost(network: Network) {
                callback(false)
            }
        }
        
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        
        connectivityManager.registerNetworkCallback(request, networkCallback)
    }
}

6.3 设备兼容性

kotlin 复制代码
// 检查设备能力
fun checkDeviceCapabilities(): DeviceCapabilities {
    val cameraEnumerator = Camera2Enumerator(context)
    
    return DeviceCapabilities(
        hasFrontCamera = cameraEnumerator.deviceNames.any { 
            cameraEnumerator.isFrontFacing(it) 
        },
        hasBackCamera = cameraEnumerator.deviceNames.any { 
            cameraEnumerator.isBackFacing(it) 
        },
        supportsHardwareEncoding = checkHardwareEncodingSupport()
    )
}

7. 总结

7.1 核心要点

平台 SDK 特点
Android google-webrtc 原生性能好
iOS GoogleWebRTC 原生性能好
Flutter flutter_webrtc 跨平台
React Native react-native-webrtc 跨平台

7.2 最佳实践

复制代码
移动端 WebRTC 最佳实践:

1. 权限管理
   - 提前请求权限
   - 处理权限拒绝

2. 生命周期
   - 正确释放资源
   - 处理后台切换

3. 性能优化
   - 启用硬件加速
   - 自适应分辨率
   - 电量优化

4. 网络处理
   - 监听网络变化
   - ICE 重启
   - 断线重连

7.3 系列总结

恭喜你完成了 WebRTC 技术专栏的全部学习!

本系列共 25 篇文章,涵盖了:

  • Part 1: WebRTC 基础与快速入门 (5 篇)
  • Part 2: 信令与会话管理 (6 篇)
  • Part 3: 媒体传输深入讲解 (6 篇)
  • Part 4: 音视频编码与媒体处理 (3 篇)
  • Part 5: 工程实践 (5 篇)

希望这个系列能帮助你深入理解 WebRTC 技术,构建出色的实时通信应用!


参考资料

  1. WebRTC Native Code
  2. Flutter WebRTC
  3. React Native WebRTC

相关推荐
Dreamboat¿2 小时前
解析PHP安全漏洞:Phar反序列化、Filter链与文件包含的高级利用与防御
android·网络·php
周杰伦的稻香4 小时前
MySQL中常见的慢查询与优化
android·数据库·mysql
他们叫我技术总监4 小时前
Python 列表、集合、字典核心区别
android·java·python
2401_882351528 小时前
Flutter for OpenHarmony 商城App实战 - 地址编辑实现
android·java·flutter
42nf9 小时前
Android 根据platform.pk8和platform.x509.pem生成.jks文件
android·.pk8和.pem生成.jks
摘星编程10 小时前
React Native for OpenHarmony 实战:DisplayInfo 显示信息详解
android·react native·react.js
_李小白10 小时前
【Android 美颜相机】第六天:GPUImageView解析
android·数码相机
Mr_sun.11 小时前
Day04——权限认证-基础
android·服务器·数据库
北辰当尹13 小时前
第27天 安全开发-PHP应用&TP框架&路由访问&对象操作&内置过滤绕过&核心漏洞
android·安全·php