腾讯云游戏多媒体引擎Native SDK 快速接入_音视频解决方案_同尘科技

游戏多媒体引擎 1年前 (2023-09-28) 浏览 49

为方便开发者调试和接入腾讯云游戏多媒体引擎产品 API,这里向您介绍 Native 工程快速接入文档。GME 快速入门文档只提供最主要的接入接口,协助用户进行接入。

使用 GME 重要事项

GME 分为两个部分,提供实时语音服务、语音消息及转文本服务,使用这两个服务都依赖 Init 和 Poll 等核心接口。关于 Init 接口例如使用了实时语音服务,同时也需要使用语音消息服务,只需要调用一次 Init 初始化接口

接口调用流程图



接入步骤

核心接口

初始化 GME周期性调用 Poll 触发回调

实时语音

加入实时语音房间打开或关闭麦克风打开或关闭扬声器退出语音房间

语音消息

鉴权初始化启动流式语音识别停止录制反初始化 GME

核心接口接入

1. 下载 SDK

进入下载指引页面,下载对应的 客户端 SDK。

2. 引入头文件

JavaObject-CC++

import com.gme.TMG.ITMGContext;
#import "GMESDK/TMGEngine.h"#import "GMESDK/QAVAuthBuffer.h"
#include "auth_buffer.h"#include "tmg_sdk.h"

3. 获取单例

在使用语音功能时,需要首先获取 ITMGContext 对象。

函数原型

JavaObject-CC++

public static ITMGContext GetInstance(Context context)
+ (ITMGContext*) GetInstance;
 static ITMGContext* ITMGContextGetInstance()

示例代码

JavaObject-CC++

//MainActivity.javaimport com.gme.TMG.ITMGContext;ITMGContext tmgContext = ITMGContext.GetInstance(this);
//TMGSampleViewController.mITMGContext* _context = [ITMGContext GetInstance];
ITMGContext* context = ITMGContextGetInstance();

4. 设置回调

接口类采用 Delegate 方法用于向应用程序发送回调通知。将回调函数注册给 SDK,用于接收回调的信息,需要在进房之前设置。

函数原型及示例代码

设置回调,用于接收回调的信息,需要在进房之前设置。JavaObject-CC++

//ITMGContextpublic abstract int SetTMGDelegate(ITMGDelegate delegate);
//MainActivity.javatmgContext.SetTMGDelegate(TMGCallbackDispatcher.getInstance());
ITMGDelegate 
//TMGSampleViewController.mITMGContext* _context = [ITMGContext GetInstance];_context.TMGDelegate = [DispatchCenter getInstance];
//在初始化 SDK 时候m_pTmgContext = ITMGContextGetInstance();m_pTmgContext->SetTMGDelegate(this);//在析构函数中CTMGSDK_For_AudioDlg::~CTMGSDK_For_AudioDlg(){    ITMGContextGetInstance()->SetTMGDelegate(NULL);}

回调示例

在构造函数中重写此回调函数,对回调参数进行处理。JavaObject-CC++

//MainActivity.javatmgContext.SetTMGDelegate(TMGCallbackDispatcher.getInstance());
//RealTimeVoiceActivity.javapublic void OnEvent(ITMGContext.ITMG_MAIN_EVENT_TYPE type, Intent data) { if (type == ITMG_MAIN_EVENT_TYPE_ENTER_ROOM) { //回调处理 }}
//需要参考 TMGCallbackDispatcher.java、TMGCallbackHelper.java以及 TMGDispatcherBase.java
//TMGRealTimeViewController.mTMGRealTimeViewController ()

