数字人流媒体开发指南

本文介绍数字人流媒体服务的接入流程和开发方案,以及示例代码。

数字人流媒体服务(包括3D数字人流媒体服务和2D数字人流媒体服务)是将数字人在云端渲染出的视频画面通过流媒体的方式输出到客户端,平台目前提供了阿里云RTC流媒体渠道和标准RTMP流媒体渠道两种渠道。播报数字人(对应开放平台的“咨询播报”场景)和互动数字人(对应开放平台的“客服助理”场景)使用阿里云RTC渠道,平台会将数字人渲染的视频流推到阿里云RTC服务器,客户可以使用阿里云RTC的客户端SDK进行拉流播放对应视频流;推流数字人(对应开放平台的“虚拟主播”场景)使用标准RTMP渠道,平台可以将数字人渲染的视频流通过RTMP协议推流到支持对应协议的直播平台,客户可以从对应的直播平台观看视频流。

下面通过一个典型的客户端-服务端架构产品介绍数字人流媒体服务整体的接入流程。

1. 完整的技术链路图

image
  • 整体架构上分为客户应用、虚拟数字人开放平台服务和阿里云RTC服务

  • 客户应用分为客户端(包括:网页,小程序,APP等)和服务端(java服务器,python服务器等)

    • 如果客户只有客户端,如一个APP,没有服务端的情况,目前平台的服务端API使用的是阿里云的openapi,支持android直接调用(java sdk)

2. 核心链路介绍

2.1 启动数字人流媒体服务

  • 目的:

    • 启动一路数字人流媒体服务,数字人的视频画面将被推送到阿里云RTC服务器(推流数字人是直接推流到对应的直播平台)

  • 核心流程:

    • 客户应用客户端向客户应用服务端发起启动数字人流媒体服务请求,客户应用服务端收到请求后通过调用虚拟数字人开放平台服务端SDK的StartInstance接口启动一路数字人流媒体服务(目前平台服务端SDK支持java/python/php三种开发语言,具体接入方案可参考服务端API接入。),拿到接口返回的sessionId和RTC流媒体服务参数,然后返回给客户应用客户端

2.2 拉取数字人视频流

  • 目的:

    • 客户端拉流对应的数字人视频流,并在客户端进行显示

  • 核心流程:

    • 通过上一步服务端返回的RTC流媒体服务参数,接入阿里云RTC客户端SDK进行拉流,目前阿里云RTC客户端SDK支持全平台接入(Android/IOS/Web/Windows等),接入方案可参考客户端SDK接入

2.3 向数字人发送播报指令

  • 目的:

    • 驱动数字人播报对应的文本

  • 核心流程:

    • 客户应用客户端向客户应用服务端发起驱动数字人播报文本请求,客户应用服务端收到请求后通过调用虚拟数字人开放平台服务端SDK的SendText接口驱动数字人播报,具体可参考文档:数字人播报API接入指南

2.4 停止数字人流媒体服务

  • 目的:

    • 在使用完数字人流媒体服务之后停止对应流媒体服务,释放资源(平台根据数字人流媒体服务启动的数量进行计费,请在使用完成后及时停止,避免造成无流媒体服务可用的情况)

  • 核心流程:

    • 客户应用客户端向客户应用服务端发起停止数字人流媒体服务请求,客户应用服务端收到请求后通过调用虚拟数字人开放平台服务端SDK的StopInstance接口停止数字人流媒体服务

3. 完整流媒体服务示例代码

3.1 引入二方包

<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>avatar20220130</artifactId>
  <version>${使用最新版本}</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>${选择一个合适版本}</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 {
        // 请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.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初始化账号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);
        // 访问的域名
        config.endpoint = "avatar.cn-zhangjiakou.aliyuncs.com";
        return new Client(config);
    }

    /**
     * 启动一路数据流媒体服务
     *
     * @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()) {
            // 调用成功
            System.out.println("启动成功,sessionId:" + responseBody.getData().getSessionId());
            return responseBody.getData();
        } else {
            // 调用失败
            System.out.println("启动失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("启动失败");
        }
    }

    /**
     * 停止一路数据流媒体服务
     *
     * @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()) {
            // 调用成功
            System.out.println("停止成功,sessionId:" + responseBody.getData().getSessionId());
        } else {
            // 调用失败
            System.out.println("停止失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("停止失败");
        }
    }

    /**
     * 发送文本消息
     *
     * @param tenantId
     * @param sessionId
     * @param text
     */
    public void sendText(Long tenantId, String sessionId, String text) throws Exception {
        System.out.println(formatCurrentTime() + ":开始发送文本消息:" + text);
        SendTextRequest request = new SendTextRequest();
        request.setTenantId(tenantId);
        request.setSessionId(sessionId);
        // 本次播报是否打断当前数字人正在播报的内容
        // true表示打断,false的话会等待当前数字人正在播报的内容结束才会播报本次播报内容
        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()) {
            // 调用成功
            System.out.println(formatCurrentTime() + ":发送文本消息成功, unqieCode: " + request.getUniqueCode());
        } else {
            // 调用失败
            System.out.println("发送文本消息失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("发送文本消息失败");
        }
    }

    /**
     * 发送指令消息
     *
     * @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()) {
            // 调用成功
            System.out.println(formatCurrentTime() + ":发送指令消息成功");
        } else {
            // 调用失败
            System.out.println("发送指令消息失败,原因:" + responseBody.getCode() + ":" + responseBody.getMessage());
            throw new Exception("发送指令消息失败");
        }
    }

    public static void main(String[] args) throws Exception {
        // 开发者信息定义,获取方式参考文档:https://help.aliyun.com/document_detail/479093.html
        Long tenantId = null;
        String appId = null;
        AvatarOnlineSample sample = new AvatarOnlineSample();
        // 1. 初始化
        sample.init();

        // 2. 启动一路数字人流媒体服务
        StartInstanceResponseBodyData response = sample.startInstance(tenantId, appId);
        // 2.1 获取启动成功的sessionId
        String sessionId = response.getSessionId();
        // 2.2 获取对应的channel信息用于客户端拉流,可以使用平台提供的测试页面快速体验:https://help.aliyun.com/document_detail/392352.html
        StartInstanceResponseBodyDataChannel channel = response.getChannel();
        System.out.println(JSONObject.toJSONString(channel, SerializerFeature.PrettyFormat));
        // 2.3 示例代码这里sleep,可以去平台的测试页面拉流查看是否成功
        Thread.sleep(60000);

        // 3. 发送消息驱动数字人播报,详细可参考文档:https://help.aliyun.com/document_detail/478895.html
        // 3.1 发送一条测试文本消息
        //     发送之后数字人会进行播报,示例代码通过sleep模拟等待数字人播报,实际可根据业务情况发送下一条播报
        sample.sendText(tenantId, sessionId, formatCurrentTime() + ",我是虚拟数字人,这是一段示例代码,我正在播报一条示例文本");
        Thread.sleep(15000);
        // 3.2 发送一条测试文本消息,自定义数字人动作,详细可参考文档:https://help.aliyun.com/document_detail/478895.html
        //     注意:实际测试的时候可能会因为使用的数字人不同以及行业不同,导致下面的自定义动作不生效
        sample.sendText(tenantId, sessionId, "<speak>" + formatCurrentTime() + ",大家好<vh-action code=\"animation_4960\" interrupt=\"true\"/>,这是一段虚拟数字人播报测试文本,欢迎大家! </speak>");
        Thread.sleep(15000);
        // 3.3 发送一条测试指令消息,目前指令只支持打断数字人播报
        sample.sendText(tenantId, sessionId, formatCurrentTime() + ",虚拟数字人是一项整合了语音、图像、3D美术、自然语言处理等多领域综合且复杂的技术。阿里云虚拟数字人开放平台,依托阿里达摩院强大的AI技术能力,为阿里云客户提供了低门槛、轻量级、易集成的3D数字人、2D数字人实时和离线驱动能力,让阿里云客户能够轻松的将数字人能力接入到自己的业务中,从而体验到数字人技术带来的业务提升;同时平台还提供了丰富的数字人资产形象库,以及为了方便非开发人员使用和体验还提供了完善的数字人视频创作SAAS产品。");
        Thread.sleep(5000);
        sample.sendCommand(tenantId, sessionId, "INTERRUPT", null);
        Thread.sleep(3000);

        // 4. 停止数字人流媒体服务,释放资源
        sample.stopInstance(tenantId, sessionId);
    }

    private static String formatCurrentTime() {
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss,SSS");
        return format.format(new Date());
    }
}

以上就是一个典型的客户端-服务端架构产品使用数字人流媒体服务的整体链路,关于播报数字人、互动数字人等具体数字人流媒体服务的接入可参考下方详细的接入指南: