更新记录

1.0.0(2026-08-07)

eryue-webrtc 音视频通话插件

基于 Google WebRTC 的 uni-app UTS 原生插件,完整实现 Android / iOS 双平台音视频通话,包含原生控制覆盖层 UI(静音、关摄像头、翻转、听筒、挂断、折叠)。


平台支持

平台 最低版本 WebRTC 依赖 视频渲染器
Android 5.0 (API 21) org.webrtc:google-webrtc:1.0.32006 SurfaceViewRenderer
iOS 12.0 GoogleWebRTC ~> 1.3 RTCMTLVideoView (Metal)

为什么 iOS 用 ~> 1.3 ~> 1.1 只有老旧的 RTCEAGLVideoView(OpenGL),~> 1.3 开始提供 RTCMTLVideoView(Metal,性能更好)。


目录结构

eryue-webrtc/
├── package.json                     # 插件标识 + 引擎版本
├── interface.uts                    # 公共接口声明(跨平台契约)
├── README.md                        # 本文档
├── utssdk/
│   ├── app-android/
│   │   ├── AndroidManifest.xml      # 权限声明
│   │   ├── config.json              # Android 依赖配置
│   │   ├── index.uts                # UTS 桥接层(JS ↔ Kotlin)
│   │   ├── EryueWebRTCModule.kt     # 原生模块(方法转发 + 回调包装)
│   │   └── EryueWebRTC.kt           # 核心实现(WebRTC + Canvas UI Overlay)
│   └── app-ios/
│       ├── config.json              # iOS 依赖配置
│       ├── EryueWebRTC.podspec      # CocoaPods 定义
│       ├── index.uts                # UTS 桥接层(JS ↔ Swift)
│       ├── EryueWebRTC.swift        # 核心实现(WebRTC + CALayer UI Overlay)
│       └── eryue-webrtc-mock-test.js # Mock 测试脚本(Factory 单例 + destroy 守卫)

架构设计

分层模型

┌─────────────────────────────────────────────────────┐
│                  JS 业务层(Vue 3)                    │
│   callService.js → webrtcAdapter.js → 业务页面         │
└────────────────────────┬────────────────────────────┘
                         │ 直接 import 或通过 adapter
┌────────────────────────▼────────────────────────────┐
│   interface.uts  ←  跨平台接口契约(所有方法声明)       │
└────────────────────────┬────────────────────────────┘
                         │
        ┌────────────────┴────────────────┐
        ▼                                  ▼
┌──────────────────────┐        ┌──────────────────────┐
│ app-android/index.uts│        │  app-ios/index.uts   │
│ 桥接层 + destroyed 守卫 │        │  桥接层 + destroyed 守卫 │
└──────────┬───────────┘        └──────────┬───────────┘
           │                                │
           ▼                                ▼
┌──────────────────────┐        ┌──────────────────────┐
│  EryueWebRTCModule   │        │    EryueWebRTC        │
│  (Kotlin 模块封装)     │        │    (Swift @objc class) │
└──────────┬───────────┘        └──────────┬───────────┘
           │                                │
           ▼                                ▼
┌─────────────────────────────────────────────────────┐
│        WebRTC 引擎 + 原生 UI 覆盖层(跨平台)             │
│  Factory 单例 | ICE 缓冲 | EglBase/AudioSession        │
│  Canvas/BezierPath 矢量图标 | WindowManager/UIWindow   │
└─────────────────────────────────────────────────────┘

线程模型

平台 WebRTC 引擎 UI 操作 回调派发
Android 单线程 globalExecutorWebRTC-Worker-Global 主线程 mainHandler mainHandler.post 切主线程回调 JS
iOS 串行队列 DispatchQueue("com.eryue.webrtc.queue") DispatchQueue.main mainQueue.async 切主线程回调 JS

关键:所有回调都切到主线程再传给 JS 层,避免跨线程操作 WebView 导致崩溃。