- (void)OnEvent:(ITMG_MAIN_EVENT_TYPE)eventType data:(NSDictionary *)data { NSString *log = [NSString stringWithFormat:@"OnEvent:%d,data:%@", (int)eventType, data]; [self showLog:log]; NSLog(@"====%@====", log); switch (eventType) { // Step 6/11 : Perform the enter room event case ITMG_MAIN_EVENT_TYPE_ENTER_ROOM: { int result = ((NSNumber *)[data objectForKey:@"result"]).intValue; NSString *error_info = [data objectForKey:@"error_info"];
[self showLog:[NSString stringWithFormat:@"OnEnterRoomComplete:%d msg:(%@)", result, error_info]];
if (result == 0) { [self updateStatusEnterRoom:YES]; } } break;
} }
//需要参考 DispatchCenter.h、DispatchCenter.m
//头文件中声明virtual void OnEvent(ITMG_MAIN_EVENT_TYPE eventType,const char* data);//示例代码void CTMGSDK_For_AudioDlg::OnEvent(ITMG_MAIN_EVENT_TYPE eventType, const char* data){    switch(eventType)    {    case ITMG_MAIN_EVENT_TYPE_XXXX_XXXX:        {            //对回调进行处理        }        break;    }}
参数 类型 含义
type ITMGContext.ITMG_MAIN_EVENT_TYPE 回调的事件类型
data Intent 消息类型 回调的相关信息,事件数据

5. 初始化 SDK

未初始化前,SDK 处于未初始化阶段,需要通过接口 Init 初始化 SDK,才可以使用实时语音服务、语音消息服务及转文本服务。调用 Init 接口的线程必须于其他接口在同一线程,建议都在主线程调用接口。

接口原型

JavaObject-CC++

public abstract int Init(String sdkAppId, String openId);
-(int)InitEngine:(NSString*)sdkAppID openID:(NSString*)openID;
ITMGContext virtual int Init(const char* sdkAppId, const char* openId)
参数 类型 含义
sdkAppId string 来自 腾讯云控制台 的 GME 服务提供的 AppID,获取请参见 服务开通指引。
openID string openID 只支持 Int64 类型(转为 string 传入),规则由 App 开发者自行制定,App 内不重复即可。如需使用字符串作为 Openid 传入,可 提交工单 联系开发者。

示例代码

JavaObject-CC++

//MainActivity.javaint nRet = tmgContext.Init(appId, openId);if (nRet == AV_OK ){    // Step 4/11: Poll to trigger callback    //https://cloud.tencent.com/document/product/607/15210#.E8.A7.A6.E5.8F.91.E4.BA.8B.E4.BB.B6.E5.9B.9E.E8.B0.83    EnginePollHelper.createEnginePollHelper();    showToast("Init success");}else if (nRet == AV_ERR_HAS_IN_THE_STATE) // 已经初始化过了,可以认为本次操作是成功的{    showToast("Init success");}else{    showToast("Init error errorCode:" + nRet);}
//TMGSampleViewController.mQAVResult result = [_context InitEngine:self.appIDTF.text openID:self.openIDTF.text];if (result == QAV_OK) {    self.isSDKInit = YES;}
#define SDKAPPID3RD "14000xxxxx"cosnt char* openId="10001";ITMGContext* context = ITMGContextGetInstance();context->Init(SDKAPPID3RD, openId);

6. 触发事件回调

通过在 update 里面周期的调用 Poll 可以触发事件回调。Poll 是 GME 的消息泵,GME 需要周期性的调用 Poll 接口触发事件回调。如果没有调用 Poll ,将会导致整个 SDK 服务运行异常。详情请参见 Sample Project 中的 EnginePollHelper 文件。

示例代码

JavaObject-CC++

//MainActivity.javaEnginePollHelper.createEnginePollHelper();
//EnginePollHelper.javaprivate Handler mhandler = new Handler(); private Runnable mRunnable = new Runnable() { @Override public void run() { if (s_pollEnabled) { if (ITMGContext.GetInstance(null) != null) ITMGContext.GetInstance(null).Poll(); } mhandler.postDelayed(mRunnable, 33); } };//周期性调用 Poll 请参考 EnginePollHelper.java 写法
//TMGSampleViewController.m[EnginePollHelper createEnginePollHelper];//需要参考 EnginePollHelper.m 以及 EnginePollHelper.h
void TMGTestScene::update(float delta){  ITMGContextGetInstance()->Poll();}

7. 本地鉴权计算

生成 AuthBuffer,用于相关功能的加密和鉴权,如正式发布请使用后台部署密钥,后台部署请参见 鉴权密钥。

接口原型

JavaObject-CC++

