# 一、兼容性说明

类别 兼容范围
系统 支持Android 5.0 ~ Android 13 版本,鸿蒙系统未做兼容性验证
机型 上市的Android手机和平板、及符合具体能力性能要求的Android系统设备
网络 依赖网络
开发环境 建议使用Android Studio 进行开发

# 二、SDK组成

将SDK zip包解压缩,得到如下文件:

├── Demo SDK的使用DEMO,DEMO中已经集成了SDK,您可以参考DEMO,集成SDK。集成前,请先测通DEMO,了解调用原理。 ├── ReleaseNote.txt SDK版本日志 ├── libs 使用虚拟人所需要的依赖库 │ └── AIKit.aar AIKit框架库,虚拟人基于AIKit框架开发 │ ├── vms.aar 虚拟人业务接口封装层 │ └── xrtcsdk-4.4.0.aar 虚拟人依赖的音视频库

# 三、接口调用流程

# 四、快速集成

# 4.1 导入SDK库

拷贝libs下所有aar到您项目的libs目录下,然后在主工程的build.gradle文件中,增加如下配置:

// 已忽略无关代码
dependencies {
   //虚拟人SDK及依赖库
    api files('libs/AIKit.aar')
    api files('libs/vms.aar')
    //音视频依赖库
    api files('libs/xrtcsdk-4.4.0.aar')
    implementation 'com.squareup.okhttp3:okhttp:3.10.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
}

# 4.2 SDK初始化

在使用前,需要先获取已授权的应用信息(appId、apiKey、apiSecret),如果没有,请联系项目经理获取。使用SDK时,SDK只需要初始化一次。SDK 初始化示例代码如下:

// 初始化参数构建
BaseLibrary.Params params = BaseLibrary.Params.builder()
                    .appId("$appId")
                    .apiKey("$apiKey")
                    .apiSecret("$apiSecret")
                    .workDir("/sdcard/iflytek/VMS")//SDK工作路径,这里为绝对路径,路径可自行指定,此处仅为示例
                    .build();
//注册授权结果监听
VmsHelper.getInst().registerAuthListener(coreListener);
// 初始化
VmsHelper.getInst().initEntry(MainActivity.this.getApplicationContext(), params);
 
 
//授权结果回调
private CoreListener coreListener = new CoreListener() {
    @Override
    public void onAuthStateChange(final ErrType type, final int code) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                switch (type) {
                    case AUTH:
                        //txt_result.setText("SDK状态:授权结果码" + code);
                        break;
                    case HTTP:
                        //txt_result.setText("SDK状态:HTTP认证结果" + code);
                        break;
                    default:
                        //txt_result.setText("SDK状态:其他错误");
                }
            }
        });
    }
};
参数 类型 必填 说明
apiKey String 平台创建应用后,生成的唯一应用标识
apiSecret String 平台创建应用后,生成的唯一应用秘钥
appID String 平台创建应用后,生成的应用ID
workDir String SDK工作目录,用户可自行指定,但要确保有访问权限

onAuthStateChange 方法说明:

参数 类型 必填 说明
type ErrType SDK错误类型,0 表示授权错误,1 表示 http 请求错误
code int 错误码,0 表示正常

首次初始化成功后,再次初始化不会调用该方法。若初始化失败,再次调用初始化,会再次回调此方法。

# 4.3 注册虚拟人会话状态回调

虚拟人状态回调 AiResponseListener实,示例代码如下:

 AiResponseListener vmsListener = new AiResponseListener() {
   
    @Override
    public void onResult(String ability, int handleID, List<AiResponse> outputData, Object usrContext) {
        if (null != outputData && outputData.size() > 0) {
 
            Log.i(TAG, "VMS:onResult:handleID:" + handleID + ":" + outputData.size() + ",ability:" + ability);
            for (int i = 0; i < outputData.size(); i++) {
                byte[] bytes = outputData.get(i).getValue();
                if (bytes == null) {
                    continue;
                }
                String key = outputData.get(i).getKey();
                Log.i(TAG, "VMS:onResult内容:" + key + "," + new String(bytes));
                switch (key) {
                    case "session":
                        String session = new String(bytes);  // 会话的session,驱动等接口必须携带该字段,start成功后会回调此字段
                        break;
                    case "res_id": //上传背景的回调
                        resId = new String(bytes);
                        showTip("上传成功!Start后生效。res_id:" + resId);
                        break;
                    case "realtime_status": //用于判断虚拟人播放状态(仅支持ws接口)
                        JSONObject jsonObject = null;
                        try {
                            jsonObject = new JSONObject(new String(bytes));
                            String global_status = jsonObject.optString("vmr_status", "");
                            if (global_status.equals("2")) { //0开始 1继续 2结束
                                showTip("播放完毕");
                            }
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        break;
                }
 
            }
        }
    }
 
    //事件回调
    @Override
    public void onEvent(String ability, int handleID, int event, List<AiResponse> eventData, Object usrContext) {
        Log.i(TAG, "VMS:onEvent:ability:" + ability + ",event:" + event);
        if (ability.equals(VmsHelper.ABILITY_VMS_CTRL) && event == 19) { //追加模式输入间隔超时,需要重置到首帧
            interactiveModeIndex = 0;   //用于追加模式下传首帧还是中间帧
        }
    }
 
    //错误通知
    @Override
    public void onError(String ability, int handleID, int err, String msg, Object usrContext) {
        showTip("错误通知:" + msg);
        Log.e(TAG, "VMS:onError,ability " + ability + " ERROR::" + msg + ",err code:" + err);
        if(ability.equals("vms2d_ping")){   //ping报错
 
        }
        switch (err){
            case 20014:  //打断模式时,打断之前的驱动,会返回此code
                Log.w(TAG, "当前任务被打断,Ability " + ability + " ERROR::" + msg + ",err code:" + err);
            break;
            default:
                showTip("错误通知:" + msg);
                Log.e(TAG, "错误通知,Ability " + ability + " ERROR::" + msg + ",err code:" + err);
            break;
        }
    }
};
//添加虚拟人回调监听
VmsHelper.getInst().registerListener(vmsListener);
  • onResult:
变量名 类型 非空 说明
ability String 能力标识ID
handleID int 会话ID
usrContext Object 用户自定义标识
responseData List 能力执行结果
  • onEvent:
变量名 类型 非空 说明
ability String 能力标识ID
handleID int 会话ID
event enum 0=未知;1=开始;2=结束;3=超时;4=进度
usrContext Object 用户自定义标识
eventData List 事件消息数据
  • onError:
变量名 类型 非空 说明
ability String 能力标识ID
handleID int 会话ID
err int 错误码
msg String 错误描述
usrContext Object 用户自定义标识

目前通过onError返回的错误码有两类,一类是云端错误码,一类是SDK错误码(以18开头),具体错误码信息请参见第六章。

  • AiResponse对象:
变量名 类型 非空 说明
key String 输出数据名称
type enum 输出数据类型,0=音频;1=文本;2=图片;3=视频;5=个性化数据
value byte[] 字节数组类型输出数据
varType enum 输出数据变量类型,0=字节数组;1=整型;2=实型;3=布尔型
len int 输出数据长度

# 4.4能力调用

# 4.4.1启动虚拟人

参考代码如下:

  //传入拉流视图
ViewGroup virtualContainer = findViewById(R.id.ll_virtual);
VmsHelper.getInst().setVmsView(virtualContainer);
 
//从配置文件读取参数
AiRequest.Builder in = AiRequest.builder();
in.header().param("uid", mUid);
if (!TextUtils.isEmpty(resId)) {    //背景
    in.param("res_id", resId);
}
 
in.service("vmr");
AvatarB avatarB = avatarCtrlB.getCurrentSelectItem();
in.param("avatar_id", avatarB.getAvatar_id());//形象id  avatarB.getAvatar_id()
if (!TextUtils.isEmpty(resId)) {    //有背景
    in.param("scale", 0.9); //[0.1, 1.0] 主播在背景图像中的大小 (仅在个性化背景中生效)
    in.param("move_h", 0); //[-4096, +4096] 主播平移像素距离 (仅在个性化背景中生效)
    in.param("move_v", 0); //[-4096, +4096] 控制主播在展示画面中的上下移动距离(仅在个性化背景中生效
}else {
    //视频分辨率:宽,高
    //width must be one of [1920 1280 640 360 720 1080]
    //height must be one of [1080 720 360 640 1280 1920]
    in.param("width", 1080).param("height", 1920);
}
//响应数据控制
AiRequest.Builder streamBuilder = AiRequest.builder();
streamBuilder.param("protocol", "xrtc");
if (fps > 0) {
    streamBuilder.param("fps", fps); //视频帧率 13-25
}
if (bitrate > 0) {
    streamBuilder.param("bitrate", bitrate); //视频的码率 800 ~ 2000
}
in.param("stream", streamBuilder);
 
VMSConfig vmsConfig = VMSConfig.builder()
        .videoFillMode(videoFillMode)
        .videoRotation(videoRotation)
        .videoMirrorType(VideoMirrorType.VIDEO_MIRROR_TYPE_DISABLE)
        .videoStreamType(VideoStreamType.VIDEO_STREAM_TYPE_BIG)
        .useWebSocket(isWebSocket)   //是否使用使用ws接口,非实ws接口不支持时返回状态数据 默认使用ws接口
        .interactiveMode(InteractiveMode.valueOf(interactiveMode))  //模式 0 追加模式。1打断模式
        .build();
int ret = VmsHelper.getInst().start(in.build(), vmsConfig);
 
if (ret != 0) {
    showTip("start失败");
}

# 4.4.2 输入数据

输入数据包含音频数据和文本数据两种类型

音频输入参考代码如下:

byte[] byteArray = readStream(filePath);
if (byteArray == null || byteArray.length <= 0) {
    Log.e(TAG, "audioDrive 当前文件大小异常!");
    return;
}
 
int leftAudioBytes = byteArray.length;
int byteWrites = 1024;   //每次输入的字节数
int writeLen = 0;
int index = 0;
 
isAudioRunning = true;
while (leftAudioBytes > 0) {
 
    boolean isLast = false;
    if (leftAudioBytes > byteWrites) {
        writeLen = byteWrites;
    } else {
        isLast = true;
        writeLen = leftAudioBytes;
    }
    leftAudioBytes -= writeLen;
 
    byte[] part = Arrays.copyOfRange(byteArray, index * byteWrites, index * byteWrites + writeLen);
 
    index++;
 
    AiRequest.Builder aiRequest = AiRequest.builder();
    //协议头部,用于描述平台特性的参数
    aiRequest.header()
            .param("uid", mUid)
            .param("session", mSession);
 
    if (checkbox_nit.isChecked()) {   //非打断模式
        //需要实时返回状态数据时传入
        aiRequest.service("vms_dispatch");
        AiRequest.Builder paramBuilder = AiRequest.builder();
        paramBuilder.param("vmr_status", 1);
        paramBuilder.param("vmr_action_status", 1);
        aiRequest.param("realtime_status", paramBuilder);
    }
 
    AiAudio aiAudio = null;
 
    if (index <= 1) {
        aiAudio = AiAudio.get("audio").data(part).begin().valid();  //开始帧
    } else if (isLast || !isAudioRunning) {
        aiAudio = AiAudio.get("audio").data(part).end().valid();     //结束帧
    } else {
        aiAudio = AiAudio.get("audio").data(part).cont().valid();   //中间帧
    }
    aiRequest.payload(aiAudio);//音频
    int ret = VmsHelper.getInst().audioCtrl(aiRequest.build());
 
    if (ret != 0) {
        Log.e(TAG, "audioDrive:start write failed:" + ret);
        return;
    }
    SystemClock.sleep(10);  //防止太快了服务器处理不过来
}
# 文本输入参考代码如下:
AiRequest.Builder aiRequest = AiRequest.builder();
 
       aiRequest.header()
               .param("uid", mUid)
               .param("session", mSession)
               .param("scene", "main_box");
       AiText aiText = null;
       if (checkbox_nit.isChecked()) { //非打断模式
           aiText = AiText.get("text").data(text).end().valid();
       } else {
           aiText = AiText.get("text").data(text).once().valid();
       }
 
       aiRequest.payload(aiText);//文本
       //动作协议
       /**
        * "type":"action" 动作
        * value 动作类型
        * wb 对应文本起始位置
        * we 对应文本结束位置
        */
       String cw = "{\"avatar\":[{\"type\":\"action\",\"value\":\"" + 动作id + "\",\"wb\":0,\"we\":" + text.length() + "}]}";
       AiText aiCtrlW = AiText.get("ctrl_w").data(cw).once().valid();
       aiRequest.payload(aiCtrlW);//动作
       int pitch = 55; //(int) (50 + Math.random() * 100)
       Log.d(TAG, "动作驱动文本:" + cw + "," + pitch);
       //发音人
       aiRequest.service("tts")
               .param("vcn", vcn)
               .param("speed", 48)
               .param("pitch", pitch);
       aiRequest.param("volume", 100);
 
 
       aiRequest.service("vms_dispatch");
       aiRequest.param("interactive_mode", interactiveMode);
       //请求服务端实时返回状态数据
       if (isWebSocket) {
           AiRequest.Builder paramBuilder = AiRequest.builder();
           paramBuilder.param("tts_status", 1);
           paramBuilder.param("vmr_status", 1);
           paramBuilder.param("vmr_action_status", 1);
           aiRequest.param("realtime_status", paramBuilder);
       }
 
       int ret = VmsHelper.getInst().textCtrl(aiRequest.build());
       aiRequest.clear();
       Log.d(TAG, "enterText ret:" + ret);
       if (ret != 0) {
           showTip("输入文本指令失败");
       }
  • # interactive_mode、vms_dispatch参数说明:

功能标识 功能描述 数据类型 取值范围 必填 默认值
interactive_mode 打断模式 int [0,1] (0 追加模式 1 打断模式) 1 打断模式
realtime_status 实时返回状态数据 object
tts_status 文本合成状态 0 关闭 1打开 int 0 - 1 0
vmr_status 渲染引擎文本实时响应状态 0 关闭 1打开 int 0 - 1 0
vmr_action_status 渲染引擎动作实时响应状态 0 关闭 1打开 int 0 - 1 0

# 4.4.3 上传背景

# 参考代码如下:
String base64Str = Base64.encodeToString(readStream(filePath), Base64.DEFAULT);
AiRequest.Builder aiRequest = AiRequest.builder();
aiRequest.header().param("app_id", getString(R.string.appid)).param("uid", mUid).end();
AiText aiText = AiText.get("background_data").data(base64Str).once().valid();
aiRequest.payload(aiText);//文本
 
int code = VmsHelper.getInst().loadData(aiRequest.build());
Log.d(TAG, "uploadBackground:output code is " + code);
if (code != 0) {
    showTip("背景上传失败:" + code);
}
  • # 上传成功后,可在上文注册的 AiResponseListener 回调中获取 res_id ,在 VmsHelper.getInst().start 接口参数中传入,即可显示背景。

# 4.4.4 重置接口

reset 接口用于中止当前正在进行中的所有驱动任务,回到虚拟形象静默状态


AiRequest.Builder aiRequest = AiRequest.builder();
aiRequest.header()
        .param("uid", mUid)
        .param("session", mSession);
int ret = VmsHelper.getInst().reset(aiRequest.build());
if (ret == 0) {
    isAudioRunning = false;
    interactiveModeIndex = 0; //如为追加模式,下次驱动需要重新从首帧开始
}
Log.d(TAG, "reset:" + ret);

# 4.4.5 更新VmsConfig

VmsHelper.getInst().updateVmsConfig 用于更新start接口传入的VMSConfig参数(全量更新),如切换前正在进行中会话的追加模式/打断模式 


VMSConfig vmsConfig = VMSConfig.builder()
        .useWebSocket(isWebSocket)   //是否使用使用ws接口,非实ws接口不支持时返回状态数据 默认使用ws接口
        .interactiveMode(InteractiveMode.valueOf(interactiveMode))  //模式 0 追加模式。1打断模式 可在会话期间更改
        .build();
VmsHelper.getInst().updateVmsConfig(vmsConfig);

# 4.4.6 回调

通过 AiResponseListener 监听回调,参考 3.4.2 节实现,在onResult 方法中根据获取到的key字段来区分各子能力结果,然后进行客制化处理。

# 4.4.7 关闭虚拟人

当调用start开启会话后,用户可以进行多轮输入,但无论进行多少轮输入,在程序退出前,一定要调用end方法。

//关闭虚拟人``int` `ret = VmsHelper.getInst().end();

# 4.5 设置视频流相关监听

您可以通过VmsHelper.getInst().setXrtcListener 获得来自视频拉流相关的各类事件通知(比如:音视频状态参数、错误码等)、自定义渲染回调等。示例代码如下:

VmsHelper.getInst().setXrtcListener(new VmsHelper.XrtcListener() {
    @Override
    public void onUserVideoAvailable(String userId, boolean available) {
 
    }
 
    @Override
    public void onError(int errCode, String errMsg, Bundle extraInfo) {
 
    }
 
    @Override
    public void onFirstVideoFrame(String userId, int streamType, int width, int height) {
 
    }
 
    @Override
    public void onNetworkQuality(IXRTCCloudDef.IXRTCQuality localQuality, ArrayList<IXRTCCloudDef.IXRTCQuality> remoteQuality) {
 
    }
 
    @Override
    public void onConnectionLost() {
 
    }
 
    @Override
    public void onTryToReconnect() {
 
    }
 
    @Override
    public void onConnectionRecovery() {
 
    }
 
    @Override
    public void onEnterRoom(int result) {
 
    }
 
    @Override
    public void onExitRoom(int reason) {
 
    }
 
    @Override
    public void onRenderVideoFrame(String userId, int streamType, IXRTCCloudDef.IXRTCVideoFrame videoFrame) {
 
    }
});

# 4.5.1 自定义视频渲染回调说明

void onRenderVideoFrame(String userId, int streamType, IXRTCCloudDef.IXRTCVideoFrame (opens new window) frame)

出入参 参数 说明
IN userId 视频源的 userId,该参数可以不用理会
IN streamType 视频流类型,例如是摄像头画面还是屏幕分享画面等等
IN frame 视频帧数据
RETURN void

XRTCVideoFrame说明:

属性 类型 必填 描述
pixelFormat int 视频的像素格式,透明通道视频帧为BGRA格式
bufferType int 视频数据结构类型,目前只支持XRTCVideoBufferType_Buffer
data byte[] 视频数据
width int 视频宽度
height int 视频高度
timestamp long 视频帧的时间戳,单位毫秒
rotation int 视频像素的顺时针旋转角度

pixelFormat 支持如下格式 IXRTCCloudDef.XRTC_VIDEO_PIXEL_FORMAT_I420、 IXRTCCloudDef.XRTC_VIDEO_PIXEL_FORMAT_BGRA32、 IXRTCCloudDef.XRTC_VIDEO_PIXEL_FORMAT_RGBA、 IXRTCCloudDef.XRTC_VIDEO_PIXEL_FORMAT_BGR24

# 4.6 逆初始化SDK

当不再使用SDK时,需调用逆初始化方法释放资源,示例代码如下:

JLibrary.getInst().unInit();

# 五.错误码

错误码有两张表,一个是SDK错误码,一个是云端错误码。

# 5.1 SDK错误码列表

该错误码字典适用于 AEE SDK 内核 v2.2.3_rc1 及以上版本。

错误码 问题分类 含义
错误码 问题分类 含义
18000 授权错误 本地 license 文件不存在
18001 授权错误 授权文件内容非法
18002 授权错误 授权文件解析失败
18003 授权错误 payload 内容缺失
18004 授权错误 signature 内容缺失
18005 授权错误 授权已过期
18006 授权错误 授权时间错误,设备时间比标准时间慢 30 分钟以上
18007 授权错误 授权应用不匹配(apiKey、apiSecret)
18008 授权错误 授权文件激活过期
18009 授权错误 授权 App 信息指针为空
18010 授权错误 离线授权激活文件指定平台与设备平台不匹配
18011 授权错误 离线授权激活文件指定架构与设备 CPU 架构不匹配
18012 授权错误 离线授权激活文件中包含 license 个数异常
18013 授权错误 离线授权激活文件中未找到当前设备
18014 授权错误 离线授权激活文件中设备指纹安全等级非法
18015 授权错误 硬件授权验证失败
18016 授权错误 离线授权激活文件内容非法
18017 授权错误 离线授权激活文件中协议头非法
18018 授权错误 离线授权激活文件中指纹组成项个数为0
18100 资源错误 资源鉴权失败
18101 资源错误 资源格式解析失败
18102 资源错误 资源(与引擎)不匹配
18103 资源错误 资源参数不存在(指针为 NULL)
18104 资源错误 资源路径打开失败
18105 资源错误 资源加载失败,workDir 内未找到对应资源
18106 资源错误 资源卸载失败, 卸载的资源未加载过
18200 引擎错误 引擎鉴权失败
18201 引擎错误 引擎动态加载失败
18202 引擎错误 引擎未初始化
18203 引擎错误 引擎不支持该接口调用
18204 引擎错误 引擎 craete 函数指针为空
18300 SDK错误 SDK 不可用
18301 SDK错误 SDK 没有初始化
18302 SDK错误 SDK 初始化失败
18303 SDK错误 SDK 已经初始化
18304 SDK错误 SDK 不合法参数
18305 SDK错误 SDK 会话 handle 为空
18306 SDK错误 SDK 会话未找到
18307 SDK错误 SDK 会话重复终止
18308 SDK错误 超时错误
18309 SDK错误 SDK正在初始化中
18310 SDK错误 SDK会话重复开启
18400 系统错误 工作目录无写权限
18401 系统错误 设备指纹获取失败,设备未知
18402 系统错误 文件打开失败
18403 系统错误 内存分配失败
18404 系统错误 设备指纹比较失败
18500 参数错误 未找到该参数 key
18501 参数错误 参数范围溢出,不满足约束条件
18502 参数错误 SDK 初始化参数为空
18503 参数错误 SDK 初始化参数中 AppId 为空
18504 参数错误 SDK 初始化参数中 ApiKey为空
18505 参数错误 SDK 初始化参数中 ApiSecret 为空
18506 参数错误 ability 参数为空
18507 参数错误 input 参数为空
18508 参数错误 输入数据参数 Key 不存在
18509 参数错误 必填参数确实
18510 参数错误 output参数缺失
18520 编解码错误 不支持的编解码类型
18521 编解码错误 编解码handle指针为空
18522 编解码错误 编解码模块条件编译未打开
18600 协议错误 协议中时间戳字段缺失
18601 协议错误 协议中未找到该能力 ID
18602 协议错误 协议中未找到该资源 ID
18603 协议错误 协议中未找到该引擎 ID
18604 协议错误 协议中引擎个数为 0
18605 协议错误 协议未被初始化解析
18606 协议错误 协议能力接口类型不匹配
18607 协议错误 预置协议解析失败
18608 云端错误 能力不存在,appid未授权或授权已过期
18700 云端错误 通用网络错误
18701 云端错误 网络不通
18702 云端错误 网关检查不过
18703 云端错误 云端响应格式不对
18704 云端错误 应用未注册
18705 云端错误 应用 ApiKey & ApiSecret 校验失败
18706 云端错误 引擎不支持的平台架构
18707 云端错误 授权已过期
18708 云端错误 授权数量已满
18709 云端错误 未找到该 App 绑定的能力
18710 云端错误 未找到该 App 绑定的能力资源
18711 云端错误 SDK请求参数云端无法解析
18712 云端错误 网络请求 404 错误
18713 云端错误 设备指纹安全等级不匹配
18714 云端错误 服务端无法查询到api_key,请检查api_key和api_secret信息是否填写正确
18715 云端错误 未找到该SDK ID
18716 云端错误 未找到该组合能力集合
18717 云端错误 SDK组合能力授权不足
18718 云端错误 无效授权应用签名
18719 云端错误 应用签名不唯一
18720 授权错误 能力schema不可用
18721 授权错误 竞争授权: 未找到能力集模板
18722 授权错误 竞争授权: 能力不在模板能力集模板中
18801 在线能力错误 连接建立出错
18802 在线能力错误 结果等待超时

# 5.2 常见云端错误码


ret="20014", ws打断功能异常
ret="10101", 引擎回话已结束
ret="10110", 授权不足
ret="10221", 服务端没有可用连接 
ret="10222", 请求超时
ret="11200", 功能未授权或授权到期
ret="11203", 并发流控超限
ret="20001", session 无效
ret="20004", 请求类型异常
ret="20007", 动作指令异常
ret="20015", 当前形象id 不存在
ret="20017", 文本审核不通过
ret="10163", json shema 校验异常
ret="20012", 同步请求调用超时
ret="20016", 当前发音人不存在
在线
咨询
建议
反馈
体验
中心