全局单例设计

资源 Android iOS 跨实例共享 destroy 时销毁
PeerConnectionFactory companion object + @Volatile + synchronized static var + NSLock ❌ 保留复用
视频编解码器 在 Factory 创建时一并创建 同上 ❌ 保留复用
音频编解码器 同上 同上 ❌ 保留复用
EglBase companion object 全局单例 — (不需要) ❌ 保留复用
AudioSession 实例级 实例级

为什么 Factory 不随 destroy 销毁? 首次创建 Factory 需要几毫秒(加载编解码器动态库),销毁重建会增加下次通话启动延迟。真正占内存的是 PeerConnection 和轨道,这些在 destroy 时已释放。

destroyed 守卫机制

所有公开方法和回调派发都检查 destroyed 标志,防止 destroy 后继续操作引擎:

// Android
fun createOffer(userId: String) {
    if (destroyed) return
    queueExecutor.execute {
        if (destroyed) return
        pc?.createOffer(...)
    }
}

// iOS
func createOffer(_ userId: String) {
    if destroyed { return }
    queue.async { [weak self] in
        guard let self = self, !self.destroyed else { return }
        self.peerConnection?.offer(for: ...)
    }
}

// Swift 回调里也守卫
private func notifyCallback(_ event: String, data: [String: Any]) {
    if destroyed { return }
    mainQueue.async { [weak self] in
        guard let self = self, !self.destroyed else { return }
        self.callback?(data)
    }
}

Mock 测试验证:先 destroy() 再调用所有方法,20/20 项全部拦截成功。

ICE 候选缓冲机制

setRemoteDescription 必须在 addIceCandidate 之前,但网络到来的时序不确定:

时序 A(正常):  setRemoteDescription → addIceCandidate ✓
时序 B(ICE 先到): addIceCandidate → ❌ 无 RemoteDescription 会崩

解决方案:ICE 候选先到则缓存,remote SDP 设置成功后自动冲刷:

// 两端实现完全一致
fun addIceCandidate(c) {
    if (!remoteSdpSet) { pendingIceCandidates.add(c); return }
    pc.addIceCandidate(c)
}
fun setRemoteDescription(sdp) {
    pc.setRemoteDescription(sdp) {
        remoteSdpSet = true
        flushPendingIceCandidates() // 自动冲刷
    }
}

视频渲染层级

Android 和 iOS 都面临同样的问题:WebRTC 视频渲染层必须在 WebView 之下(SurfaceView / MTKView 是独立的原生 Surface/Window),WebView 要穿透显示。

Android(SurfaceView + WindowManager Overlay)

┌───────────────────────────────────────┐
│  WindowManager Overlay (z-order 9999) │  ← 控制按钮覆盖层
├───────────────────────────────────────┤
│  WebView (背景透明化)                  │  ← uni-app 页面
├───────────────────────────────────────┤
│  SurfaceView localRenderer (小窗)      │  ← setZOrderOnTop(true)
├───────────────────────────────────────┤
│  SurfaceView remoteRenderer (全屏)     │  ← 底层视频
└───────────────────────────────────────┘

透明化处理(让 SurfaceView 穿透 WebView):

  1. window.setBackgroundDrawable(null)
  2. decorView.setBackgroundColor(TRANSPARENT)
  3. contentView.setBackgroundColor(TRANSPARENT)
  4. 递归遍历 WebView 子树设 isOpaque=false + backgroundColor=.clear
  5. window.setFormat(TRANSLUCENT)

iOS(RTCMTLVideoView + UIWindow Overlay)

┌───────────────────────────────────────┐
│  UIWindow controlOverlay (top)        │  ← 原生控制覆盖层
├───────────────────────────────────────┤
│  RCTRootView / WKWebView (透明)        │  ← uni-app 页面
├───────────────────────────────────────┤
│  MTKView localVideoView (小窗)         │  ← 圆角 mask
├───────────────────────────────────────┤
│  MTKView remoteVideoView (全屏)        │  ← 底层视频
└───────────────────────────────────────┘
  VideoContainerView (rootView insert at:0)

