Windows(PC) SDK 互动直播开发手册

网易云通信服务概要

网易云通信 SDK 为移动提供完善的音视频直播与实时互动开发框架,屏蔽其内部复杂细节,对外提供较为简洁的 API 接口,方便第三方应用快速集成互动直播功能,主要有:

注:SDK 兼容的系统有 Windows xp(sp2及以上)、Windows 7、Windows 8/8.1、Windows 10。SDK 从V3.2.5版本开始全面支持32位和64位程序接入。

开发准备

SDK内容

SDK不提供debug版本的动态链接库供开发者调试,如果遇到问题请联系技术支持或在线客服。

快速接入SDK

关于如何快速集成SDK到现有项目,初始化SDK登录退出,发送/接收第一条IM消息,发送/接收第一条聊天室消息,如何开始基于IM Demo源码(官网下载页面可以下载)开发的信息请前往Windows(PC) SDK Getting Started

SDK数据目录

当收到多媒体消息后,SDK 会负责下载这些多媒体文件,同时SDK 还要记录一些log,因此SDK 需要一个数据缓存目录。该目录由第三方App 通过nim_client_init 初始化接口传入,默认为存放到系统的AppData 目录下,App 传入一个目录名即可,SDK 会自动生成用户数据缓存目录。数据缓存目录默认为"{系统的AppData 目录}\{App 传入的目录名}\NIM\{某个用户对应的用户数据目录}”,还可以由App 完全自定义用户数据目录,需要传入完整的路径,并确保读写权限正确。如果第三方App 需要清除缓存功能,可扫描该目录下的文件,按照你们的规则清理即可。 具体某个用户对应的缓存目录下面包含如下子目录:

SDK 提供了接口nim_tool_get_user_specific_appdata_dir 获取某个用户对应的具体类型的App data 目录(如图片消息文件存放目录,语音消息文件存放目录等)(注意:需要调用nim_free_buf 接口释放其返回的内存)。

接口调用与约定

SDK 提供的所有API 都是 C接口 ,根据模块划分为不同的API 文件,根据业务需要可以显式或者隐式的调用(3.4.0开始)。显式调用下,开发者可以在程序进行到任何时间点按需加载SDK dll, 功能完成后如果不需要继续使用NIM可以卸载SDK dll, 更加灵活和节省内存, 该模式下开发者只需要将SDK包里nim_c_sdk\include 目录下的模块定义头文件(命名方式如nim_xxx_def.h)加入到工程项目中。隐式调用下,开发者除了链接lib文件外,需要将SDK包里nim_c_sdk 目录里的API 头文件(命名方式如nim_xxx.h)以及相关常量定义的头文件等其他头文件一并加入到工程项目中。一般来说,每个模块都有对应的API 头文件和相关常量的定义头文件,命名上一一对应。

SDK 提供了3种类型的接口。

  1. 全局广播类通知的注册接口。

    常见的几类消息或通知是通过该注册的回调函数中通知开发者的,比如收到的消息,会话数据更新,用户相关数据的变更等,开发者需要在登录前提前注册好这些接口,例如:

    C++

     //注册数据同步结果的回调
     nim::DataSync::RegCompleteCb(nbase::Bind(&nim_comp::DataSyncCallback::SyncCallback, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    
     /* 以下注册的回调函数,都是在收到服务器推送的消息或事件时执行的。因此需要在程序开始时就注册好。 */
     //注册重连、被踢、掉线、多点登录、把移动端踢下线的回调
     nim::Client::RegReloginCb(&nim_comp::LoginCallback::OnReLoginCallback);
     nim::Client::RegKickoutCb(&nim_comp::LoginCallback::OnKickoutCallback);
     nim::Client::RegDisconnectCb(&nim_comp::LoginCallback::OnDisconnectCallback);
     nim::Client::RegMultispotLoginCb(&nim_comp::LoginCallback::OnMultispotLoginCallback);
     nim::Client::RegKickOtherClientCb(&nim_comp::LoginCallback::OnKickoutOtherClientCallback);
     nim::Client::RegSyncMultiportPushConfigCb(&nim_comp::MultiportPushCallback::OnMultiportPushConfigChange);
    
     //注册返回发送消息结果的回调,和收到消息的回调。
     nim::Talk::RegSendMsgCb(nbase::Bind(&nim_comp::TalkCallback::OnSendMsgCallback, std::placeholders::_1));
     nim::Talk::RegReceiveCb(nbase::Bind(&nim_comp::TalkCallback::OnReceiveMsgCallback, std::placeholders::_1));
     nim::Talk::RegRecallMsgsCallback(nbase::Bind(&nim_comp::TalkCallback::OnReceiveRecallMsgCallback, std::placeholders::_1, std::placeholders::_2));
     nim::Talk::RegReceiveMessagesCb(nbase::Bind(&nim_comp::TalkCallback::OnReceiveMsgsCallback, std::placeholders::_1));
     nim::MsgLog::RegMessageStatusChangedCb(nbase::Bind(&nim_comp::TalkCallback::OnMsgStatusChangedCallback, std::placeholders::_1));

    C#

    //注册数据同步结果的回调
    NIM.DataSync.RegCompleteCb(DataSyncDelegate cb);
    //登录结果通知,开发者可以通过注册该事件获取登录结果,或者在调用登录接口NIM.ClientAPI.Login 时填充回调函数
    NIM.ClientAPI.LoginResultHandler
    //注册客户端掉线回调
    NIM.ClientAPI.RegDisconnectedCb(Action handler);
    //注册客户端多点登录通知回调
    NIM.ClientAPI.RegMultiSpotLoginNotifyCb(MultiSpotLoginNotifyResultHandler handler);
    //注册将登录当前账号的其他端踢下线的结果回调
    NIM.ClientAPI.RegKickOtherClientCb(KickOtherClientResultHandler handler);
    //注册当前客户端被踢下线通知回调
    NIM.ClientAPI.RegKickoutCb(KickoutResultHandler handler)
    //注册自动重连结果回调
    NIM.ClientAPI.RegAutoReloginCb(LoginResultDelegate handler, string jsonExtension = null)

     //接收消息
    NIM.TalkAPI.OnReceiveMessageHandler += ReceiveMessageHandler;
    //发送消息结果
    NIM.TalkAPI.OnSendMessageCompleted += SendMessageResultHandler;
    //注册消息撤回结果回调
    NIM.TalkAPI.RegRecallMessageCallback(RecallMessageDelegate cb);

C

    // 数据同步结果通知(nim_data_sync)
    void nim_data_sync_reg_complete_cb(nim_data_sync_cb_func cb, const void *user_data);        
    // 接收会话消息通知(nim_talk)
    void nim_talk_reg_receive_cb(const char *json_extension, nim_talk_receive_cb_func cb, const void *user_data);        
    // 接收系统消息通知(nim_sysmsg)
    void nim_sysmsg_reg_sysmsg_cb(const char *json_extension, nim_sysmsg_receive_cb_func cb, const void *user_data);    
    // 发送消息结果通知(nim_talk)
    void nim_talk_reg_arc_cb(const char *json_extension, nim_talk_arc_cb_func cb, const void *user_data);
    // 发送自定义系统通知的结果通知(nim_talk)
    void nim_talk_reg_custom_sysmsg_arc_cb(const char *json_extension, nim_custom_sysmsg_arc_cb_func cb, const void *user_data);
    // 群组事件通知(nim_team)
    void nim_team_reg_team_event_cb(const char *json_extension, nim_team_event_cb_func cb, const void *user_data);    
    // 帐号被踢通知(nim_client)
    void nim_client_reg_kickout_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);        
    // 网络连接断开通知(nim_client)
    void nim_client_reg_disconnect_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);    
    // 将本帐号的其他端踢下线的结果通知(nim_client)
    void nim_client_reg_kickout_other_client_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data); 
    // 好友通知 
    void nim_friend_reg_changed_cb(const char *json_extension, nim_friend_change_cb_func cb, const void *user_data);    
    // 用户特殊关系通知
    void nim_user_reg_special_relationship_changed_cb(const char *json_extension, nim_user_special_relationship_change_cb_func cb, const void *user_data);    
    ...
  1. 异步接口。

    回调函数作为参数,传入执行接口,然后执行接口时,会触发传入的回调函数。注意,回调函数中如果涉及到跨线程资源的需要开发者切换线程操作,即使不涉及到资源问题,也要保证回调函数中不会处理耗时任务,以免堵塞SDK底层线程。

  2. 同步接口。

    为方便开发者使用,我们提供了一些同步接口,调用接口获取的内容同步返回,以block命名的都是同步接口,该类会堵塞SDK线程,谨慎使用。

从版本2.7.0开始,服务器和客户端上线了频控策略,与服务器有交互的接口(接口命名结尾为_online的查询接口以及命令类接口,如同意群邀请等)增加频控控制,开发者关注错误码416。

C++ SDK

为了方便桌面应用开发者更快的接入网易云通信SDK,PC SDK下载包中还提供了官方的C++ 封装层 项目文件及源码,接入和使用方法请看Windows(PC) SDK开发手册(C++ 封装层),目前IM Demo(C++)源码就是通过接入和调用该SDK完成IM功能的。

C# SDK

网易云通信SDK还提供了C# 程序集,方便.net 开发人员接入,PC SDK下载包中包括官方的C# 封装层 项目文件及源码,接入和使用方法请看Windows(PC) SDK开发手册(C# 封装层),目前IM Demo(C#)源码就是通过接入和调用该SDK完成IM功能的。

如果开发者在调用C接口过程中或者解析接口返回结果过程中出现疑问,可以参考和借鉴C++封装层。

初始化SDK

准备工作:将SDK 相关的dll 文件(nim.dll,nim_audio.dll, nim_tools_http.dll, nrtc.dll)放到App 的运行目录下,并将SDK 的配置文件目录nim_conf(目录里包含一个ver_ctrl.dll文件)放到App 的运行目录下。SDK 基于vs2010 开发,如果App 没有对应的运行时库文件(msvcp100.dll和msvcr100.dll),请将其放到App 的运行目录下。

准备工作完成后,在程序启动时,如果直接使用C接口,需要调用LoadLibrary 函数动态加载nim.dll,然后调用GetProcAddress获取API 接口:nim_client_init,调用此接口初始化NIM SDK。同时,SDK 能力的一些参数以及如果SDK 需要连接独立部署的服务器的地址等配置也是在初始化SDK 时传入。例如:

C++

nim::SDKConfig config;

//组装SDK能力参数(必填)
config.database_encrypt_key_ = "Netease";     //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
config.sdk_log_level_ = ;                     //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
config.preload_image_quality_ = ;             //int 预下载图片质量,选填,范围0-100
config.preload_image_resize_ = ;             //string 预下载图片基于长宽做内缩略,选填,比如宽100高50,则赋值为100x50,中间为字母小写x
config.sync_session_ack_ = true;             //bool 选填,消息已读未读是否多端同步,默认true
config.login_max_retry_times_ = ;             //int 登录重试最大次数,如需设置建议设置大于3次,默认填0,SDK默认设置次数

//组装SDK独立部署的服务器配置(选填)
config.use_private_server_ = true;
config.rsa_public_key_module_ = "http://xxx";
config.default_nos_download_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.lbs_address_ = "http://xxx";
config.nos_lbs_address_ = "http://xxx";
config.default_link_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.default_nos_upload_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.default_nos_access_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");

bool ret = nim::Client::Init("45c6af3c98409b18a84451215d0bdd6e", "Netease", "", config); // 载入网易云通信sdk,初始化安装目录和用户目录,第一个参数是appkey(此处填写的是测试示例)

C#

C# 程序在运行时需要使用到 C++ SDK 的dll,必须将SDK相关的dll 文件(nim.dll,nim\_audio.dll, nim\_tools\_http.dll, nrtc.dll)放到最终的生成文件目录,如果缺少运行时库文件(msvcp100.dll和msvcr100.dll)也需要拷贝到生成目录中,否则可能会出现找不到dll的异常。

C# 提供了参数配置类 NimUtility.NimConfig,通过创建该类对象完成参数配置。
var config = new NimUtility.NimConfig();
config.CommonSetting = new SdkCommonSetting();

//组装SDK能力参数
config.CommonSetting.DataBaseEncryptKey = "Netease"; //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
config.CommonSetting.LogLevel = SdkLogLevel.Pro;  //SDK 日志记录级别 
config.CommonSetting.PredownloadAttachmentThumbnail = True;   //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
config.CommonSetting.UsePriviteServer = False;    //是否使用自定义服务器,默认False,如果使用自定义服务器,需设置config.PrivateServerSetting 对象

//组装SDK独立部署的服务器配置
config.PrivateServerSetting = new SdkPrivateServerSetting();
config.PrivateServerSetting.RSAPublicKey = "http://xxx";
config.PrivateServerSetting.DownloadServerList = new List<string>({"",""});
config.PrivateServerSetting.LbsAddress = "http://xxx";
config.PrivateServerSetting.NOSLbsAddress = "http://xxx";
config.PrivateServerSetting.LinkServerList = new List<string>({"",""});
config.PrivateServerSetting.UploadServerList = new List<string>({"",""});
config.PrivateServerSetting.AccessServerList = new List<string>({"",""});

bool ret = NIM.ClientAPI.Init("45c6af3c98409b18a84451215d0bdd6e", "NIMCSharpDemo", null, config); // 载入网易云通信sdk,初始化安装目录和用户目录,第一个参数是appkey(此处填写的是测试示例)

C

typedef bool(*nim_client_init)(const char *app_data_dir, const char *app_install_dir, const char *json_extension);

void foo()
{
    //获取SDK初始化接口
    HINSTANCE hInst = LoadLibraryW(L"nim.dll");
    nim_client_init func = (nim_client_init) GetProcAddress(hInst, "nim_client_init");

    //组装SDK初始化参数
    Json::Value config_root;

    //组装SDK能力参数(必填)
    Json::Value config_values;
    config_values[kNIMAppKey] = "xxxx"; //用户的APP key
    config_values[kNIMDataBaseEncryptKey] = ""; //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
    config_values[kNIMPreloadAttach] = ;        //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
    config_values[kNIMPreloadImageQuality] = ;  //int 预下载图片质量,选填,范围0-100
    config_values[kNIMPreloadImageResize] = ;   //string 预下载图片基于长宽做内缩略,选填,比如宽100高50,则赋值为100x50,中间为字母小写x
    config_values[nim::kNIMSyncSessionAck] = ;            //bool 选填,消息已读未读是否多端同步,默认true
    config_values[nim::kNIMLoginRetryMaxTimes] = ;        //int 登录重试最大次数,如需设置建议设置大于3次,默认填0,SDK默认设置次数
    config_root[kNIMGlobalConfig] = config_values;

    //组装SDK独立部署的服务器配置(选填)
    Json::Value server_values;
    server_values[kNIMLbsAddress] = "http://xxx";             //lbs地址
    server_values[kNIMNosLbsAddress] = "http://xxx";        //nos lbs地址
    server_values[kNIMDefaultLinkAddress].append("xxx.xxx.xxx.xxx:xxxx");                //默认的link地址
    server_values[kNIMDefaultNosUploadAddress].append("http://xxx.xxx.xxx.xxx:xxxx");     //默认的nos上传地址
    server_values[kNIMDefaultNosDownloadAddress].append("http://xxx.xxx.xxx.xxx:xxxx");    //默认的nos下载地址
    server_values[kNIMRsaPublicKeyModule] = "";                //密钥
    server_values[kNIMRsaVersion] = 0;                        //密钥版本号      
    config_root[kNIMPrivateServerSetting] = server_values;
    config_root[kNIMAppKey] = "45c6af3c98409b18a84451215d0bdd6e"; //必填appkey(此处填写的是测试示例)

    //初始化SDK
    func("appdata path", "app installation path", config_root.toStyledString().c_str());
}

在程序退出前,调用接口nim_client_cleanup 进行NIM SDK 的清理工作,然后调用FreeLibrary 函数释放nim.dll,对于清理工作的注意事项请查看后续的"登出/退出和清理SDK"章节。

C++

nim::Client::Cleanup();

C#

NIM.ClientAPI.Cleanup();

C

typedef    void(*nim_client_cleanup)(const char *json_extension);

void foo()
{
    nim_client_cleanup func = (nim_client_cleanup) GetProcAddress(hInst, "nim_client_cleanup");
    func("");
    FreeLibrary(hInst);
}

登录与登出

登录集成必读

客户端代理

SDK目前支持配置代理,支持Socket4/5,暂不支持Http代理,音视频模块只支持Socket5代理。

C++

void foo()
{
    nim::Global::SetProxy(nim::kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

C#

void foo()
{
    NIM.GlobalAPI.SetProxy(NIM.NIMProxyType.kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

C

typedef void(*nim_global_set_proxy)(NIMProxyType, const char*, int, const char*, const char*);

void foo()
{
    nim_global_set_proxy func = (nim_global_set_proxy) GetProcAddress(hInst, "nim_global_set_proxy");
    func(kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

首次登录

SDK 登录后会同步群信息,离线消息,漫游消息,系统通知等数据,所以首次登录前需要 提前注册所需的全局广播类通知的回调 (具体说明请参阅API 文档),以下以注册最近会话变更通知回调为例:

C++

void OnSessionChangeCallback(nim::NIMResCode rescode, const nim::SessionData& data, int total_unread_counts)
{
    if (rescode != nim::kNIMResSuccess)
    {
        return;
    }

    switch (data.command_)
    {
    case nim::kNIMSessionCommandAdd:
    case nim::kNIMSessionCommandUpdate:
    case nim::kNIMSessionCommandMsgDeleted:
    break;
    ···
    }
}

void foo()
{
    //注册全局会话列表变更通知函数
    nim::Session::RegChangeCb(&OnSessionChangeCallback);
}

C#

//C#通过订阅会话列表变更事件来获取更新通知:
NIM.Session.SessionAPI.RecentSessionChangedHandler += OnSessionChanged;

private void OnSessionChanged(object sender, SessionChangedEventArgs e)
{
    //根据会话列表变更类型进行相应的操作
    if (e.Info.Command == NIMSessionCommand.kNIMSessionCommandAdd ||
        e.Info.Command == NIMSessionCommand.kNIMSessionCommandUpdate)
    {
        //TODO:会话列表新增、更新

    }
    else if (e.Info.Command == NIMSessionCommand.kNIMSessionCommandRemove)
    {
        //TODO:删除会话项
    }
}

C

//全局会话列表变更通知函数
void CallbackSessionChange(int rescode, const char *result, int total_unread_counts, const char *json_extension, const void *user_data)
{
    if (rescode == kNIMResSuccess)
    {
        Json::Value value;
        Json::Reader reader;
        if (reader.parse(result, value))
        { 
            nim::NIMSessionCommand command = nim::NIMSessionCommand(value[nim::kNIMSessionCommand].asInt());
            switch (command)
            {
                case kNIMSessionCommandAdd:
                    ...
                case kNIMSessionCommandUpdate:    
                    ...
                ...                
            }

        }
    }
    else
    {
        ...
    }
}

typedef void(*nim_session_reg_change_cb)(const char *json_extension, nim_session_change_cb_func cb, const void *user_data);

void foo()
{
    //注册全局会话列表变更通知函数
    nim_session_reg_change_cb func = (nim_session_reg_change_cb)GetProcAddress(hInst, "nim_session_reg_change_cb");
    func(nullptr, &CallbackSessionChange, nullptr);    // 会话列表变更通知(nim_session)
}

登录

C++

void OnLoginCallback(const nim::LoginRes& login_res, const void* user_data)
{
    if (login_res.res_code_ == nim::kNIMResSuccess)
    {
        if (login_res.login_step_ == nim::kNIMLoginStepLogin)
        {
            ···
        }
    }
    else
    {
        ···
    }
}

void foo()
{
    nim::Client::Login(app_key, "app acount", "token", &OnLoginCallback);
}

C#

private void HandleLoginResult(NIM.NIMLoginResult result)
{
    //在UI线程进行处理登录结果
    Action action = () =>
    {
        if (result.LoginStep == NIM.NIMLoginStep.kNIMLoginStepLogin)
        {
            if (result.Code == NIM.ResponseCode.kNIMResSuccess)
            {
                //TODO:登录成功
            }
            else
            {
                //TODO:登录失败
            }
        }
    };
    this.Invoke(action);
}

private void foo()
{
    NIM.ClientAPI.Login(appkey, "app acount", "token", HandleLoginResult);
}

C

void CallbackLogin(const char* res, const void *user_data)
{
    Json::Value values;
    Json::Reader reader;
    if (reader.parse(res, values) && values.isObject())
    {
        if (values[kNIMLoginStep].asInt() == kNIMLoginStepLogin && values[kNIMErrorCode].asInt() == 200)
        {
            ...
        }
        ...
    }
}

typedef    void(*nim_client_login)(const char *app_key, const char *account, const char *password, const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_login func = (nim_client_login) GetProcAddress(hInst, "nim_client_login");
    func("app key", "app account", "token", nullptr, &CallbackLogin, nullptr);
    //app key: 应用标识,不同应用之间的数据(用户,消息,群组等)是完全隔离的。开发自己的应用时,需要替换成自己申请来的app key
    //注意:替换成客户自己的app key之后,请不要对登录密码进行md5加密。
}

自动重新登录

SDK 在网络连接断开后,会监听网络状况,如果网络状况好转,那么SDK 会进行重新登录,登录结果可以在注册的重新登录回调函数中处理。

注意 自动重新登录的机制可能会失效:

如果重新登录回调函数返回的的错误号既不是kNIMResSuccess,也不是网络错误相关错误号(kNIMResTimeoutError或者kNIMResConnectionError),那么说明自动重新登录的机制已经失效,需要开发者退回到登录界面进行手动重新登录。(详见nim_client_reg_auto_relogin_cb接口说明)

手动重新登录

SDK 在重新登录失败后,可以由用户手动调用重新登录接口nim_client_relogin。

c++

nim::Client::Relogin();

c#

NIM.ClientAPI.Relogin();

c

typedef void(*nim_client_relogin)(const char *json_extension);
void foo()
{
    nim_client_relogin func = (nim_client_relogin) GetProcAddress(hInst, "nim_client_relogin");
    func(nullptr);
}

登出/退出和清理SDK

执行接口nim_client_logout 进行登出或退出,登出或退出的过程因为涉及到和服务器的交互以及需要将缓存中的数据持久化到本地,因此根据实际情况回调通知会有1s到20s左右的延迟。清理SDK必须在完全退出(收到退出回调)后执行。

Trick:因为退出和其他接口一样,总是在某些情况下会出现未及时返回需要上层界面等待的情况(比如网络原因导致服务器返回慢甚至超时,缓存数据大持久化到本地慢等原因),因此开发者可以通过暂时隐藏UI,让进程等待退出完成的回调后再执行清理SDK,退出程序的工作。

C++

void LoginCallback::OnLogoutCallback(nim::NIMResCode res_code)
{
    //如果是退出程序
    nim::Client::Cleanup();
    ...

    //如果是登出到登录界面,这里不能执行清理SDK工作,否则会报错
    ...
}

void foo()
{
    //登出到登录界面(nim::kNIMLogoutChangeAccout) 或者 退出程序(nim::kNIMLogoutAppExit)
    nim::Client::Logout(nim::kNIMLogoutAppExit, &OnLogoutCallback);
}

C#

private void foo()
{
    System.Threading.Semaphore s = new System.Threading.Semaphore(0, 1);
    NIM.ClientAPI.Logout(NIM.NIMLogoutType.kNIMLogoutAppExit, (r) =>
    {
        s.Release();
    });
    //需要logout执行完才能退出程序
    s.WaitOne(TimeSpan.FromSeconds(10));
    NIM.ClientAPI.Cleanup();
}

C

void CallbackLogout(const char* res, const void *user_data)
{
    ...
}

typedef    void(*nim_client_logout)(nim::NIMLogoutType logout_type, const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_logout func = (nim_client_logout) GetProcAddress(hInst, "nim_client_logout");
    func(kNIMLogoutAppExit, nullptr, &CallbackLogout, nullptr);
}

断网重连通知

通过接口nim_client_reg_disconnect_cb 来注册连接断开回调函数,断开连接后根据实际需要做后续处理(比如提示用户)。

C++

void OnDisconnectCallback()
{
    ···
}

void foo()
{
    nim::Client::RegDisconnectCb(&OnDisconnectCallback);
}

C#

private foo()
{
    NIM.ClientAPI.RegDisconnectedCb(() =>
    {
         MessageBox.Show("网络连接断开,网络恢复后后会自动重连");
    });
}

C

void CallbackDisconnect(const char* res, const void* user_data)
{
    //解析res
}

typedef void(*nim_client_reg_disconnect_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_reg_disconnect_cb func = (nim_client_reg_disconnect_cb) GetProcAddress(hInst, "nim_client_reg_disconnect_cb");
    func(nullptr, &CallbackDisconnect, nullptr);
}

被踢出通知

通过接口nim_client_reg_kickout_cb 来注册被踢出回调函数,当收到此通知时,一般是退出程序然后回到登录窗口,回调函数参数中详细说明了被踢出的原因。

C++

void OnKickoutCallback(const nim::KickoutRes& res)
{
    ···
}

void foo()
{
    nim::Client::RegKickoutCb(&OnKickoutCallback);
}

C#

private foo()
{
    NIM.ClientAPI.RegKickoutCb((r) =>
    {
        MessageBox.Show(r.Dump(), "被踢下线,请注意账号安全");
    });
}

C

void CallbackKickout(const char* res, const void* user_data)
{
    //解析res
}

typedef void(*nim_client_reg_kickout_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_reg_kickout_cb func = (nim_client_reg_kickout_cb) GetProcAddress(hInst, "nim_client_reg_kickout_cb");
    func(nullptr, &CallbackKickout, nullptr);
}

多端登录

通过调用nim_client_reg_multispot_login_notify_cb 来注册多端登录通知的回调函数,当用户在某个客户端登录时,其他没有被踢掉的端会触发这个回调函数,并携带当前时间登录的设备列表的数据。

网易云通信内置踢人策略为:移动端(Android,iOS)互踢,桌面端(PC,Web)互踢,移动端和桌面端共存。

如果当前的互踢策略无法满足业务需求的话,可以联系我们取消内置互踢,根据多端登录的回调和当前的设备列表,判断本设备是否需要被踢出。如果需要踢出,直接调用登出接口并在界面上给出相关提示即可。

c++

void LoginCallback::OnMultispotLoginCallback(const nim::MultiSpotLoginRes& res)
{
    ...
}
nim::Client::RegMultispotLoginCb(&nim_comp::LoginCallback::OnMultispotLoginCallback);

c#

//注册多端登录通知回调
NIM.ClientAPI.RegMultiSpotLoginNotifyCb(OnMultiSpotLogin);

//result 包含多端登录的其他端信息
private static void OnMultiSpotLogin(NIMMultiSpotLoginNotifyResult result)
{

}

c

static void CallbackMutliSpotLogin(const char* json_res, const void* user_data)
{
    Json::Reader reader;
    Json::Value values;
    if (json_res != nullptr && reader.parse((std::string)json_res, values) && values.isObject())
    {
        ...
    }
}

typedef void(*nim_client_reg_multispot_login_notify_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);
void foo()
{
    nim_client_reg_multispot_login_notify_cb func = (nim_client_reg_multispot_login_notify_cb) GetProcAddress(hInst, "nim_client_reg_multispot_login_notify_cb");
    func(nullptr, &CallbackMutliSpotLogin, nullptr);
}

网易云通信 ID

网易云通信的每个用户由网易云通信 ID 唯一标识。网易云通信 ID是由用户帐号和token成功同步到网易云通信服务器后自动分配的,可以通过 NIMMessage 的 from 字段获取消息发送方的网易云通信 ID。

互动直播

网易云通信提供音视频互动直播推流和连麦功能(多人音视频的扩展):

互动直播相关概念

互动直播房间与网易云通信音视频多人会议的房间概念一致,以房间名称为唯一标识。

互动直播房间需要先创建成功后才能加入,当所有用户都离开房间后,可以复用该房间名重新创建。

互动直播房间的主用户,推流地址的指定者,直播的主画面源。需要首先加入房间,连麦者才能加入。

互动直播房间的次用户,直播辅画面源,与主播加入同一房间,即能实现实时连麦互动。需要主播在房间时才能加入。

互动直播中除了主播和连麦者,观看直播画面的其他用户。可以通过停止播放直播并加入互动房间转变为连麦者。

网易云通信互动直播基于音视频多人会议开发,通过将多人会议中用户的音视频数据处理后推送给视频流服务器实现直播和实时连麦。

在功能的提供上,互动直播复用多人音视频接口,增加互动开关、推流地址指定与切换、直播角色指定等扩展设置

互动直播流程

网易云通信提供简单的互动推流和连麦接口,只需要创建并加入互动房间即可以实现直播推流;连麦者以相同的房间名加入即可以实现实时连麦互动。

调用nim_vchat_create_room接口创建一个互动直播房间,以房间名称为唯一标识。

主播首先加入已创建的互动房间,需要指定推流地址kNIMVChatRtmpUrl(指定推流地址的一端被认为是互动的主播),kNIMVChatBypassRtmp用于设置是否开启直播推流。调用加入接口nim_vchat_join_room进入房间,其中kNIMVChatRtmpUrl等扩展字段填写在参数json_extension中。其中NIMVChatVideoSplitMode的定义说明见图文介绍;在NIMVChatVideoSplitMode设置为kNIMVChatSplitCustomLayout时可使用自定义布局,说明见布局参数配置介绍

static const char *kNIMVChatRtmpUrl            = "rtmp_url";        /**< string 直播推流地址(加入多人时有效),非空代表主播旁路直播, kNIMVChatBypassRtmp决定是否开始推流 */
static const char *kNIMVChatBypassRtmp        = "bypass_rtmp";    /**< int 是否旁路推流(如果rtmpurl为空是连麦观众,非空是主播的推流控制), >0表示是 */
static const char *kNIMVChatRtmpRecord        = "rtmp_record";    /**< int 是否开启服务器对直播推流录制(需要开启服务器能力), >0表示是 */
static const char *kNIMVChatSplitMode        = "split_mode";        /**< int 主播控制的直播推流时的分屏模式,见NIMVChatVideoSplitMode */
static const char *kNIMVChatCustomLayout    = "custom_layout";    /**< string 自定义布局,当主播选择kNIMVChatSplitCustomLayout模式时生效 */

主播收到成功加入房间的反馈(nim_vchat_cb_func回调中对应的kNIMVideoChatSessionTypeConnect类型)后,即可以在观众端使用拉流播放器观看主播直播。

连麦者使用主播相同的房间名加入房间,需要打开互动直播推流开关kNIMVChatBypassRtmp(大于0标识打开),不要指定推流地址,调用加入接口nim_vchat_join_room进入房间;连麦者成功加入房间后,观众端就可以观看到连麦互动直播。

通过监听视频数据回掉接口,我们可以得到本地采集和接收到的远端画面数据,然后在客户端绘制(可参考demo实现)

主播可以调用nim_vchat_update_rtmp_url接口,实时切换推流地址,实现某些视频流平台的防盗链功能。

直播过程中可以使用nim_vchat_set_custom_data切换使用自定义的音频或视频数据。然后使用nim_vchat_custom_audio_data发送自定义音频数据,使用nim_vchat_custom_video_data发送自定义视频数据。

实现共享屏幕:应用上层获取屏幕数据后使用自定义视频数据接口发送即可。

通过回掉函数nim_vchat_cb_func,当已经加入互动房间的用户收到其他用户进出的通知kNIMVideoChatSessionTypePeopleStatus,其中回调函数中code对应NIMVideoChatSessionStatus, json返回kNIMVChatUid。

kNIMVideoChatSessionStatusJoined表示连麦开始。

kNIMVideoChatSessionStatusLeaved表示连麦结束,此时连麦者应该主动退出房间,主播不要退出房间。

通过回掉函数nim_vchat_cb_func,可监听自己与服务器的连接状态kNIMVideoChatSessionTypeConnect,以及所有成员的当前网络好坏情况kNIMVideoChatSessionTypeNetStatus。主播可通过kNIMVideoChatSessionTypeLiveState 得到直播推流的服务器状态。

调用nim_vchat_end接口退出互动直播房间。当所有加入改房间的用户都已离开,就可以复用该房间名重新创建了。

音视频设备

提供麦克风、扬声器(听筒)、摄像头等设备的遍历、启动关闭、监听的接口,使用相关功能前需要调用OleInitialize来初始化COM库。如果用户需要采集播放器音频,需要使用nim_audio_hook.dll,使用nim_vchat_start_device接口打开设备类型kNIMDeviceTypeAudioHook,device_path对应播放器本地全路径。

遍历设备

在启动设备前或者设备有变更后需要遍历设备,其中kNIMDeviceTypeAudioOut和kNIMDeviceTypeAudioOutChat都为听筒设备,遍历的时候等同

void nim_vchat_enum_device_devpath(NIMDeviceType type, const char *json_extension, nim_vchat_enum_device_devpath_sync_cb_func cb, const void *user_data) 
    param[in] type NIMDeviceType 见nim_device_def.h
    param[in] json_extension 无效的扩展字段
    param[in] cb 结果回调见nim_device_def.h
    param[in] user_data 无效的扩展字段
    return void 无返回值,设备信息由同步返回的cb通知

C++

void EnumDevCb(bool ret, nim::NIMDeviceType type, const char* json, const void*)
{
    if (ret)
    {
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus) && valus.isArray())
        {
            int num = valus.size();
            for (int i=0;i<num;++i)
            {
                Json::Value device;
                device = valus.get(i, device);
                MEDIA_DEVICE_DRIVE_INFO info;
                info.device_path_ = device[nim::kNIMDevicePath].asString();
                info.friendly_name_ = device[nim::kNIMDeviceName].asString();
            }
        }
    }
}

void foo()
{
    nim::VChat::EnumDeviceDevpath(nim::kNIMDeviceTypeAudioIn, &EnumDevCb);
}

C#

void foo()
{
    NIMDeviceInfoList cameraDeviceinfolist = NIM.DeviceAPI.GetDeviceList(NIM.NIMDeviceType.kNIMDeviceTypeVideo);
}

C

void EnumDevCb(bool ret, nim::NIMDeviceType type, const char* json, const void*)
{
    if (ret)
    {
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus) && valus.isArray())
        {
            int num = valus.size();
            for (int i=0;i<num;++i)
            {
                Json::Value device;
                device = valus.get(i, device);
                MEDIA_DEVICE_DRIVE_INFO info;
                info.device_path_ = device[nim::kNIMDevicePath].asString();
                info.friendly_name_ = device[nim::kNIMDeviceName].asString();
            }
        }
    }
}

typedef void(*nim_vchat_enum_device_devpath)(nim::NIMDeviceType type, const char *json_extension, nim_vchat_enum_device_devpath_sync_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_enum_device_devpath func = (nim_vchat_enum_device_devpath) GetProcAddress(hInst, "nim_vchat_enum_device_devpath");

    func(nim::kNIMDeviceTypeAudioIn, "", &EnumDevCb, nullptr);
}

启动设备

可以启动麦克风、摄像头、本地采集音频播放器、对方语音播放器等,设备路径填空的时候会启动系统首选项

启动设备,同一NIMDeviceType下设备将不重复启动,不同的设备会先关闭前一个设备开启新设备
void nim_vchat_start_device(NIMDeviceType type, const char* device_path, unsigned fps, const char *json_extension, nim_vchat_start_device_cb_func cb, const void *user_data)
    param[in] type NIMDeviceType 见nim_device_def.h
    param[in] device_path 设备路径对应kNIMDevicePath,如果是kNIMDeviceTypeAudioHook,对应播放器本地全路径
    param[in] fps 摄像头为采样频率(一般取30),其他NIMDeviceType无效(麦克风采样频率由底层控制,播放器采样频率也由底层控制)
    param[in] json_extension 无效的扩展字段
    param[in] cb 结果回调见nim_device_def.h
    param[in] user_data APP的自定义用户数据,SDK只负责传回给回调函数cb,不做任何处理!
    return void 无返回值

C++

void StartDeviceCb(nim::NIMDeviceType type, bool ret, const char *json, const void*)
{

}

void foo()
{
    nim::VChat::StartDevice(nim::kNIMDeviceTypeVideo, device_path, 50, width, height, &StartDeviceCb);
}

C#

void foo()
{
   NIM.DeviceAPI.StartDeviceResultHandler handle = (type, ret) =>
    {
        Action action = () =>
        {
            if(ret)
            {

            }
            else
            {
                MessageBox.Show("启动摄像头设备失败");
            }

        };
        this.Invoke(action);
    };
    NIM.DeviceAPI.StartDevice(NIM.NIMDeviceType.kNIMDeviceTypeVideo, camera_device_path, 50, handle);//开启摄像头
}

C

void StartDeviceCb(nim::NIMDeviceType type, bool ret, const char *json, const void*)
{

}

typedef void(*nim_vchat_start_device)(nim::NIMDeviceType type, const char* device_path, unsigned fps, const char* json_extension, nim_vchat_start_device_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_start_device func = (nim_vchat_start_device) GetProcAddress(hInst, "nim_vchat_start_device");

    std::string json_value;
    Json::FastWriter fs;
    Json::Value value;
    value[nim::kNIMDeviceWidth] = width;
    value[nim::kNIMDeviceHeight] = height;
    json_value = fs.write(value);

    fun(nim::kNIMDeviceTypeVideo, device_path.c_str(), 50, json_value.c_str(), &StartDeviceCb, nullptr);
}

结束设备

void nim_vchat_end_device(NIMDeviceType type, const char *json_extension)
    param[in] type NIMDeviceTypenim_device_def.h
    param[in] json_extension 无效的扩展字段
    return void 无返回值

C++

void foo()
{
    nim::VChat::EndDevice(nim::kNIMDeviceTypeVideo);
}

C#

void foo()
{
    NIM.DeviceAPI.EndDevice(NIM.NIMDeviceType.kNIMDeviceTypeAudioOut);
}

C

typedef void(*nim_vchat_end_device)(nim::NIMDeviceType type, const char *json_extension);

void foo()
{
    nim_vchat_end_device func = (nim_vchat_end_device) GetProcAddress(hInst, "nim_vchat_end_device");

    fun(nim::kNIMDeviceTypeVideo);
}

监听设备状态

通过设置设备监听,可以定时获知设备状态并启动非主动结束的设备,这里只对麦克风和摄像头、及伴音hook有效

void nim_vchat_add_device_status_cb(NIMDeviceType type, nim_vchat_device_status_cb_func cb)
    param[in] type NIMDeviceTypekNIMDeviceTypeAudioInkNIMDeviceTypeVideo有效) 见nim_device_def.h
    param[in] cb 结果回调见nim_device_def.h
    return void 无返回值

C++

void DeviceStatusCb(nim::NIMDeviceType type, UINT status, const char* path, const char *json, const void *)
{

}

void foo()
{
    nim::VChat::AddDeviceStatusCb(nim::kNIMDeviceTypeVideo, &DeviceStatusCb);
}

C#

void foo()
{
   NIM.DeviceAPI.AddDeviceStatusCb(NIM.NIMDeviceType.kNIMDeviceTypeVideo, (type, status, devicePath) =>
    {
    });
}

C

void DeviceStatusCb(nim::NIMDeviceType type, UINT status, const char* path, const char *json, const void *)
{

}

typedef void(*nim_vchat_add_device_status_cb)(nim::NIMDeviceType type, nim_vchat_device_status_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_add_device_status_cb func = (nim_vchat_add_device_status_cb) GetProcAddress(hInst, "nim_vchat_add_device_status_cb");

    fun(nim::kNIMDeviceTypeVideo, &DeviceStatusCb);
}

由于开启设备监听后会定时检查设备状态,在不需要时结束监听

void nim_vchat_remove_device_status_cb(NIMDeviceType type)
    param[in] type DeviceTypekNIMDeviceTypeAudioInkNIMDeviceTypeVideo有效) 见nim_device_def.h
    return void 无返回值

C++

void foo()
{
    nim::VChat::RemoveDeviceStatusCb(nim::kNIMDeviceTypeVideo);
}

C#

void foo()
{
    NIM.DeviceAPI.RemoveDeviceStatusCb(NIM.NIMDeviceType.kNIMDeviceTypeVideo);
}

C

typedef void(*nim_vchat_remove_device_status_cb)(nim::NIMDeviceType type);

void foo()
{
    nim_vchat_remove_device_status_cb func = (nim_vchat_remove_device_status_cb) GetProcAddress(hInst, "nim_vchat_remove_device_status_cb");

    fun(nim::kNIMDeviceTypeVideo);
}

辅助的摄像头

启动辅助的摄像头,摄像头数据通过nim_vchat_set_video_data_cb设置采集回调返回(json返回kNIMDeviceId),不直接通过视频通话发送给对方,并且不参与设备监听检测

void nim_vchat_start_extend_camera(const char *id, const char *device_path, unsigned fps, const char *json_extension, nim_vchat_start_device_cb_func cb, const void *user_data)
    param[in] id 摄像头标识,用于开关及数据回调时的对应,不能为空。(同一id下设备将不重复启动,如果设备device_path不同会先关闭前一个设备开启新设备)
    param[in] device_path 设备路径对应kNIMDevicePath
    param[in] fps 摄像头为采样频率
    param[in] json_extension 打开摄像头是允许设置 kNIMDeviceWidth 和 kNIMDeviceHeight,并取最接近设置值的画面模式
    param[in] cb 结果回调见nim_device_def.h
    param[in] user_data APP的自定义用户数据,SDK只负责传回给回调函数cb,不做任何处理!
    return void 无返回值

C++

void VChatCallback::StartDeviceCb(nim::NIMDeviceType type, bool ret, const char *json, const void*)
{
    ...
}
void foo()
{
    nim::VChat::StartDevice(kNIMDeviceTypeVideo, device_path, 50, width, height, &VChatCallback::StartDeviceCb);
}

C#

/// <summary>
/// 启动辅助的摄像头,摄像头数据通过SetVideoCaptureDataCb设置采集回调返回,不直接通过视频通话发送给对方,并且不参与设备监听检测
/// </summary>
/// <param name="id">摄像头标识,用于开关及数据回调时的对应,不能为空。(同一id下设备将不重复启动,如果设备device_path不同会先关闭前一个设备开启新设备)</param>
/// <param name="device_path">设备路径</param>
/// <param name="fps">摄像头为采样频率</param>
/// <param name="json_extension">打开摄像头是允许设置 kNIMDeviceWidth 和 kNIMDeviceHeight,并取最接近设置值的画面模式</param>
/// <param name="handler">回调</param>
NIM.DeviceAPI.StartExtendCamera(string id, string device_path, uint fps, string json_extension, StartDeviceResultHandler handler);

C

void StartDeviceCb(nim::NIMDeviceType type, bool ret, const char *json, const void*)
{

}

typedef void(*nim_vchat_start_extend_camera)(const char *id, const char *device_path, unsigned fps, const char *json_extension, nim_vchat_start_device_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_start_extend_camera func = (nim_vchat_start_extend_camera) GetProcAddress(hInst, "nim_vchat_start_extend_camera");

    std::string json_value;
    Json::FastWriter fs;
    Json::Value value;
    value[nim::kNIMDeviceWidth] = width;
    value[nim::kNIMDeviceHeight] = height;
    json_value = fs.write(value);

    fun("video_extend", device_path.c_str(), 50, json_value.c_str(), cb, nullptr);
}

结束辅助摄像头

void nim_vchat_stop_extend_camera(const char *id, const char *json_extension)
    param[in] id 摄像头标识id,如果为空,则关闭所有辅助摄像头
    param[in] json_extension 无效的扩展字段
    return void 无返回值

C++

nim::VChat::EndDevice(kNIMDeviceTypeVideo);

C#

/// <summary>
/// 结束辅助摄像头
/// </summary>
/// <param name="id">摄像头标识id,如果为空,则关闭所有辅助摄像头</param>
NIM.DeviceAPI.StartExtendCamera(string id);

C

typedef void(*nim_vchat_stop_extend_camera)(const char *id, const char *json_extension);

void foo()
{
    nim_vchat_stop_extend_camera func = (nim_vchat_stop_extend_camera) GetProcAddress(hInst, "nim_vchat_stop_extend_camera");

    fun("video_extend", "");
}

监听音频数据

通过设置监听,可以获得音频的pcm数据包,通过启动设备kDeviceTypeAudioOut和kDeviceTypeAudioOutChat由底层播放,可以不监听

void nim_vchat_set_audio_data_cb(bool capture, const char *json_extension, nim_vchat_audio_data_cb_func cb, const void *user_data)
    param[in] capture true 标识监听麦克风采集数据,false 标识监听通话中对方音频数据
    param[in] json_extension 扩展Json string:kNIMDeviceSampleRate(要求返回的音频数据为指定的采样频,缺省为0使用默认采样频)
    param[in] cb 结果回调见nim_device_def.h
    param[in] user_data APP的自定义用户数据,SDK只负责传回给回调函数cb,不做任何处理!
    return void 无返回值

C++

void SetDataCb(unsigned __int64 time, const char *data, unsigned int size, const char *json_extension, const void *user_data)
{

}

void foo()
{
    nim::VChat::SetAudioDataCb(true, &SetDataCb);
}

C#

void AudioDataCaptureHandler(UInt64 time, IntPtr data, uint size, int rate)
{

}

void foo()
{
   NIM.DeviceAPI.SetAudioCaptureDataCb(AudioDataCaptureHandler);
}

C

void SetDataCb(unsigned __int64 time, const char *data, unsigned int size, const char *json_extension, const void *user_data)
{

}

typedef void(*nim_vchat_set_audio_data_cb)(bool capture, const char* json_extension, nim_vchat_audio_data_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_set_audio_data_cb func = (nim_vchat_set_audio_data_cb) GetProcAddress(hInst, "nim_vchat_set_audio_data_cb");

    fun(true, "", &SetDataCb, nullptr);
}

监听视频数据

通过设置监听,可以获得视频的ARGB数据包

void nim_vchat_set_video_data_cb(bool capture, const char *json_extension, nim_vchat_video_data_cb_func cb, const void *user_data)
    param[in] capture true 标识监听采集数据,false 标识监听通话中对方视频数据
    param[in] json_extension Json string 返回kNIMVideoSubType(缺省为kNIMVideoSubTypeARGB)
    param[in] cb 结果回调见nim_device_def.h
    param[in] user_data APP的自定义用户数据,SDK只负责传回给回调函数cb,不做任何处理!
    return void 无返回值

C++

void SetDataCb(unsigned __int64 time, const char *data, unsigned int size, unsigned int width, unsigned int height, const char *json_extension, const void *user_data)
{

}

void foo()
{
    nim::VChat::SetVideoDataCb(true, &SetDataCb);
}

C#

void VideoDataCaptureHandler(UInt64 time, IntPtr data, UInt32 size, UInt32 width, UInt32 height, string json_extension, IntPtr user_data)
{

}

void foo()
{
    JObject jo = new JObject();
    jo.Add(new JProperty("subtype", NIMVideoSubType.kNIMVideoSubTypeI420));
    string json_extention = jo.ToString();
    NIM.DeviceAPI.SetVideoCaptureDataCb(VideoDataCaptureHandler,json_extention);
}

C

void SetDataCb(unsigned __int64 time, const char *data, unsigned int size, unsigned int width, unsigned int height, const char *json_extension, const void *user_data)
{

}

typedef void(*nim_vchat_set_video_data_cb)(bool capture, const char* json_extension, nim_vchat_video_data_cb_func cb, const void *user_data);

void foo()
{
    nim_vchat_set_video_data_cb func = (nim_vchat_set_video_data_cb) GetProcAddress(hInst, "nim_vchat_set_video_data_cb");

    Json::FastWriter fs;
    Json::Value value;
    value[nim::kNIMVideoSubType] = kNIMVideoSubTypeI420;
    std::string json_value = fs.write(value);
    fun(true, json_value.c_str(), &SetDataCb, nullptr);
}

音频设置

设置音频音量,默认255,且音量均由软件换算得出,设置麦克风音量自动调节后麦克风音量参数无效

void nim_vchat_set_audio_volumn(unsigned char volumn, bool capture)
    param[in] volumn 结果回调见nim_device_def.h
    param[in] capture true 标识设置麦克风音量,false 标识设置播放音量
    return void 无返回值

C++

void foo()
{
    nim::VChat::SetAudioVolumn(200, true);
}

C#

void foo()
{
    NIM.DeviceAPI.AudioCaptureVolumn = 200;
}

C

typedef void(*nim_vchat_set_audio_volumn)(unsigned char volumn, bool capture);

void foo()
{
    nim_vchat_set_audio_volumn func = (nim_vchat_set_audio_volumn) GetProcAddress(hInst, "nim_vchat_set_audio_volumn");

    func(200, true);
}

获取音频音量

unsigned char nim_vchat_get_audio_volumn(bool capture)
    param[in] capture true 标识获取麦克风音量,false 标识获取播放音量
    return unsigned char 音量值

C++

void foo()
{
    nim::VChat::GetAudioVolumn(true);
}

C#

void foo()
{
    byte value = NIM.DeviceAPI.AudioCaptureVolumn;
}

C

typedef unsigned char(*nim_vchat_get_audio_volumn)(bool capture);

void foo()
{
    nim_vchat_get_audio_volumn func = (nim_vchat_get_audio_volumn) GetProcAddress(hInst, "nim_vchat_get_audio_volumn");

    func(true);
}

设置麦克风音量自动调节

void nim_vchat_set_audio_input_auto_volumn(bool auto_volumn)
    param[in] auto_volumn true 标识麦克风音量自动调节,false 标识麦克风音量不调节,这时nim_vchat_set_audio_volumn中麦克风音量参数起效
    return void 无返回值

C++

void foo()
{
    nim::VChat::SetAudioInputAutoVolumn(true);
}

C#

void foo()
{
    NIM.DeviceAPI.AudioCaptureAutoVolumn = true;
}

C

typedef void(*nim_vchat_set_audio_input_auto_volumn)(bool auto_volumn);

void foo()
{
    nim_vchat_set_audio_input_auto_volumn func = (nim_vchat_set_audio_input_auto_volumn) GetProcAddress(hInst, "nim_vchat_set_audio_input_auto_volumn");

    func(true);
}

获取是否自动调节麦克风音量

bool nim_vchat_get_audio_input_auto_volumn()
    return bool true 标识麦克风音量自动调节,false 标识麦克风音量不调节

C++

void foo()
{
    bool value = nim::VChat::GetAudioInputAutoVolumn();
}

C#

void foo()
{
    bool value = NIM.DeviceAPI.AudioCaptureAutoVolumn;
}

C

typedef bool(*nim_vchat_get_audio_input_auto_volumn)();

void foo()
{
    nim_vchat_get_audio_input_auto_volumn func = (nim_vchat_get_audio_input_auto_volumn) GetProcAddress(hInst, "nim_vchat_get_audio_input_auto_volumn");

    bool is_auto = func();
}

自定义数据接口

自定义音频数据接口, 采样位深只支持16或32, kNIMDeviceSampleRate支持8000,16000,32000,44100

bool nim_vchat_custom_audio_data(unsigned __int64 time, const char *data, unsigned int size, const char *json_extension);
    param[in] time 时间毫秒级
    param[in] data 音频数据pcm格式
    param[in] size data的数据长度
    param[in] json_extension 扩展Json string kNIMDeviceSampleRate采样频和kNIMDeviceSampleBit采样位深 默认如{"sample_rate":16000, "sample_bit":16}
    return bool true 调用成功,false 调用失败

C++

(参见IM demo)
...

C#

/// <summary>
/// 自定义音频数据接口, 采样位深只支持16或32, kNIMDeviceSampleRate支持8000,16000,32000,44100
/// </summary>
/// <param name="time">时间毫秒级</param>
/// <param name="data">音频数据pcm格式</param>
/// <param name="size">data的数据长度 sizeof(char)</param>
/// <param name="info">采样频和采样位深 默认如{"sample_rate":16000, "sample_bit":16}</param>
/// <returns></returns>
NIM.DeviceAPI.CustomAudioData(ulong time, IntPtr data, uint size, NIMCustomAudioDataInfo info)

C

暂无

自定义视频数据接口

bool nim_vchat_custom_video_data(unsigned __int64 time, const char *data, unsigned int size, unsigned int width, unsigned int height, const char *json_extension);
    param[in] time 时间毫秒级
    param[in] data 视频数据, 默认为yuv420格式
    param[in] size data的数据长度
    param[in] width  画面宽度,必须是偶数
    param[in] height  画面高度,必须是偶数
    param[in] json_extension  扩展Json string,kNIMVideoSubType视频数据格式(缺省为kNIMVideoSubTypeI420)
    return bool true 调用成功,false 调用失败

C++

//参见IM demo
void foo()
{
    static int64_t timestamp = 0;
    std::string data;
    data.resize(1280 * 720 * 4);
    int32_t w, h;
    w = 0;
    h = 0;
    bool ret = nim_comp::VideoManager::GetInstance()->video_frame_mng_.GetVideoFrame("", timestamp, (char*)data.c_str(), w, h, false, false);
    if (ret)
    {
        int32_t width = w;
        int32_t height = h;
        int32_t wxh = width*height;
        int32_t data_size = wxh * 3 / 2;

        //均方差滤波
        std::string beauty_src_data;
        beauty_src_data.resize(wxh * 4);
        nim_comp::YUV420ToARGB((char*)data.c_str(), (char*)beauty_src_data.c_str(), width, height);

        //采用色彩平衡算法进行美白
        nim_comp::colorbalance_rgb_u8((unsigned char*)beauty_src_data.c_str(), wxh, (size_t)(wxh * 2 / 100), (size_t)(wxh * 8 / 100), 4);

        nim_comp::ARGBToYUV420((char*)beauty_src_data.c_str(), (char*)data.c_str(), width, height);

        //采用均方差滤波进行磨皮
        nim_comp::smooth_process((uint8_t*)data.c_str(), width, height, 10, 0, 200);

        //保存用于预览
        std::string json;
        video_frame_mng_.AddVideoFrame(true, 0, data.c_str(), data_size, w, h, json, nim_comp::VideoFrameMng::Ft_I420);
        //发送
        nim::VChat::CustomVideoData(0, data.c_str(), data_size, w, h, nullptr);
    }
}

C#

//参见IM demo
void foo(object sender, VideoEventAgrs e)
{

    uint size=Convert.ToUInt32(e.Frame.Width*e.Frame.Height*3/2);
    //处理数据
    byte[] i420=NIMDemo.LivingStreamSDK.YUVHelper.ARGBToI420(e.Frame.Data,e.Frame.Width,e.Frame.Height);
    Beauty.Smooth.smooth_process(i420,e.Frame.Width,e.Frame.Height,10,0,200);
    e.Frame.Data = NIMDemo.LivingStreamSDK.YUVHelper.I420ToARGB(i420, e.Frame.Width, e.Frame.Height);

    //发送自定义数据
    TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
    ulong time = Convert.ToUInt64(ts.TotalMilliseconds);
    NIMDemo.LivingStreamSDK.YUVHelper.i420Revert(ref i420,e.Frame.Width,e.Frame.Height);
    IntPtr unmanagedPointer = Marshal.AllocHGlobal(i420.Length);
    Marshal.Copy(i420, 0, unmanagedPointer, i420.Length);
    NIM.DeviceAPI.CustomVideoData(time, unmanagedPointer, size, (uint)e.Frame.Width, (uint)e.Frame.Height);
    Marshal.FreeHGlobal(unmanagedPointer);
}

C

typedef bool(*nim_vchat_custom_video_data)(unsigned __int64 time, const char *data, unsigned int size, unsigned int width, unsigned int height, const char *json_extension);

void foo()
{
    nim_vchat_custom_video_data func = (nim_vchat_custom_video_data) GetProcAddress(hInst, "nim_vchat_custom_video_data");

    func(time, data, size, width, height, "");
}

音频采集数据处理开关

设置底层针对麦克风采集数据处理开关接口,默认全开(此接口是全局接口,在sdk初始化后设置一直有效)。aec 标识回音消除功能,ns标识降噪功能,vid标识人言检测功能。

void nim_vchat_set_audio_process_info(bool aec, bool ns, bool vid)
    param[in] aec true 标识打开回音消除功能,false 标识关闭
    param[in] ns true 标识打开降噪功能,false 标识关闭
    param[in] vid true 标识打开人言检测功能,false 标识关闭
    return void 无返回值

C++

void foo()
{
    nim::VChat::SetAudioProcess(false, false, false);
}

C#

NIM.DeviceAPI.SetAudioProcessInfo(bool aec,bool ns,bool vid)

C

typedef bool(*nim_vchat_set_audio_process_info)(bool aec, bool ns, bool vid);

void foo()
{
    nim_vchat_set_audio_process_info func = (nim_vchat_set_audio_process_info) GetProcAddress(hInst, "nim_vchat_set_audio_process_info");

    func(false, false, false);
}

音视频通话示例

对所有异步回调接口,在实现时post到ui线程处理

//本地设备遍历
struct MEDIA_DEVICE_DRIVE_INFO
{
    std::string device_path_;
    std::string friendly_name_;
};
std::list<MEDIA_DEVICE_DRIVE_INFO> list_audioin_;
std::list<MEDIA_DEVICE_DRIVE_INFO> list_audioout_;
std::list<MEDIA_DEVICE_DRIVE_INFO> list_video_;
void GetDeviceByJson(std::list<MEDIA_DEVICE_DRIVE_INFO>& list, const char* json)
{
    Json::Value valus;
    Json::Reader reader;
    if (reader.parse(json, valus) && valus.isArray())
    {
        int num = valus.size();
        for (int i=0;i<num;++i)
        {
            Json::Value device;
            device = valus.get(i, device);
            MEDIA_DEVICE_DRIVE_INFO info;
            info.device_path_ = device[kNIMDevicePath].asString();
            info.friendly_name_ = device[kNIMDeviceName].asString();
            list.push_back(info);
        }
    }
}
//遍历设备的回调为同步接口,不需要post
void EnumDevCb(bool ret, NIMDeviceType type, const char* json, const void*)
{
    Print("EnumDevCb: ret=%d, NIMDeviceType=%d\r\njson: %s", ret, type, json);
    if (ret)
    {
        switch (type)
        {
        case kNIMDeviceTypeAudioIn:
            GetDeviceByJson(list_audioin_, json);
            break;
        case kNIMDeviceTypeAudioOut:
            GetDeviceByJson(list_audioout_, json);
            break;
        case kNIMDeviceTypeVideo:
            GetDeviceByJson(list_video_, json);
            break;
        }
    }
}
void EnumDev()
{
    nim_vchat_enum_device_devpath(kNIMDeviceTypeVideo, "", EnumDevCb, nullptr);
    nim_vchat_enum_device_devpath(kNIMDeviceTypeAudioIn, "", EnumDevCb, nullptr);
    nim_vchat_enum_device_devpath(kNIMDeviceTypeAudioOut, "", EnumDevCb, nullptr);
}

