基于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);
// 尝试设备登录(使用带密钥的扩展登录函数)
// 如不使用authkey,可替换为IOTC_Device_Login函数
int ret = IOTC_Device_LoginEx((char*)arg, &auth_option);
printf("IOTC_Device_LoginEx() ret = %d\n", ret);
// 登录成功则退出循环
if (ret == IOTC_ER_NoERROR) {
break;
}
// 登录失败则处理错误并重试
else {
PrintErrHandling(ret);
// 等待5秒后重试(可被程序退出信号中断)
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) {
// 当无用户访问且网络畅通时(网络畅通可通过ping等方式检测)
if (/* 无用户访问条件 */ && /* 网络畅通条件 */) {
printf("device logout from p2p server, will call IOTC_Listen_Exit\n");
#ifdef _RESTART_P2P_MODULE_
// 重启P2P模块:调用后IOTC_Listen会返回IOTC_ER_EXIT_LISTEN
IOTC_Listen_Exit();
break;
#elif defined(_REINIT_SOCKET_)
// 仅重置底层socket,不重新登录
IOTC_ReInitSocket(0);
continue;
#elif defined(_FORCE_LOGIN_)
// 重置socket并强制重新登录(无需重新初始化)
IOTC_ReInitSocket(0);
IOTC_Device_Update_Authkey(device_authkey);
continue;
#endif
}
}
// 5秒后再次检查(可被程序退出信号中断)
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;
}
// 检查登录状态(7表示与P2P服务器通信正常)
if (login_state == 7) {
// 状态正常,重置失败计数器
login_fail_count = 0;
} else {
// 状态异常,递增失败计数器
if (++login_fail_count > 10) { // 超过10次检查异常(约50秒)
// 当无用户访问且网络畅通时(网络畅通可通过ping等方式检测)
if (/* 无用户访问条件 */ && /* 网络畅通条件 */) {
printf("device logout from p2p server, will call IOTC_Listen_Exit\n");
#ifdef _RESTART_P2P_MODULE_
// 重启P2P模块:调用后IOTC_Listen会返回IOTC_ER_EXIT_LISTEN
IOTC_Listen_Exit();
break;
#elif defined(_REINIT_SOCKET_)
// 仅重置底层socket,不重新登录
IOTC_ReInitSocket(0);
continue;
#elif defined(_FORCE_LOGIN_)
// 重置socket并强制重新登录(无需重新初始化)
IOTC_ReInitSocket(0);
IOTC_Device_Update_Authkey(device_authkey);
continue;
#endif
}
}
}
// 5秒后再次检查(可被程序退出信号中断)
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) {
// 监听连接请求(超时时间1000ms)
int SID = IOTC_Listen(1000);
if (SID < 0) {
// 处理监听错误
PrintErrHandling(SID);
// 超过最大会话数
if (SID == IOTC_ER_EXCEED_MAX_SESSION) {
#if TEST_FAST_RECOVERY
// 快速恢复模式:等待5秒检查是否仍达最大在线人数
for (int i = 0; i < 5 && /* 在线人数已经达到最大 */; i++) {
sleep(1);
}
#else
// 普通模式:等待5秒后重试
sleep(5);
#endif
continue;
}
// 监听超时(正常情况,继续监听)
else if (SID == AV_ER_TIMEOUT) {
continue;
}
// 退出监听(需要重启)
else if (SID == IOTC_ER_EXIT_LISTEN) {
need_reboot = 1;
break;
}
// 其他异常(可能需要重启)
else {
// 程序异常处理逻辑
}
}
else {
// 成功获取会话ID,创建线程处理AV服务(avServStart是阻塞接口)
int* sid = (int*)malloc(sizeof(int));
*sid = SID;
pthread_t Thread_ID;
int ret = pthread_create(&Thread_ID, NULL, &thread_ForAVServerStart, (void*)sid);
if (ret < 0) {
printf("pthread_create failed ret[%d]\n", ret);
free(sid); // 创建线程失败时释放内存
} else {
pthread_detach(Thread_ID); // 分离线程,自动释放资源
gOnlineNum++; // 增加在线人数计数
}
}
}
// 处理重启逻辑
if (need_reboot) {
// 停止所有数据收发
// 等待所有连接退出
// 等待其他AV/IOTC相关API退出
// 反初始化AV和IOTC模块
avDeInitialize();
IOTC_DeInitialize();
// 跳转到程序开始处重新执行
goto BEGIN;
}访问处理
创建数据通道:
// 初始化AV服务启动配置结构体
AVServStartInConfig avStartInConfig;
AVServStartOutConfig avStartOutConfig;
// 清空输入配置结构体,避免脏数据
memset(&avStartInConfig, 0, sizeof(AVServStartInConfig));
// -------------------------- 配置输入参数(核心参数)--------------------------
// 结构体大小(固定赋值,用于SDK版本兼容)
avStartInConfig.cb = sizeof(AVServStartInConfig);
// 关联的IOTC会话ID(由IOTC_Listen返回)
avStartInConfig.iotc_session_id = SID;
// 数据通道使用的IOTC通道ID(默认0为主通道)
avStartInConfig.iotc_channel_id = 0;
// 通道创建超时时间(单位:秒)
avStartInConfig.timeout_sec = 30;
// 密码验证回调函数(用于客户端密码校验)
avStartInConfig.password_auth = ExPasswordAuthCallBackFn;
// 设备端功能限制(0表示不限制,具体参考SDK文档的AV Module Service Type Definition)
avStartInConfig.server_type = 0;
// 重传模式配置(1=开启重传,2=关闭重传)
avStartInConfig.resend = 1;
// 非必须回调:修改密码请求(按需实现)
avStartInConfig.change_password_request = ExChangePasswordCallBackFn;
// 非必须回调:设备能力查询(按需实现)
avStartInConfig.ability_request = ExAbilityRequestFn;
// -------------------------- 条件配置:Token认证(按需启用)--------------------------
#if ENABLE_TOKEN_AUTH
// 当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
// 启用DTLS加密(推荐,安全性更高)
avStartInConfig.security_mode = AV_SECURITY_DTLS;
#else
// 使用默认SIMPLE加密(兼容性优先)
avStartInConfig.security_mode = AV_SECURITY_SIMPLE;
#endif
// -------------------------- 配置输出参数 --------------------------
// 结构体大小(固定赋值,用于接收SDK返回数据)
avStartOutConfig.cb = sizeof(AVServStartOutConfig);
// -------------------------- 启动AV服务 --------------------------
// avIndex:返回的AV数据通道索引,后续数据操作基于此索引
int avIndex = avServStartEx(&avStartInConfig, &avStartOutConfig);
// -------------------------- 处理启动结果 --------------------------
if (avIndex < 0) {
printf("avServStartEx failed!! SID[%d] code[%d]!!!\n", SID, avIndex);
printf("thread_ForAVServerStart: exit!! SID[%d]\n", SID);
// 启动失败:关闭对应的IOTC会话,更新在线人数,退出线程
IOTC_Session_Close(SID);
gOnlineNum--;
pthread_exit(0);
}avServStartEx除了可以拿到avIndex,还可以拿到此通道相关的一些其他特性,具体可以参考:
/**
* @brief AV服务启动输出配置结构体
* @details 用于接收 avServStartEx 接口启动AV服务后的返回信息,包含通道能力、认证方式、用户标识等关键结果
*/
typedef struct AVServStartOutConfig {
uint32_t cb; ///< 结构体大小(必须赋值为 sizeof(AVServStartOutConfig)),用于SDK版本兼容性校验
int32_t resend; ///< 当前AV通道是否支持重传模式(1=支持重传,0=不支持重传),与输入配置的 resend 字段对应
int32_t two_way_streaming; ///< 当前AV通道是否支持全双工通信(1=支持全双工,0=不支持),标识通道数据收发能力
AvAuthType auth_type; ///< 实际使用的认证方式,枚举类型(如 AV_AUTH_PASSWORD=密码验证,AV_AUTH_TOKEN=Token验证)
char account_or_identity[256]; ///< 客户端(APP)在 avClientStartEx 中传入的用户ID/身份标识,用于区分当前连接的客户端身份
} AVServStartOutConfig;
typedef AVServStartOutConfig * LPAVSERV_START_OUT_CONFIG; 全双工的通道,在进行对讲的时候,不需要建立新的avIndex。
接收IO指令:
// 循环接收并处理AV通道的IO控制指令
while (gProcessRun) {
// 接收IO控制指令:超时时间1000ms,缓冲区大小AV_MAX_IOCTRL_DATA_SIZE
int ret = avRecvIOCtrl(
avIndex,
&ioType,
(char*)ioCtrlBuf,
AV_MAX_IOCTRL_DATA_SIZE,
1000
);
// 处理接收结果
if (ret >= 0) {
// 成功接收指令:调用处理函数(支持图像、声音、对讲、回放等业务逻辑)
// 注:IO指令定义参考SDK文档「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. 关闭AV通道与IOTC会话,更新在线人数
printf("SID[%d], avIndex[%d], thread_ForAVServerStart exit!!\n", SID, avIndex);
// 关闭AV服务通道
avServStop(avIndex);
// 关闭IOTC会话连接
IOTC_Session_Close(SID);
// 减少在线人数计数
gOnlineNum--;发送视频:
// 循环发送视频帧(程序运行期间持续执行)
while (gProcessRun) {
char* frame; // 视频帧数据指针(当前代码未使用,可根据实际需求补充逻辑)
int size; // 视频帧大小(当前代码未使用,可根据实际需求补充逻辑)
// 1. 从编码缓冲区取出图像帧(需补充实际取帧逻辑,如:frame = get_encoded_video_frame(&size);)
// 2. 轮询所有需要接收视频的AV通道,逐个发送视频帧
foreach (int _avIndex, avIndex_need_video) { // foreach:遍历需视频推送的avIndex列表
int avIndex = _avIndex; // 当前待发送的AV通道索引
// 发送视频帧数据(携带帧信息FRAMEINFO_t)
int ret = avSendFrameData(
avIndex,
videoBuf, // 视频数据缓冲区(需确保数据有效)
videoSize, // 视频数据大小
&frameInfo, // 视频帧信息(如帧类型、分辨率等)
sizeof(FRAMEINFO_t) // 帧信息结构体大小
);
// 处理发送结果
if (ret < 0) {
// 情况1:缓冲区溢出(帧丢失,需标记后续发送关键帧避免花屏)
if (ret == AV_ER_EXCEED_MAX_SIZE) {
need_iframe_flag_by_avIndex = 1; // 标记该avIndex需后续补发I帧
}
// 情况2:其他发送异常(停止向该avIndex推送视频)
else {
unregedit_client_from_video(SID); // 从视频推送列表中注销该客户端
continue; // 跳过当前avIndex,处理下一个
}
}
}
}如果需要加上动态码率,可以参考以下规则:
- 如果有多人在同时观看,且大部分是P2P/RLY模式,降低码率(或者分辨率)
- 如果有某个通道重传使用率一直维持在较高水平,则降低码率。
参考代码:
// 存储需要关键帧(I帧)的AV通道索引集合(需自行实现队列/链表逻辑)
array_int avIndex_video_array_need_keyFrame;
// 循环发送视频帧(程序运行期间持续执行)
while (gProcessRun) {
char* frame; // 视频帧数据指针(需补充实际取帧逻辑)
int size; // 视频帧大小(需补充实际取帧逻辑)
// 1. 从编码缓冲区取出图像帧(需补充:frame = get_encoded_video_frame(&size, &frameInfo);)
// 2. 轮询所有需要接收视频的AV通道,逐个处理发送逻辑
foreach (int _avIndex, avIndex_array_need_video) { // foreach:遍历需视频推送的avIndex列表
int avIndex = _avIndex; // 当前待发送的AV通道索引
// -------------------------- 关键帧(I帧)逻辑判断 --------------------------
// 检查当前帧类型 + 该avIndex是否在「需I帧列表」中
if (frameInfo.flag == IPC_FRAME_FLAG_IFRAME) {
// 情况1:当前帧是I帧
if (avIndex_video_array_need_keyFrame.contains(avIndex)) {
// 已获取I帧,从「需I帧列表」中移除该avIndex,清除标记
avIndex_video_array_need_keyFrame.remove(avIndex);
}
} else {
// 情况2:当前帧是非I帧(P/B帧)
if (avIndex_video_array_need_keyFrame.contains(avIndex)) {
// 该avIndex仍需I帧,跳过当前非I帧,继续等待后续I帧
// (可选优化:此处可调用强制编码I帧的接口,如force_encode_iframe())
continue;
}
}
// -------------------------- 发送视频帧 --------------------------
int ret = avSendFrameData(
avIndex,
videoBuf, // 视频数据缓冲区(需确保与取出的frame数据一致)
videoSize, // 视频数据大小(需与实际帧大小匹配)
&frameInfo, // 视频帧信息(包含帧类型、分辨率等关键参数)
sizeof(FRAMEINFO_t) // 帧信息结构体大小(固定值)
);
// -------------------------- 发送结果处理 --------------------------
if (ret < 0) {
// 子情况1:缓冲区溢出(帧丢失,需标记该avIndex后续补I帧,避免花屏)
if (ret == AV_ER_EXCEED_MAX_SIZE) {
avIndex_video_array_need_keyFrame.append(avIndex);
}
// 子情况2:其他发送异常(停止向该avIndex推送视频,清理关联资源)
else {
unregedit_client_from_video(SID); // 从视频推送列表注销该客户端
continue; // 跳过当前avIndex,处理下一个通道
}
}
}
}对讲:
可以参考: 基于AVAPIs的对讲实现
常见的问题以及处理
如何监测传输状况?
/**
* @brief AV通道缓冲区使用率检查线程
* @details 独立线程定时遍历所有发送中通道,监控重传缓冲区使用率,
* 根据使用率触发不同级别的提示或优化操作,避免缓冲区溢出
* @param arg 线程入参(当前未使用,可根据需求扩展)
*/
void thread_avCheckBufferUsageRate(void* arg) {
// 线程主循环(程序运行期间持续执行)
while (programRunning) {
// 遍历所有正在发送数据的AV通道(all_index_in_send_queue:发送队列中的avIndex集合)
foreach (int avIndex, all_index_in_send_queue) {
// 获取当前通道的重传缓冲区使用率(返回值为0.0~1.0的浮点数,1.0表示缓冲区满)
float rate = avResendBufUsageRate(avIndex);
// 根据使用率分级处理
if (rate <= 0.0f) {
// 级别1:使用率≤0%,传输状态顺畅,无数据堆积
printf("[AV Buffer Check] avIndex[%d] - 传输顺畅 (usage rate: %.2f%%)\n",
avIndex, rate * 100);
}
else if (rate > 0.03f && rate < 0.7f) {
// 级别2:使用率3%~70%,数据开始累积(需关注但暂无需干预)
printf("[AV Buffer Check] avIndex[%d] - 数据在累积中 (usage rate: %.2f%%)\n",
avIndex, rate * 100);
}
else if (rate >= 0.7f) { // 修正:包含等于70%的情况,避免边界遗漏
// 级别3:使用率≥70%,数据累积较多,缓冲区溢出风险高
printf("[AV Buffer Check] avIndex[%d] - 数据累积较多,可能导致缓冲区溢出 (usage rate: %.2f%%)\n",
avIndex, rate * 100);
// 建议优化操作(需在其他线程执行,避免阻塞当前监控线程)
// 1. 触发缓冲区清理(如调用avClearResendBuf(avIndex),需确认SDK是否支持)
// 2. 降低视频分辨率(如从1080P调整为720P,减少单帧数据量)
// 3. 降低视频帧率(如从30fps调整为15fps,减少单位时间内数据量)
}
}
// 每1秒检查一次(平衡监控精度与CPU占用)
sleep(1);
}
// 线程退出提示(可选,便于日志排查)
printf("[AV Buffer Check] Thread exited normally\n");
}//清空缓存区
/**
* @brief 清理AV通道重传缓冲区的线程函数
* @details 独立线程用于执行缓冲区重置操作,避免在监控线程中直接调用导致阻塞
* @param arg 传入需要清理缓冲区的avIndex(需通过指针传递int类型)
*/
void thread_CleanResendBuffer(void* arg) {
// 解析入参:获取需要清理缓冲区的AV通道索引
int avIndex = *(int*)arg;
// 释放入参内存(假设调用方通过malloc分配,此处需对应释放避免泄漏)
free(arg);
// 重置AV通道缓冲区:
// - RESET_ALL:重置所有类型的缓冲区(需确认枚举定义)
// - 5000:超时时间(单位:毫秒),避免无限阻塞
avServResetBuffer(avIndex, RESET_ALL, 5000);
// 操作完成提示(可选,便于调试)
printf("[Buffer Clean] avIndex[%d] buffer reset completed\n", avIndex);
// 线程退出
pthread_exit(NULL);
}目前开启重传模式,设备端才会有缓存区,每个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 | 码流自适应接口定义 |
其他文章:
