通话提醒异常排查指南

发起通话成功后,微信后台会使用微信消息通道向用户推送通话提醒。要收到通话提醒,手机端需要满足下列条件:

  • 至少需微信客户端 8.0.30 支持,为保证最佳效果,建议使用 >= 8.0.39 版本。Mac/Windows 微信暂不支持通话提醒;
  • 设备端网络通畅。断网、弱网环境,或受到安卓系统省流、省电策略的限制,会导致通知接收有概率发生延迟或一段时间内无法收到
    • iOS 系统,微信在后台时,推送由苹果统一进行;微信在前台时,推送走微信的消息通道。
    • 安卓系统通知统一走微信的消息通道。某些系统设置(如「智能省流量」、「休眠时始终保持网络连接」、「电池优化」、「省电策略」等)可能影响应用的网络情况(参考第 4 节),使微信消息通道中断,导致无法收到消息或消息延迟。
  • 当前用户已登录手机微信客户端。

通话异常排查指南

在通话过程中,如果碰到「无法发起通话」,「通话发起后异常退出」,「接听方一接听就挂断」,「通话异常退出」等问题,可以参考本文进行排查。

一次通话有 「拨打方」(或「来电方」、「主叫」)「接听方」(或「被叫」) 两个角色。通常,我们可以将一次硬件和微信小程序之间的 VoIP 通话分为 「发起」、「加入」、「等待」和「通话」 四个阶段。不同阶段可能会有不同类型的问题,在排查时,应首先根据表现确定是哪个阶段出现异常,在按照具体阶段的指引进行进一步分析。

建议使用插件 2.3.2 及以上版本。

1. 发起阶段

发起阶段是指调用插件 initByCaller 接口或 Linux SDK wx_voip_session_call 创建 VoIP 房间的阶段。在此阶段中,微信后台会进行一系列通话前置的检查操作,包括但不限于:

  • voipToken 的有效性。
  • 用户和设备之间是否存在授权关系。
  • 微信小程序流量包是否有余量,或设备是否已绑定有效的 license。

校验通话后,微信后台会向接听方推送通话提醒。

在发起阶段如果失败,接口会返回 errCode,开发者可以根据插件文档的说明来排查问题原因,比较常见的错误有以下几类。

(1) 用户未授权设备 (errCode: 9)

设备要和微信用户通话,必须先进行授权,具体过程请参考《用户授权设备》文档

出现此错误,常见的有以下情况:

  • 未使用 wx.requestDeviceVoIP 向用户请求过授权,或请求后用户拒接授权;
  • 用户曾经授权过,但是后续取消了授权;
  • 用户从最近使用中删除了微信小程序。此时会清空该用户和微信小程序间的所有授权记录;
  • 传入的 openId 不是要拨打给用户的,例如:授权的是家长,这里传入了孩子的 openId。

在使用设备组的情况下,常见还有以下情况:

  • 用户授权了设备组 A,但设备未被添加到设备组 A 中或已被 removeIotGroupDevice 接口移除;
  • 用户授权了设备组 A,但设备被使用 addIotGroupDevice (force_add=true) 强制转移到了另一设备组 B,也会导致设备从设备组 A 里移除。

优化建议

建议使用授权状态查询 接口,判断用户和设备/设备组直接是否存在授权关系。

  • 手机微信内发起通话前,建议提前调用wx.getDeviceVoIPList 查询用户已授权设备的列表,判断设备已被授权再发起通话,否则应请求用户重新授权;
  • 设备发起通话前,建议提前调用插件 getIotBindContactList 接口判断设备和用户间是否存在授权关系,存在时再发起通话,否则应提示用户重新授权;

对于设备组,可以使用 getIotGroupInfo 查询设备组中的设备列表。

(2) 设备呼叫手机微信 voipToken 错误 (errCode: 13)

对于使用设备认证 SDK 注册的设备(此时 voipToken 传入 SDK 获取到的 deviceToken),常见有以下情况:

  • deviceToken 过期。deviceToken 是有一定有效期的,需要定时进行更新,如果获取时间过久会失效。
  • 未传入 voipToken 字段或传入空字符串。此方式仅适用于使用 WMPF registerMiniProgramDevice 接口注册的设备。