//监听设备回调
void AudioStatus(NIMDeviceType type, UINT status, const char* path, const char*)
{
    //
}
void VideoStatus(NIMDeviceType type, UINT status, const char* path, const char*)
{
    //
}

//监听设备,建议在需要的时候开启,不需要的时候调用停止监听
void AddDeviceStatusCb()
{
    nim_vchat_add_device_status_cb(kNIMDeviceTypeVideo, VideoStatus);
    nim_vchat_add_device_status_cb(kNIMDeviceTypeAudioIn, AudioStatus);
}
//停止监听设备
void RemoveDeviceStatusCb()
{
    nim_vchat_remove_device_status_cb(kNIMDeviceTypeVideo);
    nim_vchat_remove_device_status_cb(kNIMDeviceTypeAudioIn);
}

//音视频通话
//通话回调函数
void OnVChatEvent(NIMVideoChatSessionType type, __int64 channel_id, int code, const char *json_extension, const void *user_data)
{
    std::string json = json_extension;
    switch (type)
    {
    case nim::kNIMVideoChatSessionTypeStartRes:{
        if (code == 200)
        {
            //发起通话成功
        }
        else
        {
            EndVChat();            
        }
    }break;
    case nim::kNIMVideoChatSessionTypeInviteNotify:{
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus))
        {
            std::string uid = valus[nim::kNIMVChatUid].asString();
            int mode = valus[nim::kNIMVChatType].asInt();
            bool exist = false;//是否已经有音视频通话存在
            if (exist)
            {
                //通知对方忙碌
                VChatControl(channel_id, nim::kNIMTagControlBusyLine);
                //拒绝接听,可由用户在ui触发
                VChatCalleeAck(channel_id, false);
            }
            else
            {
                //通知对方收到邀请
                VChatControl(channel_id, nim::kNIMTagControlReceiveStartNotifyFeedback);
                //接听,可由用户在ui触发
                VChatCalleeAck(channel_id, true);
            }
        }
    }break;
    case nim::kNIMVideoChatSessionTypeCalleeAckRes:{
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus))
        {
            //std::string uid = valus[nim::kNIMVChatUid].asString();
            bool accept = valus[nim::kNIMVChatAccept].asInt() > 0;
            if (accept && code != 200)
            {
                //接听失败,结束当前通话
                EndVChat();    
            }
        }
    }break;
    case nim::kNIMVideoChatSessionTypeCalleeAckNotify:{
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus))
        {
            //std::string uid = valus[nim::kNIMVChatUid].asString();
            bool accept = valus[nim::kNIMVChatAccept].asInt() > 0;
            if (!accept)
            {
                //对方拒绝,结束当前通话
                EndVChat();    
            }
        }
    }break;
    case nim::kNIMVideoChatSessionTypeControlRes:{
        //控制命令发送结果
    }break;
    case nim::kNIMVideoChatSessionTypeControlNotify:{
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus))
        {
            //std::string uid = valus[nim::kNIMVChatUid].asString();
            int type = valus[nim::kNIMVChatType].asInt();
            //收到一个控制通知
        }
    }break;
    case nim::kNIMVideoChatSessionTypeConnect:{
        if (code != 200)
        {
            //连接音视频服务器失败
        }
        else
        {
            //连接音视频服务器成功,启动麦克风、听筒、摄像头
            StartDevice();
        }
    }break;
    case nim::kNIMVideoChatSessionTypePeopleStatus:{
        if (code == nim::kNIMVideoChatSessionStatusJoined)
        {
            //对方进入
        }
        else if (code == nim::kNIMVideoChatSessionStatusLeaved)
        {
            //对方离开,结束
            EndVChat();    
        }
    }break;
    case nim::kNIMVideoChatSessionTypeNetStatus:{
        //网络状况变化
    }break;
    case nim::kNIMVideoChatSessionTypeHangupRes:{
        //挂断结果
    }break;
    case nim::kNIMVideoChatSessionTypeHangupNotify:{
        //收到对方挂断
    }break;
    case nim::kNIMVideoChatSessionTypeSyncAckNotify:{
        Json::Value valus;
        Json::Reader reader;
        if (reader.parse(json, valus))
        {
            bool accept = valus[nim::kNIMVChatAccept].asInt() > 0;
            //在其他端处理
        }
    }break;
    }
}
//一个简单的视频数据绘制
void DrawPic(bool left, unsigned int width, unsigned int height, const char* data)
{
    if (!IsWindow(m_hWnd))
    {
        return;
    }
    int pos = 10;
    const char* scr_data = data;
    int w = 0;
    int top = 30;
    if (!left)
    {
        RECT rect;
        GetWindowRect(m_hWnd, &rect);
        w = rect.right - rect.left;
        pos = w - width - 20;
    }
    // 构造位图信息头
    BITMAPINFOHEADER bih;
    memset(&bih, 0, sizeof(BITMAPINFOHEADER));
    bih.biSize = sizeof(BITMAPINFOHEADER);
    bih.biPlanes = 1;
    bih.biBitCount = 32;
    bih.biWidth = width;
    bih.biHeight = height;
    bih.biCompression = BI_RGB;
    bih.biSizeImage = width * height * 4;

    // 开始绘制
    HDC    hdc = ::GetDC(m_hWnd);
    PAINTSTRUCT ps;
    ::BeginPaint(m_hWnd, &ps);
    // 使用DIB位图和颜色数据对与目标设备环境相关的设备上的指定矩形中的像素进行设置
    SetDIBitsToDevice(
        hdc, pos, top,
        bih.biWidth, bih.biHeight,
        0, 0, 0, bih.biWidth,
        scr_data,
        (BITMAPINFO*)&bih,
        DIB_RGB_COLORS);
    ::EndPaint(m_hWnd, &ps);
    ::ReleaseDC(m_hWnd, hdc);
}
//本地视频数据监听
void VideoCaptureData(unsigned __int64 time, const char* data, unsigned int size, unsigned int width, unsigned int height, const char*)
{
    static int capture_video_num = 0;
    capture_video_num++;
    if (capture_video_num % 2 == 0)
    {
        return;
    }
    DrawPic(true, width, height, data);
}
//接收视频数据监听
void VideoRecData(unsigned __int64 time, const char* data, unsigned int size, unsigned int width, unsigned int height, const char*)
{
    DrawPic(false, width, height, data);
}
//初始化回调接口
void InitVChatCb()
{
    nim_vchat_set_cb_func(OnVChatEvent);
    nim_vchat_set_video_data_cb(true, "", VideoCaptureData, nullptr);
    nim_vchat_set_video_data_cb(false, "", VideoRecData, nullptr);
}
//启动设备
void StartDevice()
{
    nim_vchat_start_device(kNIMDeviceTypeAudioIn, "", 0, "", nullptr, nullptr);
    nim_vchat_start_device(kNIMDeviceTypeAudioOutChat, "", 0, "", nullptr, nullptr);
    nim_vchat_start_device(kNIMDeviceTypeVideo, "", 50, "", nullptr, nullptr);
}
//停用设备
void EndDevice()
{
    nim_vchat_end_device(kNIMDeviceTypeVideo, "");
    nim_vchat_end_device(kNIMDeviceTypeAudioIn, "");
    nim_vchat_end_device(kNIMDeviceTypeAudioOutChat, "");
}
//发起邀请
void StartVChat()
{
    Json::FastWriter fs;
    Json::Value value;
    value[kNIMVChatUids].append("uid");
    std::string json_value = fs.write(value);
    nim_vchat_start(kNIMVideoChatModeVideo, json_value.c_str(), nullptr);
}
//结束通话
void EndVChat()
{
    EndDevice();
    //当结束当前音视频会话时都可以调用nim_vchat_end,sdk底层会对通话进行判断清理
    nim_vchat_end("");
}