透明化处理:递归遍历找 WKWebView / UIWebView,设 isOpaque=false + backgroundColor=.clear


原生控制覆盖层 (Overlay)

通话接通后插件在原生层绘制 UI 覆盖层,脱离 WebView,性能更好

功能按钮

action 图标 通话类型 说明
mute

平台兼容性

uni-app(3.8.1)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-vue插件版本 app-nvue app-nvue插件版本 Android Android插件版本 iOS iOS插件版本 鸿蒙
1.0.0 1.0.0 × × 1.0.0 1.0.0 5.0 1.0.0 12 1.0.0 -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(3.8.1)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - - - - -

eryue-webrtc 音视频通话插件

基于 Google WebRTC 的 uni-app UTS 原生插件,完整实现 Android / iOS 双平台音视频通话,包含原生控制覆盖层 UI(静音、关摄像头、翻转、听筒、挂断、折叠)。


平台支持

平台 最低版本 WebRTC 依赖 视频渲染器
Android 5.0 (API 21) org.webrtc:google-webrtc:1.0.32006 SurfaceViewRenderer
iOS 12.0 GoogleWebRTC ~> 1.3 RTCMTLVideoView (Metal)

为什么 iOS 用 ~> 1.3 ~> 1.1 只有老旧的 RTCEAGLVideoView(OpenGL),~> 1.3 开始提供 RTCMTLVideoView(Metal,性能更好)。


目录结构

eryue-webrtc/
├── package.json                     # 插件标识 + 引擎版本
├── interface.uts                    # 公共接口声明(跨平台契约)
├── README.md                        # 本文档
├── utssdk/
│   ├── app-android/
│   │   ├── AndroidManifest.xml      # 权限声明
│   │   ├── config.json              # Android 依赖配置
│   │   ├── index.uts                # UTS 桥接层(JS ↔ Kotlin)
│   │   ├── EryueWebRTCModule.kt     # 原生模块(方法转发 + 回调包装)
│   │   └── EryueWebRTC.kt           # 核心实现(WebRTC + Canvas UI Overlay)
│   └── app-ios/
│       ├── config.json              # iOS 依赖配置
│       ├── EryueWebRTC.podspec      # CocoaPods 定义
│       ├── index.uts                # UTS 桥接层(JS ↔ Swift)
│       ├── EryueWebRTC.swift        # 核心实现(WebRTC + CALayer UI Overlay)
│       └── eryue-webrtc-mock-test.js # Mock 测试脚本(Factory 单例 + destroy 守卫)

架构设计

分层模型

┌─────────────────────────────────────────────────────┐
│                  JS 业务层(Vue 3)                    │
│   callService.js → webrtcAdapter.js → 业务页面         │
└────────────────────────┬────────────────────────────┘
                         │ 直接 import 或通过 adapter
┌────────────────────────▼────────────────────────────┐
│   interface.uts  ←  跨平台接口契约(所有方法声明)       │
└────────────────────────┬────────────────────────────┘
                         │
        ┌────────────────┴────────────────┐
        ▼                                  ▼
┌──────────────────────┐        ┌──────────────────────┐
│ app-android/index.uts│        │  app-ios/index.uts   │
│ 桥接层 + destroyed 守卫 │        │  桥接层 + destroyed 守卫 │
└──────────┬───────────┘        └──────────┬───────────┘
           │                                │
           ▼                                ▼
┌──────────────────────┐        ┌──────────────────────┐
│  EryueWebRTCModule   │        │    EryueWebRTC        │
│  (Kotlin 模块封装)     │        │    (Swift @objc class) │
└──────────┬───────────┘        └──────────┬───────────┘
           │                                │
           ▼                                ▼
