更新记录
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 | 单线程 globalExecutor(WebRTC-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):
window.setBackgroundDrawable(null)decorView.setBackgroundColor(TRANSPARENT)contentView.setBackgroundColor(TRANSPARENT)- 递归遍历 WebView 子树设
isOpaque=false+backgroundColor=.clear 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 | 单线程 globalExecutor(WebRTC-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):
window.setBackgroundDrawable(null)decorView.setBackgroundColor(TRANSPARENT)contentView.setBackgroundColor(TRANSPARENT)- 递归遍历 WebView 子树设
isOpaque=false+backgroundColor=.clear 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.json 的 app-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 原生实现
自定义基座制作
原生代码改动后,必须重新制作自定义基座才能生效。
步骤
- HBuilderX → 运行 → 运行到手机或模拟器 → 制作自定义调试基座
- 版本号递增(如 1.2.8 → 1.2.9),否则 HBuilderX 会跳过编译
- 等待打包完成并安装到手机
- 重新运行项目
改动类型影响
| 改动类型 | 是否需要重做基座 |
|---|---|
.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 | 初始版本:完整音视频通话 + 原生控制覆盖层 |

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 12
赞赏 0
下载 12490860
赞赏 1939
赞赏
京公网安备:11010802035340号