帮你快速理解、总结文档立即下载

Android 智能设备

最近更新时间:2026-10-21 10:05:01
本文档已由 AI 辅助审校
我的收藏
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 回调说明:
回调
触发时机
说明
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
分辨率
320x480
帧率
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;

@Override
public void onCallAccepted(TXIoTCallSession.Contact contact) {
Log.i(TAG, "onCallAccepted: " + contact.userId);
mInCall = true;
startCallStreams(); // 见步骤 5
}

@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(); // 见步骤 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] 范围内。

联系我们

如发现文档或产品问题,欢迎通过 联系我们 提交反馈。