┌─────────────────────────────────────────────────────┐
│        WebRTC 引擎 + 原生 UI 覆盖层(跨平台)             │
│  Factory 单例 | ICE 缓冲 | EglBase/AudioSession        │
│  Canvas/BezierPath 矢量图标 | WindowManager/UIWindow   │
└─────────────────────────────────────────────────────┘

线程模型

平台 WebRTC 引擎 UI 操作 回调派发
Android 单线程 globalExecutorWebRTC-Worker-Global 主线程 mainHandler mainHandler.post 切主线程回调 JS
iOS 串行队列 DispatchQueue("com.eryue.webrtc.queue") DispatchQueue.main mainQueue.async 切主线程回调 JS

关键:所有回调都切到主线程再传给 JS 层,避免跨线程操作 WebView 导致崩溃。

全局单例设计

资源 Android iOS 跨实例共享 destroy 时销毁
PeerConnectionFactory companion object + @Volatile + synchronized static var + NSLock ❌ 保留复用
视频编解码器 在 Factory 创建时一并创建 同上 ❌ 保留复用
音频编解码器 同上 同上 ❌ 保留复用
EglBase companion object 全局单例 — (不需要) ❌ 保留复用
AudioSession 实例级 实例级

为什么 Factory 不随 destroy 销毁? 首次创建 Factory 需要几毫秒(加载编解码器动态库),销毁重建会增加下次通话启动延迟。真正占内存的是 PeerConnection 和轨道,这些在 destroy 时已释放。

destroyed 守卫机制

所有公开方法和回调派发都检查 destroyed 标志,防止 destroy 后继续操作引擎:

// Android
fun createOffer(userId: String) {
    if (destroyed) return
    queueExecutor.execute {
        if (destroyed) return
        pc?.createOffer(...)
    }
}

// iOS
func createOffer(_ userId: String) {
    if destroyed { return }
    queue.async { [weak self] in
        guard let self = self, !self.destroyed else { return }
        self.peerConnection?.offer(for: ...)
    }
}

// Swift 回调里也守卫
private func notifyCallback(_ event: String, data: [String: Any]) {
    if destroyed { return }
    mainQueue.async { [weak self] in
        guard let self = self, !self.destroyed else { return }
        self.callback?(data)
    }
}

Mock 测试验证:先 destroy() 再调用所有方法,20/20 项全部拦截成功。

ICE 候选缓冲机制

setRemoteDescription 必须在 addIceCandidate 之前,但网络到来的时序不确定:

时序 A(正常):  setRemoteDescription → addIceCandidate ✓
时序 B(ICE 先到): addIceCandidate → ❌ 无 RemoteDescription 会崩

解决方案:ICE 候选先到则缓存,remote SDP 设置成功后自动冲刷:

// 两端实现完全一致
fun addIceCandidate(c) {
    if (!remoteSdpSet) { pendingIceCandidates.add(c); return }
    pc.addIceCandidate(c)
}
fun setRemoteDescription(sdp) {
    pc.setRemoteDescription(sdp) {
        remoteSdpSet = true
        flushPendingIceCandidates() // 自动冲刷
    }
}

视频渲染层级

Android 和 iOS 都面临同样的问题:WebRTC 视频渲染层必须在 WebView 之下(SurfaceView / MTKView 是独立的原生 Surface/Window),WebView 要穿透显示。

Android(SurfaceView + WindowManager Overlay)

┌───────────────────────────────────────┐
│  WindowManager Overlay (z-order 9999) │  ← 控制按钮覆盖层
├───────────────────────────────────────┤
│  WebView (背景透明化)                  │  ← uni-app 页面
├───────────────────────────────────────┤
│  SurfaceView localRenderer (小窗)      │  ← setZOrderOnTop(true)
├───────────────────────────────────────┤
│  SurfaceView remoteRenderer (全屏)     │  ← 底层视频
└───────────────────────────────────────┘

