网关设备接入
此章节内容仅针对网关含NVR(以下简称网关,也称主设备)相关设备接入进行说明。
概述
AIRTC云平台支持设备直连云平台,也支持网关直连云平台,而网关下的子设备,则通过网关接入云平台。
网关通过接入DeviceSDK可以将网关和子设备的拓扑关系以及子设备的在线状态上报到AIRTC云平台,在客户端可以查看网关和子设备的拓扑关系以及各自的在线状态,并且可以对子设备进行直播、回放、物模型配置、PTZ控制等相关操作。
适用场景
适用于子设备不能直连或者需要拓扑关系管理的场景,例如:接入Wi-Fi网关、蓝牙网关、ZigBee网关、NVR等。
同一个网关下支持接入不同品类的子设备,如:单目摄像头、多目摄像头、烟感、温湿度传感器等。
三元组信息
-
您需要在AIRTC控制台生成网关设备的 productKey/deviceName/deviceSecret 三元组信息,并将其写入到网关设备中。
-
通过DeviceSDK初始化网关设备的时候,三元组信息和uuid信息必填(productKey/deviceName/deviceSecret /uuid)。
-
关于子设备的三元组信息,有两种情况:
-
子设备本身没有三元组信息:初始化子设备的时候,只需要填写 deviceName和uuid信息,子设备 deviceName 和 uuid由厂商自行命名,但是需要保证deviceName 和 uuid在当前产品型号下的唯一性。建议采用 【网关的deviceName + 子设备标识】作为子设备的deviceName。子设备的productKey和deviceSecret信息,由DeviceSDK动态向平台获取,并内部维护,此场景下所有子设备的productKey都是相同的
-
子设备本身就有三元组信息:初始化子设备的时候,将三元组信息和uuid一并传入DeviceSDK,DeviceSDK使用传入的三元组信息将设备注册到平台
-
网关接入过程
子设备接入之前,需要先将网关接入到云平台,网关的接入过程和普通IPC接入流程类似,需要同时接入IoTSDK和StreamSDK,具体流程可以参考普通IPC接入流程: IoTSDK接入 和 StreamSDK接入
子设备接入过程
子设备不直接连接AIRTC云平台,所以无需为子设备安装AIRTC DeviceSDK。子设备完全由厂商自行开发。
网关发现子设备、监控子设备在线状态,以及将来自AIRTC云平台的信令发送给子设备等功能,均由网关厂商自行实现,或网关厂商与子设备厂商定义协议实现。
网关接入到AIRTC云平台之后,子设备上线过程如下:
- 子设备接入网关
- 网关调用 xs_iot_open 接口创建子设备
- 网关调用 xs_iot_connect 接口连接子设备
- 网关设备DeviceSDK向云平台发起子设备的登录请求(子设备和网关复用同一条信令连接),并获取子设备在平台上的绑定关系,如果未绑定,则动态发起子设备的绑定流程
- 子设备登录成功之后,客户端会显示当前子设备在线
网关接入到AIRTC云平台之后,子设备下线过程如下:
- 子设备从网关移除
- 网关调用 xs_iot_disconnect 接口通知云平台子设备的在线状态
- 网关调用 xs_iot_close 接口将子设备和网关的拓扑关系在云平台上解绑
DeviceSDK接入流程步骤
步骤1:初始化网关设备
- 初始化设备SDK(包括IoTSDK以及StreamSDK)
/*初始化rtcx-iotsdk*/
static void initIotSdk()
{
/*初始化iotsdk回调函数指针*/
xs_iot_callbackFun_st cbFun;
initIotCallbackFun(&cbFun); //初始化回调函数指针列表
/*设置设备的工作模式,区分长上电设备和低功耗保活设备*/
xs_iotInitParam_st initParam;
memset(&initParam, 0, sizeof(initParam));
initParam.configPath = "/etc/config"; //必须是flash中真实存在的可读写路径
initParam.workMode = XS_IOT_WORK_MODE_LONG_TIME_POWER;
initParam.maxSubDeviceNum = SUB_DEV_COUNT; //网关设备,必须设置最大支持的子设备个数
ret = xs_iot_init(&cbFun, &initParam);
}
/*初始化rtcx-streamsdk*/
static void initStreamSdk()
{
/*初始化StreamSDK配置参数*/
xs_streamConfig_st initConfig;
memset(&initConfig,0,sizeof(initConfig));
initConfig.device_type = XS_DEVICE_TYPE_GATEWAY; //设备类型设置成 XS_DEVICE_TYPE_GATEWAY
initConfig.sub_num = SUB_DEV_COUNT; //最大子设备个数设置一下
/*初始化streamsdk回调函数指针*/
xs_stream_callbackFun_st streamCbFun;
memset(&streamCbFun, 0, sizeof(streamCbFun));
initStreamCallbackFun(&streamCbFun); //初始化回调函数指针列表
xs_stream_Init(&initConfig, &streamCbFun);
}
- 创建网关设备,建立连接
/*创建网关,并建立连接,返回生成的deviceId*/
static xs_int32 initPrimaryDevice(xs_deviceInfo_st* deviceInfo)
{
/*创建网关*/
xs_int32 devId = xs_iot_open(XS_IOT_DEV_TYPE_MASTER, deviceInfo);
if (devId < 0){
//创建网关错误
return devId;
}
xs_int32 code = xs_iot_connect(devId);
if (code != XS_STREAM_EC_SUCCESS) {
//连接错误
}
/*启动streamsdk服务*/
xs_stream_start();
return devId;
}
步骤2:添加子设备
创建子设备,并建立连接,子设备建立连接之后,SDK内部触发自动绑定流程
static xs_int32 createSubDevice(xs_deviceInfo_st* deviceInfo)
{
if (!deviceInfo->deviceName) {
//子设备只需要有deviceName即可,productkey和device secret信息SDK内部维护
return -1;
}
int32_t devId = xs_iot_open(XS_IOT_DEV_TYPE_SLAVE, &g_devAuth[devSeq].devInfo);
if (devId < 0){
return -1;
}
//注意:如果子设备是普通IOT设备(无IPC功能),不需要调用streamsdk相关接口
xs_DeviceParams_st deviceParam;
memset(&deviceParam,0,sizeof(deviceParam));
deviceParam.lensCount = 1; //网关子设备,支持单目和多目子设备
xs_stream_setDeviceParams(devId, &deviceParam);
//创建子设备流,此函数可以参考SDK demo程序
createStreamSource(devId);
//子设备上线
xs_iot_connect(devId);
return devId;
}
子设备上线(xs_iot_connect)后,SDK会将拓扑关系和在线状态同步到云端,云端会保存子设备和网关的绑定关系以及在线状态。
步骤3:子设备上下线
当子设备和网关拓扑关系建立之后,如果子设备和网关处于连接状态,需要通过xs_iot_connect通知SDK,SDK会将状态同步到云端,此时APP上看到设备是在线状态。
当子设备和网关连接断开之后(拓扑关系还在),需要通过 xs_iot_disconnect函数通知SDK,SDK会将状态同步到云端,此时APP上看到设备是离线状态。
子设备上线和离线的通知也是通过下面的回调函数触发,通过devId进行区分
/**
* @brief 设备在线状态回调
* @param [IN] devId: 设备ID,xs_iot_open接口返回的ID
* @param [IN] connectStatus: 在线状态,1表示设备在线, 0 表示设备离线
* @return 0:成功, 其他值:错误码
*/
typedef xs_int32(*xs_connectStatusCallback)(xs_int32 devId, xs_int32 connectStatus);
步骤4:子设备业务处理
子设备的业务处理,比如:物模型相关的回调、智能事件上报、自定义事件上报等。也和普通IPC设备采用相同的接口,区别就是通过子设备的devId进行区分。具体接口和功能可以参考普通IPC的接入流程文档,见IoTSDK接入和StreamSDK接入。
步骤5:删除子设备
当子设备和网关的拓扑关系解除之后,需要通过 xs_iot_close 函数通知SDK,SDK会通知云端子设备已经解绑,此时APP上就不会再显示子设备信息
static void destroySubDevice(xs_int32 devId)
{
/*先销毁流,再销毁设备*/
destroyStreamSource(devId);
xs_iot_disconnect(devId);
xs_iot_close(devId);
}
注意:子设备绑定和解绑的操作可以在离线状态下执行,SDK内部会记录子设备和网关的拓扑关系,待设备和云端重新建立建立后,会将拓扑关系和在线状态同步到云端。
步骤6:反初始化网关设备
程序退出时,可以反初始化网关设备(不会影响平台端网关设备和用户的绑定关系),此步骤是非必要操作,也可以不执行
/*反初始化设备*/
static void uninitPrimaryDevice(xs_deviceInfo_st* deviceInfo)
{
xs_iot_disconnect(devId);
xs_iot_close(devId);
}
跨镜头回放
功能概述
网关设备支持跨子设备回放(也称跨镜头播放):APP 在一次回放会话中,可以动态切换查看不同子设备的录像数据。设备侧只需为网关(主设备)创建一条音频流和一条视频流,通过在这条流通道中塞入不同子设备的音视频数据来实现切换,无需为每个子设备单独创建回放通道。
SDK 通过视频 SEI(Supplemental Enhancement Information)将当前推流对应的子设备 deviceName 透传给 APP,APP 据此识别当前 GOP(Group of Pictures)属于哪个子设备。
前提条件
使用跨子设备回放功能前,需要满足以下条件:
-
必须为主设备(网关)创建音频流和视频流:SDK 回放通道建立在主设备的流通道之上。如果主设备没有创建音频和视频两条流,回放通道无法正常建立,跨子设备回放功能不可用。
-
所有子设备的音视频编码格式必须与主设备一致:由于多个子设备共用同一条 RTC 流通道,SDK 在创建回放流时采用的是主设备流的编码参数(视频编码格式、音频编码格式和采样率等)。如果某个子设备的录像数据编码格式与主设备不一致,会导致 APP 端解码异常。
-
子设备的
deviceName必须唯一:每个子设备的deviceName在当前产品型号下必须唯一,SDK 通过deviceName标识当前推流的子设备。
实现步骤
1. 创建主设备的音视频流
在子设备上线之前,需要先为主设备(网关)创建音频流和视频流。流的编码参数需要和所有子设备的录像数据编码格式保持一致。
/* 为主设备创建音视频流 */
void createMasterStreamSource(xs_deviceAuth_st* masterDevAuth)
{
xs_videoEncodeParam_st videoEncode;
memset(&videoEncode, 0, sizeof(videoEncode));
videoEncode.encode = XS_ENCODE_TYPE_H264; // 需和子设备录像编码格式一致
videoEncode.bpsMax = 8 * 1024 * 1024;
xs_audioEncodeParam_st audioEncode;
memset(&audioEncode, 0, sizeof(audioEncode));
audioEncode.encode = XS_AUDIO_TYPE_AAC; // 需和子设备录像编码格式一致
audioEncode.sampleRate = 16000;
audioEncode.channelCnt = 1;
audioEncode.sampleBit = 16;
/* 创建主码流(streamId=0),返回 serviceId */
int serviceId = xs_stream_createStream(masterDevAuth, 0, 0, &videoEncode, &audioEncode, XS_FALSE);
}
2. 处理回放回调
当 APP 发起回放请求时,SDK 会触发 xs_startPlaybackByTime 回调。回调参数中的 streamIndex->devId 为本次回放目标子设备的 ID。设备侧需要根据此 devId 找到对应子设备的录像数据并开始推流。
回调参数说明:
streamIndex->devId:目标子设备 IDpbTime->serviceId:主设备流的 serviceId(复用主设备的流通道)pbTime->startTime:回放起始时间(UTC 毫秒)pbTime->seekId:seek 标识pbTime->extInfo:扩展信息字符串指针(xs_char*,以\0结尾),由 APP 下发,设备侧透传使用,可用于跨镜头自动播放列表等场景。为NULL表示无扩展信息。仅在开始回放时下发一次,seek时不会重复携带。pbTime->extInfoLen:扩展信息长度(含\0终止符),extInfo为NULL时为0。
/* 按时间开始回放回调 */
xs_int32 onStartPlaybackByTime(const xs_streamIndex_st* streamIndex, xs_playbackTimeInfo_st* pbTime)
{
/* streamIndex->devId: 目标子设备 ID */
/* pbTime->serviceId: 主设备流的 serviceId(复用主设备的流通道) */
/* pbTime->startTime: 回放起始时间(UTC 毫秒) */
/* pbTime->seekId: seek 标识 */
/* pbTime->extInfo: 扩展信息指针(xs_char*),NULL表示无扩展信息,设备侧可自行解析使用 */
/* pbTime->extInfoLen: 扩展信息长度(含'\0'),extInfo为NULL时为0 */
/* 根据 streamIndex->devId 查找子设备的录像数据,开始推流 */
startPushSubDeviceData(pbTime->serviceId, streamIndex, pbTime->startTime, pbTime->seekId);
return XS_STREAM_EC_SUCCESS;
}
3. 推流时填写 deviceName(关键)
通过 xs_stream_inputVideo 塞入视频帧时,必须在 xs_videoInfo_st.deviceName 中填写当前数据所属子设备的 deviceName。SDK 会在相关帧前将 deviceName 作为 SEI 透传给 APP,APP 据此识别当前画面属于哪个子设备。
xs_videoInfo_st vinfo = {0};
vinfo.encode = XS_ENCODE_TYPE_H264;
vinfo.frame_type = isKeyFrame ? XS_VIDEO_FRAME_I : XS_VIDEO_FRAME_P;
vinfo.payload = videoData;
vinfo.payload_size = videoDataSize;
vinfo.utcms = currentPktTime;
vinfo.pts = pts;
vinfo.seq = seq++;
vinfo.seekId = seekId;
/* 关键:填写当前推流对应的子设备 deviceName */
/* 最长 63 字节(保留 1 字节存储 '\0') */
snprintf(vinfo.deviceName, sizeof(vinfo.deviceName), "%s", currentSubDeviceName);
xs_stream_inputVideo(serviceId, &vinfo);
注意:
deviceName按devId区分。同一个子设备的所有镜头共用同一个deviceName。- 如果
deviceName为空字符串,SDK 不会插入 SEI,APP 将无法识别当前画面的子设备归属。- SDK 内部仅做
memcpy,不会持有传入指针,设备侧需保证在xs_stream_inputVideo调用期间指针有效。
4. 处理 seek 切换子设备
当 APP 在回放过程中切换到另一个子设备时,SDK 会触发 xs_seekPlaybackByTime 回调。此时 streamIndex->devId 可能指向新的目标子设备,设备侧需要:
- 切换到新子设备的录像数据源
- 更新推流时的
vinfo.deviceName为新子设备的deviceName - 触发一次强制关键帧(让 SDK 立即在下一个 I 帧前插入新的 deviceName SEI)
/* 回放 seek 回调 */
xs_int32 onPlaybackSeek(const xs_streamIndex_st* streamIndex, xs_int32 serviceId,
xs_uint64 seekTime, xs_int32 seekId)
{
/* 更新 seekId 和起始时间 */
updateSeekId(serviceId, seekId);
updateStartTime(serviceId, seekTime);
/* 如果 devId 发生变化,切换到新子设备的数据源 */
if (streamIndex->devId != getCurrentDevId(serviceId)) {
/* 查找新子设备的 deviceName */
const char* newDeviceName = getDeviceNameByDevId(streamIndex->devId);
/* 切换数据源并更新 deviceName */
switchSubDeviceDataSource(serviceId, streamIndex->devId, newDeviceName);
/* 触发强制关键帧,让 SDK 立即发送新的 deviceName SEI */
forceKeyFrame(serviceId);
}
return XS_STREAM_EC_SUCCESS;
}