版本變更
版本 | 描述 | 時間 |
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. 查詢數字人項目信息
入參 QueryAvatarProjectRequest
參數名 | 類型 | 是否必填 | 說明 |
projectId | String | Y | 項目ID |
出參 QueryAvatarProjectResponse
參數名 | 類型 | 說明 |
status | String | 啟動結果:DEPLOYING - 發布中 DEPLOYED - 已發布 DEPLOY_FAIL - 發布失敗 |
projectName | String | 名稱 |
agentId | String | 智能體id |
errorMsg | String | 發布失敗原因 |
2. 啟動會話
入參 StartAvatarSessionRequest
參數名 | 類型 | 是否必填 | 說明 |
projectId | String | Y | 項目ID |
requestId | String | Y | 請求id用于冪等 |
出參 StartAvatarSessionResponse
參數名 | 類型 | 說明 |
sessionId | String | 會話id |
channelToken | String | 頻道信息 |
webSocketUrl | String | 流式交互鏈接 |
channelToken 字段解析
{
"channelId":"123",//頻道ID
"token":"", // 令牌
"expireTime":600,//過期時間(單位秒)
"nonce":"",//隨機數
"userId":"",//用戶ID
"appId":""http://應用ID
}
3. 停止會話
入參 StopAvatarSessionRequest
參數名 | 類型 | 是否必填 | 說明 |
projectId | String | Y | 項目ID |
sessionId | String | Y | 會話ID |
出參 StopAvatarSessionResponse
參數名 | 類型 | 說明 |
status | String | 停止結果:Stopped - 已停止 StoppedFail - 停止失敗 |
4. 有效資源查詢
入參 QueryAvatarResourceRequest
參數名 | 類型 | 是否必填 | 說明 |
出參 QueryAvatarResourceResponse
參數名 | 類型 | 說明 |
queryResourceInfoList | List<QueryResourceInfo> | 資源信息 |
QueryResourceInfo
參數名 | 類型 | 說明 |
resourceId | String | 資源id |
type | String | 資源類型: STANDARD - 2D數字人實時互動【基礎版】 ADVANCED - 2D數字人實時互動【高級版】 |
validPeriodTime | String | 有效期(時間戳) 例如:1719904342237 |
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)
}