AuthBuffer public native byte[] genAuthBuffer(int sdkAppId, String roomId, String openId, String key)
//TMGSampleViewController.m[EnginePollHelper createEnginePollHelper];//需要参考 EnginePollHelper.m 以及 EnginePollHelper.h
void TMGTestScene::update(float delta){  ITMGContextGetInstance()->Poll();}
参数 类型 含义
appId int 来自腾讯云控制台的 AppId 号码。
roomId string 房间号,最大支持127字符(离线语音房间号参数必须填 null)。
openId string 用户标识。与 Init 时候的 openId 相同。
key string 来自腾讯云 控制台 的权限密钥。

示例代码

JavaObject-CC++

//GMEAuthBufferHelper.javaimport com.tencent.av.sig.AuthBuffer;//头文件public byte[] createAuthBuffer(String roomId)    {        byte[] authBuffer;        // 生成鉴权秘钥,在开发调试阶段可以使用GME SDK提供的接口生成;        // 应用正式发布时建议使用服务器生成,可参考https://cloud.tencent.com/document/product/607/12218        if (TextUtils.isEmpty(roomId))        {            authBuffer =  AuthBuffer.getInstance().genAuthBuffer(Integer.parseInt(mAppId), "0", mOpenId, mKey);        }else        {            authBuffer =  AuthBuffer.getInstance().genAuthBuffer(Integer.parseInt(mAppId), roomId, mOpenId, mKey);        }        return authBuffer;    }
// 生成鉴权秘钥,在开发调试阶段可以使用GME SDK提供的接口生成;// 应用正式发布时建议使用服务器生成,可参考https://cloud.tencent.com/document/product/607/12218
//实时语音鉴权 NSData* authBuffer = [QAVAuthBuffer GenAuthBuffer:SDKAPPID3RD.intValue roomID:self.roomIdTF.text openID:_openId key:_key];//语音消息鉴权NSData* authBuffer = [QAVAuthBuffer GenAuthBuffer:(unsigned int)SDKAPPID3RD.integerValue roomID:nil openID:self.openId key:AUTHKEY];
// 生成鉴权秘钥,在开发调试阶段可以使用GME SDK提供的接口生成;// 应用正式发布时建议使用服务器生成,可参考https://cloud.tencent.com/document/product/607/12218unsigned int bufferLen = 512;unsigned char retAuthBuff[512] = {0};QAVSDK_AuthBuffer_GenAuthBuffer(atoi(SDKAPPID3RD), roomId, "10001", AUTHKEY,retAuthBuff,bufferLen);

实时语音接入

1. 加入房间

用生成的鉴权信息进房,加入房间默认不打开麦克风及扬声器。返回值为 AV_OK 的时候代表调用成功,不代表进房成功。

接口原型

JavaObject-CC++

public abstract int EnterRoom(String roomID, int roomType, byte[] authBuffer);
-(int)EnterRoom:(NSString*) roomId roomType:(int)roomType authBuffer:(NSData*)authBuffer;
ITMGContext virtual int EnterRoom(const char*  roomID, ITMG_ROOM_TYPE roomType, const char* authBuff, int buffLen);
参数 类型 含义
roomId String 房间号,最大支持127字符
roomType int 请使用 FLUENCY 类型音质进入房间
authBuffer byte[] 鉴权码

示例代码

JavaObject-CC++

//RealTimeVoiceActivity.javabyte[] authBuffer =  GMEAuthBufferHelper.getInstance().createAuthBuffer(roomId);ITMGContext.GetInstance(this).EnterRoom(roomId, roomType, authBuffer);
//TMGRealTimeViewController.m[[ITMGContext GetInstance] EnterRoom:self.roomIdTF.text roomType:(int)self.roomTypeControl.selectedSegmentIndex + 1 authBuffer:authBuffer];
ITMGContext* context = ITMGContextGetInstance();context->EnterRoom(roomID, ITMG_ROOM_TYPE_FLUENCY, (char*)retAuthBuff,bufferLen);

加入房间事件回调

加入房间完成后会发送信息 ITMG_MAIN_EVENT_TYPE_ENTER_ROOM,在 OnEvent 函数中进行判断回调后处理。如果回调为成功,即此时进房成功,开始进行计费计费问题参考:购买指南。计费相关问题。使用实时语音后,如果客户端掉线了,是否还会继续计费?示例代码:回调处理相关参考代码,包括加入房间事件以及断网事件。JavaObject-CC++

