
圖 1:基于AVAPIs的文件下載整體流程示意圖
一、IO交互流程
文件下載的指令(相關(guān)內(nèi)容:
AV幀信息通用定義)交互通過(guò)
avSendIOCtrl/
avRecvIOCtrl 接口完成,分為文件列表查詢(xún)、下載請(qǐng)求、響應(yīng)三個(gè)關(guān)鍵階段,最終觸發(fā)通道創(chuàng)建和數(shù)據(jù)傳輸。
階段1:文件列表查詢(xún)
? APP 調(diào)用 avSendIOCtrl(IOCTRL_FILEMANAGER_FILE_LIST_REQ),傳入查詢(xún)條件(時(shí)間范圍、文件類(lèi)型等,對(duì)應(yīng) stFileListReq 結(jié)構(gòu)體);
? 設(shè)備端通過(guò) avRecvIOCtrl 接收請(qǐng)求,查詢(xún)符合條件的文件列表,通過(guò) avSendIOCtrl(IOCTRL_FILEMANAGER_FILE_LIST_RESP) 回復(fù)(對(duì)應(yīng) stFileListResp 結(jié)構(gòu)體)。
階段2:下載請(qǐng)求發(fā)起
? 用戶(hù)在APP端選中待下載文件,APP 調(diào)用 avSendIOCtrl(IOCTRL_FILEMANAGER_FILE_DOWNLOAD_REQ),傳入待下載文件列表(對(duì)應(yīng) stFileDownloadReq 結(jié)構(gòu)體)。
階段3:下載響應(yīng)確認(rèn)
? 設(shè)備端接收下載請(qǐng)求后,根據(jù)硬件性能(如CPU、帶寬)和軟件資源,自定義分配傳輸通道數(shù);
? 設(shè)備端通過(guò) avSendIOCtrl(IOCTRL_FILEMANAGER_FILE_DOWNLOAD_RESP) 回復(fù)APP,包含分配的IOTC通道ID 和傳輸協(xié)議類(lèi)型(此處為AVAPIs),對(duì)應(yīng) stFileDownloadResp 結(jié)構(gòu)體。
階段4:通道創(chuàng)建與數(shù)據(jù)傳輸
? APP 和設(shè)備端根據(jù)響應(yīng)中的 IOTC通道ID,分別創(chuàng)建對(duì)應(yīng)的AV通道;
? 通道創(chuàng)建成功后,設(shè)備端開(kāi)始發(fā)送文件數(shù)據(jù),APP端接收并存儲(chǔ)數(shù)據(jù)。
階段5:通道銷(xiāo)毀
? 所有文件下載完成(檢測(cè)到 endFlag=1),APP 和設(shè)備端分別銷(xiāo)毀AV通道,釋放資源。
二、AV通道的創(chuàng)建和銷(xiāo)毀
通道創(chuàng)建需基于已建立的P2P會(huì)話(huà)(SID)和分配的IOTC通道ID,APP端與設(shè)備端的創(chuàng)建邏輯對(duì)稱(chēng),且必須開(kāi)啟 resend=1(重傳模式)確保數(shù)據(jù)可靠傳輸。
(一)APP端(客戶(hù)端)通道操作
APP端作為文件接收方,通過(guò) avClientStartEx 創(chuàng)建AV客戶(hù)端通道,avClientStop 銷(xiāo)毀通道。
int createDataChannelOfClientForDownload(int sid, int iotc_channel_id)
{
AVClientStartInConfig in;
AVClientStartOutConfig out;
memset(&in,0, sizeof(in));
memset(&out,0, sizeof(out));
in.cb = sizeof(in);
in.iotc_session_id = sid; // P2P會(huì)話(huà)ID(已建立的會(huì)話(huà))
in.iotc_channel_id = iotc_channel_id; // 設(shè)備分配的IOTC通道ID
in.timeout_sec = 30; // 連接超時(shí)時(shí)間(30秒)
in.resend = 1; //此處務(wù)必要開(kāi)啟resend模式(確保數(shù)據(jù)可靠傳輸)
in.security_mode = AV_SECURITY_AUTO;
in.auth_type = AV_AUTH_PASSWORD;
in.account_or_identity = "camera account"; // 設(shè)備賬號(hào),需跟設(shè)備端一致
in.password_or_token = "camera password"; // 設(shè)備密碼,需跟設(shè)備端一致
in.sync_recv_data = 0; // 0=異步接收數(shù)據(jù)(推薦)
in.dtls_cipher_suites = nullptr; // DTLS加密套件(默認(rèn)NULL)
out.cb = sizeof(out);
return avClientStartEx(&in, &out); // 創(chuàng)建通道,返回AV通道索引(avIndex)
}
說(shuō)明:resend必須設(shè)為1,否則丟包時(shí)無(wú)法重傳導(dǎo)致文件損壞;SDK版本需區(qū)分認(rèn)證類(lèi)型,避免認(rèn)證失敗。
void destoryDataChannelOfClient(int avIndex)
{
if(avIndex < 0)
return ;
avClientStop(avIndex); // 銷(xiāo)毀AV通道,釋放資源
}
(二)設(shè)備端(服務(wù)端)通道操作
設(shè)備端作為文件發(fā)送方,通過(guò) avServStartEx 創(chuàng)建AV服務(wù)端通道,avServStop 銷(xiāo)毀通道。
int createDataChannelOfDeviceForDownload(int sid, int iotc_channel_id)
{
AVServStartInConfig avStartInConfig;
AVServStartOutConfig avStartOutConfig;
clearMemory(&avStartInConfig, sizeof(AVServStartInConfig));
clearMemory(&avStartOutConfig, sizeof(avStartOutConfig));
avStartInConfig.cb = sizeof(AVServStartInConfig);
avStartInConfig.iotc_session_id = sid; // P2P會(huì)話(huà)ID
avStartInConfig.iotc_channel_id = iotc_channel_id; // 分配的IOTC通道ID
avStartInConfig.timeout_sec = 30; // 超時(shí)時(shí)間(30秒)
avStartInConfig.password_auth = ExPasswordAuthCallBackFn; // 密碼認(rèn)證回調(diào)(v3版本用)
avStartInConfig.server_type = 0; // 服務(wù)端類(lèi)型(默認(rèn)0)
avStartInConfig.resend = 1; // 必須開(kāi)啟重傳模式
avStartInConfig.security_mode = AV_SECURITY_DTLS; // 啟用DTLS加密(安全傳輸)
//avStartInConfig.json_request = ExJsonRequest; // JSON請(qǐng)求回調(diào)(可選)
avStartOutConfig.cb = sizeof(AVServStartOutConfig);
return avServStartEx(&avStartInConfig, &avStartOutConfig); // 創(chuàng)建通道,返回AV通道索引
}
說(shuō)明:security_mode設(shè)為AV_SECURITY_DTLS啟用加密傳輸,保護(hù)文件數(shù)據(jù)安全;password_auth回調(diào)用于v3版本的密碼校驗(yàn)。
void destoryDataChannelOfDevice(int avIndex)
{
if(avIndex < 0)
return ;
avServStop(avIndex); // 銷(xiāo)毀AV通道,釋放資源
}
三、數(shù)據(jù)傳輸流程
數(shù)據(jù)傳輸依賴(lài)
avSendFrameData(設(shè)備端發(fā)送)和
avRecvFrameData2(APP端接收)接口,文件信息通過(guò)
FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t 結(jié)構(gòu)體攜帶(相關(guān)內(nèi)容:
AV幀信息通用定義),傳輸單元為“文件信息+二進(jìn)制數(shù)據(jù)”。
(一)設(shè)備端(發(fā)送方)數(shù)據(jù)發(fā)送
設(shè)備端按文件分幀讀取二進(jìn)制數(shù)據(jù),搭配文件信息幀頭發(fā)送,支持多文件連續(xù)傳輸,處理丟包重傳(錯(cuò)誤碼-20006)。
#define MAX_BUFFER_SIZE 10243 // 單次發(fā)送最大緩沖區(qū)大?。筛鶕?jù)實(shí)際調(diào)整)
// 從文件讀取二進(jìn)制數(shù)據(jù)(工具函數(shù))
int readBinaryDataFromFile(file f, char* buffer, size_t bufferSize)
{
return f.read(buffer, bufferSize);
}
// 發(fā)送文件列表(核心函數(shù))
void sendFileList(void* arg)
{
int avIndex = createDataChannelOfDeviceForDownload(sid, iotc_channel_id);
if(avIndex < 0){
printf("createDataChannelOfDeviceForDownload failed, error code:%d\n", avIndex);
return ;
}
foreach(auto f, files){ // 遍歷待發(fā)送文件列表
// 打開(kāi)文件
f.open();
// 初始化文件信息幀頭
FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t frmInfo = {0};
strcpy(frmInfo.fileName, f.name().c_str()); // 文件名(含擴(kuò)展名)
frmInfo.fileSize = f.size(); // 文件總大?。ㄗ止?jié))
while(1){
char buffer[MAX_BUFFER_SIZE] = {0};
// 讀取文件數(shù)據(jù)
int readSize = readBinaryDataFromFile(f, buffer, MAX_BUFFER_SIZE);
if(readSize < MAX_BUFFER_SIZE){
if(readSize <= 0)
break; // 讀取完畢,退出循環(huán)
// 若為文件列表中最后一個(gè)文件,設(shè)置結(jié)束標(biāo)志
if(f.isLastFileOfList()){
frmInfo.endFlag = 1;
}
}
frmInfo.frmSize = readSize; // 當(dāng)前幀數(shù)據(jù)大小
int ret = 0;
// 發(fā)送數(shù)據(jù):遇-20006(丟包)則重傳
do{
ret = avSendFrameData(
avIndex, // AV通道索引
buffer, // 二進(jìn)制數(shù)據(jù)緩沖區(qū)
readSize, // 數(shù)據(jù)長(zhǎng)度
(const void*)&frmInfo, // 文件信息幀頭
sizeof(FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t) // 幀頭長(zhǎng)度
);
if(ret < 0){
if(ret == -20006){ // 丟包錯(cuò)誤,等待后重傳
msleep(20);
} else { // 其他錯(cuò)誤,退出發(fā)送
break;
}
}
} while(ret == -20006);
// 異常退出:關(guān)閉文件并銷(xiāo)毀通道
if(ret < 0){
printf("avSendFrameData error :%d\n", ret);
f.close();
goto LAB_END;
}
// 最后一幀發(fā)送完成,退出當(dāng)前文件循環(huán)
if(frmInfo.endFlag){
break;
}
}
f.close(); // 關(guān)閉當(dāng)前文件
}
// 等待重傳緩沖區(qū)清空(避免數(shù)據(jù)殘留)
int i_count = 0;
while(i_count++ < 10*300){ // 最多等待30秒
float userate = avResendBufUsageRate(avIndex); // 獲取重傳緩沖區(qū)使用率
if(userate > 0.0){
msleep(100);
} else {
break; // 緩沖區(qū)為空,可安全銷(xiāo)毀通道
}
}
LAB_END:
destoryDataChannelOfDevice(avIndex); // 銷(xiāo)毀通道
}
(二)APP端(接收方)數(shù)據(jù)接收
APP端循環(huán)接收數(shù)據(jù),解析幀頭信息,處理文件切換(新文件打開(kāi)/舊文件關(guān)閉),驗(yàn)證結(jié)束標(biāo)志,完成后關(guān)閉文件和通道。
#define MAX_BUFFER_SIZE 300*1024 // 接收緩沖區(qū)大?。ńㄗh大于發(fā)送端緩沖區(qū))
void recvDataAndSaveToLocalFile(void *arg)
{
// 創(chuàng)建接收通道
int avIndex = createDataChannelOfClientForDownload(sid, iotc_channel_id);
if(avIndex < 0){
printf("createDataChannelOfClientForDownload failed\n");
return;
}
char buffer[MAX_BUFFER_SIZE];
string fileName;
uint fileSize;
uint dataSize;
int endFlag;
FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t frmInfo;
int tmpInt1, tmpInt2, tmpInt3, tmpInt4;
uint frmNo;
file f; // 本地文件對(duì)象
// 循環(huán)讀取數(shù)據(jù)
while(1){
int ret = avRecvFrameData2(
avIndex, // AV通道索引
buffer, // 接收緩沖區(qū)
MAX_BUFFER_SIZE, // 緩沖區(qū)最大長(zhǎng)度
&tmpInt1, // 預(yù)留參數(shù)(忽略)
&tmpInt2, // 預(yù)留參數(shù)(忽略)
(char*)&frmInfo, // 接收文件信息幀頭
sizeof(FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t), // 幀頭長(zhǎng)度
&tmpInt3, // 預(yù)留參數(shù)(忽略)
&frmNo // 幀序號(hào)(忽略)
);
if(ret < 0){
if(ret == AV_ER_DATA_NOREADY){ // 暫無(wú)數(shù)據(jù),短暫等待
msleep(5);
continue;
} else if(ret == AV_ER_INCOMPLETE_FRAME || ret == AV_ER_LOSED_THIS_FRAME){
printf("lost data, frameNo:%d\n", frmNo); // 幀不完整/丟失,繼續(xù)接收
continue;
} else { // 其他錯(cuò)誤(如通道斷開(kāi)),退出循環(huán)
printf("avRecvFrameData2 return error:%d\n", ret);
break;
}
}
// 處理文件切換:未打開(kāi)文件→打開(kāi)新文件;文件名變化→關(guān)閉舊文件,打開(kāi)新文件
if(!f.isOpen()){
fileName = frmInfo.fileName;
f.setFileName(fileName);
if(!f.open(QIODevice::WriteOnly)){ // 以只寫(xiě)模式打開(kāi)文件
printf("open file failed:%s\n", fileName.toUtf8().data());
break;
}
} else {
if(fileName != frmInfo.fileName){ // 檢測(cè)到新文件
f.close(); // 關(guān)閉舊文件
fileName = frmInfo.fileName;
f.setFileName(fileName);
if(!f.open(QIODevice::WriteOnly)){
printf("open file failed:%s\n", fileName.toUtf8().data());
break;
}
}
}
// 寫(xiě)入本地文件(ret為實(shí)際接收的數(shù)據(jù)長(zhǎng)度)
f.write(buffer, ret);
// 檢測(cè)結(jié)束標(biāo)志:最后一個(gè)文件的最后一包,退出循環(huán)
if(frmInfo.endFlag){
printf("file download complete, fileName:%s\n", fileName.toUtf8().data());
break;
}
}
// 關(guān)閉本地文件
if(f.isOpen()){
f.close();
}
// 銷(xiāo)毀通道,釋放資源,建議收完最后一幀后延遲1s再關(guān)閉通道
destoryDataChannelOfClient(avIndex);
}
四、結(jié)束標(biāo)志的判斷
結(jié)束標(biāo)志通過(guò) FRAMEINFO_FOR_UPLOAD_DOWNLOAD_t 結(jié)構(gòu)體的 endFlag 字段判斷,規(guī)則如下:
? endFlag = 0:非最后一個(gè)文件的數(shù)據(jù)包,或最后一個(gè)文件的非最后一包數(shù)據(jù);
? endFlag = 1:僅當(dāng)所有待下載文件中,最后一個(gè)文件的最后一包數(shù)據(jù) 才設(shè)為1,用于標(biāo)識(shí)整個(gè)下載流程結(jié)束。
注意:多文件連續(xù)傳輸時(shí),前序文件的最后一包 endFlag 仍為0,僅最后一個(gè)文件的最后一包設(shè)為1,避免APP端誤判提前關(guān)閉通道。