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