//RealTimeVoiceActivity.javapublic void OnEvent(ITMGContext.ITMG_MAIN_EVENT_TYPE type, Intent data) {    if (type == ITMG_MAIN_EVENT_TYPE_ENTER_ROOM)    {        // Step 6/11 : Perform the enter room event        int nErrCode = TMGCallbackHelper.ParseIntentParams2(data).nErrCode;        String strMsg = TMGCallbackHelper.ParseIntentParams2(data).strErrMsg;        if (nErrCode == AV_OK)        {            appendLog2MonitorView("EnterRomm success");        }else        {            appendLog2MonitorView(String.format(Locale.getDefault(), "EnterRomm errCode:%d errMsg:%s", nErrCode, strMsg));        }    }}		
//TMGRealTimeViewController.m
- (void)OnEvent:(ITMG_MAIN_EVENT_TYPE)eventType data:(NSDictionary *)data { NSString *log = [NSString stringWithFormat:@"OnEvent:%d,data:%@", (int)eventType, data]; [self showLog:log]; NSLog(@"====%@====", log); switch (eventType) { // Step 6/11 : Perform the enter room event case ITMG_MAIN_EVENT_TYPE_ENTER_ROOM: { int result = ((NSNumber *)[data objectForKey:@"result"]).intValue; NSString *error_info = [data objectForKey:@"error_info"];
[self showLog:[NSString stringWithFormat:@"OnEnterRoomComplete:%d msg:(%@)", result, error_info]];
if (result == 0) { [self updateStatusEnterRoom:YES]; } } break;
}
  void TMGTestScene::OnEvent(ITMG_MAIN_EVENT_TYPE eventType,const char* data){  switch (eventType) {      case ITMG_MAIN_EVENT_TYPE_ENTER_ROOM:      {          ListMicDevices();          ListSpeakerDevices();              std::string strText = "EnterRoom complete: ret=";          strText += data;          m_EditMonitor.SetWindowText(MByteToWChar(strText).c_str());          }  }  }

错误码

错误码值 原因及建议方案
7006 鉴权失败,原因如下:AppID 不存在或者错误authbuff 鉴权错误鉴权过期 openId 不符合规范
7007 已经在其它房间
1001 已经在进房过程中,然后又重复了此操作。建议在进房回调返回之前不要再调用进房接口
1003 已经进房了在房间中,又调用一次进房接口
1101 确保已经初始化 SDK,确保 openId 是否符合规则,或者确保在同一线程调用接口,以及确保 Poll 接口正常调用

2. 开启或关闭麦克风

此接口用来开启关闭麦克风。加入房间默认不打开麦克风及扬声器。

示例代码

JavaObject-CC++

//RealTimeVoiceActivity.javaITMGContext.GetInstance(this).GetAudioCtrl().EnableMic(true);
//TMGRealTimeViewController.m[[[ITMGContext GetInstance] GetAudioCtrl] EnableMic:YES];
ITMGContextGetInstance()->GetAudioCtrl()->EnableMic(true);

3. 开启或关闭扬声器

此接口用于开启关闭扬声器。

示例代码

JavaObject-CC++

//RealTimeVoiceActivity.javaITMGContext.GetInstance(this).GetAudioCtrl().EnableSpeaker(true);
//TMGRealTimeViewController.m[[[ITMGContext GetInstance] GetAudioCtrl] EnableSpeaker:YES];
ITMGContextGetInstance()->GetAudioCtrl()->EnableSpeaker(true);

4. 退出房间

通过调用此接口可以退出所在房间。需等待退房回调并进行处理。

示例代码

JavaObject-CC++

//RealTimeVoiceActivity.javaITMGContext.GetInstance(this).ExitRoom();
//TMGRealTimeViewController.m[[ITMGContext GetInstance] ExitRoom];
ITMGContext* context = ITMGContextGetInstance();context->ExitRoom();

退出房间回调

退出房间完成后会有回调,消息为 ITMG_MAIN_EVENT_TYPE_EXIT_ROOM。示例代码如下:JavaObject-CC++

