09-CSDN-01-云对讲模块实战总结
React Native + WebRTC 实战:云对讲模块设计
技术栈:React Native 0.75 + WebRTC + 海康 SDK + 阿里云 Push
适用:可视对讲、视频通话、远程开门
一、功能是什么
业主在小区的楼下门口机或家里室内机上按呼叫键 → 云端平台转发 → 业主手机 App 收到来电、响铃 → 接听 → 视频通话 → 远程开门。
核心场景:
| 场景 | 说明 |
|---|---|
| 来电通知 | 门口机呼叫 → App 收到来电(实时) |
| 视频通话 | 接听后可看门口画面、可对讲 |
| 远程开门 | 通话中点"开锁"按钮,门口机开门 |
| 多人同步 | A 用户和 B 用户都收到呼叫,一方接听后另一方自动退出 |
关键要求:
- 实时性极强(30s 超时自动挂断)
- 杀进程也能收到来电(业主在抖音也能收到门口呼叫)
- 状态一致性(被他人接听、对方挂断、手势返回都要正确退出)
二、用到的技术栈
| 技术 | 角色 | 为什么用它 |
|---|---|---|
| 阿里云 Push + APNs/厂商通道 | 系统级推送,杀进程兜底 | 唯一能在 App 进程被杀时送达通知的通道 |
| WebSocket #1(自家后端) | 云对讲来电通知通道 | App 在前台时实时推送 {type:1, 3} 来电事件URL: wss://CLOUD_IP/newhomewss/ws/app/{token} |
| WebSocket #2(思悦云平台) | 思悦 WebRTC 的信令通道 | 交换 SDP offer/answer + ICE 候选 URL: wss://webrtc.insprid.cn/new_wss?token=xxx&type=access |
| 海康萤石 SDK(CloudOpenSDK) | 海康对讲的音视频 + 控制 | 闭源私有协议,老小区硬件兼容 |
| WebRTC + ICE/STUN/TURN | 思悦对讲的音视频传输 | P2P 传输,延迟 50-200ms,自带编解码 + NAT 穿透 |
| React Native 原生模块桥接 | 把海康 SDK 暴露给 JS | 海康 SDK 是 OC/Java 代码,JS 无法直接调用,必须桥接 |
| 双重锁(global.callStatus + global._isNavigatingToCall) | 防重复跳转 | 阿里云 Push 和 WebSocket #1 都可能触发跳转 |
| setInterval 轮询(2 秒) | 检测通话状态变化 | 海康 SDK 是同步拉取接口,没事件订阅 |
💡 重要:项目里的 WebSocket 是两个不同的连接:
- WebSocket #1 = 通知通道(自家后端,海康+思悦共用,因为它只发"有呼叫"事件)
- WebSocket #2 = 思悦 WebRTC 信令通道(思悦云平台,只思悦在用)
三、核心架构:两个云对讲供应商共存
项目里同时对接了两家云对讲供应商(海康萤石 + 思悦),不同小区用的硬件不同,App 层要兼容两套。
3.0 两个供应商对比
| 维度 | 海康萤石 | 思悦 |
|---|---|---|
| 协议 | 海康私有协议(闭源 SDK) | WebRTC(开源标准) |
| 音视频传输 | 通过 CloudOpenSDK 内部实现 | WebRTC P2P(client ↔ client) |
| 信令通道 | 海康 SDK 内部处理(不需要 JS 关心) | 额外开一个 WebSocket(wss://webrtc.insprid.cn/new_wss) |
| 通知通道 | ⚠️ 同一个 WebSocket(自家后端) | ⚠️ 同一个 WebSocket(自家后端) |
| 鉴权 | accessToken(萤石 SDK) | token(思悦云) |
| 设备标识 | doorDeviceSerial + deviceCode | pid + room |
| 适用小区 | 老小区(已有海康设备) | 新小区(思悦设备) |
3.0.1 共用 WebSocket #1(关键设计)
┌─────────────────────────┐
│ 自家后端 WebSocket #1 │
│ wss://CLOUD_IP/ │
│ newhomewss/ws/app/ │
└─────────┬───────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 海康小区 │ │ 思悦小区 │
│ 收到 type=1 │ │ 收到 type=3 │
│ → 跳 HikPlayer │ │ → 跳 SiruisCall │
└──────────────────┘ └──────────────────┘
为什么两个供应商共用一个 WebSocket?
因为 WebSocket 通知通道只关心"有呼叫"事件,不关心具体是哪个供应商。
前端收到 {type:1} 或 {type:3} 后,根据 videoCallPlatform 字段路由到对应页面。
// src/pages/Home/index.js resolveCallNavigation
const platform = callingDevice.videoCallPlatform;
switch (platform) {
case 3: // 思悦
return {
screenName: 'SiruisCloudCall', // WebRTC 通话页
params: {pid, token, room, ...}, // 思悦专属参数
};
default: // 海康(默认)
return {
screenName: 'HikCloudTalkPlayer', // 海康通话页
params: {device, user}, // 海康专属参数
};
}
3.0.2 JS 层面对的差异
虽然后端选了 Sea Talk 或海康,对 JS 代码来说只是路由到不同的页面:
// src/pages/Home/index.js
{modalProps?.type == 'AC' && <AC {...modalProps} />}
{modalProps?.type == 'Curtain' && <Curtain {...modalProps} />}
云对讲的路由也是一样的模式:
// 同一个 WebViewModal,根据 type 渲染不同组件
<DeviceModal>
{modalProps?.type === 'HikCloudTalkPlayer' && <HikCloudTalkPlayer {...modalProps} />}
{modalProps?.type === 'SiruisCloudCall' && <SiruisCloudCall {...modalProps} />}
</DeviceModal>
四、核心技术原理(面试必备)
4.1 WebRTC 是什么
WebRTC = Web Real-Time Communication
- W3C 标准,浏览器/客户端实时通信 API
- 三大核心能力:
- MediaStream:获取摄像头/麦克风
- RTCPeerConnection:建立 P2P 连接(处理 NAT 穿透)
- RTCDataChannel:P2P 数据传输
WebRTC 不带信令:SDP/ICE 候选交换需要应用层自己实现(用 WebSocket)。
4.2 WebRTC vs WebSocket 对比
| 维度 | WebRTC | WebSocket |
|---|---|---|
| 模型 | P2P(点对点) | C/S(客户端-服务器) |
| 传输内容 | 音视频流 + 数据 | 文本/任意数据 |
| 编解码 | ✅ Opus/H.264 自带 | ❌ 不带 |
| NAT 穿透 | ✅ 内置 STUN/TURN | ❌ 没有 |
| 延迟 | 50-200ms | 200ms+(经服务器) |
| 服务器压力 | 低(不转发音视频) | 高(所有数据过服务器) |
| 适用 | 音视频通话 | 聊天/股票/推送 |
4.3 双通道推送原理
┌─────────────────────────────────────────────────────────────────┐
│ 双通道协作:阿里云 Push + WebSocket │
└─────────────────────────────────────────────────────────────────┘
门口机呼叫
↓
云端平台
↓
┌──────┴──────┐
↓ ↓
阿里云 Push 公司后端
(系统级通知) (WebSocket 推送)
↓ ↓
① 杀进程也能收 ② 仅 App 进程存活时收
③ 触发通知横幅 ③ 触发 onMessage 回调
↓ ↓
└──────┬──────┘
↓
App 收到 + 跳来电页
关键设计:通知只是"敲门",把 App 拉回前台。真正的跳转靠 App 启动后 WebSocket 重连 + 主动查询。
4.4 为什么 2 秒轮询
海康 SDK 的 getDeviceStatus 是同步拉取接口,没有持久化的"状态变化"事件订阅。
所以用 setInterval 自己实现轮询:
- 每 2 秒调一次
getRecordTalkDevicesStatus(id) - 检查
connected(是否被他人接听)、endTime(是否已结束) - 检测到变化 → 自动退出
为什么不担心性能问题? 2 秒间隔足够用户感知不到,且服务端只是简单查询。
4.5 通话状态机
通话有 3 个状态、4 个转换路径,用状态机图一图讲清楚:
五、实现架构
六、核心实现:来电入口
6.1 checkCallStatus 核心代码
// src/pages/Home/index.js
const checkCallStatus = useCallback(async (userInfo) => {
// ① 前置校验
if (!userInfo?.phoneNumber) return;
if (global.callStatus === true) return; // 锁1:已在通话页
if (global._isNavigatingToCall) return; // 锁2:正在跳转
try {
// ② 拉设备状态
const res = await getUserTalkDevicesStatus(userInfo.phoneNumber);
const data = JSON.parse(res);
// ③ 找正在响铃的设备
const callingDevice = data.find(item => item.callState === 1);
if (!callingDevice) {
if (IS_ANDROID) HikVideoTalkActivityModule.cancelAllNotification();
return;
}
// ④ 根据平台选目标
const navigateInfo = resolveCallNavigation(callingDevice, userInfo);
if (!navigateInfo) return;
// ⑤ 二次校验(防重复)
if (global.callStatus || global._isNavigatingToCall) return;
// ⑥ 上锁 + 跳转
global._isNavigatingToCall = true;
setTimeout(() => {
try {
navigation.navigate(navigateInfo.screenName, navigateInfo.params);
} finally {
global._isNavigatingToCall = false;
}
}, 100); // ← 100ms 让 navigator 准备
} catch (e) {
console.error('checkCallStatus failed:', e);
}
}, [navigation]);
6.2 三个核心设计
设计 1:双重锁防重复跳转
global.callStatus = true; // 通话页挂载时设置
global._isNavigatingToCall = true; // 跳转前设置
// 100ms 后跳转
setTimeout(() => {
navigation.navigate(...);
global._isNavigatingToCall = false;
}, 100);
为什么 100ms? 给 navigator 准备时间,给锁一个"观察窗口"防止并发。
设计 2:2 秒轮询检测通话状态
// HikCloudTalkPlayer.js componentDidMount
this.currCloudTalkStatus = setInterval(async () => {
const data = JSON.parse(await getRecordTalkDevicesStatus(device.id));
// 情况 1:被别人接听
if (data.connected === 1 && data.employeeNo !== user.employeeNo) {
Toast.show('该通话已被他人接听');
this.leaveTalkPage();
}
// 情况 2:已结束
else if (data.endTime) {
Toast.show('该通话已结束');
this.leaveTalkPage();
}
}, 2000);
设计 3:AppState 监听防后台占用
// HikCloudTalkPlayer.js _handleAppStateChange
_handleAppStateChange = async (nextAppState) => {
// 进后台 → 主动挂断
if (curAppState === 'active' && nextAppState.match(/inactive|background/)) {
const status = await this.getCloudTalkDeviceStatus();
if ((IS_IOS && status === 2) || (IS_ANDROID && status === 'ring')) {
this.rejectCalling(); // 响铃态 → 拒接
} else if ((IS_IOS && status === 3) || (IS_ANDROID && status === 'onCall')) {
this.hangUpCalling(); // 通话态 → 挂断
}
}
};
这段代码做了什么?
| 状态 | iOS | Android | 检测到进后台时 | 作用 |
|---|---|---|---|---|
| 响铃中(未接) | 2 | ‘ring’ | rejectCalling() | 主动拒接,避免响铃挂后台 |
| 通话中 | 3 | ‘onCall’ | hangUpCalling() | 主动挂断,释放摄像头/麦克风 |
为什么要做?
iOS 在后台长时间占用摄像头,系统会强杀 App 释放硬件。这会导致:
- 通话突然中断,对方黑屏
- 门口机那边还不知道,30s 后才超时挂断
- 业主体验极差
所以主动挂断 + 释放资源,让门口机立即收到 hangUp 信令。
iOS 还是 Android 都不行?
- iOS:摄像头后台占用 → 系统强杀
- Android:厂商杀进程很激进(华为 EMUI、小米 MIUI 等 5 分钟杀进程)
项目没做微信那种"后台悬浮通话",原因:
- iOS 需要
UIBackgroundModes: voip+ PushKit 注册 VoIP Push(项目里只有remote-notification) - Android 需要前台服务
FOREGROUND_SERVICE权限 + 启动前台服务 - 业务场景不需要:云对讲是"看一眼就开门",默认 30s 超时
- 改造工作量大,当前主动挂断是保守但可靠的方案
如果想改造支持后台通话(工作量 1-2 周):
| 平台 | 改造 |
|---|---|
| iOS | 1. Info.plist 加 UIBackgroundModes: voip, audio2. AppDelegate.m 注册 PKPushRegistry(PushKit)3. 申请 VoIP Push 证书 4. 接听页加 PiP(画中画)支持 |
| Android | 1. AndroidManifest.xml 加 FOREGROUND_SERVICE 权限2. 写一个 ForegroundService 服务 3. 接听时 startForegroundService 带通知4. 接听页加 PiP 支持 |
七、完整调用流程
7.1 来电通知流程(双通道并行)
门口机按呼叫键
↓
云端平台
├─→ 阿里云 Push(系统通知栏) ← 即使杀进程也送达
└─→ WebSocket 推 {type:1, ...} ← App 在前台就实时
↓
App 收到:
├─ WebSocket onMessage → checkCallStatus → 跳转来电页
└─ 通知点击 → 冷启动 App → WebSocket 重连 → 主动查询 → 跳转
7.2 通话中状态变化检测(2 秒轮询)
通话页挂载
↓
setInterval(2000ms) → getRecordTalkDevicesStatus
↓
├─ connected=1 且 employeeNo != 当前用户 → 被他人接听 → 自动退出
├─ endTime 存在 → 通话已结束 → 自动退出
└─ 正常 → 继续轮询
7.3 接听/拒接/挂断/开锁
用户点"接听"按钮
↓
HikVideoTalkActivityModule.answer(serial, verifyCode, blockNo, floor, unitNo, roomNo)
↓
海康 SDK answer() + startVisualTalk()
↓
音视频流开始(RTCView 渲染)
↓
用户点"开锁"
↓
Api.doorOpen({deviceSerial, employeeNo, buildingId}) ← 后端开锁接口
↓
用户点"挂断"
↓
HikVideoTalkActivityModule.hangUp(serial, blockNo, ...)
↓
云端信令 + 本地 stopVisualTalk
↓
clearInterval(轮询) + navigation.goBack()
7.4 语音 vs 视频:两个供应商的本质差异
很多读者以为"WebRTC 就必须视频",其实 WebRTC 只是传输框架,音/视频可选。两个供应商对音视频的实现差异很大。
7.4.1 思悦(WebRTC)的音视频控制
// src/pages/CloudTalk/SiruisCloudCall.js
const stream = await mediaDevices.getUserMedia({
video: false, // ← 关键:业主端不发本地视频!
audio: isAudioEnabled ? {
echoCancellation: true, // 回声消除
noiseSuppression: true, // 噪音抑制
} : false,
});
| 维度 | 思悦业主端 | 思悦门口端 |
|---|---|---|
| 本地视频 | ❌ 不发送 | - |
| 本地音频 | ✅ 发送(带回声消除) | - |
| 接收视频 | ✅ 显示门口画面(RTCView 渲染 remoteStream) | - |
| 接收音频 | ✅ 听门口的声音 | - |
思悦的设计哲学:业主只看门口画面,业主不露脸。
- 业主端
video: false→ 摄像头不开 - 但
RTCView渲染remoteStream→ 业主能看到门口
7.4.2 海康(私有协议)的音视频控制
// iOS HikVideoTalkActivityModule.m
[CloudOpenSDK startVisualTalk:serial channelNo:1 verifyCode:verifyCode
completion:^(BOOL succeed, NSError *error) {
// 启动视频通话
}];
| 维度 | 海康业主端 | 海康门口端 |
|---|---|---|
| 本地视频 | ✅ 发送(门口机能看到业主) | - |
| 本地音频 | ✅ 发送 | - |
| 接收视频 | ✅ 显示门口画面 | - |
| 接收音频 | ✅ 听门口的声音 | - |
海康的设计哲学:双向视频(互相能看到对方)。
- 调用
startVisualTalk→ 视频 + 音频双向
7.4.3 两个方案对比
| 维度 | 海康(闭源) | 思悦(WebRTC) |
|---|---|---|
| 业主端摄像头 | ✅ 开(默认) | ❌ 不开(video: false) |
| 业主本地视频流 | 门口机看到业主画面 | 无 |
| 音频能力 | ✅ 双向 | ✅ 双向 |
| 业主显示门口画面 | ✅ | ✅ |
| 能否纯音频通话 | ✅ 海康 SDK 支持 startVoiceTalk(项目未实现) | ✅ 默认就是"业主纯音频 + 门口视频" |
7.4.4 能否完全关闭摄像头?
| 场景 | 海康 | 思悦 |
|---|---|---|
| 业主端关闭摄像头 | 改用 startVoiceTalk(需改造) | ✅ 已经是这样了(默认 video: false) |
| 纯语音通话 | ✅ 支持 | ✅ 默认就是纯语音(业主端) |
| 门口端没有摄像头 | ❌ 大多门口机只有摄像头 | ❌ 大多门口机只有摄像头 |
| 双向纯语音 | 需海康云端配置 | 需思悦云端配置 |
结论:
- 思悦:业主端默认纯音频(不露脸),门口端仍是视频(看门口)
- 海康:默认双向视频,要改成"业主纯音频"需改海康 SDK 调用
- 双方都可改成纯语音通话,工作量不大(1-2 天)
八、4 个关键技术参数
| 参数 | 来源 | 用途 |
|---|---|---|
| doorDeviceSerial | 后端 API 返回(/home_user/{phone}) | 萤石设备序列号,海康 SDK 用 |
| deviceCode(verifyCode) | 后端 API 返回 | 萤石设备验证码(6 位大写字母),海康 SDK 用 |
| accessToken | 后端 API 返回(getCloudTalkUserInfo) | 萤石 SDK 鉴权 |
| pid/token/room(思悦) | WebSocket type=3 消息携带 | 思悦 WebRTC 信令的前置参数 |
WebSocket 消息 type:
| type | 含义 |
|---|---|
| 0 | 被异地登录 |
| 1 | 海康门口机呼叫 |
| 2 | 室内机 app 呼叫门禁 |
| 3 | 思悦门口机呼叫 |
九、其他项目想做这个功能怎么落地
9.1 技术选型建议
| 场景 | 推荐方案 |
|---|---|
| 几百用户 | 海康 SDK + 阿里云 Push + 阿里云 WebSocket |
| 几万用户 | 自研 WebRTC + 独立信令服务(WebSocket)+ CDN |
| 跨国延迟敏感 | WebRTC P2P(不经过国内服务器) |
| 已有 IM 系统 | 直接复用 IM 的 WebSocket 做信令 |
9.2 协议设计
WebSocket 消息格式:
{
"type": 1,
"deviceSerial": "D55138380",
"deviceCode": "RGQSDA",
"buildingId": 123,
"employeeNo": "liuhao",
"timestamp": 1692345678901
}
WebRTC 信令格式(SDP/ICE):
{
"type": "offer", // offer/answer/candidate
"sdp": "v=0...", // SDP 内容
"candidate": "...", // ICE 候选(可选)
"from": "user_a",
"to": "user_b"
}
9.3 实施步骤
第 1 步:选型
- 简单方案:海康 SDK(闭源但稳)
- 高定制方案:WebRTC + 自研信令服务(推荐)
- ⚠️ 注意:来电通知通道**不是 MQTT**,是 WebSocket(自家后端)+ 阿里云 Push
第 2 步:服务端开发
- 门口机呼叫 → 后端推送
- WebSocket 服务(前台实时推送)
- 思悦 WebRTC 信令服务(单独 WebSocket)
- 阿里云 Push / APNs / 厂商通道配置
第 3 步:客户端推送
- 阿里云 Push 集成
- WebSocket 长连接
- 自动重连(3 秒)
第 4 步:接听页开发
- RTCView 渲染视频流
- 接听/拒接/挂断/开锁
- AppState 监听防后台
- 2 秒轮询状态
第 5 步:双重锁防重复
- global.callStatus
- global._isNavigatingToCall
- setTimeout(100ms) 包 navigate
第 6 步:双通道兜底
- WebSocket 在前台实时推送
- 通知点击后冷启动 + WebSocket 重连
- 重连后立即查询 callState === 1
第 7 步:测试
- 杀进程点通知能否跳页
- 多人同时接听能否正确退出
- 后台运行能否挂断
9.4 注意事项
| 注意点 | 说明 |
|---|---|
| WebRTC 需要 STUN/TURN | NAT 穿透必须,自己搭 coturn 或用 Twilio |
| SDP 交换是异步的 | 严格按 offer → answer → ICE 顺序 |
| ICE 候选要缓冲 | SDP 没处理完就收到 ICE 候选,要 buffer |
| 通话状态要用轮询兜底 | SDK 没事件订阅就自己轮询 |
| 后台必须主动挂断 | iOS 长时间占用摄像头会被强杀 |
| 状态码跨端不一致 | iOS 数字 1/2/3,Android 字符串 ‘idle’/‘ring’/‘onCall’ |
| 通话超时机制 | 默认 30s,门口机会自动挂断 |
| 不要在 WebSocket 里发大数据 | 音视频流走 P2P/WebRTC,不走 WebSocket |
十、本项目特殊点(避免踩坑)
| 坑 | 解决方案 |
|---|---|
| 重复跳转 | 双重锁 + setTimeout(100ms) |
| iOS 后台被强杀 | AppState 监听主动挂断 |
| 状态码跨端不一致 | 工具函数统一封装 |
| 被他人接听还在响铃 | 2 秒轮询检测 employeeNo |
| 杀进程收不到 | 阿里云 Push + 厂商通道兜底 |
| 手势返回通话没断 | componentWillUnmount 清理资源 |
| 思悦 WebRTC 没信令 | 用 WebSocket 单独做信令通道 |
十一、面试问答速记
Q1:为什么用 WebRTC 不用 WebSocket 做云对讲?
- WebRTC 自带编解码(Opus/H.264)
- 内置 STUN/TURN,NAT 穿透
- P2P 传输,延迟低(50-200ms)
- 服务器压力小(不转发音视频流)
Q2:双通道推送怎么协作?
- 阿里云 Push(杀进程兜底)+ WebSocket(前台实时)
- 通知对云对讲只是"敲门",把 App 拉回前台
- 真正跳转靠 WebSocket 重连 + 主动查 callState
Q3:怎么避免重复跳转?
- 双重锁:
global.callStatus+global._isNavigatingToCall setTimeout(100ms)包 navigate,让 navigator 准备
Q4:杀进程后用户怎么收到呼叫?
- 阿里云 Push 通过 APNs/厂商通道送达
- 用户点通知 → 冷启动 App → WebSocket 重连 → 主动查 → 跳转
Q5:海康和思悦怎么共存?
- 后端按房屋返回
videoCallPlatform字段 - 前端根据字段路由到
HikCloudTalkPlayer或SiruisCloudCall - 共用一个 WebSocket 通知通道
十二、一句话总结
云对讲 = 双通道推送(Push + WebSocket)+ 海康 SDK/WebRTC 双方案 + 双重锁防重复跳转 + 2 秒轮询兜底 + AppState 防后台占用。
十三、系统分层架构
整个项目的部署架构 4 层组成:
系列文章:① 云对讲模块(本文) ② 智能家居模块
所有评论(0)