透明化处理(让 SurfaceView 穿透 WebView):

  1. window.setBackgroundDrawable(null)
  2. decorView.setBackgroundColor(TRANSPARENT)
  3. contentView.setBackgroundColor(TRANSPARENT)
  4. 递归遍历 WebView 子树设 isOpaque=false + backgroundColor=.clear
  5. window.setFormat(TRANSLUCENT)

iOS(RTCMTLVideoView + UIWindow Overlay)

┌───────────────────────────────────────┐
│  UIWindow controlOverlay (top)        │  ← 原生控制覆盖层
├───────────────────────────────────────┤
│  RCTRootView / WKWebView (透明)        │  ← uni-app 页面
├───────────────────────────────────────┤
│  MTKView localVideoView (小窗)         │  ← 圆角 mask
├───────────────────────────────────────┤
│  MTKView remoteVideoView (全屏)        │  ← 底层视频
└───────────────────────────────────────┘
  VideoContainerView (rootView insert at:0)

透明化处理:递归遍历找 WKWebView / UIWebView,设 isOpaque=false + backgroundColor=.clear


原生控制覆盖层 (Overlay)

通话接通后插件在原生层绘制 UI 覆盖层,脱离 WebView,性能更好

功能按钮

action 图标 通话类型 说明
mute 🎤 麦克风 audio + video 静音切换
video 📷 摄像头 仅 video 开关摄像头
switch 🔄 翻转 仅 video 前后摄像头切换
speaker 🔊 听筒 audio + video 扬声器/听筒切换
hangup ☎️ 挂断 audio + video 不切换激活态,直接挂断
minimize ⬅️ 左箭头 audio + video 左上角折叠按钮,不切换激活态

按钮激活态切换(只有功能按钮生效)

  • 默认态:半透明黑背景 rgba(0,0,0,0.35) + 白色图标
  • 激活态:白色背景 rgba(255,255,255,0.95) + 深色图标 #222222

图标绘制

全部使用矢量路径纯代码绘制,无图片资源依赖

平台 绘制方式
Android Canvas.drawPath() + Path (SVG path 转 Android Path)
iOS CAShapeLayer + UIBezierPath

布局结构