//RealTimeVoiceActivity.javapublic void OnEvent(ITMGContext.ITMG_MAIN_EVENT_TYPE type, Intent data) {    if (ITMGContext.ITMG_MAIN_EVENT_TYPE.ITMG_MAIN_EVENT_TYPE_EXIT_ROOM == type)        {            //收到退房成功事件        }}
//TMGRealTimeViewController.m-(void)OnEvent:(ITMG_MAIN_EVENT_TYPE)eventType data:(NSDictionary *)data{NSLog(@"OnEvent:%lu,data:%@",(unsigned long)eventType,data);switch (eventType) {    case ITMG_MAIN_EVENT_TYPE_EXIT_ROOM:    {        //收到退房成功事件    }        break;    }}
void TMGTestScene::OnEvent(ITMG_MAIN_EVENT_TYPE eventType,const char* data){switch (eventType) {    case ITMG_MAIN_EVENT_TYPE_EXIT_ROOM:    {        //进行处理        break;        }    }}

语音消息接入

1. 鉴权初始化

在初始化 SDK 之后调用鉴权初始化,authBuffer 的获取参见上文实时语音鉴权信息接口 genAuthBuffer。

接口原型

JavaObject-CC++

public abstract int ApplyPTTAuthbuffer(byte[] authBuffer);
-(QAVResult)ApplyPTTAuthbuffer:(NSData *)authBuffer;
ITMGPTT virtual int ApplyPTTAuthbuffer(const char* authBuffer, int authBufferLen)
参数 类型 含义
authBuffer String 鉴权

示例代码

JavaObject-CC++

//VoiceMessageRecognitionActivity.javabyte[] authBuffer = GMEAuthBufferHelper.getInstance().createAuthBuffer("");ITMGContext.GetInstance(this).GetPTT().ApplyPTTAuthbuffer(authBuffer);
//TMGPTTViewController.mNSData* authBuffer =  [QAVAuthBuffer GenAuthBuffer:(unsigned int)SDKAPPID3RD.integerValue roomID:nil openID:self.openId key:AUTHKEY];[[[ITMGContext GetInstance] GetPTT] ApplyPTTAuthbuffer:authBuffer];
ITMGContextGetInstance()->GetPTT()->ApplyPTTAuthbuffer(authBuffer,authBufferLen);

2. 启动流式语音识别

此接口用于启动流式语音识别,同时在回调中会有实时的语音转文字返回。停止录音调用 StopRecording,停止之后才有回调。

接口原型

JavaObject-CC++

public abstract int StartRecordingWithStreamingRecognition (String filePath);
public abstract int StopRecording();
-(int)StartRecordingWithStreamingRecognition:(NSString *)filePath;
-(QAVResult)StopRecording;
ITMGPTT virtual int StartRecordingWithStreamingRecognition(const char* filePath) 
ITMGPTT virtual int StopRecording()
参数 类型 含义
filePath String 存放的语音路径

示例代码

JavaObject-CC++

//VoiceMessageRecognitionActivity.javaITMGContext.GetInstance(this).GetPTT().StartRecordingWithStreamingRecognition(recordfilePath);
//TMGPTTViewController.mQAVResult ret = [[[ITMGContext GetInstance] GetPTT] StartRecordingWithStreamingRecognition:[self pttTestPath]];if (ret == 0) {    self.currentStatus = @"开始流式录音";} else {    self.currentStatus = @"开始流式录音失败";}
ITMGContextGetInstance()->GetPTT()->StartRecordingWithStreamingRecognition(filePath);

流式语音识别回调

启动流式语音识别后,需要在回调函数 OnEvent 中监听回调消息,事件消息分为 ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE ,在停止录制并完成识别后才返回文字,相当于一段话说完才会返回识别的文字。根据需求在 OnEvent 函数中对相应事件消息进行判断。传递的参数包含以下4个信息。

消息名称 含义
result 用于判断流式语音识别是否成功的返回码
text 语音转文字识别的文本
file_path 录音存放的本地地址
file_id 录音在后台的 url 地址,录音在服务器存放90天

示例代码JavaObject-CC++

//VoiceMessageRecognitionActivity.javaimport static com.tencent.TMG.ITMGContext.ITMG_MAIN_EVENT_TYPE.ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE;public void OnEvent(ITMGContext.ITMG_MAIN_EVENT_TYPE type, Intent data) {    if (type == ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE)        {            // Step 1.3/3 handle ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE event            mIsRecording = false;            if (nErrCode ==0)            {                String recordfilePath = data.getStringExtra("file_path");                mRecFilePathView.setText(recordfilePath);
String recordFileUrl = data.getStringExtra("file_id"); mRecFileUrlView.setText(recordFileUrl); } else { appendLog2MonitorView("Record and recognition fail errCode:" + nErrCode); } }
}
//TMGPTTViewController.m
- (void)OnEvent:(ITMG_MAIN_EVENT_TYPE)eventType data:(NSDictionary*)data { NSNumber *number = [data objectForKey:@"result"]; switch (eventType) { case ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE: { if (data != NULL &&[[data objectForKey:@"result"] intValue]== 0) { self.translateTF.text = [data objectForKey:@"text"] ; self.currentStatus = @"流式转换完成"; } } break; }
  void TMGTestScene::OnEvent(ITMG_MAIN_EVENT_TYPE eventType,const char* data){    switch (eventType) {        case ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_COMPLETE:        {            HandleSTREAM2TEXTComplete(data,true);            break;        }        ...        case ITMG_MAIN_EVNET_TYPE_PTT_STREAMINGRECOGNITION_IS_RUNNING:        {            HandleSTREAM2TEXTComplete(data, false);            break;        }    }  }  void CTMGSDK_For_AudioDlg::HandleSTREAM2TEXTComplete(const char* data, bool isComplete)  {    std::string strText = "STREAM2TEXT: ret=";    strText += data;    m_EditMonitor.SetWindowText(MByteToWChar(strText).c_str());    Json::Reader reader;    Json::Value root;    bool parseRet = reader.parse(data, root);    if (!parseRet) {        ::SetWindowText(m_EditInfo.GetSafeHwnd(),MByteToWChar(std::string("parse result Json error")).c_str());    }        else        {            if (isComplete) {                                ::SetWindowText(m_EditUpload.GetSafeHwnd(), MByteToWChar(root["file_id"].asString()).c_str());                            }                            else {                                    std::string isruning = "STREAMINGRECOGNITION_IS_RUNNING";                                    ::SetWindowText(m_EditUpload.GetSafeHwnd(), MByteToWChar(isruning).c_str());                                 }    }  }

错误码

错误码 含义 处理方式
32775 流式语音转文本失败,但是录音成功 调用 UploadRecordedFile 接口上传录音,再调用 SpeechToText 接口进行语音转文字操作
32777 流式语音转文本失败,但是录音成功,上传成功 返回的信息中有上传成功的后台 url 地址,调用 SpeechToText 接口进行语音转文字操作
32786 流式语音转文本失败 在流式录制状态当中,请等待流式录制接口执行结果返回

3. 停止录音

此接口用于停止录音。此接口为异步接口,停止录音后会有录音完成回调,成功之后录音文件才可用。

接口原型

JavaObject-CC++

public abstract int StopRecording();
-(QAVResult)StopRecording;
ITMGPTT virtual int StopRecording();

示例代码

JavaObject-CC++

//VoiceMessageRecognitionActivity.javaITMGContext.GetInstance(this).GetPTT().StopRecording();
//TMGPTTViewController.m
- (void)stopRecClick { // Step 3/12 stop recording, need handle ITMG_MAIN_EVNET_TYPE_PTT_RECORD_COMPLETE event // https://cloud.tencent.com/document/product/607/15221#.E5.81.9C.E6.AD.A2.E5.BD.95.E9.9F.B3 QAVResult ret = [[[ITMGContext GetInstance] GetPTT] StopRecording]; if (ret == 0) { self.currentStatus = @"停止录音"; } else { self.currentStatus = @"停止录音失败"; } }
  ITMGContextGetInstance()->GetPTT()->StopRecording();



对音视频的解决方案有疑惑?想了解解决方案收费? 联系解决方案专家

腾讯云限时活动1折起,即将结束: 马上收藏

同尘科技为腾讯云授权服务中心,购买腾讯云享受折上折,更有现金返利:同意关联,立享优惠

阿里云解决方案也看看?: 点击对比阿里云的解决方案

- 0人点赞 -

发表点评 (0条)

not found

暂无评论,你要说点什么吗?