好视通aPaaS SDK(application Platform as a Service 应用平台即服务SDK,简称应用SDK)是好视通将多年在远程教育、远程培训、远程招聘、应急指挥等行业积累的云服务能力提供给第三方开发者,帮助开发者快速的搭建全方位、低延迟、高质量的音视频解决方案。
好视通SDK方案可以与好视通行业方案结合满足各种场景需求,客户可以通过服务器API以及应用SDK对业务进行管理。同时该方案也可以与好视通行业方案的软件、硬件共同使用,客户可通过SDK与自身业务软件相结合,打造垂直领域的解决方案。
SDK主要提供以下功能
功能 | 功能说明 |
---|---|
音视频 | 支持一对一、一对多的音视频交流,参会者支持同时打开多个摄像头 |
外设管理 | 允许参会者管理自己的音视频外设的参数配置 |
屏幕共享 | 支持Web端把桌面作为数据进行共享以及接收 |
文字聊天 | 允许参会者聊天或与其他人私聊 |
布局 | 允许切换布局来同时显示多路视频 |
参会者管理 | 获取当前会中参会人的信息、权限、音视频状态,以及对其进行管理 |
会控 | 收取会议中的事件并响应, 支持控制权限的申请 |
共享白板 | 支持创建和接收白板数据 |
如需使用SDK,需要联系商务在企业先开通开发者功能。
开通后,可在新版公有云后台创建SDK应用并获取clientID和clientSecret,clientID和clientSecret用于校验其合法性。
私有云客户请联系商务获取服务器部署与SDK功能支持。
clientID和clientSecret是好视通设计的一种识别应用的唯一签名,可以保护客户的云服务资源不被非认证的应用盗用消耗。
clientID和clientSecret是一起使用的,可以用于一个平台应用也可用于多个平台应用。如果clientID和clientSecret一起被泄漏,可通过停用或者删除来阻止攻击者使用。开发者只用再替换一套clientID和clientSecret继续做认证。
推荐clientID和clientSecret由服务端进行分发,这样可以更快的实现clientID和clientSecret的更新。
把clientID和clientSecret放到客户端代码内是一种不推荐的做法,只适合快速运行Demo时提高便利性。客户端代码容易被反编译破译出其中的密钥信息。一旦clientID和clientSecret泄漏,攻击者就可能攻击您所在的服务器。
推荐将clientID和clientSecret放到您的服务器上,然后通过您内部的可信任鉴权方式让客户端获取到服务器上的clientID和clientSecret。同时这种方式也能方便用户在clientID和clientSecret泄漏时进行替换。
步骤1:下载SDK Demo
先在官网下载Demo
步骤2:导入项目
步骤3:编译运行
打包时需要将上层APP文件和SDK的hstsdk\bin\release目录下所有文件都加入安装包中(debug版同理)。
安装APP的同时需要安装FMPrinter(用于会议中的部分功能),如上图,只需运行hstsdk\FMPrinter目录下的reinstall.bat脚本即可自动安装FMPrinter。
1.1.1 获取SDK
点击下载:
dll 目录有Debug和Release两种编译方式的动态库和依赖文件,集成时需要全部拷贝到自己程序的运行目录。
lib 目录有Debug和Release两种编译方式的链接库,集成时需要放在自己工程设置的库目录路径中。
include 目录有集成时需要包含的头文件,使用时都放在同一个目录,包含ICommonsDef.h一个文件即可。
1.1.2 新建VS2015 工程
1.1.3配置工程
右键项目属性,Release,Debug 都设置成相应的配置路径。
注:常规配置下输出目录与目标文件名、扩展名与链接器配置中输出文件中目录、输出文件名、扩展名需要一一对应。
将从官网下载SDK包解压目录下dll 目录中Debug/Release 中的所有文件 dll、exe 文件拷入到输出路径对应的Debug/ Release目录。
注:
(1)Debug 需要从官网下载 SdkDemo解压后的 Debug 目录中拷贝下列文件和目录到自己工程输出的debug目录:
然后将SDK 解压的Debug 目录下的dll和exe文件 全部拷贝到debug 目录。
(2)Release 需要从 SdkDemo的 Release 拷贝下列文件和目录:
注:skin目录为SdkDemo皮肤文件,集成SDK不需要demo的皮肤文件。
(1) 右键工程属性,设置对应的头文件目录。
(2)将SDK include 目录拷贝到工程配置的头文件目录。
(1)右键工程,属性,设置lib 库目录。
(2) 拷贝SDK 目录的lib 到对应设置的lib目录(debug 和release 拷贝对应的lib目录)。
头文件包含以及lib引用示例代码:
#include "ICommonsDef.h" //SDK 相关包含 #ifdef _DEBUG #pragma comment(lib,"winsdkd.lib") //SDK 相关包含 #else #pragma comment(lib,"winsdk.lib") //SDK 相关包含 #endif
这样,基本集成完SDK相关文件,就可以在程序中调用SDK 接口,和使用SDK相关数据结构了。
1.1.4 常见问题以及解决方案
(1)Debug检查config.data、 meetingMgrCfg.data、UIConfig.xml、Resource目录下文件以及目录中res.xml、FMUIFrameWork.dll等SDK 动态库。
(2)Release检查config.data、 meetingMgrCfg.data、uiconfig.data、Resource目录下文件以及目录中
es.data、FMUIFrameWork.dll等SDK 动态库。
1.2.1 SDK文档常用语说明
文档常用语表:
常用语 | 含义 | 备注 |
---|---|---|
SDKCallback | 表示添加给ISdkManager* 的回调函数 | EV_SDK_前缀处理 |
LoginCallback | 表示添加给ILoginManager* 的回调函数 | EV_LOGIN_前缀处理 |
Manager* | 表示SDK相关接口管理类 | |
Other Callback | 表示除SDKCallback, LoginCallback 外的其他Callback | |
Other Manager* | 表示除ISdkManager, ILoginManager* 外的其他Manager* | |
MeetingCallback | 会议回调 | EV_MEETING_前缀处理 |
WBCallback | 白板回调 | EV_SHARE..._WB前缀处理 |
VideoCallback | 视频相关回调 | EV_VIDEO_前缀处理 |
AudioCallback | 音频相关回调 | EV_AUDIO_前缀处理 |
ChatCallback | 聊天回调 | EV_MEETING_前缀处理 |
UserCallback | 用户回调 | EV_USER_前缀处理 |
ScreenCallback | 屏幕共享回调 | EV_SHARE前缀处理 |
1.2.2 SDK应用整体结构图
1.2.3 SDK调用时序
1.2.4 回调函数结构
typedef std::function<void(PVOID)> SNOTIFY; Eg: void aaa(PVOID param);
1.2.5 事件结构
(1) 结构体
struct SEventData { SDK_EVENT ev; ErrCode ec; PVOID pl; SEventData() {} SEventData(SDK_EVENT v, ErrCode c, PVOID p) :ev(v), ec(c), pl(p) {} };
(2)SDK 通知事件需要在哪里处理介绍:
前缀 | 使用 |
---|---|
EV_SDK | 在SDKCallback 中处理 |
EV_LOGIN | 在LoginCallback中处理 |
EV_MEETING | 在MeetingCallback中处理 |
EV_CHAT | 在ChatCallback中处理 |
EV_VIDEO | 在VideoCallback中处理 |
EV_AUDIO | 在AudioCallback中处理 |
EV_USER | 在UserCallback中处理 |
EV_SHARE | 在ShareCallback中处理 |
EV_PERMISSON | 在PermissionCallback 中处理 |
EV_SHARE...WB | 在WBCallback 中处理 |
ErrCode 表示一些异步操作的通知结果 ERR_SUCCESS 表示成功。
PVOID pl; 表示一些事件回调上来的数据,需要转换成对应的类型后使用 。
注:所有的Manager的回调可以是同一个回调函数。
通过getSdkManager() 接口获取ISdkManager* 实例。
FS_SDK::ISdkManager* m_MeetingApi = nullptr; FS_SDK::ILoginManager* m_LoginManager = nullptr; FS_SDK::IAudioManager* m_AudioManager = nullptr; FS_SDK::IVideoManager* m_VideoManager = nullptr; FS_SDK::IChatManager* m_ChatManager = nullptr; FS_SDK::IMeetingManager* m_MeetingManager = nullptr; FS_SDK::IUserManager* m_UserManager = nullptr; FS_SDK::IScreenShareManager* m_ShareManager = nullptr; FS_SDK::IPermissionManager* m_PermissionManager = nullptr; FS_SDK::IWBShareManager* m_WBManager = nullptr; m_MeetingApi = getSdkManager(); if (!m_MeetingApi) { g_pSuperLog->WriteFile(_T("getSdkManager调用失败! 返回nullptr\n")); break; } // 这些必须同时初始化 m_LoginManager = m_MeetingApi->getLoginManager(); m_AudioManager = m_MeetingApi->getAudioManager(); m_VideoManager = m_MeetingApi->getVideoManager(); m_ChatManager = m_MeetingApi->getChatManager(); m_MeetingManager = m_MeetingApi->getMeetingManager(); m_UserManager = m_MeetingApi->getUserManager(); m_ShareManager = m_MeetingApi->getScreenShareManager(); m_PermissionManager = m_MeetingApi->getPermissionManager(); m_WBManager = m_MeetingApi->getWBShareManager(); if (!m_LoginManager || !m_AudioManager || !m_VideoManager || !m_MeetingManager \ || !m_UserManager || !m_ShareManager || !m_ChatManager || !m_PermissionManager \ || !m_WBManager) { g_pSuperLog->WriteFile(L"SDK 组件管理初始化失败\n"); break; } //end
通过ISdkManager* 实例调用addEventListener()接口添加回调。
// 添加sdk 通知监听 m_MeetingApi->addEventListener([&](PVOID pData) { SDKCallBack(pData); });
2.3.1 SDK 初始化代码
示例代码:
FS_SDK::ErrCode hR = m_MeetingApi->initSdk(_T("127.0.0.1:1089")); if (FS_SDK::ERR_SUCCESS != hR) { g_pSuperLog->WriteLogMsg("m_MeetingApi->Init Failed ,Error Code %d\n", hR); }
初始化后会有事件回调 EV_SDK_INIT 通知给SDKCallBack。
2.3.2 添加登录回调
SDKCallBack收到EV_SDK_INIT 事件后添加登录回调。
示例代码:
void CSdkManager::SDKCallBack(PVOID pParam) { g_pSuperLog->WriteLogMsg("CSdkManager::SDKCallBack Enter \n"); FS_SDK::SEventData* pData = (FS_SDK::SEventData*)pParam; if (!pData) { g_pSuperLog->WriteLogMsg("SDKCallBack 通知数据结构为 nullptr \n"); g_pSuperLog->WriteLogMsg("CSdkManager::SDKCallBack pData == nullptr leave \n"); return; } g_pSuperLog->WriteLogMsg("SDKCallBack 通知事件: %d\n", pData->ev); if (m_bClose) { g_pSuperLog->WriteLogMsg("CSdkManager::SDKCallBack m_bClose == true leave \n"); return; } switch (pData->ev) { case FS_SDK::EV_SDK_INIT: { g_pSuperLog->WriteLogMsg("Notify: %s\n", "EV_SDK_INIT"); g_pSuperLog->WriteFile(L"初始化登录回调\n"); m_LoginManager->addEventListener([&](PVOID pData) { LoginCallBack(pData); }); } break; default: break; } g_pSuperLog->WriteLogMsg("CSdkManager::SDKCallBack leave \n"); }
2.3.3 添加其他回调
LoginCallBack收到EV_LOGIN_ROOM_INIT 事件后添加登录回调。
示例代码:
void CSdkManager::LoginCallBack(PVOID pParam) { FS_SDK::SEventData* pData = (FS_SDK::SEventData*)pParam; if (!pData) { g_pSuperLog->WriteLogMsg("LoginCallBack 通知数据结构为 nullptr \n"); g_pSuperLog->WriteLogMsg("CSdkManager::LoginCallBack leave \n"); return; } g_pSuperLog->WriteLogMsg("LoginCallBack 通知事件: %d \n", pData->ev); if (m_bClose) { g_pSuperLog->WriteLogMsg("CSdkManager::LoginCallBack m_bClose == true leave \n"); return; } switch (pData->ev) { case FS_SDK::EV_LOGIN_ROOM_INIT: { g_pSuperLog->WriteLogMsg("Notify: %s\n", "EV_LOGIN_ROOM_INIT"); g_pSuperLog->WriteFile(L"初始化会议相关监听回调\n"); m_ChatManager->addEventListener([&](PVOID pData) { ChatCallBack(pData); }); m_ShareManager->addEventListener([&](PVOID pData) { ShareCallBack(pData); }); m_AudioManager->addEventListener([&](PVOID pData) { AudioCallBack(pData); }); m_VideoManager->addEventListener([&](PVOID pData) { VideoCallBack(pData); }); m_UserManager->addEventListener([&](PVOID pData) { UserCallBack(pData); }); m_MeetingManager->addEventListener([&](PVOID pData) { MeetingCallBack(pData); }); m_PermissionManager->addEventListener([&](PVOID pData) { PermissionCallBack(pData); }); m_WBManager->addEventListener([&](PVOID pData) { WBCallBack(pData); }); } break; default: break; } }
EV_LOGIN_ENV_INIT 事件回调后设置ClientInfo。
示例代码:
FS_SDK::LSdkTokenParam tokenparam; tokenparam.strOauthKey = m_LoginParam.strClientID.c_str(); tokenparam.strOauthSecret = m_LoginParam.strSecret.c_str(); FS_SDK::ErrCode hR = CSdkManager::GetInstance()->SetSdkClientInfo(tokenparam); if (hR != FS_SDK::ERR_SUCCESS) { g_pSuperLog->WriteFile(L"设置ClientID或者Secret失败!\n"); }
注意:setServerIp 后需要重新调用 setClientIdInfo, 否则无法登录。
必须在LoginCallBack 回调收到EV_LOGIN_ENV_INIT 事件后,才可以调用登录接口。
示例代码:
FS_SDK::ErrCode CSdkManager::Login(const char* username, const char* pwd, const char* meetid) { g_pSuperLog->WriteLogMsg("账号登录, 用户: %s, 密码:******, 会议室号:%s ,\n", username, meetid); return m_LoginManager->loginAccount(username, pwd, meetid); }
示例代码:
FS_SDK::ErrCode CSdkManager::LoginRoomID(const char* nickname, const char* pwd, const char* meetid) { g_pSuperLog->WriteLogMsg("会议室号登录, 昵称:%s, 会议密码:******, 会议室号:%s ,\n", nickname, meetid); return m_LoginManager->loginRoomID(nickname, pwd, meetid); }
ERR_LOGIN_SUCCESS才能确定登录成功。
case FS_SDK::EV_MEETING_ENTER: { g_pSuperLog->WriteLogMsg("初始化完成 EV_MEETING_ENTER\n"); FS_SDK::LUserCommond* pBc = STATIC_CAST_(FS_SDK::LUserCommond*, pData->pl); if (pBc) { m_dwLocalUserId = pBc->uUserID; CSdkManager::GetInstance()->SetLocalUserID(m_dwLocalUserId); } } break;
1、MeetingManager 中 exitMeeting退出会议
2、收到EV_LOGIN_EXIT_ROOM, EV_MEETING_CLOSE事件后需要调用releaseSdk() 接口
3、当收到EV_SDK_UNINIT 事件后方可关闭上层应用程序
4、收到EV_MEETING_KICK 需要调用exitMeeting() 接口
5、未登录时关闭程序需要调用releaseSdk()接口
struct AudioChannel { S_BYTE id = 0;; S_BYTE state = 0;; //状态 S_BYTE is_have_audio = 0; // S_INT32 cap_dev_index = 0; //索引 S_INT32 operation = 0;; //操作 DEV_OPERATION_ADD S_UINT32 source_id = 0; //源 S_TCHAR name[256] = { 0 }; }; //RoomUserInfo 中 audio_channel 判断音频状态
注: state等于0 未广播状态;等于1 正在申请广播状态;等于2 表示正在广播。
广播自己音频流程图如下:
广播他人音频:
处理他人音频申请:
主动发起广播代码示例如下:
FS_SDK::ErrCode CSdkManager::BoardUserAudio(DWORD dwUserId) { g_pSuperLog->WriteLogMsg("广播用户%d音频\n", dwUserId); FS_SDK::ErrCode hr = m_AudioManager->broadcastAudio(dwUserId, true); if (hr == FS_SDK::ERR_SUCCESS ) { g_pSuperLog->WriteFile(L"广播用户音频成功!\n"); } else { g_pSuperLog->WriteLogMsg("广播用户音频失败! ERR : %d\n", hr); } return hr; } FS_SDK::ErrCode CSdkManager::StopBoardUserAudio(DWORD dwUserId) { g_pSuperLog->WriteLogMsg("停止广播用户%d音频\n", dwUserId); FS_SDK::ErrCode hr = m_AudioManager->broadcastAudio(dwUserId, false); if (hr == FS_SDK::ERR_SUCCESS) { g_pSuperLog->WriteFile(L"停止广播用户音频成功!\n"); } else { g_pSuperLog->WriteLogMsg("停止广播用户音频失败! ERR : %d\n", hr); } return hr; }
他人音频广播状态通知:
struct LUserBroadcast { S_UINT32 tType = 0;//类型 S_BYTE bRecv = 0;//0:停止,1等待,2广播 S_BOOL bShare = 0;//1表示广播 S_BYTE bChannel = 0;//设备ID HWND hWnd = 0;//显示的窗口句柄 S_UINT32 uUserID = 0;//被操作用户 };
接收SDK消息处理代码示例如下:
case FS_SDK::EV_AUDIO_MEDIA_MSG://音频状态 { g_pSuperLog->WriteLogMsg("音频状态变更 EV_AUDIO_MEDIA_MSG\n"); FS_SDK::LUserBroadcast* pBc = (FS_SDK::LUserBroadcast*)pData->pl; if (!pBc) { g_pSuperLog->WriteLogMsg("FS_SDK::EV_AUDIO_MEDIA_MSG 视频数据 == nullptr \n"); break; } // 自己对应处理自己的业务逻辑 notify->OnAudioState(pBc->uUserID, pBc->bRecv); g_pSuperLog->WriteLogMsg("音频状态变更 接收: %s\n", pBc->bRecv ? "接收" : "不接收"); }
控制他人音频代码示例如下:
FS_SDK::ErrCode hR = m_AudioManager->agreeApplyAudio(dwUserId, bAgree);
/* * @brief 广播\停止广播视频 * @param dwUserId 要广播\停止广播的用户ID * @param bChannel 要广播\停止广播的设备id * @param bState 是否广播 true 广播, false 停止广播 * @return */ virtual ErrCode broadcastVideo(S_UINT32, S_BYTE channel, S_BOOL bState); /* * @brief 申请广播视频和放弃申请广播视频 * @param bChanel 通道 * @param state state 2 申请 0 放弃申请 * @return */ virtual ErrCode applyBroadcastVideo(S_BYTE channel, S_BOOL bState); /* * @brief * @param dwUserId 被同意或者被拒绝申请广播视频用户id * @param channel 通道id * @param bAgree 同意 true 拒绝 false * @return */ virtual ErrCode agreeApplyVideo(S_UINT32 dwUserId, S_BYTE channel, S_BOOL bAgree);
广播自己视频流程:
广播他人视频
需要使用的接口:
isScreenSharing(); //是否正在屏幕共享 isMultiShareEnable(); //是否支持多人共享 getCurrentDataShareCount(); //当前共享数量(包括白板) //接收远程屏幕,只需要自己加窗口句柄 startRemoteScreenShareView(S_UINT32 user, S_BYTE audioChannel, HWND hwnd); //停止接收远程屏幕 stopRemoteScreenShareView(S_UINT32 user, S_BYTE audioChannel); startScreenShare(); //开始屏幕共享 stopScreenShare(S_UINT32 user); //停止屏幕共享
流程图如下:
部分实例代码如下:
if ((ScreenShareManager::GetInstance()->IsScreenSharing()\ || ScreenShareManager::GetInstance()->getCurrentDataSharerCount() > 0)\ && !ScreenShareManager::GetInstance()->isEnableMultiShare() \ && !ScreenShareManager::GetInstance()->IsLocalShare()) { uicontrol::MessageBoxWithOK(m_hWnd, _T("会议同时只允许一人共享,请等待对方结束后再共享!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } if (ScreenShareManager::GetInstance()->IsLocalShare() && ScreenShareManager::GetInstance()->IsScreenSharing())// 切换到共享桌面 { MeetingShareToolBar::GetInstance()->SelectShare(SHARE_DESKTOP_INDEX); return; } if (ScreenShareManager::GetInstance()->isEnableMultiShare() && ScreenShareManager::GetInstance()->IsScreenSharing()) { uicontrol::MessageBoxWithOK(m_hWnd, _T("会议中有人正在共享桌面,请等待对方结束后再共享!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } FS_SDK::ErrCode hR = ScreenShareManager::GetInstance()->StartShareScreen(); if (FS_SDK::ERR_SUCCESS != hR) { if (FS_SDK::ERR_NO_PRIVILEGE == hR || FS_SDK::ERR_NO_ADMIN_PRIVILEGE == hR) { if (CommonRight::IsLocalManager()) { uicontrol::MessageBoxWithOK(m_hWnd, _T("您暂无屏幕共享权限!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); } else { uicontrol::MessageBoxWithOK(m_hWnd, _T("没有屏幕共享权限,请申请管理员!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); } return; } g_pSuperLog->WriteFile(L"开启屏幕共享失败!\n"); uicontrol::MessageBoxWithOK(m_hWnd, _T("开启屏幕共享失败!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; }
流程图如下:
停止共享实例代码如下:
DWORD dwLocalUserID = CSdkManager::GetInstance()->GetLocalUserID(); if (!CommonRight::IsLocalScreenSharing() && !PermissionManager::GetInstance()->CheckUserPermission(FS_SDK::CAN_SHARE_CLOSE_OTHER_APP, dwLocalUserID)) { uicontrol::MessageBoxWithOK(m_hWnd, _T("您不能关闭他人共享,请申请管理员!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } FS_SDK::ErrCode hR = ScreenShareManager::GetInstance()->StopShareScreen(); if (FS_SDK::ERR_SUCCESS != hR) { if (FS_SDK::ERR_NO_PRIVILEGE == hR || FS_SDK::ERR_NO_ADMIN_PRIVILEGE == hR) { uicontrol::MessageBoxWithOK(m_hWnd, _T("没有屏幕共享权限,请申请管理员!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } g_pSuperLog->WriteFile(L"结束屏幕共享失败!\n"); uicontrol::MessageBoxWithOK(m_hWnd, _T("结束屏幕共享失败!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; }
流程图如下:
示例代码如下:
case FS_SDK::EV_SHARE_SCREEN: //屏幕共享 { g_pSuperLog->WriteFile(L"屏幕共享通知 EV_SHARE_SCREEN\n"); FS_SDK::LShareInfo* pSh = (FS_SDK::LShareInfo*)pData->pl; if (!pSh) { break; } g_pSuperLog->WriteLogMsg("屏幕共享通知: 用户:%d , state: %d, 通道: %d \n", pSh->user_id, pSh->bState, pSh->bChannel); if (pSh->bState) { m_dwShareUserId = pSh->user_id; pSh->hWnd = m_pMeetingDataDlg->GetHWND(); m_ShareManager->startRemoteScreenShareView(pSh->user_id, pSh->bChannel, pSh->hWnd); } else { m_ShareManager->stopRemoteScreenShareView(pSh->user_id, pSh->bChannel); } notify->OnShareScreen(pSh); } break;
当接收屏幕共享的窗口大小改变时,需要主动调用该接口刷新屏幕共享流。
FS_SDK::ErrCode hR = m_ShareManager->updateScreenShareWnd(hWnd);
流程图如下:
实例代码如下,创建白板实例代码:
if ((ScreenShareManager::GetInstance()->IsScreenSharing()\ || ScreenShareManager::GetInstance()->getCurrentDataSharerCount() > 0)\ && !ScreenShareManager::GetInstance()->isEnableMultiShare() \ && !ScreenShareManager::GetInstance()->IsLocalShare()) { uicontrol::MessageBoxWithOK(m_hWnd, _T("会议同时只允许一人共享,请等待对方结束后再共享!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } if (MeetingShareToolBar::GetInstance()->GetWBShareDataSize() == MAX_WB_SHARE_SIZE) { uicontrol::MessageBoxWithOK(m_hWnd, _T("当前打开的文档数量已超过限制,请先关闭一些已打开的文档!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_WARN); return; } FS_SDK::ErrCode hr = WBShareManager::GetInstance()->StartWBShare(); if (hr == FS_SDK::ERR_SUCCESS) { g_pSuperLog->WriteFile(L"共享白板成功\n"); //MeetingShareToolBar::GetInstance()->SetShareSuccess(); } else if (FS_SDK::ERR_NO_ADMIN_PRIVILEGE == hr || FS_SDK::ERR_NO_PRIVILEGE == hr) { uicontrol::MessageBoxWithOK(m_hWnd, _T("没有权限共享白板!"), STRING_TIATLE, _T("确定"), uicontrol::MBI_INFO, MSG_CLOSE_TIME); }
if (FS_SDK::ERR_SUCCESS == WBShareManager::GetInstance()->StopWBShare(index)) { //success }
WBCallback示例代码如下:
case FS_SDK::EV_SHARE_START_WB: { g_pSuperLog->WriteLogMsg("共享白板消息 EV_SHARE_WB"); FS_SDK::LShareWhiteBoard* pSh = (FS_SDK::LShareWhiteBoard*)pData->pl; if (pSh && notify) { notify->OnWBShare(pSh, true); } } break; case FS_SDK::EV_SHARE_STOP_WB: { g_pSuperLog->WriteLogMsg("共享白板消息 EV_SHARE_WB"); FS_SDK::LShareWhiteBoard* pSh = (FS_SDK::LShareWhiteBoard*)pData->pl; if (pSh && notify) { notify->OnWBShare(pSh, false); } } break;
详情见SdkDemo WBShareManager.h、WBShareManager.cpp
激活白板调用情况:
FS_SDK::ErrCode hR = m_WBManager->activeWB(index);
插件FMPrinter, 从SDK中的FMPrinter文件夹中获取;
插件如下:
使用时,安装时运行 FMPrinter.exe 即可
eg:在 Inno Setup 打包脚本中[Run]节点下写入
[Files]
Source: "{#ComonBinDir}\ZX\FMPrinter\*"; DestDir: "{userappdata}\{#MyPrinterDirName}"; Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall;Check:CheckVersion
;{#ComonBinDir}\ZX\FMPrinter 打包的那个路径下的所有文件拷贝到 安装的路径中
[Run]
Filename: "{userappdata}\{#MyPrinterDirName}\FMPrinter.exe";Parameters: "-u";Flags: runhidden;Check:CheckVersion
;相当于cmd FMPrinter.exe -u
Filename: "{userappdata}\{#MyPrinterDirName}\FMPrinter.exe";Flags: runhidden;Check:CheckVersion
;{userappdata}\{#MyPrinterDirName}\FMPrinter.exe 表示安装的插件所在位置,相当于绝对路径
如果只是开发阶段,只需要在命令行中运行即可;eg:
安装的时候也是只需要像在命令行执行安装FMPrinter插件即可
注:
1、如果没有安装此插件,无法进行文档转换;相当于共享文档功能不可用
2、要使用共享office等相关文档,需安装office或者WPS
//会议室窗口布局类 struct RoomWndState { //区域布局风格 enum AreaLayoutStyle { AREA_LAYOUT_STYLE_TILE = 0,//平铺风格类型 AREA_LAYOUT_STYLE_TABLE = 1,//tab切换风格类型 AREA_LAYOUT_STYLE_SPLIT = 2,//分屏风格类型 AREA_LAYOUT_STYLE_POP = 3 //弹出风格类型 }; //会议室窗口布局模式 enum LayoutMode { LAYOUT_MODE_NORMAL = 1, //默认模式 LAYOUT_MODE_DATA, //数据模式 LAYOUT_MODE_VIDEO, //视频固定模式(1~64分屏) LAYOUT_MODE_DATAFULL, //视频模式全屏 }; //分屏风格 enum SplitStyle { SPLIT_STYLE_AUTO = 0, //默认自动 SPLIT_STYLE_1 = 1, //一分屏 SPLIT_STYLE_2 = 2, SPLIT_STYLE_P_IN_P = 3, //画中画 SPLIT_STYLE_4 = 4, SPLIT_STYLE_6 = 6, SPLIT_STYLE_9 = 9, SPLIT_STYLE_12 = 12, SPLIT_STYLE_16 = 16, SPLIT_STYLE_25 = 25, SPLIT_STYLE_36 = 36, SPLIT_STYLE_49 = 49, SPLIT_STYLE_64 = 64, SPLIT_STYLE_NO = 50, //无分屏(如无视频/纯语音,兼容某个版本应用) SPLIT_STYLE_1_FOCUS = 1001, //一焦点 SPLIT_STYLE_2_FOCUS = 1002, //2焦点 }; //数据类型 enum DataType { DATA_TYPE_CONTAINER = 0, //默认,容器类型,(如视频区域,可以显示数据,则用容器类型) DATA_TYPE_WB = 1, //白板 DATA_TYPE_APPSHARE = 2, //屏幕共享 DATA_TYPE_WEB = 3, //Web协同浏览 DATA_TYPE_MEDIASHARE = 4, //媒体共享 DATA_TYPE_VOTE = 5, //电子投票 DATA_TYPE_VIDEO = 6, //视频 DATA_TYPE_VIDEOPOLLING = 7 //U视频轮巡 }; //数据块信息 struct DataBlock { S_BYTE pos = 0; //索引顺序 DataType data_type = DATA_TYPE_CONTAINER; //类型 S_UINT32 data_id = 0; //业务数据ID //白板id //视频,屏幕共享,媒体共享 为用户ID //,其他的需要在创建的时候手动生成 S_UINT32 user_data = 0; //用户自定义类型 //白板 id //视频 通道ID //屏幕共享,媒体共享 为空 //,其他的需要在创建的时候手动生成 }; typedef LVector<DataBlock> DataBlockVector; //数据区域信息 struct AreaData { S_BYTE id = 0; S_BYTE screen_id = 0; //所属屏ID AreaLayoutStyle style = AREA_LAYOUT_STYLE_TILE; //区域布局样式 S_UINT32 user_data = 0; //自定义字段 //新版本(tab风格中为当前选中的数据块pos;split分屏风格中为:SplitStyle,多少分屏) //所以需要应用层自己查找到对应pos,白板则需要继续以白板的active回调设置的当前激活白板 //如果主讲是老版本,未使用新协议进行交互,splite 一样,tab风格中则userData字段值为:当前选中的数据类型 DataBlockVector data_block_vector; //数据块列表 }; struct FullArea { AreaLayoutStyle style = AREA_LAYOUT_STYLE_TILE; //全屏区域风格 S_UINT32 user_data = 0;//自定义字段,暂时无用 typedef LVector<S_BYTE> IDVector; IDVector id_set; //全屏区域显示的区域ID集合 //包含全屏下需要显示的区域ID //根据idSet是否为空判断,当前是否存在区域处于全屏状态 }; S_BYTE screen_id = 0; //屏幕ID //用以区分主屏/分屏,从0递增 //可扩展用于同步分屏上的布局 LayoutMode layout_mode = LAYOUT_MODE_NORMAL; //原 bMode 客户端的当前屏的布局模式 //(标准布局/数据布局/视频布局) //可扩展用于新的布局模式 FullArea full_area; //全屏区域 //FullType fullType; //原 bFull 客户端全屏风格 //(自动/数据全屏/视频全屏/数据+主讲视频全屏/数据+视频全屏) //可扩展用户全屏模式下的风格 AreaData tab_area; //tab风格区域 //(windows客户端数据区域) //可扩展,一个屏可以有多个tab风格区域 AreaData split_area; //split风格区域 //(视频区域) //可扩展,一个屏可以有多个split风格区域 DataBlock full_data_block; //原FullVideoMedia //单个全屏的数据项(单个视频选择全屏) //可扩展,用于单独全屏任何数据模块 }; typedef LVector<RoomWndState> RoomWndStateVector; struct LMeetingLayoutInfo { S_BOOL valid_full_mode;//保持全屏模式(即使同步信息中为非全屏,也不退出全屏) S_UINT32 user; //同步者 RoomWndStateVector wnd_state; //窗口布局 可保存多屏 };
实例代码如下:
void CMeetingLayoutDlg::ModifyVideoPos(const FS_SDK::RoomWndState& state) { for (auto &videopos : state.split_area.data_block_vector)// 解析视频显示顺序 { if (videopos.data_type != FS_SDK::RoomWndState::DATA_TYPE_VIDEO) { continue; } DWORD dwUserID = videopos.data_id; BYTE bMediaID = videopos.user_data; int nPos = videopos.pos; g_pSuperLog->WriteLogMsg("视频: 用户id : %d, 通道:%d POS:%d \n", dwUserID, bMediaID, nPos); CDlgVideo *pDlgVideo = FindVideo(dwUserID, bMediaID, m_lsVideo); if (nullptr == pDlgVideo) { //布局消息没有查看的视频先不管// 服务器bug 会缓存布局信息 continue; } else { m_VideoRelay.SetVideoID(nPos, dwUserID, bMediaID); continue; } } if (LayoutCommon::IsSingleVideoValid(state))// 单个视频全屏 { DWORD dwUserID = state.full_data_block.data_id; BYTE bMediaID = state.full_data_block.user_data; CDlgVideo *pDlgVideo = FindVideo(dwUserID, bMediaID, m_lsVideo); if (pDlgVideo) { pDlgVideo->OnFullScreen(); } } else { VideoDlgState(&m_fullVideoItem); } RelayWindow(); if (state.tab_area.data_block_vector.size() == 0) { return; } int index = 0; FS_SDK::RoomWndState::DataBlock area; for (auto &data : state.tab_area.data_block_vector) { if (index != state.tab_area.user_data)// user_data 表示激活的白板或者其他共享块 { index++; continue; } ActiveShareTab(data, index); break; } }
详情见SdkDemo CMeetingLayoutDlg类中相关处理。
void CMeetingLayoutDlg::BoardcastLayout() { FS_SDK::LMeetingLayoutInfo layoutinfo; FS_SDK::RoomWndState wndstate; FS_SDK::RoomWndState::AreaData split_area; for (auto& video : m_lsVideo) { FS_SDK::RoomWndState::DataBlock videodata; DWORD dwUserID = video->GetVideoUserID(); BYTE bMediaID = video->GetVideoMediaID(); videodata.data_id = video->GetVideoUserID(); videodata.user_data = video->GetVideoMediaID(); videodata.pos = m_VideoRelay.GetVideoPos( dwUserID, bMediaID); videodata.data_type = FS_SDK::RoomWndState::DATA_TYPE_VIDEO; split_area.data_block_vector.push_back(videodata); } split_area.id = DEFAULT_SPLIT_AREA_ID; wndstate.split_area = split_area; FS_SDK::RoomWndState::DataBlock singlefulldata; singlefulldata.data_id = m_fullVideoItem.dwUserID; singlefulldata.user_data = m_fullVideoItem.bMediaID; singlefulldata.pos = 0; if (singlefulldata.data_id != 0) { singlefulldata.pos = m_VideoRelay.GetVideoPos(m_fullVideoItem.dwUserID, m_fullVideoItem.bMediaID); } singlefulldata.data_type = FS_SDK::RoomWndState::DATA_TYPE_VIDEO; wndstate.full_data_block = singlefulldata; //FS_SDK::RoomWndState::AreaData full_area; wndstate.tab_area.id = DEFAULT_TAB_AREA_ID; wndstate.tab_area.style = FS_SDK::RoomWndState::AREA_LAYOUT_STYLE_TABLE; LayoutCommon::LayoutToRoomWndstate(wndstate, m_VideoRelay.GetVideoRelayID()); layoutinfo.valid_full_mode = true; layoutinfo.user = CSdkManager::GetInstance()->GetLocalUserID(); MeetingShareToolBar::GetInstance()->GetTabArea(wndstate.tab_area); layoutinfo.wnd_state.push_back(wndstate); FS_SDK::ErrCode hR = MeetingManager::GetInstance()->BroadcastLayout(layoutinfo); if (FS_SDK::ERR_SUCCESS != hR) { g_pSuperLog->WriteFile(L"同步布局失败\n"); } }
权限 | 管理员 | 主持人 | 参会人 | 备注 |
---|---|---|---|---|
可多种权限同时具有 | 一场会议可存在多个 | 一场会议最多存在一个 | 一场会议可存在多个 | |
请出会议室 | 可以 | 无权限 | 无权限 | |
音视频控制 | 控制他人与自己 | 控制他人与自己 | 仅能控制自己 | 与后台设置相关 |
修改昵称/名称 | 修改他人与自己 | 仅修改自己 | 仅修改自己 | 与后台设置相关 |
申请主持人 | 直接成为主持人 | 无 | 需原主持人转让,无主持人自动成为主持人 | |
授予主持人 | 可以 | 可以,准确来说是转让 | 无权限 | |
同步布局 | 除分享外不能同步 | 默认同步 | 除分享外不能同步 | |
录制 | 有权限 | 有权限 | 所有人录制时有权限 | |
权限设置 | 仅PC能设置 | 仅PC能设置 | 无权限 | |
管理员可成为主持人,此时具有双重身份,两个角色的权限 |
ApplyToBeHost 申请主持人, 角色改变后会在UserCallback 回调中EV_USER_PRESENTER通知。
ApplyToBeHost 申请主持人, 角色改变后会在UserCallback 回调中EV_USER_ADMIN_APPLY通知。
/* * @brief * @param * @return 返回录制设置参数 */ const FS_SDK::LRecordParam& getRecordParam(); /* * @brief 设置录制参数 * @param 录制设置参数 * @return */ FS_SDK::ErrCode setLocalRecordParam(FS_SDK::LRecordParam& recordParam); /* * @brief 开启录制 * @param 录制路径 * @return */ FS_SDK::ErrCode startLocalRecord(FS_SDK::S_PWCHAR path); /* * @brief 暂停继续录制 * @param true 暂停, false 继续 * @return */ FS_SDK::ErrCode pauseLocalRecord(FS_SDK::S_BOOL pause); /* * @brief 停止录制 * @param * @return */ FS_SDK::ErrCode stopLocalRecord();
struct LRecordParam { enum class RecordType : BYTE { RECORDTYPE_MP4 = 2, //mp4 RECORDTYPE_WMV = 3, //wmv RECORDTYPE_WMA = 4 //wma }; enum class RecordQuality : BYTE { RECORDQUALITY_LOW = 1, //录制质量低 RECORDQUALITY_MEDIUM = 2, //录制质量中 RECORDQUALITY_HIGH = 3 //录制质量高 }; enum class RecordRect : BYTE { RECORDRECT_FULL = 1, //录制当前桌面窗口 RECORDRECT_SELF = 2, //录制程序窗口 RECORDRECT_SPECIAL = 3 //录制指定区域 }; RecordType record_type; //保存录制文件格式类型 RecordQuality record_quality; //质量 RecordRect record_rect; //需要录制的方式 HWND capture_hwnd; //录制的窗口句柄 RECT record_area; //录制区域 };
当 RecordRect 值为 RECORDRECT_SELF时传入 HWND capture_hwnd,程序会录制该句柄区域;
当 RecordRect 值为 RECORDRECT_FULL时,表示录制整个桌面(主屏幕;
当 RecordRect 值为 RECORDRECT_SPECIAL时, 传入 RECT record_area 值,录制该屏幕区域。
注:当 RecordType 为 RECORDTYPE_WMA 时,仅录制音频,并且是录制会中的音频,如果会中无人发言,则录制文件中是没有声音的。
录制插件在SDK中,内容如下:
安装时需要注册录制插件,可将录制插件ThirdComponent 目录与程序放在同一个文件夹;
开发时,不安装,只需要在插件目录运行ThridCtrlReg.exe即可
注: 未注册改插件,无法使用录制功能
Inno Setup 打包示例:
;解决录制的问题
Filename: {#MyCommonDirName}\ThirdComponent\ThridCtrlReg.exe; Flags: runhidden
// 判断录制权限 bool canRecord = PermissionManager::GetInstance()->CheckRoomPermission(FS_SDK::ROOM_LOCAL_RECORD); if ( !canRecord && ( !CommonRight::IsLocalManager() && !CommonRight::IsLocalPresenter() ) ) { uicontrol::MessageBoxWithOK(m_hWnd, L"没有权限录制会议!"); return; } tstring path; CSdkManager::GetInstance()->GetConfig()->ReadRecordPath(path); // 获取录制参数 auto recordParam = MeetingManager::GetInstance()->getRecordParam(); FS_SDK::ErrCode hR = FS_SDK::ERR_FAIL; switch (recordParam.record_rect) { case FS_SDK::LRecordParam::RecordRect::RECORDRECT_SELF: recordParam.capture_hwnd = m_hNotifyWnd; MeetingManager::GetInstance()->setLocalRecordParam(recordParam); hR = MeetingManager::GetInstance()->startLocalRecord(path.c_str()); break; case FS_SDK::LRecordParam::RecordRect::RECORDRECT_FULL: hR = MeetingManager::GetInstance()->startLocalRecord(path.c_str()); break; case FS_SDK::LRecordParam::RecordRect::RECORDRECT_SPECIAL: recordParam.record_area = { 0,0,800,600 }; MeetingManager::GetInstance()->setLocalRecordParam(recordParam); hR = MeetingManager::GetInstance()->startLocalRecord(path.c_str()); break; default: recordParam.record_rect = FS_SDK::LRecordParam::RecordRect::RECORDRECT_FULL; MeetingManager::GetInstance()->setLocalRecordParam(recordParam); hR = MeetingManager::GetInstance()->startLocalRecord(path.c_str()); break; } if (FS_SDK::ERR_SUCCESS == hR) { UpdateRecordState(RECORDING); } else { UpdateRecordState(RECORDSTOP); uicontrol::MessageBoxWithOK(m_hWnd, L"录制会议失败!"); } FS_SDK::ErrCode hR = MeetingManager::GetInstance()->pauseLocalRecord(pause); FS_SDK::ErrCode hR = MeetingManager::GetInstance()->stopLocalRecord();
/* * @brief 获取组织架构信息 * @param refresh 是否刷新缓存数据 * @return 错误码 */ ErrCode queryDeptInfo(S_BOOL refresh); /* * @brief 获取部门用户 * @param deptId 部门ID * @return 错误码 */ ErrCode queryDeptUser(S_UINT32 deptId); /* * @brief 邀请用户 * @param userList 受邀用户列表数组 * @param nSize 受邀用户数量 * @param pszMeetingName 会议名称(用于会前创建即时会议) * @param pszMeetingPwd 会议密码(同上) * @param pszAdminPwd 管理员密码(同上) * @return 错误码 */ ErrCode inviteUser(S_UINT32* userList, S_UINT32 nSize, S_PWCHAR pszMeetingName = nullptr, S_PWCHAR pszMeetingPwd = nullptr, S_PWCHAR pszAdminPwd = nullptr); /* * @brief 接受/拒绝邀请 * @param accept 是否接受 * @return 错误码 */ ErrCode acceptInvite(S_BOOL accept);
//组织架构信息 EV_FRONT_DEPT_INFO struct LDeptList { struct DeptNode { S_UINT32 parent_id = 0; //父部门id S_UINT32 id = 0; //部门ID FString name; //部门名称 LVector<DeptNode> children; }; DeptNode root; //根部门 FS_UINT32 totalCount = 0; //总数 FS_UINT32 totalUserCount = 0; //联系人总数 }; //部门用户列表 EV_FRONT_DEPT_USER struct LUserInfo { enum LContactUserState { USER_STATE_OFFLINE = 0, //用户离线状态 USER_STATE_MEETING, //用户会议中状态 USER_STATE_ONLINE //用户在线状态 }; S_UINT32 id = 0; //用户ID S_UINT32 deptId = 0; //部门ID S_UINT32 sortId = 0; //置顶排序ID,默认0没有置顶,值越大越前面 FString name; //用户名 FString nickname; //昵称 FString depName; //部门名称 S_UINT32 typeFlag = 0; //通讯录用户类型标识 LContactUserState status = USER_STATE_OFFLINE; //在线状态 }; using LUserList = LVector<LUserInfo>; //用户在线状态变化 EV_FRONT_USER_STATUS using LUserList = LVector<LUserInfo>; //收到邀请 EV_FRONT_INVITE_INCOME struct LInviteIncome { enum LInviteType { INVITE_TYPE_AUDIO = 0, // 音频 INVITE_TYPE_VIDEO, // 视频 INVITE_TYPE_IN_MEETING, // 会中邀请 }; S_BOOL inviting = false; S_UINT32 inviteId = 0; S_UINT32 inviteCode = 0; // 邀请码 S_UINT64 roomCreateTime = 0; // 会议室创建时间戳 S_UINT32 roomCompanyId = 0; // 会议室所属企业ID S_UINT8 isForce = 0; // 强制邀请,预留后面会中也能接收邀请 S_UINT32 proxyUserId = 0; // 代理人,预留后面web会控代理发起邀请 LInviteType inviteType = INVITE_TYPE_AUDIO; // 邀请类型<InviteType> S_UINT32 meetingId = 0; // 即时会议id,用来通过boss获取邀请人列表 S_UINT32 inviterUserId = 0; // 邀请者ID FString inviterUserName; FString inviterTerminal; }; //邀请被接受/拒绝 EV_FRONT_INVITE_ACCEPTED struct LInviteResponse { enum LRejectedReason { IRR_BYUSER = 0, //用户操作拒绝 IRR_REMOTE_TIMEOUT , //远端用户超时未处理 IRR_LOCAL_TIMEOUT //本地超时 }; S_UINT32 nInviteId = 0; S_UINT32 remoteUserId = 0; S_BOOL bAccepted = FALSE; LRejectedReason reason = IRR_LOCAL_TIMEOUT; };
成功登录会前在线后SDK立即请求组织架构、企业用户列表数据并保存在SDK层,应用层可调用 queryDeptInfo 获取组织架构,调用 queryDeptUser 获取对应部门用户,若此前SDK请求失败则重新发出请求,数据将以通知回调形式提供给应用层,从而驱动界面组织架构展示。
会前和会中均可调用 inviteUser 对通讯录在线用户发起邀请,会前发起邀请后SDK将首先创建即时会议并立即入会,然后将选择的用户邀请到该会议室。会中则直接将选择的用户邀请到当前会议室。
邀请被接受/拒绝则由SDK层响应后直接通知应用层。
邀请时序:
详情见demo。