基于AVAPIs模块开发网络摄像机-设备端
更新日期:2025/2/13
目录
相关demo路径
平台 | 例子 |
---|---|
android | SDK目录\Sample\Android\Sample_AVAPIs_Server |
linux | SDK目录\Sample\Linux\Sample_AVAPIs |
常见的配网方式
可以参考:常见的配网方式实作纲要
登录、监听和访问处理
主要使用的API: IOTCAPIs+AVAPIs
流程图:
登录
DeviceLoginInput auth_option;
printf("thread_Login start\n");
while(gProcessRun){
auth_option.cb = sizeof(DeviceLoginInput);
auth_option.authentication_type = AUTHENTICATE_BY_KEY;
memcpy(auth_option.auth_key, gIotcAuthkey, IOTC_AUTH_KEY_LENGTH);
int ret = IOTC_Device_LoginEx((char *)arg, &auth_option); //如不使用authkey,则使用IOTC_Device_Login做login
printf("IOTC_Device_LoginEx() ret = %d\n", ret);
if(ret == IOTC_ER_NoERROR) {
break;
} else {
PrintErrHandling (ret);
for(sleep_count = 0 ; sleep_count < 5 && gProcessRun ; sleep_count++)
sleep(1);
}
}
设备在上电后进行IOTC_Device_LoginEx,如果失败,需要重复调用,直到成功为止。此接口同时会开启局域网搜索功能,即使没有login成功,客户端也可以通过路由器下通过IOTC_Lan_Search等API搜索到,可以通过IOTC_Connect_ByUIDEx等API进行连线。
login成功后,就不再需要调用IOTC_Device_LoginEx,可以继续在此线程,调用IOTC_Get_LoginInfo来检查设备端是否从服务器掉线。
/*
* IOTC_Get_Login_Info用以检查login状态,可以拿到两个值。
* 返回值表示 try login失败的次数
* 输出参数 login_state 为7表示正常在login,并且有收到P2P服务器回应的login response。
*/
unsigned int login_state;
int login_fail_count = 0;
while(gProcessRun){
int ret = IOTC_Get_Login_Info(&login_state);
printf("IOTC_Get_Login_Info() ret = %d,login_state = %u\n", ret,login_state);
if(ret < IOTC_ER_NoERROR) {
break;
}
if(ret > 4){//此时已经有一段时间与服务器失联了,ret表示retry login失败的次数。
if(无用户访问 && 网络畅通){ //网络畅通可以通过ping或者其他方式做测试
printf("device logout from p2p server,will call IOTC_Listen_Exit\n");
#ifdef _RESTART_P2P_MODULE_
IOTC_Listen_Exit(); //调用此API后,IOTC_Listen会返回IOTC_ER_EXIT_LISTEN;
break;
#elif defined(_REINIT_SOCKET_)
IOTC_ReInitSocket(0);//只重置底层socket,不做login。不需要重新初始化。
continue;
#elif defined(_FORCE_LOGIN_)
IOTC_ReInitSocket(0);//重置底层socket,不做login。
IOTC_Device_Update_Authkey(device_authkey);//调用此API,将强制重新login。不需要重新初始化。
continue;
#endif
}
}
for(int sleep_count = 0 ; sleep_count < 5 && gProcessRun ; sleep_count++)
sleep(1);
}
如果是以login_state来作为判断的依据,可以参考如下代码:
unsigned int login_state;
int login_fail_count = 0;
while(gProcessRun){
int ret = IOTC_Get_Login_Info(&login_state);
printf("IOTC_Get_Login_Info() ret = %d,login_state = %u\n", ret,login_state);
if(ret < IOTC_ER_NoERROR) {
break;
}
if(login_state == 7){//与P2P服务器通信正常
login_fail_count = 0;
}
else{
if(login_fail_count++ > 10){//已经超过10次检查都是无法恢复,大概时间即10*5=50s
if(无用户访问 && 网络畅通){ //网络畅通可以通过ping或者其他方式做测试
printf("device logout from p2p server,will call IOTC_Listen_Exit\n");
#ifdef _RESTART_P2P_MODULE_
IOTC_Listen_Exit(); //调用此API后,IOTC_Listen会返回IOTC_ER_EXIT_LISTEN;
break;
#elif defined(_REINIT_SOCKET_)
IOTC_ReInitSocket(0);//只重置底层socket,不做login。不需要重新初始化。
continue;
#elif defined(_FORCE_LOGIN_)
IOTC_ReInitSocket(0);//重置底层socket,不做login。
IOTC_Device_Update_Authkey(device_authkey);//调用此API,将强制重新login。不需要重新初始化。
continue;
#endif
}
}
}
for(int sleep_count = 0 ; sleep_count < 5 && gProcessRun ; sleep_count++)
sleep(1);
}
如遇到一些小运营商的网络不稳定的情况,建议使用4.3.6.x以上的SDK,并在初始化之前调用IOTC_Device_Setup_Network_Adaption(IOTC_DEVICE_ADAPTION_MODE_CHOOSE_BEST_UDP_SERVER_OR_SWITCH_TO_TCP)开启TCP自适应。
监听
unsigned char need_reboot = 0;
while(gProcessRun)
{
// Accept connection only when IOTC_Listen() calling
int SID = IOTC_Listen(1000);
if (SID < 0) {
PrintErrHandling (SID);
if (SID == IOTC_ER_EXCEED_MAX_SESSION) {
#if TEST_FAST_RECOVERY
for(int i = 0;i<5 && 在线人数已经达到最大;i++){
sleep(1);
}
#else
sleep(5);
#end
continue;
}
else if(SID == AV_ER_TIMEOUT){
continue;
}
else if(SID == IOTC_ER_EXIT_LISTEN){
need_reboot = 1;
break;
}
else{
//程序异常,可能要重启
}
}
//因avServStart是阻塞型的接口,所以此处另外开线程去处理。
int *sid = (int *)malloc(sizeof(int));
*sid = SID;
pthread_t Thread_ID;
ret = pthread_create(&Thread_ID, NULL, &thread_ForAVServerStart, (void *)sid);
if(ret < 0) {
printf("pthread_create failed ret[%d]\n", ret);
} else {
pthread_detach(Thread_ID);
gOnlineNum++;
}
}
if(need_reboot){
//停止所有数据收发
//等待所有连接退出
//等待其他跟AV/IOTC相关的API退出
avDeInitialize();
IOTC_DeInitialize();
goto BEGIN; //重新执行程序
}
访问处理
创建数据通道:
AVServStartInConfig avStartInConfig; AVServStartOutConfig avStartOutConfig; memset(&avStartInConfig, 0, sizeof(AVServStartInConfig)); avStartInConfig.cb = sizeof(AVServStartInConfig); avStartInConfig.iotc_session_id = SID; //此为IOTC_Listen返回的session id。 avStartInConfig.iotc_channel_id = 0; //数据通道将使用的IOTC通道,默认以0为主通道 avStartInConfig.timeout_sec = 30; //创建通道超时 avStartInConfig.password_auth = ExPasswordAuthCallBackFn; //验证密码的回调 avStartInConfig.server_type = 0; //设备端支持的功能,填0表示不限制,具体可以参考SDK Readme.html的AV Module Service Type Definition avStartInConfig.resend = 1; //是否支持重传模式,1为开启重传,2位关闭重传 avStartInConfig.change_password_request = ExChangePasswordCallBackFn; //修改密码的回调,非必须实现 avStartInConfig.ability_request = ExAbilityRequestFn;//设备支持的功能的回调,非必须实现 #if ENABLE_TOKEN_AUTH //使用token验证的方式,如果APP端选择用此种方式,将进入token验证的回调 avStartInConfig.token_auth = ExTokenAuthCallBackFn;//token验证 avStartInConfig.token_delete = ExTokenDeleteCallBackFn;//删除token avStartInConfig.token_request = ExTokenRequestCallBackFn;//申请token avStartInConfig.identity_array_request = ExGetIdentityArrayCallBackFn;//查询已经存在的id #endif #if ENABLE_DTLS avStartInConfig.security_mode = AV_SECURITY_DTLS; //使用DTLS加密,建议 #else avStartInConfig.security_mode = AV_SECURITY_SIMPLE; //使用默认加密 #endif avStartOutConfig.cb = sizeof(AVServStartOutConfig); int avIndex = avServStartEx(&avStartInConfig, &avStartOutConfig); //此avIndex为数据通道Index,后续的数据参数,将直接在avIndex上进行。 if(avIndex < 0) { printf("avServStartEx failed!! SID[%d] code[%d]!!!\n", SID, avIndex); printf("thread_ForAVServerStart: exit!! SID[%d]\n", SID); IOTC_Session_Close(SID); gOnlineNum--; pthread_exit(0); }
avServStartEx除了可以拿到avIndex,还可以拿到此通道相关的一些其他特性,具体可以参考:
typedef struct AVServStartOutConfig { uint32_t cb; int32_t resend; //avIndex是否支持重传 int32_t two_way_streaming;//avIndex是否全双工 AvAuthType auth_type;//是密码验证还是token验证 char account_or_identity[256];//用户ID,app端在avClientStartEx里面填入 } AVServStartOutConfig;typedef AVServStartOutConfig * LPAVSERV_START_OUT_CONFIG;
全双工的通道,在进行对讲的时候,不需要建立新的avIndex。
接收IO指令:
while(gProcessRun) { ret = avRecvIOCtrl(avIndex, &ioType, (char *)ioCtrlBuf, AV_MAX_IOCTRL_DATA_SIZE, 1000); if(ret >= 0) { //io指令的定义,可以参考SDK Readme.html的AV Module Reference of AV IO Control部分,也可以自定义格式 Handle_IOCTRL_Cmd(SID, avIndex, ioCtrlBuf, ioType);//处理指令,涉及图像,声音,对讲,回放等等 } else if(ret != AV_ER_TIMEOUT) { printf("avIndex[%d], avRecvIOCtrl error, code[%d]\n",avIndex, ret); break; } } //1,停止发送IO数据 //2,关闭音视频发送 unregedit_client_from_video(SID); unregedit_client_from_audio(SID); //3,关闭对讲 //4,关闭回放 //5,关闭通道和连接 printf("SID[%d], avIndex[%d], thread_ForAVServerStart exit!!\n", SID, avIndex); avServStop(avIndex); IOTC_Session_Close(SID); gOnlineNum--;
发送视频:
while(gProcessRun){ char* frame; int size; //从编码缓存区取出图像帧 //轮询所有的avIndex,往需要送图像的avIndex送图像。 foreach(int _avIndex,avIndex_need_video){ int avIndex = _avIndex; int ret = avSendFrameData(avIndex, videoBuf, videoSize, &frameInfo, sizeof(FRAMEINFO_t)); if(ret<0){ if(ret == AV_ER_EXCEED_MAX_SIZE){//缓存区溢出,该帧丢失,需要关键帧,不然会花屏 need_iframe_flag_by_avIndex = 1; } else{ unregedit_client_from_video(SID);//其他异常,不再往此avIndex发送图像。 continue; } } } }
如果需要加上动态码率,可以参考以下规则:
- 如果有多人在同时观看,且大部分是P2P/RLY模式,降低码率(或者分辨率)
- 如果有某个通道重传使用率一直维持在较高水平,则降低码率。
参考代码:
array_int avIndex_video_array_need_keyFrame;//队列或者链表,用以存放需要key frame的avIndex,需自己实现 while(gProcessRun){ char* frame; int size; //从编码缓存区取出图像帧 //轮询所有的avIndex,往需要送图像的avIndex送图像。 foreach(int _avIndex,avIndex_array_need_video){ int avIndex = _avIndex; //检查此avIndex是否需要关键帧 if(frameInfo.flag == IPC_FRAME_FLAG_IFRAME){ if(avIndex_array_need_video_keyFrame.contains(avIndex)){ //关键帧,清楚标志 avIndex_video_array_need_keyFrame.remove(avIndex); } } else{ if(avIndex_video_array_need_keyFrame.contains(avIndex)){ //非关键帧,继续等待,或者强制编码I帧。 continue; } } int ret = avSendFrameData(avIndex, videoBuf, videoSize, &frameInfo, sizeof(FRAMEINFO_t)); if(ret<0){ if(ret == AV_ER_EXCEED_MAX_SIZE){//缓存区溢出,该帧丢失,需要关键帧,不然会花屏 avIndex_video_array_need_keyFrame.append(avIndex); } else{ unregedit_client_from_video(SID);//其他异常,不再往此avIndex发送图像。 continue; } } } }
对讲:
可以参考: 基于AVAPIs的对讲实现
常见的问题以及处理
如何监测传输状况?
//建议另外一个线程定时循环检查。 void thread_avCheckBufferUsageRate(void* arg){ while(programRunning){ //遍历所有正在发送的通道 foreach(int avIndex,all_index_in_send_queue){ float rate = avResendBufUsageRate(avIndex); if(rate <= 0.0){ printf("传输顺畅\n"); } else if(rate > 0.03 && rate < 0.7){ printf("数据在累积中\n"); } else if(rate > 0.7){ printf("数据在累积较多,可能导致缓存区溢出\n"); //考虑清缓存,建议放其他线程。 //考虑降低分辨率。 //考虑降低fps。 } } } sleep(1); }
//清空缓存区
void thread_CleanResendBuffer(void*arg){ int avIndex = *(int*)arg; avServResetBuffer(avIndex,RESET_ALL,5000); }
目前开启重传模式,设备端才会有缓存区,每个avIndex独享各自的缓存区,默认大小为256KB,通常建议值为1~2s的数据量。
如果设备端使用avServSetResendSize修改了设备端的缓存区,APP端建议也做调整,建议值为设备端缓存区大小的3~4倍(默认1MB)。
如何处理弱网传输?
弱网下,常见的处理措施如下:
- 降低码率
- 降低分辨率
- 清缓存区
- 丢P帧
码率如何设计?
SDK提供三种连线模式,分别为:
- LAN:局域网
- P2P:点对点
- RLY:数据转发
因为LAN模式通常网络最干净,不易受波动,所以码率可以传比较大。而RLY模式需要服务器中转,所以消耗服务器的流量,建议降低码率。
在可以动态调整码率的情况下,可以在LAN或者P2P模式传比较高清的图像,RLY传标清或者流畅的图像。
相关头文件说明
头文件 | 说明 |
---|---|
AVFRAMEINFO.h | 帧信息定义 |
AVIOCTRLDEFs.h | IO控制指令定义 |
TUTKGlobalAPIs.h | 全局的设定 |
IOTCClient.h | 客户端连线相关专用接口定义 |
IOTCCommon.h | 通用接口,默认值,错误码定义 |
IOTCDevice.h | 设备端连线相关专用接口定义 |
AVClient.h | 客户端数据发送相关接口定义 |
AVCommon.h | 通用接口,默认值,错误码定义 |
AVServer.h | 设备端数据发送相关接口定义 |
VSaaS.h | 云存储相关接口定义 |
DASA.h | 码流自适应接口定义 |
其他文章: