
圖 1:回放功能對(duì)接流程圖
一、回放功能對(duì)接指南
(一)概覽
本章節(jié)主要討論影像類(lèi)產(chǎn)品的標(biāo)準(zhǔn)功能——回放功能的對(duì)接引導(dǎo),閱覽本章節(jié)的前提是,開(kāi)發(fā)者已經(jīng)可以使用
AVAPIs進(jìn)行實(shí)時(shí)觀看。主要涉及到的模塊是:
IOTCAPIs、AVAPIs。主要涉及到的API是:
avSendIOCtrl/avRecvIOCtrl、avClientStartEx/avServStartEx、avRecvFrameData2/avSendFrameData、avClientStop/avServStop。(二)核心要點(diǎn)
- 1、回放的前提,是要先建立P2P連線和AV通道。
- 2、如果是使用
AVAPIs做下載功能,一定要開(kāi)啟resend功能。開(kāi)啟的方法就是將avClientStartEx和avServStartEx參數(shù)里面的resend設(shè)置為1。 - 3、建議設(shè)備端要檢查
avSendFrameData的返回值,如果返回值是-20006(緩存區(qū)溢出),需要進(jìn)行重傳此幀;特別是下載功能。 - 4、回放結(jié)束兩端的處理建議:
- (1)設(shè)備端:
- 送完最后一幀后,需要使用
avResendBufUsageRate檢查緩存區(qū)是否還有數(shù)據(jù)沒(méi)送出,只有緩存區(qū)清空的情況下,才代表數(shù)據(jù)已經(jīng)完全送出,才能關(guān)閉通道。 - 最后一幀,需要把frameInfo里面的tag標(biāo)記為1。非最后一幀為0。
- 緩存區(qū)清空后,建議延時(shí)1s再關(guān)閉通道,以免APP端還沒(méi)收完數(shù)據(jù)。
- 送完最后一幀后,需要使用
- (2)APP端:
- APP需要判斷frameInfo的tag是否為1,為1時(shí)表示已收完最后一幀,可以進(jìn)行關(guān)閉和釋放資源的操作。
- (1)設(shè)備端:
- 5、文件下載:文件下載的流程與上述流程基本一樣,唯一的差異是,不像回放的送流方式,在下載的時(shí)候,設(shè)備端可以不按照一幀一幀的方式送,可以每次送固定大小字節(jié)數(shù)的二進(jìn)制流給APP端,APP端只做保存即可。具體流程可以參考:基于AVAPIs的文件下載。
(三)代碼示例
1. APP端實(shí)現(xiàn)
1.1 查詢(xún)事件列表
APP端向設(shè)備發(fā)送事件列表查詢(xún)請(qǐng)求,指定查詢(xún)時(shí)間范圍和事件類(lèi)型,通過(guò)
avSendIOCtrl接口發(fā)送。/**
* 發(fā)送事件列表查詢(xún)請(qǐng)求
* @brief 向設(shè)備查詢(xún)指定時(shí)間范圍內(nèi)的所有事件記錄
* @param avIndex AV通道索引(已建立P2P連接的通道)
* @return 0=發(fā)送成功,<0=發(fā)送失?。ㄥe(cuò)誤碼參考SDK文檔)
*/
int queryEventList(int avIndex) {
SMsgAVIoctrlListEventReq req = {0};
// 設(shè)置查詢(xún)起始時(shí)間(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; // 小時(shí)(0-23)
req.stStartTime.minute = 0; // 分鐘(0-59)
req.stStartTime.second = 0; // 秒(0-59)
// 設(shè)置查詢(xún)結(jié)束時(shí)間(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; // 查詢(xún)所有類(lèi)型事件
int ret = 0;
// 發(fā)送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("查詢(xún)事件列表請(qǐng)求發(fā)送成功\n");
}
return ret;
}
說(shuō)明:
avIndex 為已建立P2P連接的AV通道索引,需確保P2P連接正常后再調(diào)用該接口;時(shí)間參數(shù)需嚴(yán)格按照結(jié)構(gòu)體定義格式填充,避免參數(shù)錯(cuò)誤導(dǎo)致查詢(xún)失敗。1.2 通知設(shè)備啟動(dòng)回放
APP端向設(shè)備發(fā)送回放啟動(dòng)指令,指定回放起始時(shí)間,通過(guò)
IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL命令碼標(biāo)識(shí)操作類(lèi)型。/**
* 發(fā)送回放啟動(dòng)指令
* @brief 通知設(shè)備啟動(dòng)指定時(shí)間點(diǎn)的回放功能
* @param avIndex AV通道索引
* @param startTime 回放起始時(shí)間結(jié)構(gòu)體
* @return 0=發(fā)送成功,<0=發(fā)送失敗
*/
int startPlayback(int avIndex, const STime *startTime) {
if (startTime == NULL) {
printf("無(wú)效參數(shù):起始時(shí)間為空\(chéng)n");
return -1;
}
SMsgAVIoctrlPlayRecord req = {0};
// 復(fù)制起始時(shí)間
req.stStartTime = *startTime;
req.command = AVIOCTRL_RECORD_PLAY_START; // 回放啟動(dòng)命令
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("回放啟動(dòng)指令發(fā)送成功\n");
}
return ret;
}
1.3 接收設(shè)備回放響應(yīng)
APP端通過(guò)
avRecvIOCtrl接收設(shè)備的回放響應(yīng),若響應(yīng)為回放啟動(dòng)成功,則創(chuàng)建回放線程處理后續(xù)音視頻數(shù)據(jù)接收。/**
* 監(jiān)聽(tīng)設(shè)備回放響應(yīng)
* @brief 循環(huán)接收設(shè)備的IO控制響應(yīng),處理回放啟動(dòng)結(jié)果
* @param avIndex AV通道索引
* @return 0=正常退出,<0=異常退出
*/
int listenPlaybackResp(int avIndex) {
int ret = 0;
int cmd = 0;
char buf[1024] = {0}; // 接收緩沖區(qū)
while (1) {
// 接收IO控制響應(yīng)(超時(shí)時(shí)間100ms)
ret = avRecvIOCtrl(avIndex, &cmd, buf, 1024, 100);
if(ret < 0){
if(ret != AV_ER_TIMEOUT){
printf("avRecvIOCtrl error:%d\n", ret);
break;
}
continue; // 超時(shí)繼續(xù)等待
}
// 處理回放控制響應(yīng)
if(IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP == cmd){
SMsgAVIoctrlPlayRecordResp *resp = (SMsgAVIoctrlPlayRecordResp*)buf;
if(resp->command == AVIOCTRL_RECORD_PLAY_START){
if(resp->result > 0){
printf("回放啟動(dòng)成功,通道ID:%d\n", resp->result);
// 創(chuàng)建回放線程,處理音視頻數(shù)據(jù)接收
pthread_t playbackThread;
if (pthread_create(&playbackThread, NULL, (void*)playbackWorker, (void*)&avIndex) != 0) {
printf("創(chuàng)建回放線程失敗\n");
} else {
pthread_detach(playbackThread); // 分離線程,自動(dòng)回收資源
}
} else {
printf("回放啟動(dòng)失敗,錯(cuò)誤碼:%d\n", resp->result);
}
break; // 處理完成退出循環(huán)
}
}
}
return ret;
}
1.4 回放線程(視頻接收)
回放線程通過(guò)
avClientStartEx創(chuàng)建AV通道(開(kāi)啟resend=1),循環(huán)調(diào)用avRecvFrameData2接收視頻幀,判斷frameInfo.tag是否為1(最后一幀),是則退出線程并關(guān)閉通道。// 全局線程控制標(biāo)志(0=運(yùn)行,1=停止)
volatile int g_playback_stop = 0;
/**
* 回放線程函數(shù)
* @brief 創(chuàng)建AV客戶(hù)端通道,接收視頻幀數(shù)據(jù)并處理
* @param arg 傳入?yún)?shù)(AV通道索引)
* @return NULL
*/
void* playbackWorker(void* arg) {
int avIndex = *(int*)arg;
AVClientStartInConfig in;
AVClientStartOutConfig out;
FRAMEINFO frameInfo = {0}; // 幀信息結(jié)構(gòu)體
int ret = 0;
int playback_index = -1;
// 初始化配置結(jié)構(gòu)體
memset(&in, 0, sizeof(in));
memset(&out, 0, sizeof(out));
in.cb = sizeof(in);
in.iotc_session_id = sid; // 會(huì)話ID(從P2P連接中獲?。?
in.iotc_channel_id = playback_channel; // 回放通道ID
in.timeout_sec = 10; // 超時(shí)時(shí)間(秒)
in.account_or_identity = "admin"; // 設(shè)備賬號(hào)
in.password_or_token = "888888"; // 設(shè)備密碼
in.resend = 1; // 開(kāi)啟resend功能,確保下載數(shù)據(jù)完整性
in.security_mode = AV_SECURITY_AUTO; // 自動(dòng)安全模式
in.auth_type = AV_AUTH_PASSWORD; // 密碼認(rèn)證方式
in.sync_recv_data = 0; // 異步接收數(shù)據(jù)
out.cb = sizeof(out);
// 創(chuàng)建AV客戶(hù)端通道
playback_index = avClientStartEx(&in, &out);
printf("avClientStartEx return %d\n", playback_index);
if(playback_index < 0){
printf("創(chuàng)建回放通道失敗\n");
return NULL;
}
// 創(chuàng)建音頻接收線程
pthread_t audioThread;
if (pthread_create(&audioThread, NULL, (void*)audioRecvWorker, (void*)&playback_index) != 0) {
printf("創(chuàng)建音頻接收線程失敗\n");
} else {
pthread_detach(audioThread);
}
// 接收視頻幀數(shù)據(jù)
unsigned char frame_buf[40960] = {0}; // 視頻幀緩沖區(qū)(40KB)
while(!g_playback_stop){
// 接收視頻幀(超時(shí)時(shí)間默認(rèn))
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); // 無(wú)數(shù)據(jù)時(shí)短暫休眠
continue;
}
printf("接收視頻幀失敗,錯(cuò)誤碼:%d\n", ret);
break;
}
else if(ret > 0){
// 解碼播放處理(開(kāi)發(fā)者需實(shí)現(xiàn)解碼邏輯)
// decodeAndRender(frame_buf, ret, &frameInfo);
// 判斷是否為最后一幀
if(frameInfo.tag == 1){
printf("收到最后一幀,準(zhǔn)備退出回放\n");
g_playback_stop = 1;
break;
}
}
}
// 等待音頻線程退出
msleep(100);
// 關(guān)閉AV通道,釋放資源
avClientStop(playback_index);
printf("回放線程退出\n");
return NULL;
}
說(shuō)明:resend參數(shù)必須設(shè)置為1,否則下載功能可能出現(xiàn)數(shù)據(jù)丟失;視頻幀緩沖區(qū)大小需根據(jù)實(shí)際碼率調(diào)整(建議不小于40KB),避免緩沖區(qū)溢出;解碼播放邏輯需根據(jù)設(shè)備端編碼格式(如H.264/H.265)實(shí)現(xiàn)。
1.5 音頻接收線程
獨(dú)立音頻線程通過(guò)
avRecvAudioData接收音頻數(shù)據(jù),解碼播放后檢查是否為最后一幀,確保音視頻同步退出。/**
* 音頻接收線程函數(shù)
* @brief 接收音頻幀數(shù)據(jù)并處理
* @param arg 傳入?yún)?shù)(回放通道索引)
* @return NULL
*/
void* audioRecvWorker(void* arg) {
int playback_index = *(int*)arg;
FRAMEINFO frameInfo = {0};
int ret = 0;
unsigned char audio_buf[10240] = {0}; // 音頻緩沖區(qū)(10KB)
while(!g_playback_stop){
// 接收音頻幀數(shù)據(jù)
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("接收音頻幀失敗,錯(cuò)誤碼:%d\n", ret);
break;
}
else if(ret > 0){
// 音頻解碼播放處理(開(kāi)發(fā)者需實(shí)現(xiàn)解碼邏輯)
// audioDecodeAndPlay(audio_buf, ret, &frameInfo);
// 判斷是否為最后一幀
if(frameInfo.tag == 1){
g_playback_stop = 1;
break;
}
}
}
printf("音頻接收線程退出\n");
return NULL;
}
2. 設(shè)備端實(shí)現(xiàn)
2.1 接收并處理回放指令
設(shè)備端接收APP的回放啟動(dòng)指令,分配通道ID后通過(guò)
avSendIOCtrl返回響應(yīng),通道分配成功則創(chuàng)建回放線程發(fā)送音視頻數(shù)據(jù)。/**
* 處理APP端回放控制指令
* @brief 接收APP的回放啟動(dòng)/停止指令,分配通道并返回響應(yīng)
* @param avIndex AV通道索引
* @return 0=正常處理,<0=處理失敗
*/
int handlePlaybackCtrl(int avIndex) {
int ret = 0;
int cmd = 0;
char buf[1024] = {0}; // 接收緩沖區(qū)
int sid = 0; // 會(huì)話ID(從P2P連接中獲?。?
while (1) {
// 接收IO控制指令(超時(shí)時(shí)間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;
// 檢查是否為回放啟動(dòng)命令
if(req->command == AVIOCTRL_RECORD_PLAY_START){
// 分配回放通道ID(自定義實(shí)現(xiàn))
int channel = get_available_channel(sid);
resp.command = AVIOCTRL_RECORD_PLAY_START; // 響應(yīng)命令碼
if(channel > 0){
resp.result = channel; // 返回分配的通道ID
printf("回放通道分配成功,通道ID:%d\n", channel);
// 發(fā)送響應(yīng)
if((ret = avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP, &resp, sizeof(resp))) >= 0){
// 創(chuàng)建回放線程,發(fā)送音視頻數(shù)據(jù)
pthread_t playbackThread;
PlaybackParam param = {sid, channel, req->stStartTime};
if (pthread_create(&playbackThread, NULL, (void*)devicePlaybackWorker, (void*)¶m) != 0) {
printf("創(chuàng)建回放線程失敗\n");
} else {
pthread_detach(playbackThread);
}
}
}
else{
resp.result = -3; // 通道分配失敗
printf("回放通道分配失敗\n");
// 發(fā)送失敗響應(yīng)
avSendIOCtrl(avIndex, IOTYPE_USER_IPCAM_RECORD_PLAYCONTROL_RESP, &resp, sizeof(resp));
}
break;
}
}
}
return ret;
}
// 回放參數(shù)結(jié)構(gòu)體
typedef struct {
int sid; // 會(huì)話ID
int channel; // 通道ID
STime startTime; // 回放起始時(shí)間
} PlaybackParam;
說(shuō)明:
get_available_channel 為通道獲取接口(示例可參考:IOTC空閑通道的獲取),需確保返回的通道ID唯一且未被占用;線程創(chuàng)建后建議設(shè)置為分離模式,避免資源泄漏;響應(yīng)命令碼需與請(qǐng)求命令碼一致,確保APP端正確識(shí)別。2.2 設(shè)備端回放線程(數(shù)據(jù)發(fā)送)
設(shè)備端回放線程通過(guò)
avServStartEx創(chuàng)建AV服務(wù)端通道(開(kāi)啟resend),讀取本地音視頻文件后通過(guò)avSendFrameData/avSendAudioData發(fā)送數(shù)據(jù);遇到緩存區(qū)溢出時(shí)重傳數(shù)據(jù);發(fā)送完成后檢查緩存區(qū)并延時(shí)1s關(guān)閉通道。// 宏定義配置
#define ENABLE_RESEND 1 // 開(kāi)啟resend功能
#define ENABLE_DTLS 0 // 關(guān)閉DTLS加密
#define MAX_FRAME_SIZE 40960 // 最大幀大?。?0KB)
// 外部函數(shù)聲明
void readFrameFromLocalFile(const STime *startTime, FrameData *frame); // 從本地文件讀取幀數(shù)據(jù)
int get_available_channel(int sid); // 獲取可用通道ID
int ExPasswordAuthCallBackFn(void* param); // 密碼認(rèn)證回調(diào)函數(shù)
// 幀數(shù)據(jù)結(jié)構(gòu)體定義
typedef struct {
bool isVideo; // 是否視頻幀
bool isAudio; // 是否音頻幀
unsigned char* data; // 幀數(shù)據(jù)指針
int dataLen; // 數(shù)據(jù)長(zhǎng)度
} FrameData;
/**
* 設(shè)備端回放線程函數(shù)
* @brief 創(chuàng)建AV服務(wù)端通道,讀取本地音視頻文件并發(fā)送給APP端
* @param arg 傳入?yún)?shù)(PlaybackParam結(jié)構(gòu)體指針)
* @return NULL
*/
void* devicePlaybackWorker(void* arg) {
PlaybackParam *param = (PlaybackParam*)arg;
int ret = -1;
AVServStartInConfig avStartInConfig;
AVServStartOutConfig avStartOutConfig;
FRAMEINFO frameInfo = {0}; // 幀信息結(jié)構(gòu)體
unsigned char buffer[MAX_FRAME_SIZE] = {0}; // 數(shù)據(jù)緩沖區(qū)
bool isLastFrame = false; // 是否最后一幀標(biāo)志
FrameData frame = {0};
frame.data = buffer; // 綁定緩沖區(qū)
// 初始化AV服務(wù)端配置
memset(&avStartInConfig, 0, sizeof(avStartInConfig));
avStartInConfig.cb = sizeof(AVServStartInConfig);
avStartInConfig.iotc_session_id = param->sid; // 會(huì)話ID
avStartInConfig.iotc_channel_id = param->channel; // 通道ID(與響應(yīng)中一致)
avStartInConfig.timeout_sec = 30; // 超時(shí)時(shí)間(秒)
avStartInConfig.password_auth = &ExPasswordAuthCallBackFn; // 密碼認(rèn)證回調(diào)
avStartInConfig.server_type = SERVTYPE_STREAM_SERVER; // 流服務(wù)器類(lèi)型
avStartInConfig.resend = ENABLE_RESEND; // 開(kāi)啟resend功能
// 安全模式配置
#if ENABLE_DTLS
avStartInConfig.security_mode = AV_SECURITY_DTLS; // 啟用DTLS加密
#else
avStartInConfig.security_mode = AV_SECURITY_SIMPLE; // 簡(jiǎn)單安全模式
#endif
avStartOutConfig.cb = sizeof(AVServStartOutConfig);
// 創(chuàng)建AV服務(wù)端通道
int playback_index = avServStartEx(&avStartInConfig, &avStartOutConfig);
if(playback_index < 0){
printf("創(chuàng)建回放服務(wù)端通道失敗,錯(cuò)誤碼:%d\n", playback_index);
return NULL;
}
printf("回放服務(wù)端通道創(chuàng)建成功,索引:%d\n", playback_index);
// 循環(huán)發(fā)送音視頻數(shù)據(jù)
while(!isLastFrame){
// 從本地文件讀取音視頻幀(需開(kāi)發(fā)者實(shí)現(xiàn)具體邏輯)
readFrameFromLocalFile(¶m->startTime, &frame);
// 解復(fù)用圖像和聲音(需開(kāi)發(fā)者實(shí)現(xiàn))
// demuxFrame(&frame);
// 初始化幀信息
memset(&frameInfo, 0, sizeof(frameInfo));
if(isLastFrame){
frameInfo.tag = 1; // 最后一幀標(biāo)記
}
// 發(fā)送幀數(shù)據(jù)
if(frame.isVideo && frame.data != NULL && frame.dataLen > 0){
// 發(fā)送視頻幀:遇-20006(緩存區(qū)溢出)則重傳
do{
ret = avSendFrameData(playback_index, frame.data, frame.dataLen, (const void*)&frameInfo, sizeof(FRAMEINFO));
if(ret == -20006){ // 緩存區(qū)溢出,等待后重傳
msleep(20);
}
} while(ret == -20006);
}
else if(frame.isAudio && frame.data != NULL && frame.dataLen > 0){
// 發(fā)送音頻幀:遇-20006(緩存區(qū)溢出)則重傳
do{
ret = avSendAudioData(playback_index, frame.data, frame.dataLen, (const void*)&frameInfo, sizeof(FRAMEINFO));
if(ret == -20006){
msleep(20);
}
} while(ret == -20006);
}
// 處理發(fā)送結(jié)果
if(ret < 0 && ret != -20006){
printf("發(fā)送幀數(shù)據(jù)失敗,錯(cuò)誤碼:%d\n", ret);
break;
}
// 檢查是否為最后一幀(需根據(jù)實(shí)際文件讀取邏輯判斷)
// isLastFrame = checkIsLastFrame();
}
// 檢查緩存區(qū)是否清空(確保所有數(shù)據(jù)已發(fā)送)
int i_count = 0;
while(i_count++ < 10*300){ // 最多等待30秒
float userate = avResendBufUsageRate(playback_index);
if(userate > 0.0){
msleep(100);
} else {
break;
}
}
// 延時(shí)1s后關(guān)閉通道,確保APP端收完數(shù)據(jù)
msleep(1000);
avServStop(playback_index);
printf("回放服務(wù)端通道關(guān)閉,線程退出\n");
return NULL;
}
特別注意
1. 生產(chǎn)環(huán)境中,readFrameFromLocalFile 和 demuxFrame 需根據(jù)實(shí)際的文件存儲(chǔ)格式(如MP4/TS)和編碼格式(H.264/H.265/AAC)實(shí)現(xiàn),確保幀數(shù)據(jù)正確讀取和解復(fù)用;
2. 緩存區(qū)溢出重傳機(jī)制必須實(shí)現(xiàn)(錯(cuò)誤碼-20006),否則可能導(dǎo)致數(shù)據(jù)丟失;
3. 最后一幀的tag標(biāo)記需嚴(yán)格設(shè)置為1,且必須等待重傳緩沖區(qū)清空后再關(guān)閉通道,避免APP端遺漏數(shù)據(jù)。
