去年我们团队接到一个实时音视频通话的需求,要在鸿蒙系统上实现一对一视频通话功能。当时鸿蒙原生生态还不算成熟,社区里关于WebRTC在鸿蒙上的资料也少得可怜,踩了不少坑才把整个链路跑通。这篇文章把我从方案选型、环境搭建、核心链路实现到问题排查的完整过程记录下来,给准备在鸿蒙上做音视频通话的朋友一个参考,尤其是那些想自己集成WebRTC而不是直接用第三方SDK的团队。
1. 为什么选WebRTC:音视频方案选型的完整对比
1.1 鸿蒙原生音视频能力有哪些不足
鸿蒙系统本身是提供音视频相关API的,比如AVPlayer、AVRecorder,还有音频管理、摄像头管理这些能力。如果只是做简单的录音播放、录像、视频播放,用系统原生的接口完全没问题。但音视频通话不一样,它要求的是低延迟、双向实时、网络自适应,这些需求远远超出了系统原生媒体接口的能力范围。
举个例子,用AVRecorder录制视频,录完是一个完整的视频文件,它面向的是“录制-存储-播放”这种非实时场景。而通话要求的是帧级别的数据流转:每一帧采集完要立刻编码、打包、发送,同时接收端要立刻解码、渲染。这个过程不是录文件,而是持续不断的实时流,需要处理抖动缓冲、丢包重传、码率自适应、回声消除、噪声抑制等一系列问题。这些能力系统原生接口都没有,也不可能在短时间里自己从头实现一遍。
另外一个问题是跨平台一致性。我们团队同时在做Android和iOS版本,WebRTC在三端都有比较成熟的方案,虽然鸿蒙是新的,但底层思路和代码结构可以复用。如果每个平台都用不同的方案,维护成本会翻好几倍。
1.2 第三方SDK和自研WebRTC怎么选
我当时面临一个很现实的选择:是直接接入声网、腾讯TRTC这类第三方RTC SDK,还是自己把WebRTC编译进鸿蒙项目里。
第三方SDK的好处很明显:成熟稳定、接入快、文档齐全、有技术支持。坏处也很明显:
- 收费按分钟计,业务量大了成本可观
- 功能被厂商锁定,底层出问题只能等厂商修复
- 私有协议,无法和其他平台自研方案互通
- 在鸿蒙上支持的SDK当时也不多,接入效果还没经过大规模验证
自研WebRTC的好处是:WebRTC是公开标准协议,可以和其他平台互通(Web端、Android、iOS都可以互联),底层可控,没有按量计费的问题,而且WebRTC的代码是开源的,社区活跃,遇到问题至少能自己定位到代码级别。
坏处也很直接:要自己编译鸿蒙版本的WebRTC库,要写NAPI桥接层,要做大量适配和测试,工期是实打实增加的。
我当时的判断是:如果鸿蒙版本只是一个短期的过渡需求,用第三方SDK快速交付更划算。但如果鸿蒙会成为长期战略平台,而且后续还打算做多端互联、私有化部署这些功能,那从一开始就把WebRTC的自研能力建起来更值得。我们最后选了后者,把自研WebRTC作为长期技术底座。
1.3 项目整体架构设计
整个项目采用分层架构,从上到下是:
- 表现层:ArkTS + ArkUI,负责UI渲染和用户交互
- 桥接层:NAPI模块,把C++的WebRTC能力封装成ArkTS可以调用的接口
- 引擎层:编译鸿蒙版的libwebrtc,提供采集、编解码、传输、渲染等核心能力
- 信令层:WebSocket长连接 + JSON协议,负责通话的建立、接听、挂断等控制信令
- 服务层:信令服务器,用于SDP交换、ICE候选转发和通话房间管理
这么设计的核心思路是:桥接层只做能力暴露,不掺业务逻辑。业务逻辑全部放在ArkTS这一层,这样就算之后底层WebRTC库升级替换,上层的业务代码也不用大改。信令层和数据传输层完全解耦,信令走WebSocket,音视频数据走WebRTC自己的UDP通道,互不干扰。
2. 鸿蒙环境下WebRTC的移植与桥接
2.1 获取鸿蒙版本的WebRTC库
这是第一道坎。WebRTC官方源码是基于主流的桌面和移动平台的,没有直接支持鸿蒙的分支。鸿蒙的底层是OpenHarmony,系统调用和Linux有相似之处,但又不是完全一样,不能直接拿Linux版本硬编。
我当时试了几条路:
第一,直接用官方WebRTC源码交叉编译到OpenHarmony的OHOS工具链。理论可行,但WebRTC的构建系统非常庞大,依赖的第三方库又多,改造成本极高,我试了两天就放弃了。
第二,找社区已经做好的鸿蒙WeRTC分支。当时有几个开源项目在做类似的事情,比如一些开发者在Gitee上维护了OpenHarmony版本的WebRTC移植。最后我们以这些社区分支为基础,对照自己项目需要的功能做裁剪和修补,这条路实际走通了。
第三,如果项目允许,也可以直接引入已经适配好的商业SDK,但前面说了,我们不想选这条路。
这里给一个非常重要的建议:拿到社区分支后,不要急着集成,先看工程的编译说明,确认配套的OpenHarmony SDK版本和DevEco Studio版本,版本不匹配的话,编译会报各种奇奇怪怪的错,排查起来极其痛苦。
我用的是社区分支配合OpenHarmony 4.0的SDK,构建工具用DevEco Studio自带的,最终生成了libwebrtc.so。这个步骤的产物是后续所有开发的基础,所以编译机器建议选一台配置高一点的,第一次全量编译可能要等半个多小时,后面增量编译会快很多。
2.2 NAPI桥接层:打通C++与ArkTS
WebRTC的核心代码是C++写的,而鸿蒙的应用层开发用的是ArkTS(TypeScript的超集),这两者之间必须有一座桥,鸿蒙官方提供的桥是NAPI(Native API)。
第一次接触NAPI的时候,我感觉它和Android的JNI挺像的,思路都是类似的:Native层暴露一组C/C++函数,JS/TS层通过一个封装对象调用这些函数,数据在这两层之间传递。
我们封装了下面这些核心接口:
// 初始化引擎 napi_value InitEngine(napi_env env, napi_callback_info info); // 创建PeerConnection napi_value CreatePeerConnection(napi_env env, napi_callback_info info); // 添加本地音视频流 napi_value AddLocalStream(napi_env env, napi_callback_info info); // 处理远端SDP napi_value SetRemoteDescription(napi_env env, napi_callback_info info); // 处理远端ICE候选 napi_value AddIceCandidate(napi_env env, napi_callback_info info); // 挂断通话 napi_value ClosePeerConnection(napi_env env, napi_callback_info info);每个C++接口的返回值要转成napi_value,回调事件通过napi_call_function或EmitAsyncCallback的方式把数据从Native层传回ArkTS层。
这里面有个关键细节:NAPI的调用默认跑在同一个线程上,如果WebRTC的回调线程直接操作NAPI环境,会出现线程安全问题。我的做法是:把WebRTC回调线程的数据先丢到一个线程安全队列里,然后在ArkTS的主线程上轮询取数据,或者用napi_threadsafe_function这个官方线程安全API来处理。推荐后者,它本身就是为了解决回调跨线程设计的,虽然第一次用起来有点绕,但稳定。
2.3 权限声明与工程配置细节
鸿蒙的权限模型比较严格,涉及麦克风、摄像头这类敏感权限,除了在module.json5里声明,还需要在运行时动态申请。
{ "module": { "requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "用于音视频通话采集声音", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.CAMERA", "reason": "用于音视频通话采集视频画面", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.INTERNET", "reason": "用于音视频数据网络传输", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING", "reason": "用于通话时保持后台运行", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } }动态申请权限在ArkTS里用abilityAccessCtrl模块:
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; import { Permissions } from '@harmonyos/Ability'; async function requestPermissions(context: common.UIAbilityContext) { const atManager = abilityAccessCtrl.createAtManager(); const permissions: Array<Permissions> = [ 'ohos.permission.MICROPHONE', 'ohos.permission.CAMERA', 'ohos.permission.INTERNET' ]; const result = await atManager.requestPermissionsFromUser(context, permissions); // 检查result数组里每一项的授权状态 }还有一个特别容易忽略的地方:如果应用有前台切换后台的需求,还必须在module.json5里配置对应的后台模式,否则一到后台麦克风和摄像头就会被系统强制关闭。鸿蒙对后台运行的管控非常严格,这一块配置错了,通话中锁屏或切到后台马上就会出问题。
3. 通话主流程:从采集到渲染的完整链路
3.1 本地音频采集与参数设置
音视频通话的第一步是采集本地音频。WebRTC内部封装了音频采集的逻辑,我们在ArkTS层通过调用桥接接口来启动采集。
音频采集有个关键参数是采样率。实际测试下来,16kHz采样率在通话场景下性价比最高,语音足够清晰,带宽占用又低。但WebRTC内部默认的处理逻辑是16kHz,如果端侧硬件采集的是48kHz,它会自己做重采样处理,所以我们不需要在应用层额外处理采样率,但要知道有这一层重采样的存在,否则排查音质问题时容易走弯路。
音频方面还有几个内置的音频处理模块非常重要:
- 回声消除:不开启的话,对方能听到自己的回音,体验极差
- 噪声抑制:过滤环境噪音,比如马路上的车声、办公室的键盘声
- 自动增益控制:根据说话人距离麦克风的远近自动调整音量
这些功能在WebRTC里默认是开启的,但前提是音频采集和播放走的都是系统的音频模块。如果某些定制ROM或特殊机型上音频路由异常,这些模块的效果会大打折扣。
在鸿蒙上,采集音频还需要注意麦克风启动时机:不要在UI刚初始化的时候就启动采集,要等通话界面完全渲染完成、用户点击了“接听”之后再启动,否则可能出现界面卡顿或者首帧音频数据丢失的情况。
3.2 视频采集、编码与码控策略
视频采集比音频复杂得多,涉及分辨率、帧率、编码格式、码率控制等多个维度。
我们项目最终选择的视频参数如下:
| 参数项 | 数值 | 说明 |
|---|---|---|
| 采集分辨率 | 1280×720 | 720P满足通话场景的画质需求,低于1080P的带宽占用 |
| 编码分辨率 | 1280×720 | 保持和采集一致,避免缩放画质损失 |
| 帧率 | 24fps | 通话场景足够流畅,比30fps省带宽 |
| 编码格式 | H.264 | 兼容性好,真机上硬编支持度高 |
| 初始码率 | 800kbps | 根据720P@24fps的经验值设置 |
| 最低码率 | 150kbps | 弱网降级的下限 |
| 最高码率 | 1200kbps | 高带宽场景的上限 |
WebRTC的码率控制是动态的,它内部有带宽估计模块,会实时探测网络状态,自适应当前码率。但我们可以通过JS接口设置一个初始值区间,让它在合理范围内调整。
设置分辨率有个容易踩的坑:摄像头硬件支持的输出格式不一定和你设置的分辨率一致。比如设置1280×720,有些摄像头硬件支持的是1920×1080,WebRTC内部会做裁剪或缩放。所以实际看到的画面可能和预期有出入,这是正常的,只要整体布局能接受就行。如果你发现画面被裁得厉害,可以调用摄像头的Capability接口查看分辨率和帧率列表,选一个最接近的格式。
视频编码优先用H.264而非VP8或VP9,原因很简单:在移动端硬编H.264几乎是标配,而且H.264在Web端的兼容性也是最好的。如果将来要和微信小程序、浏览器Web端互通,H.264的兼容性优势会更加明显。
3.3 信令交互与SDP协商
信令是整个通话流程的指挥中心。WebRTC本身不负责信令传输,它只负责媒体传输。什么是信令?就是“我邀请你通话”“我接受”“我拒绝”“交换各自的能力信息”这些消息。
我们的信令协议基于WebSocket + JSON,定义了几种消息类型:
// 发起呼叫 { "type": "call", "callerId": "userA", "calleeId": "userB", "roomId": "room123" } // 信令服务器广播来电 { "type": "incomingCall", "callerId": "userA", "roomId": "room123" } // 被叫方发送SDP Offer(反过来发起方也可能先发Offer,看设计) { "type": "offer", "sdp": "v=0\r\no=- 4611731369565326323 2 IN IP4 127.0.0.1..." } // 主叫方应答SDP Answer { "type": "answer", "sdp": "v=0\r\no=- 7221571579710023717 2 IN IP4 127.0.0.1..." } // 交换ICE候选 { "type": "iceCandidate", "candidate": "candidate:842163049 1 udp 1677729535..." }通话建立的标准流程是这样的:
- 主叫方创建
RTCPeerConnection,添加本地音视频流,创建Offer并设置本地描述 - 主叫方把
Offer通过信令服务器发给被叫方 - 被叫方收到
Offer,创建自己的RTCPeerConnection,设置远端描述,添加本地流,创建Answer并设置本地描述 - 被叫方把
Answer发回主叫方,主叫方设置远端描述 - 双方通过信令通道交换ICE候选,不断尝试建立P2P连接
- 连接建立成功,音视频数据开始传输
这里有个容易出错的地方:SDP和ICE候选的时序问题。如果ICE候选在SDP设置完成之前就到达了,PeerConnection可能还没有准备好接收候选信息,导致候选丢失。我的做法是:在收到SDP并设置远端描述之前,先把收到的ICE候选缓存起来,等远端描述设置成功后再统一添加,这样可以避免时序问题。
另外,信令服务器要处理好**“双方同时发起通话”**的场景。如果A呼叫B的同时B也呼叫A,服务器要做冲突检测,只保留一个方向,另一个方向返回409错误。
3.4 远程流渲染与扬声器切换
远程流渲染有两个层面要处理:一是拿到远端视频流之后如何显示,二是监听到远端流添加之后如何通知UI层更新视图。
我在ArkTS层写了一个视频渲染组件,底层通过XComponent来承载视频画面。XComponent是鸿蒙提供的一个可以用来嵌入原生视图的组件,在NAPI层调用WebRTC的渲染接口,把视频帧直接绘制到这个原生视图中。
核心代码大概长这样:
@Component export struct RemoteVideoView { private xComponentId: string = 'remoteVideoSurface'; private nativeApi: RemoteNativeApi = new RemoteNativeApi(); build() { Column() { XComponent({ id: this.xComponentId, type: XComponentType.SURFACE, libraryname: 'webrtc_napi' }) .onLoad(() => { // XComponent加载完成后,把surfaceId传给NAPI层 this.nativeApi.setRemoteSurface(this.xComponentId); }) .width('100%') .height('100%') } } }这里有几个细节需要注意:
XComponent必须在页面onPageShow之后加载完成才能使用,如果过早调用NAPI层的渲染接口,surface还没创建好,视频会黑屏。一个稳妥的方法是在onLoad回调里做初始化,这个回调代表XComponent已经准备好了。
远端视频流的渲染对象要提前创建。我踩过的坑是:等远端流已经到达才去创建渲染surface,结果有一个消息的竞态,导致远端画面在接通后前几秒是黑的。解决方案是在进入通话页面时就预先创建好本地预览和远端预览两个XComponent,并用一个标识标明当前是“等待远端流”的状态,一旦监听到远端流到达事件,立即把流和surface绑定。
扬声器切换用系统音频管理接口,在鸿蒙上通过audio.setAudioScene或audio.setDeviceActive来实现通话时切换听筒/扬声器。还有一个常被忽略的点:切换音频路由之后,WebRTC内部的回声消除模块需要一小段时间重新校准,所以切换后的一两秒内可能会有轻微回声或音质波动,这是正常的,要提前在UI上给用户一个心理预期,避免被当作bug反馈。
3.5 通话状态机与异常恢复
音视频通话不是一个单一流程,它是各种状态不断流转的过程。如果状态管理混乱,就会出现“呼叫成功但画面不显示”“挂断后麦克风还在录音”“切后台后通话断了找不到原因”这类诡异问题。
我整理了一个状态机,把通话生命周期拆成了下面几个状态:
| 状态 | 触发条件 | 可能切换到的状态 |
|---|---|---|
| IDLE | 初始状态 | CALLING, INCOMING |
| CALLING | 主叫方发起呼叫 | CONNECTING, CALL_FAILED, CANCELLED |
| INCOMING | 被叫方收到来电 | CONNECTING, REJECTED, TIMEOUT |
| CONNECTING | 双方SDP协商中/ICE连接中 | CONNECTED, CONNECT_FAILED |
| CONNECTED | P2P连接建立成功 | RECONNECTING, DISCONNECTED |
| RECONNECTING | 网络波动/ICE重启 | CONNECTED, DISCONNECTED |
| DISCONNECTED | 通话结束/异常断开 | IDLE |
每个状态切换的时机都对应一个明确的事件:本地用户操作(点击拨打、接听、挂断)或者远端信令(对方挂断、对方拒绝)。在ArkTS层我用一个CallStateManager类来管理状态流转,状态变更时发通知给UI刷新界面。
重要的经验是:状态机里的每一个状态都要有超时处理。比如CALLING状态超过30秒没进入CONNECTING,就自动置为CALL_FAILED并提示用户“对方无响应”。INCOMING状态超过45秒没接听,就自动置为TIMEOUT并回拨忙音。没有超时机制的状态机是不完整的,会在真实使用中卡死。
网络异常恢复也是一大重点。WebRTC自带的ICE重启机制可以在一定程度上处理网络切换问题,但状态要配合UI准确提示“网络不稳定,正在重连”。我在鸿蒙上做了这样一个处理:连续5秒没有收到远端音频包,就启动ICE重启流程,同时UI显示重连动画。超过15秒还没恢复,就直接挂了重拨。
4. 常见问题与排查实录
4.1 编译So库报错合集
编译WebRTC库的过程是整个项目最痛苦的部分之一,我记录了几个典型的报错场景和解决办法。
第一个坑是OpenHarmony SDK版本和WebRTC分支不匹配。社区分支往往基于特定版本的SDK编写,我一开始用了新版的SDK,结果编译时报了一堆undefined symbol和头文件找不到的错误。查了小半天才发现是SDK版本和分支预期不一致,换回分支对应的版本后问题直接消失。这个教训是:先读README,确认好环境要求,不要凭直觉用最新版SDK。
第二个坑是C++标准库的差异。WebRTC依赖C++17的某些特性,但鸿蒙的NDK工具链对C++17的支持在不同版本上有差异。如果编译报std::optional或std::variant相关错误,多半是工具链的C++标准库版本太旧。解决方法是升级NDK工具链或增加编译参数-DOHOS_CXX_STANDARD=17。
第三个坑是内存对齐和架构差异。编译x86模拟器版本和编译真机ARM版本,在-march等编译参数上有不同要求,如果在模拟器上测试一切正常,但真机上一跑就崩溃,先检查是不是So库架构不匹配,确保libs/arm64-v8a和libs/x86_64下都放了对的So文件。
4.2 NAPI对象生命周期引发崩溃
NAPI生命周期是最隐蔽、最难排查的崩溃源。WebRTC的回调线程把数据传到NAPI层后,如果NAPI层的对象被JS引擎回收了,再往里写数据就会直接崩溃。
我遇到过一个场景:通话结束、页面销毁后,WebRTC底层还有几个pending的回调没有执行完,回调线程一执行NAPI相关代码,App就崩了。排查了很久才发现是napi_ref没有在页面销毁时释放,导致悬垂引用。
正确的做法是:在页面销毁时主动调用NAPI层的Cleanup接口,设置一个isEngineActive标志位,让所有异步回调在执行前检查这个标志,为false就直接返回,不再调用NAPI接口。同时把之前创建的napi_ref全部napi_delete_reference掉,防止内存泄漏。
另外,建议在NAPI层加一些日志系统,不用特别复杂,OH_LOG_Print就够用,但要把关键节点都打上日志,比如InitEngine、CreatePeerConnection、SetRemoteDescription、AddIceCandidate、Close等接口的进入和退出,这样排查问题时能快速定位是上层没调用还是底层崩了。
4.3 音画不同步与回声问题
音画不同步在通话场景中的表现是:对方嘴型对不上声音,或者画面比声音快/慢。这个问题有三层原因,排查时要逐层往下走。
第一层是网络抖动。数据包传输过程中网络延迟不稳定,导致音视频数据到达时间的差异,WebRTC内部有抖动缓冲机制来处理,但如果网络质量极差,抖动缓冲补偿不了,就会出现音画不同步。这种情况下能做的是优化弱网算法策略,增加抖动缓冲区大小,代价是延迟变大,所以要平衡。
第二层是渲染时机。通常情况下音频是优先播放的,视频帧要根据音频时间戳来对齐渲染。我们在NAPI层调用WebRTC的视频渲染接口时,要确保传入的是VideoFrame的时间戳,渲染模块内部会根据时间戳和当前播放位置决定是立即渲染还是等待。
第三层是设备性能。如果鸿蒙设备性能比较弱,视频编码、解码耗时波动大,也会导致不同步。这种情况只能降低分辨率或帧率,减少编解码压力。
回声问题则多半是设备音频通路的问题。排查思路是先确认回声来源:是本地扬声器声音通过麦克风回采,还是远端回声传到本端。前者靠WebRTC自带的回声消除解决,后者要靠远端也开启回声消除。如果两边都正确开启了,还有回声,可以把音频采集和播放的延迟测量一下,看是不是硬件延迟导致回声消除算法失效。
4.4 弱网和切换网络断线
移动端弱网和网络切换是无法绕开的场景。Wi-Fi切换到蜂窝网络时,IP地址和网络路径都会变,现有连接就会断开。
我之前遇到的问题是:网络切换后WebRTC没有自动重连,需要用户挂断重拨。后来排查发现是ICE候选信息里的IP和网络切换后的实际IP不一致导致的。解决方法是监听到网络变更后,立刻触发RestartIce(),让底层重新收集候选并建立连接。
鸿蒙上监听网络变化有现成的接口,@ohos.net.connection模块可以监听网络状态。实现逻辑参考这个示例:
import connection from '@ohos.net.connection'; // 网络变化时,通知native层重启ICE connection.on('netAvailable', () => { this.nativeApi.restartIce(); }); connection.on('netLost', () => { this.ui.showToast('网络连接已断开,尝试重连中'); this.nativeApi.restartIce(); });还要注意一点:切换后的网络如果是2G或非常弱的蜂窝网络,宽带上限很窄。这时要把WebRTC的码率上限调低,比如把视频码率上限从1200kbps降到300kbps,否则会大量丢包,画面完全卡死。我的做法是在网络切换回调里,根据网络类型动态调整码率范围。
4.5 内存与功耗优化
鸿蒙设备的碎片化程度很高,低端机型和高端机型的性能差距非常大。项目做性能测试时,我们在某个低端机型上发现通话一分钟,内存上涨了快50MB,CPU占用率也居高不下。
排查下来主要问题集中在视频编码器上。在低端机上硬编H.264的性能其实还可以,但编码参数没有做适配,分辨率高、码率高,导致CPU持续高负载。优化方案是:
- 增加码率自适应的敏感度,让带宽估计模块更快地降低码率
- 在低端机上把默认编码分辨率从720P降为540P,肉眼几乎看不出差别,但CPU占用率下降了30%
- 通话时如果检测到App进入后台超过30秒,主动降低视频编码帧率到5fps,音频保持正常,这样既能保持连接,又能显著降低功耗
- 通话结束后,一定要调用
Close接口释放所有本地流和PeerConnection,特别要检查摄像头是否真正关闭,我遇到过挂断后摄像头指示灯还亮着的bug,就是因为Native层的视频采集线程没有正确释放
5. 鸿蒙适配的额外坑点
5.1 设备与系统版本差异
鸿蒙目前设备的系统版本跨度很大,从老旧的HarmonyOS 2.0到最新的6.x都有。不同版本的API行为有一些细微差异,尤其是权限管理和后台保活策略。
比如,在较老版本上,麦克风权限申请后不需要额外处理就能正常采集。但新版本对麦克风使用增加了更严格的用户提示,如果用户拒绝了权限,系统还会弹出一个“允许仅本次使用”的选项。这个交互如果没处理好,用户可能因为误点了“仅本次使用”,下次通话时权限就失效了。
针对这类问题,我的建议是在应用内做一个统一的权限检查入口,每次进入通话页面之前都重新检查一遍权限状态,发现权限不足时给出引导,而不是等采集失败了再报错。
5.2 后台保活与来电场景
音视频通话的常见场景是:用户正在和其他App交互,或手机锁屏,此时来电需要弹出通知,点击通知后进入通话页面。
鸿蒙对后台运行的限制很严。如果应用在后台运行,没有任何保活措施,过几分钟就会被系统挂起,WebRTC的底层线程全部冻结,通话自然就断了。我们最终用了两个方案叠加:
- 申请
ohos.permission.KEEP_BACKGROUND_RUNNING权限,并在module.json5里配置后台模式为audioPlayback或audioRecording,这样通话中切后台不会被立即冻结 - 在通话过程中生命周期
onBackground回调里,主动拉起前台服务(ContinuousTask),保证通话进程在后台的优先级
来电通知的实现在鸿蒙上用的是@ohos.notificationManager,点击通知跳转到指定页面需要配置want参数的uri。有一个和热搜词里“点击通知后跳转至App内某页面”相关的问题:通知的跳转URI要和Ability的路由配置一致,否则点击通知无响应。我一开始总是跳不到指定通话页面,后来发现是没有在want里正确带上通话房间ID等参数,导致目标页面拿到的是空数据。解决方案是在创建通知时把必要参数塞到want.parameters里,目标页面在onCreate中解析这个map。
写在最后
坦白说,在鸿蒙上做WebRTC自研适配,工程量比在Android上做同样的工作大概要多出一倍,但做完之后,整个团队对WebRTC底层机制的理解也上了一个台阶。如果让我重新选一次,我仍然会走自研路线,因为这套东西一旦跑通,后续在鸿蒙上加再多的音视频功能(多人会议、屏幕共享、AI降噪等)都只是在现有底座上叠功能,而不是从零开始。
最后分享一个我个人的经验:鸿蒙生态迭代速度很快,几乎每个季度都有新版本API或者工具链更新,所以不要试图把项目的依赖版本“钉死”。我现在的做法是每三个月花一天时间升级SDK和依赖库,跑一遍完整通话流程的自动化测试,及早发现兼容性问题。这个过程虽然不起眼,但能避免很多线上事故,建议做鸿蒙音视频的团队都坚持这个节奏。