一、概述
云存儲(chǔ)采用「服務(wù)器主動(dòng)取流」模式(類(lèi)似實(shí)時(shí)觀看):設(shè)備端觸發(fā)存儲(chǔ)需求時(shí),調(diào)用接口通知云服務(wù)器從設(shè)備端取流,底層基于 TUTK AVAPIs 協(xié)議。
設(shè)備端僅需對(duì)接 TUTK 公版實(shí)時(shí)音視頻協(xié)議,無(wú)需額外開(kāi)發(fā)復(fù)雜存儲(chǔ)邏輯。
(一)編碼格式要求
圖像:H264 / H265
音頻:PCM、AAC、G711 等
音頻:PCM、AAC、G711 等
二、云存儲(chǔ)模塊初始化
初始化時(shí)讀取本地云存儲(chǔ)配置文件(含 URL、Token),無(wú)配置則通過(guò)回調(diào)從 APP 端獲取并持久化。
初始化實(shí)現(xiàn)代碼
// 讀取云存儲(chǔ)配置文件(gVsaasInfoFilePath:本地配置文件路徑)
char *vsaas_info = NULL;
FILE *vsaas_info_file_ptr = fopen(gVsaasInfoFilePath, "r");
int ret = 0;
if (vsaas_info_file_ptr == NULL) {
// 未配置:從APP端獲?。ㄍㄟ^(guò)VsaasConfigChangedHandle回調(diào)保存)
printf("Enable VSaaS without preload data!\n");
ret = avEnableVSaaS(uid, NULL, VsaasConfigChangedHandle, VSaaSUpdateContractInfoHandle);
} else {
// 已配置:讀取本地文件初始化
long file_size = 0;
fseek(vsaas_info_file_ptr, 0, SEEK_END);
file_size = ftell(vsaas_info_file_ptr);
rewind(vsaas_info_file_ptr);
vsaas_info = (char*)malloc(sizeof(char)*file_size);
if (vsaas_info == NULL) {
printf("memory alloc fail!\n");
fclose(vsaas_info_file_ptr);
return -1;
}
fread(vsaas_info, 1, file_size, vsaas_info_file_ptr);
ret = avEnableVSaaS(uid, vsaas_info, VsaasConfigChangedHandle, VSaaSUpdateContractInfoHandle);
fclose(vsaas_info_file_ptr);
free(vsaas_info);
gVsaasConfigExist = true;
}
if (ret != AV_ER_NoERROR) {
printf("avEnableVSaaS error [%d]\n", ret);
return -1;
}
三、通知云服務(wù)器取流
支持「實(shí)時(shí)流(Live)」和「回放流(Playback)」兩種模式,按參數(shù)要求構(gòu)造 JSON 后調(diào)用接口。
1. 參數(shù)說(shuō)明
| 參數(shù)名 | 類(lèi)型 | 必選 | 說(shuō)明 |
|---|---|---|---|
| starttime | String | 是 | 實(shí)時(shí)流填"live",回放流填時(shí)間戳(秒) |
| event_id | Int | 是 | 默認(rèn)為 VSAAS_EVENT_GENERAL,APP將拿到這個(gè)event_id,轉(zhuǎn)為文字顯示 |
| event_file | String | 回放流必選 | 指定本地錄像文件路徑 |
| media_type | Int | 是 | 0=H264,1=H265,2=JPEG |
| channel | Int | 回放流必選 | 傳入回放專用IOTC通道ID(避免avServStartEx超時(shí)) |
通知取流實(shí)現(xiàn)代碼
/* 參數(shù)說(shuō)明:
* starttime:實(shí)時(shí)流填"live",回放流填時(shí)間戳(秒)
* event_id:默認(rèn)為 VSAAS_EVENT_GENERAL,也可以自定義 * event_file:回放流必填,指定本地錄像文件路徑
* media_type:0=H264,1=H265,2=JPEG
* channel:回放流必填,傳入回放專用IOTC通道ID(避免avServStartEx超時(shí))
*/
char att_json_str[256] = {0};
bool isLiveView = true; // true=實(shí)時(shí)流,false=回放流
bool h264 = true; // true=H264,false=H265
unsigned long current_time_sec = 1699999999; // 回放起始時(shí)間戳
char *gRecordFile = "/mnt/record/20231114_100000.h264"; // 本地錄像文件
int iotc_channel_id_for_playback = 1; // 回放IOTC通道ID
if (isLiveView) {
// 實(shí)時(shí)流模式
sprintf(att_json_str,
"{\"starttime\":\"live\",\"protocol\":\"tutk\",\"event_id\":\"%d\",\"media_type\":\"%d\"}",
VSAAS_EVENT_GENERAL,
h264 ? 0 : 1
);
} else {
// 回放流模式
sprintf(att_json_str,
"{\"starttime\":\"%lu\",\"protocol\":\"tutk\",\"event_id\":\"%d\",\"event_file\":\"%s\",\"media_type\":\"%d\",\"channel\":\"%d\"}",
current_time_sec,
VSAAS_EVENT_GENERAL,
gRecordFile,
h264 ? 0 : 1,
iotc_channel_id_for_playback
);
}
// 通知云服務(wù)器取流(超時(shí)3000ms)
Nebula_Json_Obj attr_obj = NULL;
Nebula_Json_Obj_Create_From_String(att_json_str, &attr_obj);
ret = avServNotifyCloudRecordStream(attr_obj, 3000, NULL);
printf("avServNotifyCloudRecordStream ret[%d]\n", ret);
if (ret != AV_ER_NoERROR) {
// 錯(cuò)誤處理(例:-20043=用戶未購(gòu)買(mǎi)云存儲(chǔ)方案)
printf("通知取流失敗,錯(cuò)誤碼:%d\n", ret);
}
通知成功后,云服務(wù)器會(huì)發(fā)起 IOTC 連線,通過(guò) P2P 協(xié)議取流(設(shè)備端將收到 0x1ff、0x351 拉流指令)。
四、關(guān)鍵回調(diào)函數(shù)實(shí)現(xiàn)
(一)配置變更回調(diào)(保存APP端下發(fā)的URL和Token)
APP 端將云存儲(chǔ)配置(URL、Token)以 JSON 格式下發(fā)(具體請(qǐng)參考:APP給設(shè)備端配置云存信息),設(shè)備端需保存到本地文件,供下次初始化使用。
配置變更回調(diào)實(shí)現(xiàn)代碼
static void VsaasConfigChangedHandle(const char *vsaas_config)
{
printf("Enter %s\n", __func__);
printf("收到云存儲(chǔ)配置:\n%s\n", vsaas_config);
// 保存配置到本地文件
FILE *vsaas_info_file_ptr = fopen(gVsaasInfoFilePath, "w+");
if (vsaas_info_file_ptr == NULL) {
printf("%s:打開(kāi)配置文件失敗!路徑:%s\n", __func__, gVsaasInfoFilePath);
return;
}
fwrite(vsaas_config, 1, strlen(vsaas_config), vsaas_info_file_ptr);
fclose(vsaas_info_file_ptr);
gVsaasConfigExist = true; // 標(biāo)記已配置
}
(二)合約信息更新回調(diào)(適配云存儲(chǔ)方案限制)
云存儲(chǔ)合約變更時(shí)觸發(fā)(如分辨率、碼率、幀率限制),設(shè)備端需按合約調(diào)整錄像參數(shù)。
合約信息更新回調(diào)實(shí)現(xiàn)代碼
// 全局變量:存儲(chǔ)云存儲(chǔ)合約信息
VSaaSContractInfo gVsaasContractInfo = {0};
static void VSaaSUpdateContractInfoHandle(const VSaaSContractInfo *contract_info)
{
printf("Enter %s\n", __func__);
// 打印合約信息
printf("合約類(lèi)型:%u\n", contract_info->contract_type);
printf("最大錄制時(shí)長(zhǎng):%d秒\n", contract_info->event_recording_max_sec);
printf("最大幀率:%d fps\n", contract_info->video_max_fps);
printf("最大碼率:%d kbps\n", contract_info->recording_max_kbps);
printf("最大分辨率:%dx%d\n", contract_info->video_max_width, contract_info->video_max_high);
// 保存合約信息(設(shè)備端需按此調(diào)整錄像參數(shù))
memcpy(&gVsaasContractInfo, contract_info, sizeof(VSaaSContractInfo));
}
五、注意事項(xiàng)
公版 Kalay APP 適配
需修改密碼驗(yàn)證回調(diào),增加對(duì)賬號(hào) "vsaas" 的支持(云服務(wù)器取流時(shí)使用該賬號(hào)認(rèn)證)。
密碼驗(yàn)證回調(diào)修改代碼
int ExPasswordAuthCallBackFn(const char *account, char *pwd, unsigned int pwd_buf_size)
{
// 允許設(shè)備默認(rèn)賬號(hào)和 "vsaas" 賬號(hào)認(rèn)證
if (strcmp(account, gAvAccount) != 0 && strcmp(account, "vsaas") != 0) {
return -1; // 賬號(hào)不匹配
}
// 密碼緩沖區(qū)長(zhǎng)度校驗(yàn)
if (pwd_buf_size <= strlen(gAvPassword)) {
return -1; // 緩沖區(qū)不足
}
// 填充密碼
strcpy(pwd, gAvPassword);
return 0; // 認(rèn)證成功
}
說(shuō)明:gAvAccount、gAvPassword 為設(shè)備端默認(rèn)音視頻認(rèn)證賬號(hào)密碼
六、優(yōu)化策略(實(shí)時(shí)流模式)
實(shí)時(shí)流默認(rèn)云端拉取 30s 流,設(shè)備端可通過(guò)智能算法(如人形識(shí)別、寵物識(shí)別)動(dòng)態(tài)調(diào)整存儲(chǔ)時(shí)長(zhǎng),節(jié)省云存儲(chǔ)空間。
示例場(chǎng)景:10:00:00 偵測(cè)到人形→通知云端取流;10:00:10 人形消失→停止送流→關(guān)閉連接(最終存儲(chǔ)10s)。
設(shè)備端操作步驟:
1. 識(shí)別連接來(lái)源:判斷 IOTC 連線是否為云服務(wù)器發(fā)起(通過(guò)賬號(hào) "vsaas" 區(qū)分);
2. 標(biāo)記 avIndex:記錄該連接對(duì)應(yīng)的音視頻流索引(avIndex);
3. 動(dòng)態(tài)停止存儲(chǔ):檢測(cè)到無(wú)需繼續(xù)存儲(chǔ)時(shí),停止向該 avIndex 推送音視頻數(shù)據(jù);
4. 安全關(guān)閉連接:等待 avIndex 中緩存的流全部發(fā)送完成后,關(guān)閉連接。
