TWeCall 是腾讯云物联网开发平台提供的设备与微信小程序之间的双向音视频通话能力。设备端一键呼叫,微信小程序端持续响铃提醒,接通后即可进行音视频通话,达到微信原生通话体验。
本文将介绍设备如何在 Android 平台实现获取小程序联系人列表、呼叫微信小程序、响应通话状态回调与挂断通话等,帮助您快速完成 TWeCall(微信通话)的设备端接入。
前提条件
已完成 登录与注册 文档中的登录功能,设备能够成功登录并保持在线。
已在物联网开发平台完成 TWeCall 服务激活与小程序授权,详见 微信通话服务(TWeCall)。
设备在 小程序端已授权 VoIP 通话权限(调用
getContacts 返回联系人列表非空)。说明:
通话依赖摄像头与麦克风采集,请确保已在
AndroidManifest.xml 中声明 android.permission.CAMERA、android.permission.RECORD_AUDIO 权限,并在运行时向用户申请授权,同时需使用 Android 真机(音视频能力在模拟器上无法正常工作)。接入步骤
TWeCall 设备端接入分为以下步骤:
1. 注册通话事件回调。
2. 获取联系人列表。
3. 发起呼叫。
4. 处理通话状态回调。
5. 开启摄像头与麦克风推流。
6. 挂断通话。
步骤 1:注册通话事件回调
登录成功后,通过
TXIoTCallSession.getInstance().addListener 注册 TXIoTCallSession.Listener。设备作为主叫时,需要关注对端的接听、拒绝、超时、挂断回调;设备作为被叫时,需要关注来电请求回调。Listener 回调说明:
TXIoTCallSession.Listener listener = new TXIoTCallSession.Listener() {@Overridepublic void onCallRequested(TXIoTCallSession.Contact contact, TXIoTCallSession.Option option) {// 被叫:收到来电请求,这里选择接听,也可调用 reject 拒绝TXIoTCallSession.getInstance().accept(contact.userId, null);}@Overridepublic void onCallAccepted(TXIoTCallSession.Contact contact) {// 对端已接听,开始推流(见步骤 5)}@Overridepublic void onCallRejected(TXIoTCallSession.Contact contact) {// 对端拒接,清理本地状态}@Overridepublic void onCallTimeout(TXIoTCallSession.Contact contact) {// 呼叫超时,清理本地状态}@Overridepublic void onCallHangup(TXIoTCallSession.Contact contact) {// 对端挂断,停止推流并清理采集}};TXIoTCallSession.getInstance().addListener(listener);
步骤 2:获取联系人列表
调用
getContacts 拉取可呼叫的微信小程序联系人列表。接口采用游标(cursor)分页,首次调用传 null,后续传入上一次回调返回的 nextCursor 继续拉取,直到 nextCursor 为空表示已到末尾,当 TXIoTValueCallback 返回的 List<Contact>,为空表示已无更多联系人,nextCursor 为空表示已是最后一页,无需继续拉取。void getContacts(String cursor, int limit, TXIoTValueCallback<ContactsResult> callback);
注意:
返回说明:
Contact 中的 userId 是发起呼叫的关键标识,由 modelId、wxAppId、openId 三段以 / 拼接而成。示例:完整拉取所有联系人
private static final int PAGE_SIZE = 10;private String mTargetUserId;private void fetchAllContacts(String cursor) {TXIoTCallSession.getInstance().getContacts(cursor, PAGE_SIZE,new TXIoTValueCallback<TXIoTCallSession.ContactsResult>() {@Overridepublic void onSuccess(TXIoTCallSession.ContactsResult result) {if (result == null || result.contacts == null) {return;}for (TXIoTCallSession.Contact contact : result.contacts) {// 记录第一个联系人作为呼叫目标if (mTargetUserId == null) {mTargetUserId = contact.userId;}}// nextCursor 非空说明还有更多联系人,继续翻页if (result.nextCursor != null && !result.nextCursor.isEmpty()) {fetchAllContacts(result.nextCursor);} else {// 拉取完成,可发起呼叫(见步骤 3)makeCall(mTargetUserId);}}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "getContacts failed: " + code + ", " + desc);}});}// 首次拉取fetchAllContacts(null);
步骤 3:发起呼叫
填写完联系人
userId 后,调用 call 向该联系人(微信小程序)发起呼叫。若呼叫小程序,userId 可取自 getContacts 返回的 userId 。呼叫体验版小程序(重要)
特别强调:
开发调试阶段,设备端必须在初始化之后、登录之前调用
callExperimentApi 显式指定呼叫的小程序版本为体验版(DEMO)或开发版(DEBUG),否则会因小程序版本不匹配导致呼叫失败或无法接通。待完成体验版功能自测后,将设备寄送至微信团队进行验证,验证通过后,小程序的 VoIP 音视频能力方可正式上线发布。小程序 VoIP 音视频正式上线后,无需再调用此接口,SDK 将默认呼叫线上版小程序。开发阶段指定体验版的方法如下,在初始化之后、登录之前通过
callExperimentApi 指定呼叫的小程序版本:/*在登录成功之前调用,指定呼叫的小程序版本"DEBUG" 开发版"DEMO" 体验版"RELEASE" 正式版*/TXIoTDeviceEngine.getInstance(context).callExperimentApi("set_wx_miniapp_flavor", "DEBUG", new TXIoTValueCallback<String>() {@Overridepublic void onSuccess(String response) {Log.i(TAG, "set_wx_miniapp_flavor success: " + response);}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "set_wx_miniapp_flavor failed: " + code + ", " + desc);}});
呼叫调用接口:
void call(String userId, Option option, TXIoTCallback callback);
呼叫选项
Option字段 | 说明 |
mediaContent | 媒体内容,见下表枚举。 |
customData | 自定义数据,随呼叫信令透传到小程序端。 |
媒体内容枚举
MediaContent取值 | 说明 |
MediaContent.AUDIO | 仅音频通话。 |
MediaContent.VIDEO | 仅视频通话。 |
MediaContent.AUDIO_VIDEO | 音视频通话(默认推荐)。 |
示例
TXIoTCallSession.Option option = new TXIoTCallSession.Option();option.mediaContent = TXIoTCallSession.MediaContent.AUDIO_VIDEO;option.customData = "";TXIoTCallSession.getInstance().call(userId, option, new TXIoTCallback() {@Overridepublic void onSuccess() {Log.i(TAG, "call request sent");}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "call failed: " + code + ", " + desc);}});
注意:
call 回调返回 onSuccess 仅表示呼叫信令已发出,并不代表通话已建立。需等待 onCallAccepted 回调后,通话才算真正接通,此时再开启摄像头与麦克风推流。步骤 4:处理通话状态回调
呼叫发出后,设备会依次收到以下回调,业务侧需在对应回调中管理本地状态:
示例:状态机式处理
private boolean mInCall = false;@Overridepublic void onCallAccepted(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallAccepted: " + contact.userId);mInCall = true;startCallStreams(); // 见步骤 5}@Overridepublic void onCallRejected(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallRejected: " + contact.userId);mInCall = false;}@Overridepublic void onCallTimeout(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallTimeout: " + contact.userId);mInCall = false;}@Overridepublic void onCallHangup(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallHangup: " + contact.userId);mInCall = false;stopCallStreams(); // 见步骤 6}
说明:
建议在收到
onCallAccepted 后再开启摄像头与麦克风,避免对端尚未接听就占用采集资源。onCallRejected、onCallTimeout、onCallHangup 均表示通话结束,应统一做资源清理。步骤 5:开启摄像头与麦克风推流
通话接通后(
onCallAccepted 触发),Android SDK 已封装摄像头采集、编码与推流,无需手动推送裸帧。只需调用以下接口即可:int openCamera(boolean frontCamera, TXCloudVideoView view);int openMicrophone();int startRemoteView(String userId, TXCloudVideoView view);
示例
private TXCloudVideoView mLocalView; // 布局中放置 <com.tencent.rtmp.ui.TXCloudVideoView/>private TXCloudVideoView mRemoteView;private void startCallStreams() {// 开启摄像头采集并推流,同时在本地控件预览(默认前置)TXIoTCallSession.getInstance().openCamera(true, mLocalView);// 开启麦克风采集并推流TXIoTCallSession.getInstance().openMicrophone();// 渲染对端画面TXIoTCallSession.getInstance().startRemoteView(mTargetUserId, mRemoteView);}
注意:
开启摄像头与麦克风前,请确保已获得
CAMERA 与 RECORD_AUDIO 运行时权限,否则采集会失败导致对端无画面或无声音。步骤 6:挂断通话
通话过程中,任意一方均可挂断。设备主动挂断调用
hangup;对端挂断时设备会收到 onCallHangup 回调。无论哪种情况,都应关闭摄像头与麦克风、停止采集。
void hangup(String userId, TXIoTCallback callback);// 关闭采集(主动挂断与收到 onCallHangup 后均需调用)private void stopCallStreams() {TXIoTCallSession.getInstance().stopRemoteView(mTargetUserId);TXIoTCallSession.getInstance().closeCamera();TXIoTCallSession.getInstance().closeMicrophone();}
示例
private void hangupCall() {if (!mInCall) {return;}TXIoTCallSession.getInstance().hangup(mTargetUserId, new TXIoTCallback() {@Overridepublic void onSuccess() {Log.i(TAG, "hangup request sent");}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "hangup failed: " + code + ", " + desc);}});}// 关闭采集(主动挂断与收到 onCallHangup 后均需调用)private void stopCallStreams() {TXIoTCallSession.getInstance().stopRemoteView(mTargetUserId);TXIoTCallSession.getInstance().closeCamera();TXIoTCallSession.getInstance().closeMicrophone();}
注意:
hangup 的 callback 用于确认挂断信令发送结果。即使主动挂断,也建议在 onCallHangup 或 callback 中统一做采集清理,避免重复释放。完整示例代码
package com.example.iotdemo;import android.os.Bundle;import android.util.Log;import androidx.annotation.Nullable;import androidx.appcompat.app.AppCompatActivity;import com.tencent.liteav.iot.TXIoTCallback;import com.tencent.liteav.iot.TXIoTCallSession;import com.tencent.liteav.iot.TXIoTError;import com.tencent.liteav.iot.TXIoTValueCallback;import com.tencent.rtmp.ui.TXCloudVideoView;public class CallActivity extends AppCompatActivity {private static final String TAG = "CallActivity";private static final int PAGE_SIZE = 50;private TXIoTCallSession mCallSession;private TXCloudVideoView mLocalView;private TXCloudVideoView mRemoteView;private boolean mInCall = false;private String mTargetUserId;private final TXIoTCallSession.Listener mListener = new TXIoTCallSession.Listener() {@Overridepublic void onCallRequested(TXIoTCallSession.Contact contact, TXIoTCallSession.Option option) {Log.i(TAG, "onCallRequested: " + contact.userId);// 被叫:这里选择接听,也可调用 reject 拒绝mTargetUserId = contact.userId;mCallSession.accept(contact.userId, null);}@Overridepublic void onCallAccepted(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallAccepted: " + contact.userId);mTargetUserId = contact.userId;mInCall = true;startCallStreams();}@Overridepublic void onCallRejected(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallRejected: " + contact.userId);mInCall = false;}@Overridepublic void onCallTimeout(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallTimeout: " + contact.userId);mInCall = false;}@Overridepublic void onCallHangup(TXIoTCallSession.Contact contact) {Log.i(TAG, "onCallHangup: " + contact.userId);mInCall = false;stopCallStreams();}};@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_call);mLocalView = findViewById(R.id.local_view);mRemoteView = findViewById(R.id.remote_view);// 前提:SDK 已完成初始化并登录成功(参见「登录与注册」)mCallSession = TXIoTCallSession.getInstance();mCallSession.addListener(mListener);// 拉取联系人,回调中自动发起呼叫fetchAllContacts(null);}private void fetchAllContacts(String cursor) {mCallSession.getContacts(cursor, PAGE_SIZE,new TXIoTValueCallback<TXIoTCallSession.ContactsResult>() {@Overridepublic void onSuccess(TXIoTCallSession.ContactsResult result) {if (result == null || result.contacts == null) {return;}for (TXIoTCallSession.Contact contact : result.contacts) {Log.i(TAG, "contact: " + contact.userId + " / " + contact.userName);if (mTargetUserId == null) {mTargetUserId = contact.userId;}}if (result.nextCursor != null && !result.nextCursor.isEmpty()) {fetchAllContacts(result.nextCursor);} else if (mTargetUserId != null) {makeCall(mTargetUserId);}}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "getContacts failed: " + code + ", " + desc);}});}private void makeCall(String userId) {TXIoTCallSession.Option option = new TXIoTCallSession.Option();option.mediaContent = TXIoTCallSession.MediaContent.AUDIO_VIDEO;mCallSession.call(userId, option, new TXIoTCallback() {@Overridepublic void onSuccess() {Log.i(TAG, "call request sent");}@Overridepublic void onError(int code, String desc) {Log.e(TAG, "call failed: " + code + ", " + desc);}});}private void startCallStreams() {int ret = mCallSession.openCamera(true, mLocalView);if (ret != TXIoTError.SUCCESS) {Log.e(TAG, "openCamera failed: " + ret);}ret = mCallSession.openMicrophone();if (ret != TXIoTError.SUCCESS) {Log.e(TAG, "openMicrophone failed: " + ret);}ret = mCallSession.startRemoteView(mTargetUserId, mRemoteView);if (ret != TXIoTError.SUCCESS) {Log.e(TAG, "startRemoteView failed: " + ret);}}private void stopCallStreams() {if (mTargetUserId != null) {mCallSession.stopRemoteView(mTargetUserId);}mCallSession.closeCamera();mCallSession.closeMicrophone();}private void hangupCall() {if (!mInCall || mTargetUserId == null) {return;}mCallSession.hangup(mTargetUserId, null);}@Overrideprotected void onDestroy() {stopCallStreams();mCallSession.removeListener(mListener);super.onDestroy();}}
说明:
1. 示例中省略了登录与初始化的细节,请参考 登录与注册 完成登录。
2. 以上仅为单人通话场景演示。监控/通话及通话中接听第二个呼入电话等复杂场景,请参考 SDK 自带 Demo 的
CallActivity。编译并运行
1. 使用
git clone 将开源 Demo 从 GitHub 克隆到本地。2. 打开 Android Studio,点击 Open 导入工程,等待 Gradle Sync 完成。
3. 连接 Android 真机,点击工具栏 Run 编译运行。进入应用后完成登录,即可在首页进入
ContactsActivity 选择联系人发起通话。# 也可在控制台打包 APK 后发送到手机安装./gradlew :app:assembleDebug
常见问题
现象 | 排查建议 |
getContacts 回调返回联系人列表为空。 | 确认设备已完成 TWeCall 激活与小程序授权;确认设备已绑定可呼叫联系人。 |
call 返回成功但回调提示 tweCall device not active。 | |
call 返回成功但回调提示 WeChat VoIP API error。 | 检查小程序中设备是否授权 VoIP 音视频通话。 |
call 返回 NOT_INITIALIZED(-4)或 NOT_LOGGED_IN(-15)。 | |
call 返回成功但长时间无 onCallAccepted。 | 等待 onCallTimeout;确认 userId 格式正确(modelId/wxAppId/openId);确认小程序端在线且已授权。 |
onCallAccepted 后对端看不到画面。 | 确认已调用 openCamera 且返回 TXIoTError.SUCCESS;确认已获得 CAMERA 运行时权限;确认摄像头未被其他应用占用。 |
onCallAccepted 后对端听不到声音。 | 确认已调用 openMicrophone 且返回 TXIoTError.SUCCESS;确认已获得 RECORD_AUDIO 运行时权限。 |
设备端看不到对端画面。 | 确认已调用 startRemoteView 且 userId 与通话对端一致;确认 TXCloudVideoView 已正确添加到布局。 |
通话结束后摄像头/麦克风仍被占用。 | 确认在 onCallHangup 回调及页面销毁(onDestroy)时调用了 closeCamera 与 closeMicrophone 释放采集资源。 |
getContacts 返回 INVALID_ARGUMENT(-3)。 | 确认 limit 在 (0, 1024] 范围内。 |