对于使用 WMPF 注册的设备,可能有以下情况:

  • 设备之前是使用设备认证 SDK 的,未使用 WMPF 的 registerMiniProgramDevice 接口重新注册过。
  • WMPF 低于 1.2.0,或插件版本低于 2.3.0。

2. 「拨打方」加入阶段

创建 VoIP 房间成功后,「拨打方」会直接加入该房间,界面上会显示「连接中…」。加入的时长一般与当前网络状态有关。

加入成功后,拨打方会触发 joinedRoomByCaller 事件。

(1) 设备之前可以拨打成功,突然开始持续失败,需重启 WMPF 才能恢复

大概率是安卓 WMPF 低版本的 bug,请升级到 >= 2.0 版本解决。如新版本仍发现类似问题,请参考第 8 节反馈。

这种情况下开发者可能会收到 joinFailCaller 事件,errMsg 包含 Already in room or joining,此时可以尝试重启 WMPF。

(2) 一直显示「连接中」(卡在本阶段),无法加入房间,通话被突然结束

可能是网络状况较差导致加入过慢,此时通话可能会因接听方等待超时而结束,触发 abortVoipendVoip 事件。

(3) 未加入房间就直接退出通话

常见原因有:

  • 微信小程序错误调用插件 forceHangUpVoip 接口挂断通话,触发 cancelVoip 事件和 endVoip 事件(Toast 提示 「通话已被微信小程序结束」)。
    • 之前排查遇到部分微信小程序会设置定时器来设置通话的最大时长,通话结束后,某些情况下计时器没有清理导致在后续某次通话时随机挂断通话。
    • 我们建议通过监听 calling 事件,并判断 keepTime 来限制通话时长,不建议使用定时器
  • 因网络超时或其他异常导致加入房间失败,这种情况下拨打方会收到 joinFailCaller 事件,可以通过 data 字段拿到 errMsg 和 message 来分析错误原因。

常见问题(FAQ)

通话相关异常,请参考《通话异常排查指南》

1. 功能相关(通用)

1.1 如何限制用户的单次通话时长?

建议使用 initByCallertimeLimit 参数。插件低版本也可以根据 calling 事件的 keepTime 字段计算通话时长。超过限制后可以调用插件 forceHangUpVoip 中断通话。

不建议使用定时器实现此功能,容易出现一些异常情况导致定时器没有被清理的情况。导致影响后续通话。

1.2 在门禁、门锁场景,如何在手机端通话页面实现「开门」等功能?

插件提供了 setCustomBtnText 接口在手机端接听页面自定义按钮,开发者可配置一个自定义的弹层来实现具体功能。

C 端用户体验如下图所示:

1.3 用户如何取消授权?

用户可以在微信小程序设置页里取消授权,或通过在最近使用中删除微信小程序来清空授权记录。请参考「处理授权失效的情况」。

1.4 如何查询用户是否已授权设备(组)?

请参考「授权状态查询」。

1.5 如何设置呼叫超时时长(长时间不接听时停止呼叫)?是否支持轮询呼叫?

开发者可以自行控制超时时间,超时后调用插件 forceHangUpVoip 接口中断通话。

超时后,开发者可以根据业务场景,选择自动拨打给其他用户,实现轮询呼叫的能力。例如 101 号房有 A、B、C 三位业主,打给 A 业主 30 秒未接听,可自动打给 B 业主,依此类推。

1.6 如何自定义手机端看到的设备端来电名称?

为强化设备通话的认知、保证用户体验统一,手机端用户授权设备名称、接听设备来电的名称需保持一致。

授权设备名称 = 来电方名称 = 开发者自定义名称 + 设备类型名称。如「艾玛的希沃网课学习机」。开发者需要考虑名称显示,对名称做好规范。

(案例示意:订阅设备名称、来电方名称、语音通话中设备名称)

1.7 使用物联网卡时,如何配置域名和 IP 白名单?

VoIP 业务依赖于微信基础服务和微信小程序相关的业务内容,涉及比较多的 IP(在几千的量级)和域名,暂时未能提供完整的域名和 IP 列表,目前建议使用非定向的流量。

如有定向流量的需求,可在微信开放社区「硬件服务」板块发帖联系我们。

注意:IP 本身会随着业务的变更而增添或者裁撤,因而暂时没办法提供稳定的列表。

1.8 音视频通话的流量使用情况?

