本开发实例是一个包含:抓取摄像头数据、图像送入编码器进行编码压缩、以及把压缩后的数据通过RTSP协议发送出去的简单核心示例。
若用户需要通过webServer对图像进行调整,以及配置摄像头的一些工作方式,则需要用户自行研发增加对应的功能。
摄像头与板卡的连接:
板卡与PC的连接:
如果您初次阅读此文档,请阅读《入门指南/开发环境准备/Easy-Eai编译环境准备与更新》,并按照其相关的操作,进行编译环境的部署。
在PC端Ubuntu系统中执行run脚本,进入EASY-EAI编译环境,具体如下所示。
cd ~/develop_environment ./run.sh
在EASY-EAI编译环境下创建存放源码仓库的管理目录:
cd /opt mkdir EASY-EAI-Toolkit cd EASY-EAI-Toolkit
通过git工具,在管理目录内克隆远程仓库
git clone https://github.com/EASY-EAI/EASY-EAI-Toolkit-C-Solution.git
注:
* 此处可能会因网络原因造成卡顿,请耐心等待。
* 如果实在要在gitHub网页上下载,也要把整个仓库下载下来,不能单独下载本实例对应的目录。
进入到对应的例程目录执行编译操作,具体命令如下所示:
cd EASY-EAI-Toolkit-C-Solution/solu-rtspIPCamera/ ./build.sh
注:
* 由于依赖库部署在板卡上,因此交叉编译过程中必须保持adb连接。
注:
* 若build.sh脚本不带任何参数,则仅会拷贝solution编译出来的可执行文件。
* 若build.sh脚本带有cpres参数,则会把Release/目录下的所有资源都拷贝到开发板上。
* 若build.sh脚本带有clear参数,则会把build/目录和Release/目录删除。
然后把编译好的结果部署到板卡中(有两种方法)。
方法一:通过执行以下命令手动部署【推荐】
cp Release/solu-* /mnt/userdata/Solu
方法二:在编译时加上编译参数自动部署
./build.sh cpres
通过按键Ctrl Shift T创建一个新窗口,执行adb shell命令,进入板卡运行环境。
adb shell
进入板卡后,定位到例程部署的位置,如下所示:
cd /userdata/Solu
运行例程前,先通过以下命令查看一下开发板网卡的地址:
ifconfig
运行例程命令如下所示:
./solu-rtspIPCamera Main &
运行打印如下所示:
[1] 3931 root@EASY-EAI-NANO:/userdata/Solu# media get entity by name: rkcif-lvds-subdev is null media get entity by name: rkcif-lite-lvds-subdev is null media get entity by name: rkisp-mpfbc-subdev is null media get entity by name: rkisp_dmapath is null media get entity by name: rkisp-mpfbc-subdev is null media get entity by name: rkisp_dmapath is null media get entity by name: rockchip-mipi-dphy-rx is null [16:45:52.418515][CAMHW]:XCAM ERROR CamHwIsp20.cpp:928: No free isp&ispp needed by fake camera! Rga built version:1.04 94a2c08 2023-04-12 10:23:53 ***************************************** Waiting for the child process terminated! ***************************************** …… mpp[3939]: mpp_rt: NOT found ion allocator mpp[3939]: mpp_rt: found drm allocator mpp[3939]: mpp_info: mpp version: 2cc04fd author: chenhaiman 2023-02-08 update build scripts mpp[3939]: mpp_log: 0x6718c0 mpi_enc_test encoder test start w 720 h 1280 type 7 mpp[3939]: mpp_enc: MPP_ENC_SET_RC_CFG bps 2880000 [180000 : 2880000] fps [30:25] gop 12 mpp[3939]: h264e_api_v2: MPP_ENC_SET_PREP_CFG w:h [720:1280] stride [720:1280] mpp[3939]: mpp_enc: send header for set cfg change input/format mpp[3939]: mpp_enc: mode cbr bps [180000:2880000:2880000] fps flex [30/1] -> fix [25/1] gop i [12] v [0] =====[frame time]: 1970-01-01 08:00:00 [000][0x67][1]------------- dataLen = 38 =====[frame time]: 2023-05-08 16:45:54 [001][0x06][1]------------- dataLen = 86 =====[frame time]: 2023-05-08 16:45:54 [002][0x06][1]------------- dataLen = 90 =====[frame time]: 2023-05-08 16:45:54 [003][0x65][1]------------- dataLen = 13747 =====[frame time]: 2023-05-08 16:45:54 [004][0x41][2]------------- dataLen = 4531 =====[frame time]: 2023-05-08 16:45:54 ……
然后在PC主机(windows系统或者Ubuntu系统都可以),打开VLC播放器。输入rtsp流地址即可通过rtsp协议播放摄像机画面。
rtsp流地址构造规则在下方【4.3.2RTSP服务器--服务器对象】中有详细讲解。
注:若无法播放,请检查网络环境是否正常(如:PC是否与开发板处于同一网段内,是否有IP冲突的情况等)
摄像机画面如下所示:
首先进入板卡环境,执行以下命令,在板卡上创建一个给本例程使用的应用目录:myapp
cd /userdata/apps/ mkdir myapp
然后回到开发环境中,通过使用“2.3方案部署”类似的操作方法,把本例程所需要的全部文件,包含:编译结果,配置文件,模型等。部署到刚刚新建的myapp目录中。
最后在板卡上创建一个run.sh脚本来管控用户所有需要的应用即可,《入门指南/应用程序开机自启动》会详细描述run.sh脚本该如何编写。
方案主逻辑代码分为三部分,如下所示。
启动代码位于:EASY-EAI-Toolkit-C-Solution/solu-rtspIPCamera/src/main.cpp
画面编码代码位于:EASY-EAI-Toolkit-C-Solution/solu-rtspIPCamera/src/enCoder/enCoder.cpp
RTSP传输代码位于:EASY-EAI-Toolkit-C-Solution/solu-rtspIPCamera/src/rtspServer/rtspServer.cpp
网络摄像头方案的实现,需要使用到easyeai-api库的以下组件。
启动代码main.cpp主要负责启动并守护编码进程和RTSP传输进程,使用到的easyeai-api库组件如下所示。
启动代码主要使用的模块信息如下所示。
组件 | 头文件以及库路径 | 描述 |
系统操作组件 | easyeai-api/common_api/system_opt | 提供系统操作函数 |
画面编码代码enCoder.cpp主要负责从mipi摄像头获取画面、把画面送入编码器、把编码后数据送入流媒体环形队列中等操作,使用到的easyeai-api库组件如下所示。
取流代码主要使用的模块信息如下所示。
组件 | 头文件以及库路径 | 描述 |
摄像头组件 | easyeai-api/peripheral_api/camera | 提供时间戳操作函数 |
编码器操作组件 | easyeai-api/media_api/endeCode_api | 提供推入编码器操作函数、编码数据输出函数 |
环形队列组件 | easyeai-api/media_api/frame_queue | 提供推入编码数据到环形队列操作函数 |
Rtsp服务器代码rtspServer.cpp主要负责从环形队列中取出NALU送入RTSP服务等操作,使用到的easyeai-api库组件如下所示。
Rtsp服务器代码主要使用模块信息如下所示。
组件 | 头文件以及库路径 | 描述 |
Rtsp协议组件 | easyeai-api/netProtocol_api/rtsp | 提供RTSP服务器操作函数 |
环形队列组件 | easyeai-api/media_api/frame_queue | 提供从环形队列取出NALU数据操作函数 |
项目的整体逻辑框图如下所示。
启动逻辑代码路径为:src/main.cpp。
主进程会创建1个RTSP服务器和1个编码器。RTSP服务器用于通过RTSP协议发送NALU包;编码器用于获取MIPI摄像头的图像,并且编码成NALU数据,送入流媒体环形队列。然后变成一个守护进程,监视RTSP服务器进程和编码器进程的运行状况,一旦崩溃则把它们重新创建。
创建RTSP服务器代码如下所示。
CreateProcess(PROCESS_RTSPSERVER_NAME, &st_TaskInfo);
创建编码器代码如下所示。
CreateProcess(PROCESS_ENCODER_NAME, &st_TaskInfo);
编码逻辑代码路径为:src/enCoder/enCoder.cpp。
首先是创建编码资源,包含:编码器、编码通道、输出通道(流媒体环形队列)。
创建编码器代码如下所示,其中MAXCHNNUM代表此编码器的可分配的最大通道数。
create_encoder(MAXCHNNUM);
创建编码通道代码如下所示,其中encodeChn_Id就是编码器自动分配出来的可用通道号,此通道Id的有效取值范围是0~MAXCHNNUM。
create_encMedia_channel(&encodeChn_Id);
创建输出通道代码如下所示,注意这个环形队列的通道数与编码器的通道无直接关系。
create_video_frame_queue_pool(DATA_INPUT_CHN 1);
初始化mipi摄像头的代码如下所示,分为两步:第一步初始化摄像头画面的宽高、显示角度;第二步改变摄像头的画面图像格式为nv12。
ret = rgbcamera_init(CAMERA_WIDTH, CAMERA_HEIGHT, 90); rgbcamera_set_format(RK_FORMAT_YCbCr_420_SP);
初始化编码包含:向编码通道绑定输出回调函数;通过设置工作参数,指导编码通道的编码工作方式。具体代码如下所示。
set_encMedia_channel_callback(encodeChn_Id, StreamOutpuHandle, NULL); memset(&wp, 0, sizeof(WorkPara)); wp.in_fmt = VFRAME_TYPE_NV12; wp.out_fmt = VCODING_TYPE_AVC; wp.out_fps = 25; wp.width = CAMERA_WIDTH; wp.height = CAMERA_HEIGHT; ret = set_encMedia_channel_workPara(encodeChn_Id, &wp, NULL);
编码通道回调函数,源码如下所示。
int32_t StreamOutpuHandle(void *obj, VideoNodeDesc *pNodeDesc, uint8_t *pNALUData) { push_node_to_video_channel(DATA_INPUT_CHN, pNodeDesc, pNALUData); return 0; }
编码通道回调函数,传入参数说明。
参数 | 描述 |
obj | 绑定解码回调函数时,传入的对象指针。以便可以在回调函数中操作此对象,此示例为NULL。 |
pNodeDesc | NAL报文描述头 |
pNALUData | NAL报文内容 |
编码通道普通工作参数,成员变量说明,如下所示:
参数 | 成员 | 描述 |
WorkPara 【必须】 |
in_fmt | 输入数据的数据格式,如VFRAME_TYPE_NV12 |
out_fmt | 输出数据的数据格式,如VCODING_TYPE_AVC | |
out_fps | 输出视频裸流帧率,如25帧 | |
width | 输出视频的宽 | |
height | 输出视频的高 | |
hor_stride | 输出视频的水平步长,通常是宽的16位对齐。 | |
ver_stride | 输出视频的垂直步长,通常是高的16位对齐。 |
编码通道高级工作参数,成员变量说明,如下所示:
参数 | 成员 | 描述 |
AdvanceWorkPara 【非必须】 |
num_frames | |
gop_mode | 高级GOP模式,默认不需要配置(填0) | |
gop_len | 0:[I帧间隔采用2*fps_out_num], n:[I帧间隔为n(1==n,即每一帧都是I帧)] | |
vi_len | ||
fps_out_flex | 0:[固定的输出帧率, 输出帧率=(fps_out_num/fps_out_den)], 1:[可变的输出帧率,完成帧编码后马上输出] | |
fps_out_num | 0:[使用默认值], n:[使用n值] -- 默认值为30 | |
fps_out_den | 0:[使用默认值], n:[使用n值] -- 默认值为1 | |
fps_in_flex | 0:[固定的输入帧率, 输入帧率=(fps_in_num/fps_in_den)], 1:[可变的输入帧率] | |
fps_in_num | 0:[使用默认值], n:[使用n值] -- 默认值为30 | |
fps_in_den | 0:[使用默认值], n:[使用n值] -- 默认值为1 | |
rc_mode | 码率控制模式(CBR、VBR、AVBR、FIXQP) | |
bps_target | 目标码率(CBR模式下使用) | |
bps_max | 码率上限(VBR模式下使用) | |
bps_min | 码率下限(VBR模式下使用) | |
split_mode | slice 切分模式(0:[不切分], 1:[根据slice大小], 2:[根据宏块或CTU个数切分]) | |
split_arg | ||
split_out | ||
osd_enable | 不生效 | |
osd_mode | 不生效 | |
user_data_enable | 不生效 | |
roi_enable | 不生效 |
此处采用一个死循环,首先从摄像头接口读出数据到pbuf,然后pbuf数据送入编码通道,最后编码通道会自动地从回调函数中输出NALU。
while(1){ ret = rgbcamera_getframe(pbuf); if(ret){ usleep(10*1000); continue; } push_frame_to_encMedia_channel(encodeChn_Id, pbuf, IMAGE_SIZE); usleep(10*1000); }
传输逻辑代码路径为:src/rtspServer/rtspServer.cpp。
RTSP服务器通过rtspServerInit()函数进行【流媒体环形队列初始化】和【RTSP服务器初始化】。其中服务器初始化包含:初始化服务端口、对多路码流进行初始化。示例如下所示。
int rtspServerInit(const char *moduleName) { RtspServer_t srv; memset(&srv, 0, sizeof(RtspServer_t)); srv.port = 554; srv.stream[0].bEnable = true; srv.stream[0].videoHooks.pConnectHook = VideoStreamConnect; srv.stream[0].videoHooks.pDataInHook = VideoStreamDataIn; srv.stream[0].audioHooks.pConnectHook = NULL; srv.stream[0].audioHooks.pDataInHook = NULL; strcpy(srv.stream[0].strName, "aabb"); create_video_frame_queue_pool(DATA_INPUT_CHN 1); //初始化流媒体环形队列 //create_audio_frame_queue_pool(DATA_INPUT_CHN 1); create_rtsp_Server(srv); //启动RTSP服务器事件循环 // 正常情况下不会走到这里 PRINT_ERROR("Fatal Error! RtspServer loop exited!"); return -1; }
RTSP服务器对象的描述结构体的内容如下所示。
描述结构体:RtspServer_t | |||
目录 | easyeai-api/netProtocol_api/rtsp/rtsp_data.h easyeai-api/netProtocol_api/rtsp/rtsp.h |
||
成员 | 描述 | ||
port | 服务器端口号 | ||
stream[n] | 该服务器所提供的码流服务 | ||
bEnable | 该码流是否使能 | ||
strName | 该码流名称。如:strName为"mainStream", url则为rtsp://username:password@192.168.1.69/mainStream |
||
videoHooks | 视频输入的钩子函数组 | ||
pConnectHook | 在客户端向服务器发起视频流连接时所调用的函数,不使用可填NULL | ||
pDataInHook | 服务端会从此函数获取视频流输入数据,不使用可填NULL | ||
audioHooks | 音频输入的钩子函数组 | ||
pConnectHook | 在客户端向服务器发起音频流连接时所调用的函数,不使用可填NULL | ||
pDataInHook | 服务端会从此函数获取音频流输入数据,不使用可填NULL |
视频连接钩子函数:在客户端向服务器发起视频流连接时所调用的函数,不使用可填NULL。
void VideoStreamConnect() { flush_video_channel(DATA_INPUT_CHN); } …… int rtspServerInit(const char *moduleName) { …… srv.stream[0].videoHooks.pConnectHook = VideoStreamConnect; …… }
视频连接钩子函数:服务端会从此函数获取视频流输入数据,不使用可填NULL。
int32_t VideoStreamDataIn(RTSPVideoDesc_t *pDesc, uint8_t *pData) { int ret = -1; VideoNodeDesc node; memset(&node, 0, sizeof(node)); ret = get_node_from_video_channel(DATA_INPUT_CHN, &node, pData); if(0 != ret){ return ret; } pDesc->frameType = node.bySubType; pDesc->frameIndex = node.dwFrameIndex; pDesc->dataLen = node.dwDataLen; pDesc->timeStamp = node.ddwTimeStamp; return 0; } …… int rtspServerInit(const char *moduleName) { …… srv.stream[0].videoHooks.pDataInHook = VideoStreamDataIn; …… }
Solution git仓库会随着产品迭代更新,不断新增解决方案代码,当前截图只作参考。
Solution工程构成如下所示,由功能组件easyeai-api和各个解决方案构成。
功能组件的描述如下所示,easyeai-api是经过高度封装的易用性组件接口,便于用户直接调用板卡资源。
功能 | 组件目录 | 组件子目录 | 描述 |
功能组件 | easyeai-api | algorithm_api | 算法组件 |
common_api | 通用组件 | ||
media_api | 多媒体组件 | ||
netProtocol_api | 网络协议组件 | ||
peripheral_api | 外设硬件组件 |
解决方案的描述如下所示,单个“solu-”开头的目录即为一个解决方案案例,代码内调用“EASY EAI-API”来满足某一实际应用场景的需求。
功能 | 工程目录 | 描述 |
解决方案 | solu-qrdecode | 二维码解决方案 |
solu-rtspMulitPlayer | RTMP推流解决方案 | |
…… | 持续更新 |
每个解决方案就是一个独立的项目,项目内包含部分如下所示,项目使用cmake构建自动编译部署。
具体介绍如下所示。
组成部分 | 描述 |
build.sh | 编译脚本,用于管理生成可执行文件后的部署准备工作,用户可自定义shell命令 |
CMakeLists.txt | 工程管理文件,用于组织整个工程结构,指导cmake生成Makefile |
include | 用于存放第三方应用库、头文件目录等 |
src | 用于存放实现本方案需求的源代码 |
可拓展的目录是指:开发过程中增加某些功能模块,功能代码。增加模式分为两种:
具体情况如下所示,第三方模块相关的文件由include/3rd_model/xxx.h、libs/3rd_model/xxx.a。自定义的功能模块为src/mySrcCode、src/mySrcCode2。
第一部分为配置部分,配置部分如下所示。(获取当前方案目录、配置工具链、提取方案名称):
配置信息如下所示。
配置项 | 描述 |
CMake要求版本 | cmake_minimum_required函数指定,要求的最低版本 |
CMAKE_SYSTEM_NAME | cmake的系统类型,交叉编译必须 |
CMAKE_CROSSCOMPILING | cmake是否启动交叉编译 |
cross.camke | camke_host_system_information获取平台信息,发现不是armv7l就导入当前平台的交叉编译配置。 |
project项目名 | 由project函数指定 |
第二部分是引入我司的功能组件库(针对当前方案进行:配置EASY EAI API头文件目录、库文件目录以及配置库链接参数):
配置信息如下所示。
配置项 | 描述 |
api_inc | 最终通过target_include_directories关键字指定目标包含的头文件路径 |
link_directories | 由link_directories关键字指定easyeai-api库所在路径 |
LINK_LIBRARIES | 由LINK_LIBRARIES关键字指定easyeai-api库文件 |
第三部分配置第三方的库(针对当前方案进行:配置第三方头文件目录、库文件目录、配置第三方库链接参数以及配置源码目录):
配置信息如下所示。
配置项 | 描述 |
custom_inc | 自定义变量custom_inc,最终通过target_include_directories函数指定目标包含的头文件路径,在源码include目录下 |
link_directories | 由link_directories函数指定第三方库所在路径 |
custom_libs | 自定义变量custom_libs,最终通过target_link_libraries函数指定目标引用的库链接参数 |
aux_source_directory | 自定义变量dir_srcs,用于添加工程代码以及自定义的个人代码 |
例如添加个人库的目录组成方式如下所示。
aux_source_directory的修改方式为:
aux_source_directory(./src ./src/mySrcCode ./src/mySrcCode2 dir_srcs)
或
aux_source_directory(./src dir_srcs) aux_source_directory(./src/mySrcCode dir_srcs) aux_source_directory(./src/mySrcCode2 dir_srcs)
第四部分配置项目的编译信息,内容如下所示:
配置项如下所示。
配置项 | 描述 |
add_executable | 编译结果为${CURRENT_FOLDER}指定,即方案目录名; 编译的源文件为${dir_srcs}指定; |
target_include_directories | 指定头文件的名字,由${api_inc}与${custom_inc}指定; |
第一部分用于提取目录用于编译操作,内容如下所示:(进入build.sh脚本所在目录,并且提取当前目录绝对路径,提取当前目录名称)
第二部分清除操作,清除目录为build、Release,内容如下所示:(执行build.sh脚本时,带入了参数“clear”,则清空编译输出)
第三部分,编译直接调用cmake,内容如下所示:(重新编译,成部署目录,并把资源自动部署进板卡)
免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如有疑问请发送邮件至:bangqikeconnect@gmail.com