简体中文

基于AVAPIs的事件回放

回放功能对接指南 | TUTK P2P SDK 开发手册
回放功能对接流程图

图 1:回放功能对接流程图

一、回放功能对接指南

(一)概览
本章节主要讨论影像类产品的标准功能——回放功能的对接引导,阅览本章节的前提是,开发者已经可以使用AVAPIs进行实时观看。
主要涉及到的模块是:IOTCAPIsAVAPIs
主要涉及到的API是:avSendIOCtrl/avRecvIOCtrlavClientStartEx/avServStartExavRecvFrameData2/avSendFrameDataavClientStop/avServStop
(二)核心要点
  1. 1、回放的前提,是要先建立P2P连线和AV通道。
  2. 2、如果是使用AVAPIs做下载功能,一定要开启resend功能。开启的方法就是将avClientStartExavServStartEx参数里面的resend设置为1。
  3. 3、建议设备端要检查avSendFrameData的返回值,如果返回值是-20006(缓存区溢出),需要进行重传此帧;特别是下载功能。
  4. 4、回放结束两端的处理建议:
    • (1)设备端:
      • 送完最后一帧后,需要使用avResendBufUsageRate检查缓存区是否还有数据没送出,只有缓存区清空的情况下,才代表数据已经完全送出,才能关闭通道。
      • 最后一帧,需要把frameInfo里面的tag标记为1。非最后一帧为0。
      • 缓存区清空后,建议延时1s再关闭通道,以免APP端还没收完数据。
    • (2)APP端:
      • APP需要判断frameInfo的tag是否为1,为1时表示已收完最后一帧,可以进行关闭和释放资源的操作。
  5. 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*)&param) != 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(&param->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. 生产环境中,readFrameFromLocalFiledemuxFrame 需根据实际的文件存储格式(如MP4/TS)和编码格式(H.264/H.265/AAC)实现,确保帧数据正确读取和解复用;

2. 缓存区溢出重传机制必须实现(错误码-20006),否则可能导致数据丢失;

3. 最后一帧的tag标记需严格设置为1,且必须等待重传缓冲区清空后再关闭通道,避免APP端遗漏数据。

即刻开启您的物联网之旅

联系解决方案专家
Kalay App
资讯安全白皮书
全球专利布局
解决方案
新闻动态
公司动态
行业资讯
媒体报道
永续发展
经营者的话
社会参与
环境永续
公司治理

+86 755 27702549

7×24小时服务热线

法律声明 隐私权条款

关注“TUTK”

TUTK服务尽在掌握

© 2022 物联智慧科技(深圳)有限公司版权所有粤ICP备14023641号
在线咨询
扫一扫

TUTK服务尽在掌握

全国免费服务热线
+86 755 27702549

返回顶部