根据测算,语音通话大概是 2MB/分钟,视频是 10-30MB/分钟。

1.9 设备无摄像头或因隐私等原因不希望传画面(门禁、门锁的用户端),如何默认禁用摄像头?

插件发起通话时可以设置 caller.cameraStatus 或 listener.cameraStatus,设置两端是否默认开启摄像头,参见 initByCaller 接口文档。

如果要禁止用户切换摄像头,可以用插件的 setUIConfig 设置 callerUI/listenerUI 的 enableToggleCamera 选项。

1.10 为何推送消息显示的通话时长和通话结束或者 endVoip 事件获取的不一致?应该如何获取准确的通话时长?

VOIP 插件 2.2.1 及以下版本,通话结束页显示的时间为本地定时器计算的时间,endVoip 事件的 keepTime 提供的也是这个时间。但是由于通话双方之间存在一定网络延迟,这里的时间可能与实际扣费时长并不一致(一般要多于实际扣费的时长)。

VOIP 插件 2.2.2 版本开始,会在通话结束后(即 endVoip 事件后)从后台获取实际扣费时长,并通过 finishVoip 事件的 keepTime 返回给开发者。通话结束页也会更新显示实际的扣费时长。

2. 功能相关(安卓设备)

2.1 安卓应用和微信小程序之间如何进行参数传递和通信?安卓应用如何接收微信小程序发来的消息?
  1. 简单的「安卓应用 -> 微信小程序」单向单次传递参数的场景,可以直接在启动微信小程序的 path 中拼接 query。
  2. 如果安卓应用要接收微信小程序发来的事件、需要双向通信或者数据量大时可以通过 WMPF 提供的通信通道(Invoke Channel)
2.2 通话完成后如何关闭微信小程序?

当设备端微信小程序只承载 VOIP 通话能力时,可能需要在通话结束后将微信小程序切后台或关闭。

微信小程序收到插件的 endVoIP 事件后,通过 WMPF 提供的通信通道(Invoke Channel)通知 App。

收到通知后,App 可以选择调用 closeWxaApp 将微信小程序切后台或关闭(可参考《性能与体验优化指南》的说明选择)。

2.3 如何判断当前微信小程序是在设备端(WMPF)还是手机端打开

在 WMPF 运行时,微信小程序能可以访问到 wmpf 这个全局变量。可以通过是否存在这个全局变量来判断:typeof wmpf !== 'undefined' 即为设备端。

注意:调用 wmpf 上的方法前,应提前判断 wmpf 这个全局变量是否存在,否则在手机微信端走到这段逻辑时会报错。

3. 异常相关(通用)

3.1 手机端未收到微信通话强提醒或提醒强度不符合预期(锁屏未提醒、未响铃、未震动等)

请参考《通话提醒异常排查指南》。

3.2 获取设备票据 getSnTicket 接口返回 48001 (api unauthorized)

微信小程序 appId 未完成硬件设备接入导致。请确认:

  • appId 对应微信小程序已在「微信小程序管理后台」完成硬件设备接入。
  • 请求时使用的 access_token 是通过完成申请的微信小程序的 appId 申请的,而不是其他微信小程序的 appId 或者移动应用的 hostAppId。
3.3 wx.requestDeviceVoIP 报错 invalid scope

微信小程序 appId 未完成硬件设备接入或接入后未申请「微信小程序音视频能力」设备能力导致。请确认微信小程序已在「微信小程序管理后台」完成硬件设备接入并申请通过微信小程序音视频能力。

3.4 wx.getEnterOptionsSync 或插件的 getPluginEnteroptions 无法获取到进入微信小程序的 query

一般有以下几种情况:

  • 这两个函数只能获得微信小程序启动时(冷启动或热启动)的参数,如果是通过 wx.navigateTo 等路由方式跳转页面的情况,则需要在对应页面的 onLoad 生命周期获取。
  • 由于插件和宿主微信小程序的安全策略限制,当微信小程序启动路径为插件页面时,需要通过 VOIP 插件提供的 getPluginEnteroptions 获取 query;当微信小程序启动路径为微信小程序页面时,需要通过 wx.getEnterOptionsSync 获取 query。

建议排查时同时打印返回值中的 path 字段,确认是否是预期的传入 querypath

3.5 接听方接听时提示「页面不存在」

一般有以下几种情况:

  • 调用插件 initByCaller 时未设置 miniprogramState,或设置了 miniprogramState: formal,此时接听方会打开正式版微信小程序。而设备 VOIP 能力尚未发布正式版。
  • 调用插件 initByCaller 时设置了 miniprogramState: trial,此时接听方会打开体验版微信小程序。而当前设置为体验版的微信小程序中并未支持设备 VOIP 能力。
  • 调用插件 initByCaller 时设置了 miniprogramState: developer,此时接听方会打开开发版微信小程序。此时接听方需要提前扫码下载与拨打方相同的开发版微信小程序方可使用。
3.6 发起通话后,插件页面一直停留在「等待进行通话」界面无反应

一般有以下几种情况:

  • 微信小程序未调用插件 initByCaller 发起通话。可能是前置逻辑异常或未走到发起通话的分支。开发者应首先确定调用了该接口。
  • 微信小程序调用插件 initByCaller 失败,可能会抛出异常或者返回了非 0 的 errCode。开发者应正确地捕获和处理接口异常,并给用户必要的提示。
3.7 为什么我在 wecopper 的设备管理里找不到公钥?

这是因为你的设备类型是微信支付刷脸设备,目前这类设备不支持硬件 Voip 模式,需要重新申请设备类型。

4. 异常相关(安卓设备)

4.1 WMPF 获取不到正确的摄像头,或摄像头画面旋转

可以使用 InitGlobalConfig 接口指定微信小程序使用的摄像头,也可以指定摄像头画面的旋转角度。

fun initGlobalConfig() {
    val jsonConfig = JSONObject()
        // 请注意:USB 摄像头和内置摄像头使用的参数名称是不一样的。
    try {
        // 案例 1:微信端画面颠倒
        jsonConfig.put("cameraPushFlip", true) // USB 摄像头需使用 usbCameraPushFlip 参数

        // 案例 2:使用内置摄像头,微信端显示画面旋转
        jsonConfig.put("cameraRotationAngle", 90) // 根据实际情况调整角度

        // 案例 3:通过指定 internalCameraName 使用设备内置摄像头(需 WMPF 2.0.0 支持)
        jsonConfig.put("internalCameraName", "xxxx")

        // 案例 4:通过指定 cameraId 使用设备内置摄像头
        jsonConfig.put("cameraId", 0)

        // 案例 5:通过直接指定摄像头设备路径使用 USB 摄像头(与案例 5 的情况二选一)
        jsonConfig.put("usbCameraName", "/dev/xx/xx/xx")

        // 案例 6:通过指定三元组使用 USB 摄像头(与案例 4 的情况二选一)
        jsonConfig.put("usbCameraProductId", 0)
        jsonConfig.put("usbCameraVendorId", 0)
        jsonConfig.put("usbSerialNumber", "xxx")

        // 案例 7:使用 USB 摄像头,WMPF 预览和微信端显示画面旋转
        jsonConfig.put("usbCameraRotationAngle", 90) // 根据实际情况调整角度

        val json = jsonConfig.toString()
        LogUtils.d(TAG, "initGlobalConfig", json)
        Api.initGlobalConfig(json)
            .subscribe({
                LogUtils.d(TAG, GsonUtils.toJson(it))
                warmLaunch()
            }, {
                LogUtils.d(TAG, GsonUtils.toJson(it))
                warmLaunch()
            })
    } catch (e: Exception) { }
}

如果设置摄像头画面旋转未生效,建议按照下列指引检查:

  • InitGlobalConfig 必须在 ActivateDevice 回调成功后、启动微信小程序前调用。建议在 ActivateDevice 的 onSuccess 回调后调用。
  • InitGlobalConfig 设置是一次性的,在每次 WMPF 启动后都需要调用。
  • 请确认设备使用的是 USB 摄像头还是内置摄像头
    • 如果通过 usbCameraName,或 usbCameraProductId + usbCameraVendorId + usbSerialNumber 指定使用 USB 摄像头,需使用 usbCameraPushFlipusbCameraRotationAngle 设置画面旋转
    • 其他情况下使用内置摄像头,可以使用 internalCameraName 指定摄像头 cameraId。此时需使用 cameraPushFlipcameraRotationAngle 设置画面旋转。此时只能设置微信客户端看到的推流画面的旋转,不能改变设备端看到的预览画面。