
图 1:回放功能对接流程图
一、回放功能对接指南
(一)概览
本章节主要讨论影像类产品的标准功能——回放功能的对接引导,阅览本章节的前提是,开发者已经可以使用
AVAPIs进行实时观看。主要涉及到的模块是:
IOTCAPIs、AVAPIs。主要涉及到的API是:
avSendIOCtrl/avRecvIOCtrl、avClientStartEx/avServStartEx、avRecvFrameData2/avSendFrameData、avClientStop/avServStop。(二)核心要点
- 1、回放的前提,是要先建立P2P连线和AV通道。
- 2、如果是使用
AVAPIs做下载功能,一定要开启resend功能。开启的方法就是将avClientStartEx和avServStartEx参数里面的resend设置为1。 - 3、建议设备端要检查
avSendFrameData的返回值,如果返回值是-20006(缓存区溢出),需要进行重传此帧;特别是下载功能。 - 4、回放结束两端的处理建议:
- (1)设备端:
- 送完最后一帧后,需要使用
avResendBufUsageRate检查缓存区是否还有数据没送出,只有缓存区清空的情况下,才代表数据已经完全送出,才能关闭通道。 - 最后一帧,需要把frameInfo里面的tag标记为1。非最后一帧为0。
- 缓存区清空后,建议延时1s再关闭通道,以免APP端还没收完数据。
- 送完最后一帧后,需要使用
- (2)APP端:
- APP需要判断frameInfo的tag是否为1,为1时表示已收完最后一帧,可以进行关闭和释放资源的操作。
- (1)设备端:
- 5、文件下载:文件下载的流程与上述流程基本一样,唯一的差异是,不像回放的送流方式,在下载的时候,设备端可以不按照一帧一帧的方式送,可以每次送固定大小字节数的二进制流给APP端,APP端只做保存即可。具体流程可以参考:基于AVAPIs的文件下载。
(三)代码示例
1. APP端实现
1.1 查询事件列表
APP端向设备发送事件列表查询请求,指定查询时间范围和事件类型,通过
avSendIOCtrl接口发送。/**
* 发送事件列表查询请求
* @brief 向设备查询指定时间范围内的所有事件记录
* @param avIndex AV通道索引(已建立P2P连接的通道)
* @return 0=发送成功,<0=发送失败(错误码参考SDK文档)
*/
int queryEventList(int avIndex) {
SMsgAVIoctrlListEventReq req = {0};
// 设置查询起始时间(1970-01-01 00:00:00)
req.stStartTime.year = 1970; // 年份
req.stStartTime.month = 1; // 月份(1-12)
req.stStartTime.day = 1; // 日期(1-31)
req.stStartTime.wday = 1; // 星期(0=周日,1=周一...6=周六)
req.stStartTime.hour = 0; // 小时(0-23)
req.stStartTime.minute = 0; // 分钟(0-59)
req.stStartTime.second = 0; // 秒(0-59)
// 设置查询结束时间(1970-01-02 00:00:00)
req.stEndTime.year = 1970;
req.stEndTime.month = 1;
req.stEndTime.day = 2;
req.stEndTime.wday = 1;
req.stEndTime.hour = 0;
req.stEndTime.minute = 0;
req.stEndTime.second = 0;
req.event = AVIOCTRL_EVENT_ALL; // 查询所有类型事件
int ret = 0;
// 发送IO控制指令
if((ret = avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_LISTEVENT_REQ, &req, sizeof(req))) < 0){
printf("send IO Ctrl failed, error=%d\n", ret);
} else {
printf("查询事件列表请求发送成功\n");
}
return ret;
}
说明:
avIndex 为已建立P2P连接的AV通道索引,需确保P2P连接正常后再调用该接口;时间参数需严格按照结构体定义格式填充,避免参数错误导致查询失败。1.2 通知设备启动回放
APP端向设备发送回放启动指令,指定回放起始时间,通过
IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL命令码标识操作类型。/**
* 发送回放启动指令
* @brief 通知设备启动指定时间点的回放功能
* @param avIndex AV通道索引
* @param startTime 回放起始时间结构体
* @return 0=发送成功,<0=发送失败
*/
int startPlayback(int avIndex, const STime *startTime) {
if (startTime == NULL) {
printf("无效参数:起始时间为空\n");
return -1;
}
SMsgAVIoctrlPlayRecord req = {0};
// 复制起始时间
req.stStartTime = *startTime;
req.command = AVIOCTRL_RECORD_PLAY_START; // 回放启动命令
int ret = 0;
if((ret = avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL, &req, sizeof(req))) < 0){
printf("send IO Ctrl failed, error=%d\n", ret);
} else {
printf("回放启动指令发送成功\n");
}
return ret;
}
1.3 接收设备回放响应
APP端通过
avRecvIOCtrl接收设备的回放响应,若响应为回放启动成功,则创建回放线程处理后续音视频数据接收。/**
* 监听设备回放响应
* @brief 循环接收设备的IO控制响应,处理回放启动结果
* @param avIndex AV通道索引
* @return 0=正常退出,<0=异常退出
*/
int listenPlaybackResp(int avIndex) {
int ret = 0;
int cmd = 0;
char buf[1024] = {0}; // 接收缓冲区
while (1) {
// 接收IO控制响应(超时时间100ms)
ret = avRecvIOCtrl(avIndex, &cmd, buf, 1024, 100);
if(ret < 0){
if(ret != AV_ER_TIMEOUT){
printf("avRecvIOCtrl error:%d\n", ret);
break;
}
continue; // 超时继续等待
}
// 处理回放控制响应
if(IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP == cmd){
SMsgAVIoctrlPlayRecordResp *resp = (SMsgAVIoctrlPlayRecordResp*)buf;
if(resp->command == AVIOCTRL_RECORD_PLAY_START){
if(resp->result > 0){
printf("回放启动成功,通道ID:%d\n", resp->result);
// 创建回放线程,处理音视频数据接收
pthread_t playbackThread;
if (pthread_create(&playbackThread, NULL, (void*)playbackWorker, (void*)&avIndex) != 0) {
printf("创建回放线程失败\n");
} else {
pthread_detach(playbackThread); // 分离线程,自动回收资源
}
} else {
printf("回放启动失败,错误码:%d\n", resp->result);
}
break; // 处理完成退出循环
}
}
}
return ret;
}
1.4 回放线程(视频接收)
回放线程通过
avClientStartEx创建AV通道(开启resend=1),循环调用avRecvFrameData2接收视频帧,判断frameInfo.tag是否为1(最后一帧),是则退出线程并关闭通道。// 全局线程控制标志(0=运行,1=停止)
volatile int g_playback_stop = 0;
/**
* 回放线程函数
* @brief 创建AV客户端通道,接收视频帧数据并处理
* @param arg 传入参数(AV通道索引)
* @return NULL
*/
void* playbackWorker(void* arg) {
int avIndex = *(int*)arg;
AVClientStartInConfig in;
AVClientStartOutConfig out;
FRAMEINFO frameInfo = {0}; // 帧信息结构体
int ret = 0;
int playback_index = -1;
// 初始化配置结构体
memset(&in, 0, sizeof(in));
memset(&out, 0, sizeof(out));
in.cb = sizeof(in);
in.iotc_session_id = sid; // 会话ID(从P2P连接中获取)
in.iotc_channel_id = playback_channel; // 回放通道ID
in.timeout_sec = 10; // 超时时间(秒)
in.account_or_identity = "admin"; // 设备账号
in.password_or_token = "888888"; // 设备密码
in.resend = 1; // 开启resend功能,确保下载数据完整性
in.security_mode = AV_SECURITY_AUTO; // 自动安全模式
in.auth_type = AV_AUTH_PASSWORD; // 密码认证方式
in.sync_recv_data = 0; // 异步接收数据
out.cb = sizeof(out);
// 创建AV客户端通道
playback_index = avClientStartEx(&in, &out);
printf("avClientStartEx return %d\n", playback_index);
if(playback_index < 0){
printf("创建回放通道失败\n");
return NULL;
}
// 创建音频接收线程
pthread_t audioThread;
if (pthread_create(&audioThread, NULL, (void*)audioRecvWorker, (void*)&playback_index) != 0) {
printf("创建音频接收线程失败\n");
} else {
pthread_detach(audioThread);
}
// 接收视频帧数据
unsigned char frame_buf[40960] = {0}; // 视频帧缓冲区(40KB)
while(!g_playback_stop){
// 接收视频帧(超时时间默认)
ret = avRecvFrameData2(playback_index, frame_buf, sizeof(frame_buf), NULL, NULL, (char*)&frameInfo, sizeof(FRAMEINFO), NULL, NULL);
if(ret < 0){
if(ret == AV_ERR_DATA_NOREADY){
msleep(5); // 无数据时短暂休眠
continue;
}
printf("接收视频帧失败,错误码:%d\n", ret);
break;
}
else if(ret > 0){
// 解码播放处理(开发者需实现解码逻辑)
// decodeAndRender(frame_buf, ret, &frameInfo);
// 判断是否为最后一帧
if(frameInfo.tag == 1){
printf("收到最后一帧,准备退出回放\n");
g_playback_stop = 1;
break;
}
}
}
// 等待音频线程退出
msleep(100);
// 关闭AV通道,释放资源
avClientStop(playback_index);
printf("回放线程退出\n");
return NULL;
}
说明:resend参数必须设置为1,否则下载功能可能出现数据丢失;视频帧缓冲区大小需根据实际码率调整(建议不小于40KB),避免缓冲区溢出;解码播放逻辑需根据设备端编码格式(如H.264/H.265)实现。
1.5 音频接收线程
独立音频线程通过
avRecvAudioData接收音频数据,解码播放后检查是否为最后一帧,确保音视频同步退出。/**
* 音频接收线程函数
* @brief 接收音频帧数据并处理
* @param arg 传入参数(回放通道索引)
* @return NULL
*/
void* audioRecvWorker(void* arg) {
int playback_index = *(int*)arg;
FRAMEINFO frameInfo = {0};
int ret = 0;
unsigned char audio_buf[10240] = {0}; // 音频缓冲区(10KB)
while(!g_playback_stop){
// 接收音频帧数据
ret = avRecvAudioData(playback_index, audio_buf, sizeof(audio_buf), NULL, NULL, &frameInfo, sizeof(FRAMEINFO), NULL);
if(ret < 0){
if(ret == AV_ERR_DATA_NOREADY){
msleep(5);
continue;
}
printf("接收音频帧失败,错误码:%d\n", ret);
break;
}
else if(ret > 0){
// 音频解码播放处理(开发者需实现解码逻辑)
// audioDecodeAndPlay(audio_buf, ret, &frameInfo);
// 判断是否为最后一帧
if(frameInfo.tag == 1){
g_playback_stop = 1;
break;
}
}
}
printf("音频接收线程退出\n");
return NULL;
}
2. 设备端实现
2.1 接收并处理回放指令
设备端接收APP的回放启动指令,分配通道ID后通过
avSendIOCtrl返回响应,通道分配成功则创建回放线程发送音视频数据。/**
* 处理APP端回放控制指令
* @brief 接收APP的回放启动/停止指令,分配通道并返回响应
* @param avIndex AV通道索引
* @return 0=正常处理,<0=处理失败
*/
int handlePlaybackCtrl(int avIndex) {
int ret = 0;
int cmd = 0;
char buf[1024] = {0}; // 接收缓冲区
int sid = 0; // 会话ID(从P2P连接中获取)
while (1) {
// 接收IO控制指令(超时时间100ms)
ret = avRecvIOCtrl(avIndex, &cmd, buf, 1024, 100);
if(ret < 0){
if(ret != AV_ER_TIMEOUT){
printf("avRecvIOCtrl error:%d\n", ret);
break;
}
continue;
}
// 处理回放控制指令
if(IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL == cmd){
SMsgAVIoctrlPlayRecordResp resp = {0};
SMsgAVIoctrlPlayRecord *req = (SMsgAVIoctrlPlayRecord*)buf;
// 检查是否为回放启动命令
if(req->command == AVIOCTRL_RECORD_PLAY_START){
// 分配回放通道ID(自定义实现)
int channel = get_available_channel(sid);
resp.command = AVIOCTRL_RECORD_PLAY_START; // 响应命令码
if(channel > 0){
resp.result = channel; // 返回分配的通道ID
printf("回放通道分配成功,通道ID:%d\n", channel);
// 发送响应
if((ret = avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP, &resp, sizeof(resp))) >= 0){
// 创建回放线程,发送音视频数据
pthread_t playbackThread;
PlaybackParam param = {sid, channel, req->stStartTime};
if (pthread_create(&playbackThread, NULL, (void*)devicePlaybackWorker, (void*)¶m) != 0) {
printf("创建回放线程失败\n");
} else {
pthread_detach(playbackThread);
}
}
}
else{
resp.result = -3; // 通道分配失败
printf("回放通道分配失败\n");
// 发送失败响应
avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP, &resp, sizeof(resp));
}
break;
}
}
}
return ret;
}
// 回放参数结构体
typedef struct {
int sid; // 会话ID
int channel; // 通道ID
STime startTime; // 回放起始时间
} PlaybackParam;
说明:
get_available_channel 为通道获取接口(示例可参考:IOTC空闲通道的获取),需确保返回的通道ID唯一且未被占用;线程创建后建议设置为分离模式,避免资源泄漏;响应命令码需与请求命令码一致,确保APP端正确识别。2.2 设备端回放线程(数据发送)
设备端回放线程通过
avServStartEx创建AV服务端通道(开启resend),读取本地音视频文件后通过avSendFrameData/avSendAudioData发送数据;遇到缓存区溢出时重传数据;发送完成后检查缓存区并延时1s关闭通道。// 宏定义配置
#define ENABLE_RESEND 1 // 开启resend功能
#define ENABLE_DTLS 0 // 关闭DTLS加密
#define MAX_FRAME_SIZE 40960 // 最大帧大小(40KB)
// 外部函数声明
void readFrameFromLocalFile(const STime *startTime, FrameData *frame); // 从本地文件读取帧数据
int get_available_channel(int sid); // 获取可用通道ID
int ExPasswordAuthCallBackFn(void* param); // 密码认证回调函数
// 帧数据结构体定义
typedef struct {
bool isVideo; // 是否视频帧
bool isAudio; // 是否音频帧
unsigned char* data; // 帧数据指针
int dataLen; // 数据长度
} FrameData;
/**
* 设备端回放线程函数
* @brief 创建AV服务端通道,读取本地音视频文件并发送给APP端
* @param arg 传入参数(PlaybackParam结构体指针)
* @return NULL
*/
void* devicePlaybackWorker(void* arg) {
PlaybackParam *param = (PlaybackParam*)arg;
int ret = -1;
AVServStartInConfig avStartInConfig;
AVServStartOutConfig avStartOutConfig;
FRAMEINFO frameInfo = {0}; // 帧信息结构体
unsigned char buffer[MAX_FRAME_SIZE] = {0}; // 数据缓冲区
bool isLastFrame = false; // 是否最后一帧标志
FrameData frame = {0};
frame.data = buffer; // 绑定缓冲区
// 初始化AV服务端配置
memset(&avStartInConfig, 0, sizeof(avStartInConfig));
avStartInConfig.cb = sizeof(AVServStartInConfig);
avStartInConfig.iotc_session_id = param->sid; // 会话ID
avStartInConfig.iotc_channel_id = param->channel; // 通道ID(与响应中一致)
avStartInConfig.timeout_sec = 30; // 超时时间(秒)
avStartInConfig.password_auth = &ExPasswordAuthCallBackFn; // 密码认证回调
avStartInConfig.server_type = SERVTYPE_STREAM_SERVER; // 流服务器类型
avStartInConfig.resend = ENABLE_RESEND; // 开启resend功能
// 安全模式配置
#if ENABLE_DTLS
avStartInConfig.security_mode = AV_SECURITY_DTLS; // 启用DTLS加密
#else
avStartInConfig.security_mode = AV_SECURITY_SIMPLE; // 简单安全模式
#endif
avStartOutConfig.cb = sizeof(AVServStartOutConfig);
// 创建AV服务端通道
int playback_index = avServStartEx(&avStartInConfig, &avStartOutConfig);
if(playback_index < 0){
printf("创建回放服务端通道失败,错误码:%d\n", playback_index);
return NULL;
}
printf("回放服务端通道创建成功,索引:%d\n", playback_index);
// 循环发送音视频数据
while(!isLastFrame){
// 从本地文件读取音视频帧(需开发者实现具体逻辑)
readFrameFromLocalFile(¶m->startTime, &frame);
// 解复用图像和声音(需开发者实现)
// demuxFrame(&frame);
// 初始化帧信息
memset(&frameInfo, 0, sizeof(frameInfo));
if(isLastFrame){
frameInfo.tag = 1; // 最后一帧标记
}
// 发送帧数据
if(frame.isVideo && frame.data != NULL && frame.dataLen > 0){
// 发送视频帧:遇-20006(缓存区溢出)则重传
do{
ret = avSendFrameData(playback_index, frame.data, frame.dataLen, (const void*)&frameInfo, sizeof(FRAMEINFO));
if(ret == -20006){ // 缓存区溢出,等待后重传
msleep(20);
}
} while(ret == -20006);
}
else if(frame.isAudio && frame.data != NULL && frame.dataLen > 0){
// 发送音频帧:遇-20006(缓存区溢出)则重传
do{
ret = avSendAudioData(playback_index, frame.data, frame.dataLen, (const void*)&frameInfo, sizeof(FRAMEINFO));
if(ret == -20006){
msleep(20);
}
} while(ret == -20006);
}
// 处理发送结果
if(ret < 0 && ret != -20006){
printf("发送帧数据失败,错误码:%d\n", ret);
break;
}
// 检查是否为最后一帧(需根据实际文件读取逻辑判断)
// isLastFrame = checkIsLastFrame();
}
// 检查缓存区是否清空(确保所有数据已发送)
int i_count = 0;
while(i_count++ < 10*300){ // 最多等待30秒
float userate = avResendBufUsageRate(playback_index);
if(userate > 0.0){
msleep(100);
} else {
break;
}
}
// 延时1s后关闭通道,确保APP端收完数据
msleep(1000);
avServStop(playback_index);
printf("回放服务端通道关闭,线程退出\n");
return NULL;
}
特别注意
1. 生产环境中,readFrameFromLocalFile 和 demuxFrame 需根据实际的文件存储格式(如MP4/TS)和编码格式(H.264/H.265/AAC)实现,确保帧数据正确读取和解复用;
2. 缓存区溢出重传机制必须实现(错误码-20006),否则可能导致数据丢失;
3. 最后一帧的tag标记需严格设置为1,且必须等待重传缓冲区清空后再关闭通道,避免APP端遗漏数据。
