这篇会介绍设备如何在 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 回调说明:
| 回调 | 触发时机 | 说明 |
| onCallRequested | 收到对端(小程序)发起的来电请求。 | 设备作为被叫时触发,在此回调中调用 accept 接听或 reject 拒绝。 |
| onCallAccepted | 对端接听呼叫。 | 设备作为主叫时触发,此时可开启摄像头与麦克风推流(见 步骤 5)。 |
| onCallRejected | 对端拒绝呼叫。 | 设备作为主叫时触发,清理本地状态。 |
| onCallTimeout | 对端无人接听,呼叫超时。 | 设备作为主叫时触发,清理本地状态,可重试或提示用户。 |
| onCallHangup | 对端主动挂断。 | 通话过程中对端挂断时触发,停止推流并清理采集(见 步骤 6)。 |
TXIoTCallSession.Listener listener = new TXIoTCallSession.Listener() { @Override public void onCallRequested(TXIoTCallSession.Contact contact, TXIoTCallSession.Option option) { // 被叫:收到来电请求,这里选择接听,也可调用 reject 拒绝 TXIoTCallSession.getInstance().accept(contact.userId, null); } @Override public void onCallAccepted(TXIoTCallSession.Contact contact) { // 对端已接听,开始推流(见步骤 5) } @Override public void onCallRejected(TXIoTCallSession.Contact contact) { // 对端拒接,清理本地状态 } @Override public void onCallTimeout(TXIoTCallSession.Contact contact) { // 呼叫超时,清理本地状态 } @Override public void onCallHangup(TXIoTCallSession.Contact contact) { // 对端挂断,停止推流并清理采集 }};TXIoTCallSession.getInstance().addListener(listener);
四、步骤 2:获取联系人列表
调用 getContacts 拉取可呼叫的微信小程序联系人列表。接口采用游标(cursor)分页,首次调用传 null,后续传入上一次回调返回的 nextCursor 继续拉取,直到 nextCursor 为空表示已到末尾。
注意:说明:
注意:如果设备调用 getContacts 后获取到联系人列表为空,说明小程序没有正常完成音视频授权。建议用户将小程序删除,重新添加小程序。小程序第一次授权时,会有如下弹窗提示。
注意:
void getContacts(String cursor, int limit, TXIoTValueCallback<ContactsResult> callback);
| 参数 | 说明 |
| cursor | 分页游标,首次传 null(SDK 内部等价于 "0"),后续传入上次回调的 nextCursor。 |
| limit | 本次拉取数量,有效范围 (0, 1024]。 |
| callback | 结果回调,返回 ContactsResult,字段见下表。 |
回调结果 ContactsResult
| 字段 | 说明 |
| contacts | 联系人列表 List<Contact>,为空表示已无更多联系人。 |
| nextCursor | 下一页游标,为空表示已是最后一页,无需继续拉取。 |
联系人结构体 Contact
| 字段 | 说明 |
| userId | 呼叫目标标识,格式 modelId/wxAppId/openId。发起呼叫的关键标识,需长期使用时请自行拷贝保存。 |
| userName | 用户昵称(可能为空)。 |
| avatarUrl | 头像 URL(可能为空)。 |
示例:完整拉取所有联系人
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>() { @Override public 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); } } @Override public 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>() { @Override public void onSuccess(String response) { Log.i(TAG, "set_wx_miniapp_flavor success: " + response); } @Override public void onError(int code, String desc) { Log.e(TAG, "set_wx_miniapp_flavor failed: " + code + ", " + desc); } });
呼叫调用接口:
void call(String userId, Option option, TXIoTCallback callback);
在设备呼叫微信小程序的场景中,设备默认接收参数如下:
| 参数项 | 默认值 |
| 视频编码 | H264 |
| 音频编码 | AAC |
| 分辨率 | 320×480 |
| 帧率 | 8 fps |
客户可根据实际产品需求修改上述参数,调用以下接口进行设置:
TXIoTDeviceEngine.getInstance(context).callExperimentApi( "set_expected_av_params", "{\\"expected_audio_codec\\":\\"G722\\"," + "\\"expected_video_codec\\":\\"MJPEG\\"," + "\\"expected_video_rotation\\":0," + "\\"max_fps\\":5," + "\\"max_video_width\\":240," + "\\"max_video_height\\":320}", new TXIoTValueCallback<String>() { @Override public void onSuccess(String response) { Log.i(TAG, "set_expected_av_params success: " + response); } @Override public void onError(int code, String desc) { Log.e(TAG, "set_expected_av_params failed: " + code + ", " + desc); } });
参数说明:
| 参数名 | 说明 | 取值范围 / 可选值 |
| expected_video_codec | 期望接收的视频编码格式。 | "H264"、"H265"、"MJPEG" |
| expected_audio_codec | 期望接收的音频编码格式。 | "AAC"、"OPUS"、"G722" |
| max_fps | 最大接收帧率。 | 5~10 |
| max_video_width | 期望接收的视频宽度。 | 根据设备实际需求填写。 |
| max_video_height | 期望接收的视频高度。 | 根据设备实际需求填写。 |
| expected_video_rotation | 期望微信发送的视频方向。 | 0:竖屏;1:横屏。 |
注意:1. 必须在 登录(login)之前调用 callExperimentApi 设置 "set_expected_av_params"。设备 SDK 登录后会立即上报 expected_av_params 参数,且整个生命周期内仅上报一次。若在登录后调用,设置将不会生效。
注意:2. 当设备需要发送 H.265(HEVC)编码的视频数据时,必须在发起呼叫前预先声明源视频编码格式,否则微信小程序端将无法正常解码播放。请严格按照以下顺序调用:先调用 callExperimentApi 设置设备端发送的视频编码为 H.265,再调用 call 发起呼叫。
注意:TXIoTDeviceEngine.getInstance(context).callExperimentApi( "set_source_av_params", "{\\"source_video_codec\\":\\"H265\\"}", null);
呼叫选项 Option
| 字段 | 说明 |
| mediaContent | 媒体内容,见下表枚举。 |
| customData | 自定义数据,随呼叫信令透传到小程序端。 |
媒体内容枚举 MediaContent
| 取值 | 说明 |
| MediaContent.UNKNOWN | 未知类型,不应主动设置。 |
| 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() { @Override public void onSuccess() { Log.i(TAG, "call request sent"); } @Override public void onError(int code, String desc) { Log.e(TAG, "call failed: " + code + ", " + desc); }});
注意:call 回调返回 onSuccess 仅表示呼叫信令已发出,并不代表通话已建立。需等待 onCallAccepted 回调后,通话才算真正接通,此时再开启摄像头与麦克风推流。
七、步骤 4:处理通话状态回调
呼叫发出后,设备会依次收到以下回调,业务侧需在对应回调中管理本地状态:
| 回调 | 业务建议处理 |
| onCallAccepted | 通话接通,开启摄像头与麦克风推流(步骤 5)。 |
| onCallRejected | 对端拒接,清理呼叫状态,恢复待机。 |
| onCallTimeout | 无人接听,清理呼叫状态,可重试或提示用户。 |
| onCallHangup | 通话中挂断,关闭摄像头与麦克风(步骤 6),停止采集。 |
示例:状态机式处理
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);
| 接口 | 说明 |
| openCamera(boolean frontCamera, TXCloudVideoView view) | 开启摄像头采集并推流。frontCamera 指定前后置摄像头;view 为本地预览控件,无需本地预览可传 null。返回 0(TXIoTError.SUCCESS)表示成功。 |
| switchCamera(boolean frontCamera) | 通话中切换前后置摄像头。 |
| closeCamera() | 关闭摄像头采集与推流(见 步骤 6)。 |
| openMicrophone() | 开启麦克风采集并推流。 |
| closeMicrophone() | 关闭麦克风采集与推流(见 步骤 6)。 |
| startRemoteView(String userId, TXCloudVideoView view) | 在指定控件上渲染对端画面,userId 为通话对端标识。 |
| stopRemoteView(String userId) | 停止渲染对端画面。 |
| muteRemoteAudio(String userId, boolean mute) | 静音/取消静音对端音频。 |
示例
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() { @Override public void onSuccess() { Log.i(TAG, "hangup request sent"); } @Override public 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() { @Override public void onCallRequested(TXIoTCallSession.Contact contact, TXIoTCallSession.Option option) { Log.i(TAG, "onCallRequested: " + contact.userId); // 被叫:这里选择接听,也可调用 reject 拒绝 mTargetUserId = contact.userId; mCallSession.accept(contact.userId, null); } @Override public void onCallAccepted(TXIoTCallSession.Contact contact) { Log.i(TAG, "onCallAccepted: " + contact.userId); mTargetUserId = contact.userId; mInCall = true; startCallStreams(); } @Override public void onCallRejected(TXIoTCallSession.Contact contact) { Log.i(TAG, "onCallRejected: " + contact.userId); mInCall = false; } @Override public void onCallTimeout(TXIoTCallSession.Contact contact) { Log.i(TAG, "onCallTimeout: " + contact.userId); mInCall = false; } @Override public void onCallHangup(TXIoTCallSession.Contact contact) { Log.i(TAG, "onCallHangup: " + contact.userId); mInCall = false; stopCallStreams(); } }; @Override protected 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>() { @Override public 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); } } @Override public 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() { @Override public void onSuccess() { Log.i(TAG, "call request sent"); } @Override public 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); } @Override protected void onDestroy() { stopCallStreams(); mCallSession.removeListener(mListener); super.onDestroy(); }}
注意:说明:
注意:1. 示例中省略了登录与初始化的细节,请参考 登录与注册 完成登录。
注意:2. 以上仅为单人通话场景演示。监控/通话及通话中接听第二个呼入电话等复杂场景,请参考 SDK 自带 Demo 的 CallActivity。
十一、编译并运行
Demo 已开源,仓库地址:github.com/Tencent-RTC/Tencent_IoT_Device,Android Demo 位于顶层 Android/ 目录。
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。 | TWeCall 是增值服务,需要在控制台激活,详见 TWeCall 激活。 |
| call 返回成功但回调提示 WeChat VoIP API error。 | 检查小程序中设备是否授权 VoIP 音视频通话。 |
| call 返回 NOT_INITIALIZED(-4)或 NOT_LOGGED_IN(-15)。 | 确认 SDK 已初始化且登录成功在线(参见 登录与注册)。 |
| 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] 范围内。 |
十三、联系我们
如发现文档或产品问题,欢迎通过 联系我们 提交反馈。