Overlay Window / WindowManager LayoutParams
├── Top Bar (居中)
│   ├── statusLabel ("通话中")
│   └── timerLabel ("00:12", 绿色 #2EE6A6 Bold)
├── CollapseBtn (左上角, 独立于 toggleControlButtons)
├── GradientMask (底部渐变: 透明 → rgba(0,0,0,0.75), 420rpx)
└── ButtonsContainer (底部, 等宽分布)
    ├── Row 1: 功能按钮 (audio=2个 / video=4个)
    └── Row 2: Hangup (160rpx, 渐变红色)

交互

  • 点击空白区域 → toggleControlButtons() 切换按钮显隐
  • 按钮点击 → 缩放 0.92 弹性反馈 → 发 onControlButton(action)
  • 折叠按钮 → 直接发 onControlButton(action="minimize")

原生回调事件

所有事件通过 notifyCallback(event, { userId, ...payload }) 派发。

初始化

event 字段 说明 超时保护
onInit success, error? 引擎初始化结果 10 秒 watchdog
onEglBaseReady success, error? EglBase 就绪(iOS 占位立即回调)

轨道 & 渲染

event 字段 说明
onAudioTrackReady trackId, success 音频轨道就绪
onVideoTrackReady trackId, success 视频轨道就绪
onVideoRenderersReady success, error? 视频渲染器就绪

PeerConnection

event 字段 说明
onPeerConnectionCreated userId, success PC 创建结果
onCreateOffer / onCreateAnswer userId, success, type, sdp, error? Offer/Answer 创建 + 设置本地描述完成
onSetLocalDescription userId, success 设置本地描述结果
onSetRemoteDescription userId, success, error? 设置远程描述结果(5 秒超时
onIceCandidate userId, sdpMid, sdpMLineIndex, candidate 本地 ICE 候选(需通过信令发送)
onAddIceCandidate userId, success, buffered? 添加远端 ICE 候选结果

连接状态

event 字段 说明
onSignalingChange stateName stable / haveLocalOffer / haveRemoteOffer / closed
onIceConnectionChange stateName new / checking / connected / disconnected / failed / closed
onIceConnected userId ICE 已连通
onIceDisconnected userId ICE 短暂断线(原生自动重连)
onIceFailed userId ICE 失败(触发 restartIce 尝试)
onPeerClosed reason PC 关闭(ice_failed 5s 未恢复 / closed

媒体

event 字段 说明
onAddStream streamId, hasAudio, hasVideo 收到远端流
onRemoteVideo userId, success 远端视频轨道到达
onCameraSwitch success, isFront, error? 切换摄像头结果

UI

event 字段 说明
onControlButton action 原生按钮点击(mute/video/switch/speaker/hangup/minimize

Android 权限

插件通过 AndroidManifest.xml 自动声明:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

运行时权限:Android 6.0+ 需在 JS 层通过 plus.android.requestPermissions 动态申请摄像头和麦克风权限。

iOS 权限

iOS 权限由系统弹窗自动处理,无需手动申请。需在 Info.plist 中声明使用原因:

Key 说明
NSCameraUsageDescription 需要访问摄像头进行视频通话
NSMicrophoneUsageDescription 需要访问麦克风进行语音通话

完整调用流程

主叫方

JS 层                            原生层
──────                           ──────
webrtc = new EryueWebRTC()
webrtc.onCallback(cb)
webrtc.init()
  → onInit(success) ──────────→ initCompleted = true
webrtc.initAudioTrack('local_audio')
  → onAudioTrackReady ────────→ localAudioTrack ready
webrtc.initVideoTrack('local_video')
  → onVideoTrackReady ────────→ 摄像头启动 + localVideoTrack ready
webrtc.setupVideoRenderers()
  → onVideoRenderersReady ────→ remoteView + localView created
webrtc.showControlOverlay('video')
  → 原生 UI 覆盖层绘制完成

webrtc.createPeerConnection(userId, iceServers)
  → onPeerConnectionCreated
webrtc.createOffer(userId)
  → onCreateOffer(sdp) ────────→ setLocalDescription 完成
  → socket.emit('call_offer', sdp) → 对端

        ... 等待对端 answer ...

webrtc.setRemoteDescription(userId, 'answer', answerSdp)
  → onSetRemoteDescription(success)
  → flushPendingIceCandidates()

        ... ICE 连通 ...

  → onIceConnected(userId)
  → webrtc.showVideoView('remote') + showVideoView('local')
  → webrtc.showTimerText()
  → webrtc.updateTimerText('00:01')  ← JS 计时器每秒同步

被叫方

JS 层                            原生层
──────                           ──────
(初始化同上...)

socket.on('call_offer', sdp)
  → webrtc.setRemoteDescription(userId, 'offer', sdp)
    → onSetRemoteDescription(success)
    → flushPendingIceCandidates()
  → webrtc.createAnswer(userId)
    → onCreateAnswer(sdp) ────→ socket.emit('call_answer', sdp) → 对端

        ... ICE 连通 ...

  → onIceConnected → webrtc.showVideoView('remote')
  → webrtc.showControlOverlay('video')

挂断

webrtc.destroy()
  → destroyed = true (守卫所有后续方法)
  → Android:
      mainHandler.post { hideControlOverlay() }
      queueExecutor.execute {
        pc.close() → null
        videoCapturer.stopCapture()
        localTrack.dispose()
        // globalFactory / eglBase 保留复用
      }
  → iOS:
      DispatchQueue.main.async { hideControlOverlay(); videoCapturer.stopCapture() }
      queue.async {
        pc.close() → nil
        localTrack.dispose()
        // globalFactory 保留复用
      }

UTS 插件注册机制

UTS 插件 不需要manifest.jsonapp-plus.modules 里配置。编译器通过 package.json + 目录结构自动识别:

package.json 里:
  "dcloudext": { "type": "uts" }
  "uni_modules": { "platforms": { "client": { "App": { "app-android": {}, "app-ios": {} } } } }

目录必须为:
  uni_modules/<plugin_id>/
    interface.uts            ← 跨平台公共接口
    utssdk/app-android/      ← Android 原生实现
    utssdk/app-ios/          ← iOS 原生实现

自定义基座制作

原生代码改动后,必须重新制作自定义基座才能生效。

步骤

  1. HBuilderX → 运行 → 运行到手机或模拟器 → 制作自定义调试基座
  2. 版本号递增(如 1.2.8 → 1.2.9),否则 HBuilderX 会跳过编译
  3. 等待打包完成并安装到手机
  4. 重新运行项目

改动类型影响

改动类型 是否需要重做基座
.kt / .swift ✅ 必须
.uts(桥接层) ✅ 必须
config.json(依赖版本) ✅ 必须(且会触发 CocoaPods / Gradle 依赖下载)
interface.uts ✅ 必须(类型契约变更)
.js / .vue ❌ 自动同步,无需

常见问题

现象 原因 解决
"uts 插件文件未发生变化,跳过编译" HBuilderX 缓存了编译结果 清理 unpackage/resources/uni_modules/eryue-webrtc/ 或改版本号
iOS 编译报 Undefined symbols for RTCMTLVideoView CocoaPods 旧版本只到 1.1 pod update 或升级 EryueWebRTC.podspec 依赖到 ~> 1.3
Android 编译报 @Volatile 找不到 Kotlin 版本低于 1.4 升级 Android Studio + Kotlin 插件
iOS pod install 失败 网络访问 cdn.cocoaPods.org 换国内镜像源(见下方)

iOS CocoaPods 国内镜像

# Podfile 顶部添加
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
source 'https://cdn.cocoapods.org/'

# 终端执行
pod repo update eryue-webrtc
pod install --repo-update

Mock 测试

插件自带 JS 层 Mock 测试,验证 Factory 单例和 destroyed 守卫逻辑:

cd utssdk/app-ios/
node eryue-webrtc-mock-test.js

当前测试项:20/20 通过

测试项 验证内容
Factory 全局单例 多次 ensure 返回同一实例
init() 幂等 已初始化后直接回调 success
destroy 后静默返回 所有方法被守卫拦截
destroy 幂等 重复调用不崩溃
新实例复用 Factory 全局 Factory 不随实例销毁
ICE 候选缓冲 remote SDP 未设时缓存,设置后冲刷
异步回调守卫 setTimeout 回调被 destroyed 拦截
完整生命周期 init → track → pc → sdp → ice → destroy 全流程

Mock 测试代码与 Swift 原生实现一一对应,可作为逻辑正确性的快速验证。


已知限制

限制 说明
视频通话必须手动请求摄像头权限 Android 6.0+ + iOS
无屏幕共享 未实现 ScreenCapturer
无 DataChannel 未实现 RTCDataChannel
全局 Factory 无法真正销毁 iOS / Android 都保留复用(设计决策)
iOS 旧系统兼容 iOS 11 及以下未测试(插件声明 minVersion=12)
无前台服务 Android 原生未启动 FOREGROUND_SERVICE,锁屏通话可能受限

版本历史

版本 平台 变更
1.0.0 Android + iOS 初始版本:完整音视频通话 + 原生控制覆盖层

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android(声明在 AndroidManifest.xml) 权限 类型 必需 用途说明 android.permission.INTERNET 网络 ✅ WebRTC 媒体流通过 UDP/TCP 传输音视频数据 android.permission.ACCESS_NETWORK_STATE 网络状态 ✅ 检测网络切换(WiFi ↔ 蜂窝),自动调整 ICE 候选 android.permission.ACCESS_WIFI_STATE WiFi 状态 ✅ 获取 WiFi IP 地址用于 ICE 候选收集 android.permission.RECORD_AUDIO 麦克风 ✅ 采集本地音频轨道(静音模式下仍然声明) android.permission.CAMERA 摄像头 ✅(视频通话时) 采集本地视频轨道;纯音频通话可在调用前不申请 android.permission.MODIFY_AUDIO_SETTINGS 音频设置 ✅ 切换听筒/扬声器、管理音频焦点 android.permission.FOREGROUND_SERVICE 前台服务 ⚠️ 可选 Android 8.0+ 保持后台通话不被杀 不申请的敏感权限(已验证源码无调用):位置、通讯录、存储、电话状态、设备 ID、通讯录、蓝牙、相册。 iOS(需宿主 App 的 Info.plist 声明) iOS 权限不写在插件里,必须由集成方在宿主 App 的 Info.plist 添加以下 Key: Info.plist Key 必需 触发场景 NSMicrophoneUsageDescription ✅ 用户发起/接听音频通话时触发系统弹窗 NSCameraUsageDescription ✅(视频通话时) 用户发起/接听视频通话时触发系统弹窗 NSAppTransportSecurity → NSAllowsArbitraryLoads ⚠️ 仅 STUN/TURN 走 HTTP 时 若 ICE 服务器使用 stun:(非加密)协议需临时允许明文;生产环境建议用 stuns: 不申请的权限:定位、通讯录、相册、蓝牙、健康数据、广告标识符。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

采集的数据 数据项 是否采集 说明 麦克风音频流 ✅ 本地采集,直接通过 P2P 发送给通话对方,不经插件开发者服务器 摄像头视频流 ✅ 本地采集,直接通过 P2P 发送给通话对方,不经插件开发者服务器 设备 IP 地址 ✅(WebRTC 内部) 用于 ICE 候选交换,建立 P2P 直连 UserAgent 字符串 ✅(WebRTC 内部) GoogleWebRTC 引擎版本标识,跟随 ICE 请求发送 用户 ID(业务传入) ✅ 宿主 App 调用 createPeerConnection(userId, ...) 时传入 设备位置信息 ❌ 不调用 CLLocationManager / LocationManager 通讯录 / 联系人 ❌ 源码零调用 IMEI / Android ID / ADID ❌ 源码零调用 TelephonyManager / AdvertisingIdClient 相册 / 文件 ❌ 无文件读写代码 应用使用行为 / 埋点 ❌ 插件本身不含任何分析/埋点 SDK 发送的服务器地址 插件本身不硬编码任何服务器地址。所有网络端点由宿主 App 在调用时动态传入: 端点类型 地址来源 协议 用途 STUN 服务器 createPeerConnection(iceServers) 参数 stun: / stuns: ICE NAT 穿透,获取公网 IP TURN 服务器 createPeerConnection(iceServers) 参数 turn: / turns: P2P 失败时的中继转发 信令服务器 宿主 App 的 JavaScript 层 WebSocket / HTTP 交换 SDP Offer/Answer 和 ICE 候选,插件不参与 GoogleWebRTC 内部 无 — 库本身不回传任何统计数据 数据用途说明 数据 用途 是否上传云端 音视频流 与通话对方建立 P2P 实时音视频会话 ❌ 直连,不经过插件开发者服务器 ICE 候选 建立 P2P 连接必要的网络地址协商 ⚠️ 发送给宿主指定的 STUN/TURN 服务器 + 通话对端 userId 业务标识,用于匹配通话双方 ⚠️ 发送给宿主的信令服务器(由宿主控制) 插件开发者不持有、不存储、不中转任何用户音视频数据。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。