HarmonyOS APP开发---"云客服"远程视频客服App,需要用到这个库
金融视频柜员的场景很特殊:客户可能转横屏看资料、可能切后台去付款、网络还可能很差。任何一个环节黑屏,都是业务事故。
@ohos/webrtc提供完整 RTC 能力,而 Surface 时序和摄像头方向这两处细节,决定了它能不能上生产。
📦 仓库地址 |ohpm install @ohos/webrtc
场景
"云客服"面向银行/保险的远程视频柜员:
- 客户办业务遇到问题 → 一键呼叫客户经理
- 双方视频通话,核实身份、讲解流程
- 客户操作不熟练 → 客户经理请求屏幕共享,看着客户的屏幕指导
- 通话可能持续十几分钟,中途客户可能转横屏看资料、切后台去付款
四个硬性要求:不能黑屏 、摄像头方向要准 、镜像要正确 、网络差时要保语音清晰。
为什么是 WebRTC
| 需求 | 自研 RTP/SRTP | @ohos/webrtc |
|---|---|---|
| NAT 穿透 | 手写 ICE/STUN/TURN | ✅ 内置 |
| 音频 3A | 手写 AEC/ANS/AGC | ✅ 内置 |
| 视频编解码 | 手写 H.264/VP8 | ✅ 硬件加速 |
| 拥塞控制 | 手写带宽估计 | ✅ 内置 |
| 屏幕共享 | ❌ 极难 | ✅ getDisplayMedia |
| 跨平台互通 | ❌ | ✅ 与浏览器/iOS/Android 互通 |
| 开发周期 | 数月到数年 | ✅ 数天 |
客服场景还有个隐藏要求 :TURN 中继必须配。客户可能在企业内网或运营商 NAT 后面,纯 P2P 穿透成功率不够。TURN 虽然增加延迟,但保证"一定能通"------客服场景接通率比延迟重要。
关键实现
Surface 时序:避免黑屏的核心
远端视频轨道到达的时机和 Surface 创建的时机不确定。轨道先到、Surface 后创建时直接绑定会失败,结果就是黑屏。
用「暂存 + onLoad 回调」兜住两种时序:
typescript
@Entry
@Component
struct ServiceCallPage {
private remoteRenderer: VideoRenderer | null = null
private pendingRemoteTrack: MediaStreamTrack | null = null // ✅ 暂存
private surfaceReady: boolean = false
build() {
Stack() {
XComponent({ id: 'remote-video', type: 'SURFACE', libraryname: 'ohos_webrtc' })
.onLoad(() => {
// ✅ Surface 已创建,此时绑定才生效
this.surfaceReady = true
this.remoteRenderer = createVideoRenderer()
if (this.pendingRemoteTrack) {
this.remoteRenderer.setVideoTrack(this.pendingRemoteTrack)
this.pendingRemoteTrack = null
}
})
.width('100%').height('100%')
}
}
// ✅ 轨道到达:Surface 就绪则直接绑,否则暂存
private onRemoteTrack(track: MediaStreamTrack) {
if (this.surfaceReady && this.remoteRenderer) {
this.remoteRenderer.setVideoTrack(track)
} else {
this.pendingRemoteTrack = track
}
}
// ✅ 切后台 Surface 被销毁重建,回前台要重挂
onPageShow() {
if (this.remoteRenderer && this.pendingRemoteTrack) {
this.remoteRenderer.setVideoTrack(this.pendingRemoteTrack)
}
}
}
这个模式解决三类黑屏:轨道先到、Surface 先到、切后台后 Surface 重建。
建连与状态监控
typescript
async startCall(roomId: string) {
this.localStream = await mediaDevices.getUserMedia({
audio: true,
video: { width: 1280, height: 720, facingMode: 'user' },
})
this.pc = new RTCPeerConnection({
iceServers: [
{ urls: ['stun:stun.service.com:3478'] },
{ urls: ['turn:turn.service.com:3478'], username: 'u', credential: 'p' }, // ✅ 必配
],
})
this.localStream.getTracks().forEach((t) => this.pc!.addTrack(t, this.localStream!))
this.pc.ontrack = (e) => e.streams[0].getVideoTracks().forEach((t) => this.onRemoteTrack(t))
this.pc.onicecandidate = (e) => { if (e.candidate) signaling.send({ type: 'ice', candidate: e.candidate, roomId }) }
// ✅ 连接状态分级处理
this.pc.onconnectionstatechange = () => {
switch (this.pc!.connectionState) {
case 'connected': this.callState = '通话中'; break
case 'disconnected': this.callState = '连接不稳定'; this.tryIceRestart(); break
case 'failed': this.callState = '连接失败'; this.reconnect(); break
}
}
const offer = await this.pc.createOffer()
await this.pc.setLocalDescription(offer)
signaling.send({ type: 'offer', sdp: offer.sdp, roomId })
}
屏幕共享指导客户
typescript
async requestScreenShare() {
try {
const screenStream = await mediaDevices.getDisplayMedia({ video: true }) // 拉起系统授权
screenStream.getVideoTracks().forEach((track) => {
this.pc!.addTrack(track, screenStream)
track.onended = () => { // 用户从通知栏停止共享
this.isScreenSharing = false
this.callState = '通话中'
}
})
this.isScreenSharing = true
this.callState = '屏幕共享中'
} catch (err) {
promptAction.showToast({ message: '屏幕共享不可用,请检查授权' })
}
}
屏幕共享涉及系统级能力,测试清单要覆盖:正常共享与停止、来电打断、切其他 App、锁屏解锁、横竖屏切换、通知栏停止、弱网流畅度。
两个容易踩的点
1. 摄像头方向要真机全场景测
社区修过一个典型问题:前置摄像头在锁定横屏窗口下,发布流与预览都 180° 倒置。竖屏测试和模拟器都发现不了,必须真机横屏实测。
上线前按这张表过一遍:
| 场景 | 预期 |
|---|---|
| 竖屏 + 前置 | 本地预览镜像、远端正常 |
| 横屏 + 前置 | ✅ 不倒置 |
| 通话中旋转屏幕 | 画面平滑过渡 |
| 切后台再回前台 | ✅ 不黑屏 |
2. 网络差时优先保语音
客服场景语音清晰度比画质重要:
typescript
private startQualityMonitor() {
this.timer = setInterval(async () => {
const stats = await this.pc!.getStats()
let rtt = 0, loss = 0
stats.forEach((r) => {
if (r.type === 'candidate-pair' && r.nominated) rtt = r.currentRoundTripTime * 1000
if (r.type === 'inbound-rtp' && r.kind === 'video') {
loss = r.packetsLost / (r.packetsReceived + r.packetsLost) * 100
}
})
if (rtt > 300 || loss > 5) {
// ✅ 降视频保音频
this.localStream?.getVideoTracks()[0]?.applyConstraints({
width: 640, height: 360, maxBitrate: 300000,
})
promptAction.showToast({ message: '网络较差,已降低画质保证通话' })
}
}, 3000)
}
音频轨不动------客户听不清经理说什么,比看不清脸严重得多。
小结
"云客服"这个场景里,@ohos/webrtc 解决了三件事:完整 RTC 协议栈让 ICE 穿透和音频 3A 开箱即用、getDisplayMedia 让经理能看着客户屏幕指导操作、Stats API 让网络质量可监控可降级。
金融客服对稳定性零容忍。Surface 时序管理、摄像头方向真机全场景验收、网络差时降视频保语音------这三条工程实践做到了,才敢上生产。