數(shù)字人流媒體開發(fā)指南
本文介紹數(shù)字人流媒體服務(wù)的接入流程和開發(fā)方案,以及示例代碼。
數(shù)字人流媒體服務(wù)(包括3D數(shù)字人流媒體服務(wù)和2D數(shù)字人流媒體服務(wù))是將數(shù)字人在云端渲染出的視頻畫面通過(guò)流媒體的方式輸出到客戶端,平臺(tái)目前提供了阿里云RTC流媒體渠道和標(biāo)準(zhǔn)RTMP流媒體渠道兩種渠道。播報(bào)數(shù)字人(對(duì)應(yīng)開放平臺(tái)的“咨詢播報(bào)”場(chǎng)景)和互動(dòng)數(shù)字人(對(duì)應(yīng)開放平臺(tái)的“客服助理”場(chǎng)景)使用阿里云RTC渠道,平臺(tái)會(huì)將數(shù)字人渲染的視頻流推到阿里云RTC服務(wù)器,客戶可以使用阿里云RTC的客戶端SDK進(jìn)行拉流播放對(duì)應(yīng)視頻流;推流數(shù)字人(對(duì)應(yīng)開放平臺(tái)的“虛擬主播”場(chǎng)景)使用標(biāo)準(zhǔn)RTMP渠道,平臺(tái)可以將數(shù)字人渲染的視頻流通過(guò)RTMP協(xié)議推流到支持對(duì)應(yīng)協(xié)議的直播平臺(tái),客戶可以從對(duì)應(yīng)的直播平臺(tái)觀看視頻流。
下面通過(guò)一個(gè)典型的客戶端-服務(wù)端架構(gòu)產(chǎn)品介紹數(shù)字人流媒體服務(wù)整體的接入流程。
1. 完整的技術(shù)鏈路圖
整體架構(gòu)上分為客戶應(yīng)用、虛擬數(shù)字人開放平臺(tái)服務(wù)和阿里云RTC服務(wù)
客戶應(yīng)用分為客戶端(包括:網(wǎng)頁(yè),小程序,APP等)和服務(wù)端(java服務(wù)器,python服務(wù)器等)
如果客戶只有客戶端,如一個(gè)APP,沒(méi)有服務(wù)端的情況,目前平臺(tái)的服務(wù)端API使用的是阿里云的openapi,支持android直接調(diào)用(java sdk)
2. 核心鏈路介紹
2.1 啟動(dòng)數(shù)字人流媒體服務(wù)
目的:
啟動(dòng)一路數(shù)字人流媒體服務(wù),數(shù)字人的視頻畫面將被推送到阿里云RTC服務(wù)器(推流數(shù)字人是直接推流到對(duì)應(yīng)的直播平臺(tái))
核心流程:
客戶應(yīng)用客戶端向客戶應(yīng)用服務(wù)端發(fā)起啟動(dòng)數(shù)字人流媒體服務(wù)請(qǐng)求,客戶應(yīng)用服務(wù)端收到請(qǐng)求后通過(guò)調(diào)用虛擬數(shù)字人開放平臺(tái)服務(wù)端SDK的StartInstance接口啟動(dòng)一路數(shù)字人流媒體服務(wù)(目前平臺(tái)服務(wù)端SDK支持java/python/php三種開發(fā)語(yǔ)言,具體接入方案可參考服務(wù)端API接入。),拿到接口返回的sessionId和RTC流媒體服務(wù)參數(shù),然后返回給客戶應(yīng)用客戶端
2.2 拉取數(shù)字人視頻流
目的:
客戶端拉流對(duì)應(yīng)的數(shù)字人視頻流,并在客戶端進(jìn)行顯示
核心流程:
通過(guò)上一步服務(wù)端返回的RTC流媒體服務(wù)參數(shù),接入阿里云RTC客戶端SDK進(jìn)行拉流,目前阿里云RTC客戶端SDK支持全平臺(tái)接入(Android/IOS/Web/Windows等),接入方案可參考客戶端SDK接入。
2.3 向數(shù)字人發(fā)送播報(bào)指令
目的:
驅(qū)動(dòng)數(shù)字人播報(bào)對(duì)應(yīng)的文本
核心流程:
客戶應(yīng)用客戶端向客戶應(yīng)用服務(wù)端發(fā)起驅(qū)動(dòng)數(shù)字人播報(bào)文本請(qǐng)求,客戶應(yīng)用服務(wù)端收到請(qǐng)求后通過(guò)調(diào)用虛擬數(shù)字人開放平臺(tái)服務(wù)端SDK的SendText接口驅(qū)動(dòng)數(shù)字人播報(bào),具體可參考文檔:數(shù)字人播報(bào)API接入指南。
2.4 停止數(shù)字人流媒體服務(wù)
目的:
在使用完數(shù)字人流媒體服務(wù)之后停止對(duì)應(yīng)流媒體服務(wù),釋放資源(平臺(tái)根據(jù)數(shù)字人流媒體服務(wù)啟動(dòng)的數(shù)量進(jìn)行計(jì)費(fèi),請(qǐng)?jiān)谑褂猛瓿珊蠹皶r(shí)停止,避免造成無(wú)流媒體服務(wù)可用的情況)
核心流程:
客戶應(yīng)用客戶端向客戶應(yīng)用服務(wù)端發(fā)起停止數(shù)字人流媒體服務(wù)請(qǐng)求,客戶應(yīng)用服務(wù)端收到請(qǐng)求后通過(guò)調(diào)用虛擬數(shù)字人開放平臺(tái)服務(wù)端SDK的StopInstance接口停止數(shù)字人流媒體服務(wù)
3. 完整流媒體服務(wù)示例代碼
3.1 引入二方包
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>avatar20220130</artifactId>
<version>${使用最新版本}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${選擇一個(gè)合適版本}</version>
</dependency>
3.2 示例代碼
package com.aliyun.avatar.sample;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.aliyun.avatar20220130.Client;
import com.aliyun.avatar20220130.models.SendCommandRequest;
import com.aliyun.avatar20220130.models.SendCommandResponse;
import com.aliyun.avatar20220130.models.SendCommandResponseBody;
import com.aliyun.avatar20220130.models.SendTextRequest;
import com.aliyun.avatar20220130.models.SendTextResponse;
import com.aliyun.avatar20220130.models.SendTextResponseBody;
import com.aliyun.avatar20220130.models.StartInstanceRequest;
import com.aliyun.avatar20220130.models.StartInstanceRequest.StartInstanceRequestApp;
import com.aliyun.avatar20220130.models.StartInstanceRequest.StartInstanceRequestUser;
import com.aliyun.avatar20220130.models.StartInstanceResponse;
import com.aliyun.avatar20220130.models.StartInstanceResponseBody;
import com.aliyun.avatar20220130.models.StartInstanceResponseBody.StartInstanceResponseBodyData;
import com.aliyun.avatar20220130.models.StartInstanceResponseBody.StartInstanceResponseBodyDataChannel;
import com.aliyun.avatar20220130.models.StopInstanceRequest;
import com.aliyun.avatar20220130.models.StopInstanceResponse;
import com.aliyun.avatar20220130.models.StopInstanceResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
/**
* <pre>
* avatar sample
* </pre>
*
*/
public class AvatarOnlineSample {
private Client client;
/**
* 初始化
*
* @throws Exception
*/
public void init() throws Exception {
// 請(qǐng)確保代碼運(yùn)行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
// 工程代碼泄露可能會(huì)導(dǎo)致 AccessKey 泄露,并威脅賬號(hào)下所有資源的安全性。以下代碼示例使用環(huán)境變量獲取 AccessKey 的方式進(jìn)行調(diào)用,僅供參考,建議使用更安全的 STS 方式,更多鑒權(quán)訪問(wèn)方式請(qǐng)參見:http://bestwisewords.com/document_detail/378657.html
String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
client = createClient(accessKeyId, accessKeySecret);
}
/**
* 使用AK&SK初始化賬號(hào)Client
*
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
public Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
Config config = new Config()
// 必填,您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 必填,您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 訪問(wèn)的域名
config.endpoint = "avatar.cn-zhangjiakou.aliyuncs.com";
return new Client(config);
}
/**
* 啟動(dòng)一路數(shù)據(jù)流媒體服務(wù)
*
* @param tenantId
* @param appId
*/
public StartInstanceResponseBodyData startInstance(Long tenantId, String appId) throws Exception {
StartInstanceRequestUser user = new StartInstanceRequestUser();
user.setUserId("avatar_sample_userId");
user.setUserName("avatar_sample_userName");
StartInstanceRequestApp app = new StartInstanceRequestApp();
app.setAppId(appId);
StartInstanceRequest request = new StartInstanceRequest();
request.setTenantId(tenantId);
request.setApp(app);
request.setUser(user);
RuntimeOptions runtime = new RuntimeOptions();
StartInstanceResponse response = client.startInstanceWithOptions(request, runtime);
StartInstanceResponseBody responseBody = response.getBody();
if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
// 調(diào)用成功
System.out.println("啟動(dòng)成功,sessionId:" + responseBody.getData().getSessionId());
return responseBody.getData();
} else {
// 調(diào)用失敗
System.out.println("啟動(dòng)失敗,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
throw new Exception("啟動(dòng)失敗");
}
}
/**
* 停止一路數(shù)據(jù)流媒體服務(wù)
*
* @param tenantId
* @param sessionId
*/
public void stopInstance(Long tenantId, String sessionId) throws Exception {
StopInstanceRequest request = new StopInstanceRequest();
request.setTenantId(tenantId);
request.setSessionId(sessionId);
RuntimeOptions runtime = new RuntimeOptions();
StopInstanceResponse response = client.stopInstanceWithOptions(request, runtime);
StopInstanceResponseBody responseBody = response.getBody();
if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
// 調(diào)用成功
System.out.println("停止成功,sessionId:" + responseBody.getData().getSessionId());
} else {
// 調(diào)用失敗
System.out.println("停止失敗,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
throw new Exception("停止失敗");
}
}
/**
* 發(fā)送文本消息
*
* @param tenantId
* @param sessionId
* @param text
*/
public void sendText(Long tenantId, String sessionId, String text) throws Exception {
System.out.println(formatCurrentTime() + ":開始發(fā)送文本消息:" + text);
SendTextRequest request = new SendTextRequest();
request.setTenantId(tenantId);
request.setSessionId(sessionId);
// 本次播報(bào)是否打斷當(dāng)前數(shù)字人正在播報(bào)的內(nèi)容
// true表示打斷,false的話會(huì)等待當(dāng)前數(shù)字人正在播報(bào)的內(nèi)容結(jié)束才會(huì)播報(bào)本次播報(bào)內(nèi)容
request.setInterrupt(true);
request.setText(text);
request.setUniqueCode(UUID.randomUUID().toString());
RuntimeOptions runtime = new RuntimeOptions();
SendTextResponse response = client.sendTextWithOptions(request, runtime);
SendTextResponseBody responseBody = response.getBody();
if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
// 調(diào)用成功
System.out.println(formatCurrentTime() + ":發(fā)送文本消息成功, unqieCode: " + request.getUniqueCode());
} else {
// 調(diào)用失敗
System.out.println("發(fā)送文本消息失敗,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
throw new Exception("發(fā)送文本消息失敗");
}
}
/**
* 發(fā)送指令消息
*
* @param tenantId
* @param sessionId
* @param code
* @param content
*/
public void sendCommand(Long tenantId, String sessionId, String code, Map<String, Object> content)
throws Exception {
SendCommandRequest request = new SendCommandRequest();
request.setTenantId(tenantId);
request.setSessionId(sessionId);
request.setCode(code);
request.setContent(content);
request.setUniqueCode(UUID.randomUUID().toString());
RuntimeOptions runtime = new RuntimeOptions();
SendCommandResponse response = client.sendCommandWithOptions(request, runtime);
SendCommandResponseBody responseBody = response.getBody();
if (null != responseBody.getSuccess() && responseBody.getSuccess()) {
// 調(diào)用成功
System.out.println(formatCurrentTime() + ":發(fā)送指令消息成功");
} else {
// 調(diào)用失敗
System.out.println("發(fā)送指令消息失敗,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
throw new Exception("發(fā)送指令消息失敗");
}
}
public static void main(String[] args) throws Exception {
// 開發(fā)者信息定義,獲取方式參考文檔:http://bestwisewords.com/document_detail/479093.html
Long tenantId = null;
String appId = null;
AvatarOnlineSample sample = new AvatarOnlineSample();
// 1. 初始化
sample.init();
// 2. 啟動(dòng)一路數(shù)字人流媒體服務(wù)
StartInstanceResponseBodyData response = sample.startInstance(tenantId, appId);
// 2.1 獲取啟動(dòng)成功的sessionId
String sessionId = response.getSessionId();
// 2.2 獲取對(duì)應(yīng)的channel信息用于客戶端拉流,可以使用平臺(tái)提供的測(cè)試頁(yè)面快速體驗(yàn):http://bestwisewords.com/document_detail/392352.html
StartInstanceResponseBodyDataChannel channel = response.getChannel();
System.out.println(JSONObject.toJSONString(channel, SerializerFeature.PrettyFormat));
// 2.3 示例代碼這里sleep,可以去平臺(tái)的測(cè)試頁(yè)面拉流查看是否成功
Thread.sleep(60000);
// 3. 發(fā)送消息驅(qū)動(dòng)數(shù)字人播報(bào),詳細(xì)可參考文檔:http://bestwisewords.com/document_detail/478895.html
// 3.1 發(fā)送一條測(cè)試文本消息
// 發(fā)送之后數(shù)字人會(huì)進(jìn)行播報(bào),示例代碼通過(guò)sleep模擬等待數(shù)字人播報(bào),實(shí)際可根據(jù)業(yè)務(wù)情況發(fā)送下一條播報(bào)
sample.sendText(tenantId, sessionId, formatCurrentTime() + ",我是虛擬數(shù)字人,這是一段示例代碼,我正在播報(bào)一條示例文本");
Thread.sleep(15000);
// 3.2 發(fā)送一條測(cè)試文本消息,自定義數(shù)字人動(dòng)作,詳細(xì)可參考文檔:http://bestwisewords.com/document_detail/478895.html
// 注意:實(shí)際測(cè)試的時(shí)候可能會(huì)因?yàn)槭褂玫臄?shù)字人不同以及行業(yè)不同,導(dǎo)致下面的自定義動(dòng)作不生效
sample.sendText(tenantId, sessionId, "<speak>" + formatCurrentTime() + ",大家好<vh-action code=\"animation_4960\" interrupt=\"true\"/>,這是一段虛擬數(shù)字人播報(bào)測(cè)試文本,歡迎大家! </speak>");
Thread.sleep(15000);
// 3.3 發(fā)送一條測(cè)試指令消息,目前指令只支持打斷數(shù)字人播報(bào)
sample.sendText(tenantId, sessionId, formatCurrentTime() + ",虛擬數(shù)字人是一項(xiàng)整合了語(yǔ)音、圖像、3D美術(shù)、自然語(yǔ)言處理等多領(lǐng)域綜合且復(fù)雜的技術(shù)。阿里云虛擬數(shù)字人開放平臺(tái),依托阿里達(dá)摩院強(qiáng)大的AI技術(shù)能力,為阿里云客戶提供了低門檻、輕量級(jí)、易集成的3D數(shù)字人、2D數(shù)字人實(shí)時(shí)和離線驅(qū)動(dòng)能力,讓阿里云客戶能夠輕松的將數(shù)字人能力接入到自己的業(yè)務(wù)中,從而體驗(yàn)到數(shù)字人技術(shù)帶來(lái)的業(yè)務(wù)提升;同時(shí)平臺(tái)還提供了豐富的數(shù)字人資產(chǎn)形象庫(kù),以及為了方便非開發(fā)人員使用和體驗(yàn)還提供了完善的數(shù)字人視頻創(chuàng)作SAAS產(chǎn)品。");
Thread.sleep(5000);
sample.sendCommand(tenantId, sessionId, "INTERRUPT", null);
Thread.sleep(3000);
// 4. 停止數(shù)字人流媒體服務(wù),釋放資源
sample.stopInstance(tenantId, sessionId);
}
private static String formatCurrentTime() {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss,SSS");
return format.format(new Date());
}
}
以上就是一個(gè)典型的客戶端-服務(wù)端架構(gòu)產(chǎn)品使用數(shù)字人流媒體服務(wù)的整體鏈路,關(guān)于播報(bào)數(shù)字人、互動(dòng)數(shù)字人等具體數(shù)字人流媒體服務(wù)的接入可參考下方詳細(xì)的接入指南: