版本变更
版本 | 描述 | 时间 |
v1.3 | 新增:
更新:
SDK更新 | 2024-10-25 |
v1.2 | 批量查询数字人项目信息 | 2024-09-29 |
v1.1 | SDK版本升级 | 2024-09-10 |
v1.0 | 新增websocket实时互动链路 | 2024-09-09 |
v0.3 | 启动会话接口新增webSocketUrl | 2024-08-22 |
v0.2 | 添加pop接口sdk的maven依赖 | 2024-08-19 |
v0.1 | 功能发布 | 2024-07-01 |
概览
交互示例
接入准备
需要接入方提前准备阿里云账号,并利用阿里云子账号生成对应的AK/SK;
阿里云主账号需要对生成AK/SK的子账号进行RAM授权;
使用阿里云主账号登录平台,签署相关法务协议。
API详情
1. 批量查询数字人项目信息
入参 ListAvatarProject
参数名 | 类型 | 是否必填 | 说明 |
projectIdList | Array | Y | 项目ID 集合 |
出参
参数名 | 类型 | 说明 |
queryAvatarProjectResultList | Array<Object> | 项目信息集合 |
Object对象字段说明
参数名 | 类型 | 说明 |
status | String | 启动结果: DRAFT - 草稿 DELETED - 已删除 DEPLOYING - 发布中 DEPLOYED - 已发布 DEPLOY_FAIL - 发布失败 OFFLINE - 已下线 |
projectName | String | 名称 |
projectId | String | 项目id |
agentId | String | 智能体id |
errorMsg | String | 错误信息 |
2. 查询数字人项目详情
2.1. 入参 QueryAvatarProjectRequest
参数名 | 类型 | 是否必填 | 说明 |
projectId | String | Y | 项目ID |
2.2. 出参 QueryAvatarProjectResponse
参数名 | 类型 | 说明 |
status | String | 启动结果: DRAFT - 草稿 DELETED - 已删除 DEPLOYING - 发布中 DEPLOYED - 已发布 DEPLOY_FAIL - 发布失败 OFFLINE - 已下线 |
projectName | String | 名称 |
agentId | String | 智能体id |
errorMsg | String | 错误信息 |
scaleType | String | 画面比例: 9:16 - 画面宽度:1080,高度:1920 16:9 - 画面宽度:1920,高度:1080 |
resSpecType | String | 资源规格: STANDARD - 2D数字人实时互动【基础版】 ADVANCED - 2D数字人实时互动【高级版】 |
frames | Array | 画面信息 |
2.2.1. frame参数说明
参数名 | 类型 | 是否必填 | 说明 |
layers | Array | 是 | 图层信息 |
videoScript | Object | 是 | 画面脚本信息 |
2.2.2. layer参数说明
参数名 | 类型 | 是否必填 | 说明 |
type | String | 是 | BACKGROUND-背景 ANCHOR-主播 |
positionX | Integer | BACKGROUND-不填 ANCHOR-必填 | X坐标 |
positionY | Integer | BACKGROUND-不填 ANCHOR-必填 | Y坐标 |
width | Integer | BACKGROUND-不填 ANCHOR-必填 | 图片宽度 |
height | Integer | BACKGROUND-不填 ANCHOR-必填 | 图片高度 |
material | Object | 是 | 素材信息 |
2.2.3. material参数说明
参数名 | 类型 | 是否必填 | 说明 |
id | String | BACKGROUND-不填 ANCHOR-必填 | 主播id |
url | String | BACKGROUND-必填 | 素材url(http地址) |
format | String | BACKGROUND-必填 | 素材格式:video/mp4,image/png image/jpg,image/jpeg |
2.2.4. videoScript参数说明
参数名 | 类型 | 是否必填 | 说明 |
voiceTemplateId | String | 是 | 声音模型id(需要跟主播匹配) |
speedRate | String | 是 | 语速倍数:范围[0.8 到 2.0] 正常语速1,仅支持一位小数 |
3. 启动会话
3. 启动会话
入参 StartAvatarSessionRequest
参数名 | 类型 | 是否必填 | 说明 |
projectId | String | Y | 项目ID |
requestId | String | Y | 请求id用于幂等 |
customPushUrl | String | N | 转推流地址 |
出参 StartAvatarSessionResponse
参数名 | 类型 | 说明 |
sessionId | String | 会话id |
channelToken | String | 频道信息 |
webSocketUrl | String | 流式交互链接 |
channelToken 字段解析
{
"channelId":"123",//频道ID
"token":"", // 令牌
"expireTime":600,//过期时间(单位秒)
"nonce":"",//随机数
"userId":"",//用户ID
"appId":""//应用ID
}
4. 停止会话
入参 StopAvatarSessionRequest
参数名 | 类型 | 是否必填 | 说明 |
projectId | String | Y | 项目ID |
sessionId | String | Y | 会话ID |
出参 StopAvatarSessionResponse
参数名 | 类型 | 说明 |
status | String | 停止结果:Stopped - 已停止 StoppedFail - 停止失败 |
5. 有效资源查询
入参 QueryAvatarResourceRequest
参数名 | 类型 | 是否必填 | 说明 |
出参 QueryAvatarResourceResponse
参数名 | 类型 | 说明 |
queryResourceInfoList | List<QueryResourceInfo> | 资源信息 |
QueryResourceInfo
参数名 | 类型 | 说明 |
resourceId | String | 资源id |
type | String | 资源类型: STANDARD - 2D数字人实时互动【基础版】 ADVANCED - 2D数字人实时互动【高级版】 |
validPeriodTime | String | 有效期(时间戳) 例如:1719904342237 |
6. 保存数字人项目
6.1. 入参 SaveAvatarProjectRequest
参数名 | 类型 | 是否必填 | 说明 |
operateType | String | 是 | 操作类型: CREATE - 创建并发布 EDIT - 编辑并发布 |
projectId | String | CREATE - 不填 EDIT-必填 | 项目id |
projectName | String | CREATE - 必填 EDIT-选填 | 项目名称 |
scaleType | String | CREATE - 必填 EDIT - 不填 | 画面比例: 9:16 - 画面宽度:1080,高度:1920 16:9 - 画面宽度:1920,高度:1080 |
resSpecType | String | CREATE - 必填 EDIT- 不填 | 资源规格: STANDARD - 2D数字人实时互动【基础版】 ADVANCED - 2D数字人实时互动【高级版】 |
agentId | String | CREATE - 选填 EDIT- 选填 | 智能体id |
frames | Array | CREATE - 必填 EDIT- 必填 | 画面信息 |
6.1.1. frame参数说明
参数名 | 类型 | 是否必填 | 说明 |
layers | Array | 是 | 图层信息 |
videoScript | Object | 是 | 画面脚本信息 |
6.1.2. layer参数说明
参数名 | 类型 | 是否必填 | 说明 |
type | String | 是 | BACKGROUND-背景 ANCHOR-主播 |
positionX | Integer | BACKGROUND-不填 ANCHOR-必填 | X坐标(详见坐标说明) |
positionY | Integer | BACKGROUND-不填 ANCHOR-必填 | Y坐标(详见坐标说明) |
width | Integer | BACKGROUND-不填 ANCHOR-必填 | 图片宽度(不能超过画面宽度) |
height | Integer | BACKGROUND-不填 ANCHOR-必填 | 图片高度(不能超过画面高度) |
material | Object | 是 | 素材信息 |
6.1.3. material参数说明
参数名 | 类型 | 是否必填 | 说明 |
id | String | BACKGROUND-不填 ANCHOR-必填 | 主播id |
url | String | BACKGROUND-必填 | 素材url(http地址) |
format | String | BACKGROUND-必填 | 素材格式:image/png image/jpg,image/jpeg |
6.1.4. videoScript参数说明
参数名 | 类型 | 是否必填 | 说明 |
voiceTemplateId | String | 是 | 声音模型id(需要跟主播匹配) |
speedRate | String | 是 | 语速倍数:范围[0.8 到 2.0] 正常语速1,仅支持一位小数 |
6.1.5. 坐标说明
坐标参数均为画面左上角对接图片左上角的距离
坐标如果存在小数,舍弃小数, 如下图:X=470, Y=700
6.2. 出参
参数名 | 类型 | 说明 |
status | String | 启动结果: DRAFT - 草稿 DELETED - 已删除 DEPLOYING - 发布中 DEPLOYED - 已发布 DEPLOY_FAIL - 发布失败 |
projectId | String | 项目id |
projectName | String | 名称 |
agentId | String | 智能体id |
errorMsg | String | 错误信息 |
7. 操作数字人项目
7.1. 入参 OperateAvatarProjectRequest
参数名 | 类型 | 是否必填 | 说明 |
projectId | String | Y | 项目ID |
operateType | String | Y | 操作类型 DELETE-删除 OFFLINE-下线 DEPLOY-发布 |
resChannelNum | Integer | N | 发布路数, 默认1路 |
resType | String | 发布操作必填 | 资源类型: FREE - 免费资源, FORMAL - 正式资源 |
7.2. 出参
参数名 | 类型 | 说明 |
success | Boolean | 结果:true - 成功 |
8. 查询项目会话状态
8.1. 入参 QuerySessionInfoRequest
参数名 | 类型 | 是否必填 | 说明 |
projectId | String | Y | 项目ID |
statusList | Array | Y | 查询的状态集合: STARTED - 开启 STOPPED - 停止 |
pageNo | Integer | Y | 页码 |
pageSize | Integer | N | 页码大小默认10 |
8.2. 出参
参数名 | 类型 | 说明 |
total | Long | 符合条件的会话总数 |
queryResourceInfoList | Array | 会话信息 |
会话信息
参数名 | 类型 | 说明 |
sessionId | String | 会话id |
status | String | 会话状态: FREE - 空闲(高级版数字人没有空闲状态) BUSY - 忙线 STOPPED - 停止 |
9. 查询数字人形象模型
ListAnchor
9.1. 入参
参数名称 | 参数类型 | 是否必填 | 说明 |
coverRate | string | N | 主播形象比例 9:16 竖版 16:9 横版 |
anchorType | string | N | 主播类型: PUBLIC_MODEL:公模 PRIVATE_MODEL:私模 默认查询公模PUBLIC_MODEL |
digitalHumanType | string | N | 主播类别: dynamicReality 动态实景数字人(暂时不可使用);staticTransparency 静态数字人 |
useScene | string | Y | 使用场景 realTimeInteractivity |
WebSocket实时互动
WebSocket对接整体流程
开启会话,获取websocket链接和RTC channel信息;
建立websocket连接;
循环发送"通道准备"数据包,直至收到"通道准备完成";
使用RTC SDK和channel信息拉取视频流;
(可选)发送查询开场白数据包,进行开场白互动;
(可选)发送文本驱动数据包进行文本驱动数字人;
(可选)发送音频驱动头包+数据包+尾包进行音频驱动数字人;
停止会话
关闭websocket连接;
建立websocket连接代码示例:
@ClientEndpoint
public class WebSocketClient {
private Session userSession = null;
public WebSocketClient(URI endpointURI) {
try {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(this, endpointURI);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnOpen
public void onOpen(Session userSession) {
this.userSession = userSession;
System.out.println("Connected to server");
}
@OnClose
public void onClose(Session userSession, CloseReason reason) {
this.userSession = null;
System.out.println("Disconnected from server: " + reason);
}
@OnMessage
public void onMessage(byte[] message) {
//1. 获取第1字节帧号
int frame = Byte.toUnsignedInt(message[0]);
//2. 获取第2字节数据类型
int dataType = Byte.toUnsignedInt(message[1]);
//3. 获取第3字节业务类型
int bizType = Byte.toUnsignedInt(message[2]);
//4. 获取第4-11字节序列号
byte[] serialNumberBytes = Arrays.copyOfRange(message, 3, 11);
String serialNumberString = new String(serialNumberBytes, StandardCharsets.UTF_8);
//5. 获取剩余内容数据,数据结构需要结合具体业务类型文档
byte[] contentJsonBytes = Arrays.copyOfRange(message, 11, message.length);
String contentJsonString = new String(contentJsonBytes, StandardCharsets.UTF_8);
//6. 执行相应业务逻辑......
}
public void sendMessage(byte[] message) {
try {
if (this.userSession != null) {
this.userSession.getBasicRemote().sendBinary(ByteBuffer.wrap(message));
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
// 1. 建立ws连接
WebSocketClient client = new WebSocketClient(URI.create("ws://127.0.0.1:7005/v1/interaction?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uSWQiOiI4ZjM3YmVhOC1lNDYxLTRhNzktODczZS00Yzg2ZTI2OWU4YTAiLCJleHAiOjE3MjU1MTY3NTIsImFsaXl1bk1haW5JZCI6IjE1Mzk3MDQ3MDY0MTMyNzgifQ.PAvCT7gY1VWNGJQc1cmubVLd76INRVZnhHdoBRV9-Rc&sessionId=8f37bea8-e461-4a79-873e-4c86e269e8a0"));
// 2. 通道准备业务
// 2.1 组装数据包
byte frameId = (byte) 0;
byte dataType = (byte) 1;
byte bizType = (byte) 131;
// 正常业务使用随机生成8位序列号即可
byte[] serialNumber = new byte[8];
byte[] contentBytes = new byte[0];
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
}
WebSocket消息结构
1. 整体结构
整体数据包格式为: 11字节包头+N字节包内容
前11字节包头格式为:1字节帧类型+1字节数据类型+1字节业务类型+8字节序列号(调用端随机生成,排查问题使用)
public class InteractionWebSocketProtocol {
// 1字节帧类型
private InteractionProtocolFrameEnum interactionProtocolFrameEnum;
// 1字节数据类型
private DataTypeEnum dataType;
// 1字节业务类型
private BusinessTypeEnum businessType;
// 8字节序列号
private String serialNumber;
// N字节消息 二进制格式
private byte[] messageBytes;
}
2. 帧类型
bit | 描述 | 备注 |
0000 0000 | 控制帧 | 用于客户端传递控制指令 |
0000 0001 | 数据帧 | 用于服务端业务数据返回 |
0000 0010 | 消息帧 | 用于服务端错误消息返回 |
0000 1110 | 心跳帧ping | 客户端心跳发起 |
0000 1111 | 心跳帧pong | 服务端心跳返回 |
3. 数据类型
bit | 描述 | 备注 |
0000 0000 | binary | 二进制音频数据使用 |
0000 0001 | json |
4. 业务类型
业务类型:
bit | 描述 | 备注 |
0000 0000 | 查询开场白 | 查询数字人开场白 |
0000 0010 | 打断 | 打断数字人视频流 |
1000 0000 | 文本驱动 | 输入文本来驱动数字人播报 |
1000 0010 | 音频驱动【开始,结束】 | 发送音频驱动开始,结束 |
1000 0001 | 音频驱动【数据】 | 发送音频驱动二进制数据 |
1000 0011 | 通道准备 | 推送RTC通道数据 |
1111 1111 | 通用业务类型 | 客户端发送ping,服务端返回pong、msg统一采用通用业务类型 |
业务内容出入参格式:
4.1. 查询开场白
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
返回:
字段名 | 类型 | 备注 |
success | Boolean | 成功标志 |
sessionId | String | 会话id |
content | String | 返回内容 |
finish | Boolean | 流式返回结束标志 |
relatedImages | String[] | 关联的图片url列表 |
relatedVideos | String[] | 关联的视频url列表 |
messageId | String | 消息ID |
4.2. 打断数字人播报
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
返回:暂无
4.3. 文本驱动数字人
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
text | String | 是 | 驱动文本 |
askType | Integer | 是 | 提问方式: 1 - 提问 2 - 播报 |
返回:
字段名 | 类型 | 备注 |
success | Boolean | 成功标志 |
sessionId | String | 会话id |
content | String | 返回内容 |
finish | Boolean | 结束标志 |
relatedImages | String[] | 关联的图片url列表 |
relatedVideos | String[] | 关联的视频url列表 |
messageId | String | 消息ID |
4.4. 音频驱动【开始】
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
type | String | 是 | 固定值:startAsrAudio |
askType | Integer | 是 | 提问方式: 1 - 提问 2 - 播报 |
返回:
字段名 | 类型 | 备注 |
sessionId | String | 会话id |
success | Boolean | 成功标志 |
4.5. 音频驱动【数据】
请求:二进制数据,pcm格式
返回:提问模式的音频识别结果返回见接口说明 第3点接收识别结果;播报模式暂无返回。
数字人播报的文本返回见4.3文本驱动数字人返回;
4.6. 音频驱动【结束】
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
type | String | 是 | 固定值:endAsrAudio |
返回:暂无
4.7. 通道准备
请求:
字段名 | 类型 | 是否必填 | 备注 |
sessionId | String | 是 | 会话id |
返回:
字段名 | 类型 | 备注 |
sessionId | String | 会话id |
status | String | INIT - 准备中 FAIL - 异常 READY - 准备成功 |
4.8. 通用业务类型
消息格式:
字段 | 类型 | 描述 |
code | int | 错误码 |
message | string | 错误描述 |
错误码:
code | 描述 |
10000 | 系统错误 |
10001 | 权限不足 |
WebSocket对接示例
1. 开场白
客户端发送:
public void sendOpenning() {
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 0; //开场白业务类型:0
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\"sessionId\":\"xxx\"}"; //需替换sessionId
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
客户端接收:
@OnMessage
public void onMessage(byte[] message) {
//1. 获取第1字节帧号
int frame = Byte.toUnsignedInt(message[0]);
//2. 获取第2字节数据类型
int dataType = Byte.toUnsignedInt(message[1]);
//3. 获取第3字节业务类型
int bizType = Byte.toUnsignedInt(message[2]);
//4. 获取第4-11字节序列号
byte[] serialNumberBytes = Arrays.copyOfRange(message, 3, 11);
String serialNumberString = new String(serialNumberBytes, StandardCharsets.UTF_8);
//5. 获取剩余内容数据,数据结构需要结合具体业务类型文档
// 如果是开场白业务
if (bizType == 0) {
byte[] contentJsonBytes = Arrays.copyOfRange(message, 11, message.length);
String contentJsonString = new String(contentJsonBytes, StandardCharsets.UTF_8);
//6. 执行相应业务逻辑......
}
}
2. 打断
客户端发送:
public void sendOpenning() {
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 2; //打断业务类型:2
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\"sessionId\":\"xxx\"}"; //需替换sessionId
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
客户端接收:无需
3. 文本驱动
客户端发送:
public void sendText() {
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 128; //文本驱动业务类型:128
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\n" +
" \"sessionId\":\"xxx\",\n" + //需替换sessionId
" \"text\":\"xxx\",\n" + //需替换text文本
" \"askType\":1 \n" + // 1:提问;2:播报
"}";
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
客户端接收:
@OnMessage
public void onMessage(byte[] message) {
//1. 获取第1字节帧号
int frame = Byte.toUnsignedInt(message[0]);
//2. 获取第2字节数据类型
int dataType = Byte.toUnsignedInt(message[1]);
//3. 获取第3字节业务类型
int bizType = Byte.toUnsignedInt(message[2]);
//4. 获取第4-11字节序列号
byte[] serialNumberBytes = Arrays.copyOfRange(message, 3, 11);
String serialNumberString = new String(serialNumberBytes, StandardCharsets.UTF_8);
//5. 获取剩余内容数据,数据结构需要结合具体业务类型文档
// 文本驱动业务
if (bizType == 128) {
byte[] contentJsonBytes = Arrays.copyOfRange(message, 11, message.length);
String contentJsonString = new String(contentJsonBytes, StandardCharsets.UTF_8);
//6. 执行相应业务逻辑......
}
}
4. 音频驱动
客户端发送:
public void sendAudio() {
// 1. 发送音频驱动头数据包
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 130; //音频驱动头尾业务类型:130
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\n" +
"\t\"sessionId\":\"xxx\",\n" +
"\t\"type\": \"startAsrAudio\",\n" +
"\t\"askType\":1 " +
"}";
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
client.sendMessage(message);
// 2. 多次发送音频二进制数据
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 0; //数据类型:binary
byte bizType = (byte) 129; //音频驱动数据业务类型:129
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
byte[] audioData = new Byte[1024]; //需替换音频数据
byte[] contentBytes = audioData;
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
client.sendMessage(message);
// 3. 发送音频驱动尾包
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 130; //音频驱动头尾业务类型:130
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\n" +
"\t\"sessionId\":\"xxx\",\n" +
"\t\"type\":\"endAsrAudio\",\n" +
"\t\"askType\": 1 " +
"}";
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
client.sendMessage(message);
}
客户端接收:
@OnMessage
public void onMessage(byte[] message) {
//1. 获取第1字节帧号
int frame = Byte.toUnsignedInt(message[0]);
//2. 获取第2字节数据类型
int dataType = Byte.toUnsignedInt(message[1]);
//3. 获取第3字节业务类型
int bizType = Byte.toUnsignedInt(message[2]);
//4. 获取第4-11字节序列号
byte[] serialNumberBytes = Arrays.copyOfRange(message, 3, 11);
String serialNumberString = new String(serialNumberBytes, StandardCharsets.UTF_8);
//5. 获取剩余内容数据,数据结构需要结合具体业务类型文档
// 获取音频驱动返回
if (bizType == 129) {
byte[] contentJsonBytes = Arrays.copyOfRange(message, 11, message.length);
String contentJsonString = new String(contentJsonBytes, StandardCharsets.UTF_8);
//6. 执行相应业务逻辑......
}
}
5. 通道准备
客户端发送:
public void channelReady() {
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 131; //通道准备业务类型:131
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{\"sessionId\":\"xxx\"}"; //需替换sessionId
byte[] contentBytes = content.getBytes(); // 正常业务使用随机生成8位序列号即可
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
客户端接收:
@OnMessage
public void onMessage(byte[] message) {
//1. 获取第1字节帧号
int frame = Byte.toUnsignedInt(message[0]);
//2. 获取第2字节数据类型
int dataType = Byte.toUnsignedInt(message[1]);
//3. 获取第3字节业务类型
int bizType = Byte.toUnsignedInt(message[2]);
//4. 获取第4-11字节序列号
byte[] serialNumberBytes = Arrays.copyOfRange(message, 3, 11);
String serialNumberString = new String(serialNumberBytes, StandardCharsets.UTF_8);
//5. 获取剩余内容数据,数据结构需要结合具体业务类型文档
// 通道准备
if (bizType == 131) {
byte[] contentJsonBytes = Arrays.copyOfRange(message, 11, message.length);
String contentJsonString = new String(contentJsonBytes, StandardCharsets.UTF_8);
//6. 执行相应业务逻辑......
}
}
6. 心跳
客户端发送:
public void sendPing() {
byte frameId = (byte) 0; //帧类型:控制帧
byte dataType = (byte) 1; //数据类型:json
byte bizType = (byte) 255; //通用心跳业务类型:255
byte[] serialNumber = new byte[8]; // 正常业务使用随机生成8位序列号即可
String content = "{}"; //无需
byte[] contentBytes = content.getBytes();
byte[] message = new byte[11 + contentBytes.length];
ByteBuffer bf = ByteBuffer.wrap(message)
.put(frameId)
.put(dataType)
.put(bizType)
.put(serialNumber)
.put(contentBytes);
message = bf.array();
// 3. 发送数据包
client.sendMessage(message);
}
客户端接收:无需
对接详情
PHP
对接示例
require 'vendor/autoload.php';
use AlibabaCloud\SDK\Imarketing\V20220704\Models\GetOssUploadSignatureRequest;
use AlibabaCloud\SDK\IntelligentCreation\V20240313\IntelligentCreation;
use Darabonba\OpenApi\Models\Config as AlibabaConfig;
$config = new AlibabaConfig();
$config->accessKeyId = '****';
$config->accessKeySecret = '****';
$config->endpoint = "intelligentcreation.cn-zhangjiakou.aliyuncs.com";
$intelligentCreationClient = new IntelligentCreation($config);
$request = new QueryAvatarProjectRequest();
$request->projectId = '111';
try {
$response = $intelligentCreationClient->queryAvatarProject($request);
var_dump($response->toMap());
} catch (TeaError $e) {
Log::error($e);
}
2.4.0
u-17374653-852f-4536-9c8f-61f96b9890ef-composer-tea.zip
composer require alibabacloud/intelligentcreation-20240313 2.4.0
Java
对接示例
package com.aliyun.intelligentcreation20240313;
import com.aliyun.intelligentcreation20240313.models.*;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
public class TestAvatarTest {
public TestAvatarTest() throws Exception {
}
public static void main(String[] args) throws Exception {
TestAvatarTest avatarTest = new TestAvatarTest();
try {
String projectId = "780931376329506816";
avatarTest.queryAvatarProjectTest(projectId);
String sessionId = avatarTest.startAvatarSessionRequest(projectId);
avatarTest.checkSessionRequest(projectId,sessionId);
avatarTest.sendTextMsgRequest(projectId,sessionId);
avatarTest.stopAvatarSessionRequest(projectId,sessionId);
avatarTest.queryAvatarResourceRequest(projectId);
} catch (TeaException e) {
Gson gson = new Gson();
System.out.println(e.getMessage());
System.out.println(gson.toJson(e.getData()));
}
}
String url = "intelligentcreation.cn-zhangjiakou.aliyuncs.com";
//初始化配置
String ak = "**";
String sk = "**";
Config config = new Config().setAccessKeyId(ak)
.setAccessKeySecret(sk)
.setEndpoint(url);
// 创建客户端
Client client = new Client(config);
void queryAvatarProjectTest(String projectId) throws Exception {
Gson gson = new Gson();
Map<String, Object> map = new HashMap<>();
map.put("projectId",projectId);
QueryAvatarProjectRequest request = QueryAvatarProjectRequest.build(map);
// 请求接口
QueryAvatarProjectResponse response = client.queryAvatarProject(request);
System.out.println(gson.toJson(response));
if (response.getStatusCode().equals(200)) {
System.out.println("queryAvatarProjectTest 请求成功");
}
}
String startAvatarSessionRequest(String projectId) throws Exception {
Gson gson = new Gson();
Map<String, Object> map = new HashMap<>();
map.put("projectId",projectId);
StartAvatarSessionRequest startAvatarSessionRequest = StartAvatarSessionRequest.build(map);
StartAvatarSessionResponse response = client.startAvatarSession(startAvatarSessionRequest);
System.out.println(gson.toJson(response));
if (response.getStatusCode().equals(200)) {
System.out.println("startAvatarSessionRequest 请求成功");
return response.getBody().getSessionId();
}
return null;
}
void stopAvatarSessionRequest(String projectId, String sessionId) throws Exception {
Gson gson = new Gson();
Map<String, Object> map = new HashMap<>();
map.put("projectId",projectId);
map.put("sessionId",sessionId);
StopAvatarSessionRequest request = StopAvatarSessionRequest.build(map);
StopAvatarSessionResponse response = client.stopAvatarSession(request);
System.out.println(gson.toJson(response));
if (response.getStatusCode().equals(200)) {
System.out.println("stopAvatarSessionRequest 请求成功");
}
}
void queryAvatarResourceRequest(String projectId) throws Exception {
Gson gson = new Gson();
Map<String, Object> map = new HashMap<>();
QueryAvatarResourceRequest request = QueryAvatarResourceRequest.build(map);
QueryAvatarResourceResponse response = client.queryAvatarResource(request);
System.out.println(gson.toJson(response));
if (response.getStatusCode().equals(200)) {
System.out.println("queryAvatarResourceRequest 请求成功");
}
}
}
2.4.0
u-17374653-852f-4536-9c8f-61f96b9890ef-java-tea.zip
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>intelligentcreation20240313</artifactId>
<version>2.4.0</version>
</dependency>
2.1.0
u-ce29b5c7-8a37-4590-98f4-71a02e5366f9-java-tea.zip
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>intelligentcreation20240313</artifactId>
<version>2.1.0</version>
</dependency>
安卓
实时音视频SDK接入文档: 实时音频快速入门指南-阿里云帮助中心
※ mLocalSurfaceContainer 是 视频流的展示容器
※ projectId 是项目Id
添加maven仓库
maven(url = "https://maven.aliyun.com/nexus/content/repositories/releases")
添加推流SDK依赖
implementation("com.aliyun.aio:AliVCSDK_ARTC:6.8.7")
implementation("com.aliyun:intelligentcreation20240313:2.1.0")
implementation("com.aliyun:tea-openapi:0.3.4")
初始化拉流引擎
private fun createEngine() {
mAliRtcEngine = AliRtcEngine.getInstance(applicationContext)
mAliRtcEngine?.setRtcEngineEventListener(object : AliRtcEngineEventListener() {
/* SDK与服务器的链接状态通知,务必处理链接失败的情况 */
override fun onConnectionStatusChange(
aliRtcConnectionStatus: AliRtcEngine.AliRtcConnectionStatus,
aliRtcConnectionStatusChangeReason: AliRtcEngine.AliRtcConnectionStatusChangeReason
) {
super.onConnectionStatusChange(
aliRtcConnectionStatus,
aliRtcConnectionStatusChangeReason
)
if (aliRtcConnectionStatus == AliRtcEngine.AliRtcConnectionStatus.AliRtcConnectionStatusFailed) {
/* TODO: 务必处理;建议业务提示客户,此时SDK内部已经尝试了各种恢复策略已经无法继续使用时才会上报 */
} else {
/* TODO: 可选处理;增加业务代码,一般用于数据统计、UI变化 */
}
}
/* SDK尝试控制本地设备异常 */
override fun OnLocalDeviceException(
aliRtcEngineLocalDeviceType: AliRtcEngine.AliRtcEngineLocalDeviceType,
aliRtcEngineLocalDeviceExceptionType: AliRtcEngine.AliRtcEngineLocalDeviceExceptionType,
s: String
) {
//TODO 发生该异常时App需要检测权限、设备硬件是否正常。
}
override fun onJoinChannelResult(
result: Int,
channel: String,
userId: String,
elapsed: Int
) {
super.onJoinChannelResult(result, channel, userId, elapsed)
Log.i(TAG, "onJoinChannelResult result=$result,channel=$channel,userId=$userId,elapsed=$elapsed")
}
override fun onLeaveChannelResult(result: Int, stats: AliRtcEngine.AliRtcStats) {
super.onLeaveChannelResult(result, stats)
Log.i(TAG, "onLeaveChannelResult result=$result")
}
})
mAliRtcEngine?.setRtcEngineNotify(object : AliRtcEngineNotify() {
/* 鉴权距离过期还有30s时会回调,务必进行鉴权时间刷新 */
override fun onAuthInfoWillExpire() {
super.onAuthInfoWillExpire()
/* TODO: 务必处理;业务触发重新获取当前channel,user的鉴权信息,然后设置refreshAuthInfo即可 */
}
/* 业务可能会触发踢人的动作,所以这个地方也需要处理 */
override fun onBye(code: Int) {
super.onBye(code)
/* TODO: 建议业务根据自己的场景,进行对应的处理 */
}
override fun onRemoteUserOnLineNotify(uid: String, elapsed: Int) {
super.onRemoteUserOnLineNotify(uid, elapsed)
Log.i(TAG, "onRemoteUserOnLineNotify uid=$uid,elapsed=$elapsed")
}
override fun onRemoteUserOffLineNotify(
uid: String,
aliRtcUserOfflineReason: AliRtcEngine.AliRtcUserOfflineReason
) {
super.onRemoteUserOffLineNotify(uid, aliRtcUserOfflineReason)
Log.i(TAG, "onRemoteUserOnLineNotify uid=$uid,aliRtcUserOfflineReason=$aliRtcUserOfflineReason")
}
override fun onRemoteTrackAvailableNotify(
uid: String,
aliRtcAudioTrack: AliRtcEngine.AliRtcAudioTrack,
aliRtcVideoTrack: AliRtcEngine.AliRtcVideoTrack
) {
super.onRemoteTrackAvailableNotify(uid, aliRtcAudioTrack, aliRtcVideoTrack)
Log.i(TAG, "onRemoteUserOnLineNotify uid=$uid,aliRtcAudioTrack=$aliRtcAudioTrack,aliRtcVideoTrack=$aliRtcVideoTrack")
mLocalSurfaceContainer.post{
if (aliRtcVideoTrack == AliRtcEngine.AliRtcVideoTrack.AliRtcVideoTrackCamera
|| aliRtcVideoTrack == AliRtcEngine.AliRtcVideoTrack.AliRtcVideoTrackBoth
) {
val remote_canvas = AliRtcVideoCanvas()
val remoteView = mAliRtcEngine?.createRenderSurfaceView(this@PreviewActivity)
if (remoteView != null) {
remoteView.setZOrderOnTop(true)
remoteView.setZOrderMediaOverlay(true)
}
remote_canvas.view = remoteView
mLocalSurfaceContainer.addView(
remote_canvas.view, FrameLayout.LayoutParams(
mLocalSurfaceContainer.width,
mLocalSurfaceContainer.height
)
)
mAliRtcEngine?.setRemoteViewConfig(
remote_canvas,
uid,
AliRtcEngine.AliRtcVideoTrack.AliRtcVideoTrackCamera
)
} else {
mAliRtcEngine?.setRemoteViewConfig(
null,
uid,
AliRtcEngine.AliRtcVideoTrack.AliRtcVideoTrackCamera
)
}
}
}
})
}
入会前引擎参数配置
private fun initEngineBeforeJoin() {
/* 可选:入会前的参数设置 */
mAliRtcEngine?.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive)
mAliRtcEngine?.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkLive)
/* 设置音频的属性 */
mAliRtcEngine?.setAudioProfile(
AliRtcEngine.AliRtcAudioProfile.AliRtcEngineStereoHighQualityMode,
AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode
)
/* 可选:摄像头预览,不设置也会进行推流 */
val canvas = AliRtcVideoCanvas()
canvas.view = mAliRtcEngine?.createRenderSurfaceView(this)
mLocalSurfaceContainer.removeAllViews()
if (canvas.view != null) {
mLocalSurfaceContainer.addView(
canvas.view,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
}
mAliRtcEngine?.setLocalViewConfig(
canvas,
AliRtcEngine.AliRtcVideoTrack.AliRtcVideoTrackCamera
)
}
初始化pop接口SDK
private fun initPopApiClient(){
val config = Config()
.setAccessKeyId(KEY_ID)
.setAccessKeySecret(KEY_SECRET)
.setEndpoint(HOST)
client = Client(config)
}
获取项目信息
private suspend fun getProjectInfo() = withContext(Dispatchers.IO) {
val map: Map<String , String> = mutableMapOf<String, String>().apply {
put(PROJECT_ID , projectId?:"")
}
client?.queryAvatarProject(QueryAvatarProjectRequest.build(map))?.apply {
Log.e(TAG , "statusCode:${statusCode}")
if (statusCode == 200) {
Log.e(TAG , "projectName:${body.projectName}")
tvProjectName.post {
tvProjectName.text = body.projectName
}
}
}
}
获取会议信息
private suspend fun getMeetingInfo() = withContext(Dispatchers.IO){
val map: Map<String , String> = mutableMapOf<String, String>().apply {
put(PROJECT_ID , projectId?:"")
}
val response: StartAvatarSessionResponse? = client?.startAvatarSession(StartAvatarSessionRequest.build(map))
response?.apply {
Log.e(TAG , "statusCode:${statusCode}")
if (statusCode == 200) {
Log.e(TAG , "sessionId:${body.sessionId},channelToken:${body.channelToken}")
sessionId = body.sessionId
val meetingJsonObj = JSONObject(body.channelToken)
var channelId = ""
var token = ""
var nonce = ""
var userId = ""
var appId = ""
var expireTime = 0L
var gslbList:MutableList<String> = mutableListOf()
if (meetingJsonObj.has(CHANNEL_ID)){
channelId = meetingJsonObj.optString(CHANNEL_ID)
}
if (meetingJsonObj.has(TOKEN)){
token = meetingJsonObj.optString(TOKEN)
}
if (meetingJsonObj.has(NONCE)){
nonce = meetingJsonObj.optString(NONCE)
}
if (meetingJsonObj.has(USER_ID)){
userId = meetingJsonObj.optString(USER_ID)
}
if (meetingJsonObj.has(APP_ID)){
appId = meetingJsonObj.optString(APP_ID)
}
if (meetingJsonObj.has(EXPIRE_TIME)){
expireTime = meetingJsonObj.optLong(EXPIRE_TIME)
}
if (meetingJsonObj.has(GSLB_LIST)) {
val gslbListJson = meetingJsonObj.optJSONArray(GSLB_LIST)
for (i in 0 until gslbListJson.length()) {
gslbList.add(gslbListJson.optString(i))
}
}
join(appId , channelId , userId , nonce,expireTime , token , gslbList.toTypedArray())
}
}
}
入会
private fun join(appId:String,channelId:String , userId:String ,
nonce:String , expireTime:Long,token:String,gslbList:Array<String>){
val userInfo = AliRtcAuthInfo()
userInfo.setAppId(appId)
userInfo.setChannelId(channelId)
userInfo.setUserId(userId)
userInfo.setNonce(nonce)
userInfo.setTimestamp(expireTime)
userInfo.setGslb(gslbList)
userInfo.setToken(token)
mAliRtcEngine?.joinChannel(userInfo, "testUserName")
resetCountDown()
}
发送问题
private fun send(){
val question = etInputQuestion.text.toString().trim()
if (TextUtils.isEmpty(question)) {
Toast.makeText(this , "question content cannot be null" , Toast.LENGTH_SHORT).show()
return
}
GlobalScope.launch {
sendText(question)
}
}
private suspend fun sendText(text:String) = withContext(Dispatchers.IO){
val map: Map<String , String> = mutableMapOf<String, String>().apply {
put(PROJECT_ID , projectId?:"")
put(SESSION_ID , sessionId)
put(REQUEST_ID , (System.currentTimeMillis()/1000).toString())
put(TEXT , text)
put(TYPE , "1")
}
client?.sendTextMsg(SendTextMsgRequest.build(map))?.apply {
onSendResponse(statusCode == 200 && body.status == "SUCCESS" , text)
}
}
private fun onSendResponse(result:Boolean , text:String){
tvExpireHint.post {
if (result) {
etInputQuestion.setText("")
questionAdapter?.data?.add(0 , text)
questionAdapter?.notifyItemInserted(0)
rvQuestion.scrollToPosition(0)
resetCountDown()
Toast.makeText(this@PreviewActivity , "发送成功" , Toast.LENGTH_SHORT).show()
}else {
Toast.makeText(this@PreviewActivity , "发送失败" , Toast.LENGTH_SHORT).show()
}
}
}
离会
private fun leave(){
mAliRtcEngine?.leaveChannel()
findViewById<LinearLayout>(R.id.llEnd).visibility = View.VISIBLE
val map: Map<String , String> = mutableMapOf<String, String>().apply {
put(PROJECT_ID , projectId?:"")
put(SESSION_ID , sessionId)
}
GlobalScope.launch {
async {
client?.stopAvatarSession(StopAvatarSessionRequest.build(map))?.apply {
Log.e(TAG , "statusCode = ${statusCode}, status = ${body.status}")
}
}
}
}
资源回收
override fun onDestroy() {
super.onDestroy()
mAliRtcEngine?.destroy()
mAliRtcEngine = null
handler.removeCallbacksAndMessages(null)
}
10min无提问,自动离会
private var handler: Handler = Handler(Looper.getMainLooper())
private val mRunnable = Runnable {
dealCountDown()
}
private fun dealCountDown(){
if (messageCount == 0) {
//离会
leave()
return
}
//小于1min ,提示
val timeStr = when {
60 == messageCount-> "01:00"
(messageCount in 1..9) -> "00:0${messageCount}"
else -> "00:${messageCount}"
}
llExpireHint.visibility = View.VISIBLE
tvExpireHint.text = "检测到您无操作,预览将于${timeStr}自动关闭,以避免占用路数"
messageCount--
handler.postDelayed(mRunnable , 1000)
}
//在入会和发送问题之后调用
private fun resetCountDown(){
handler.removeCallbacksAndMessages(null)
messageCount = 60
handler.postDelayed(mRunnable , 1000 * 60 * 9)
}