经过一个(多)月的折腾,算是把视频组件的架构和储存模块搓出来了。这篇博客自顶向下+自下到上描述rockchip风格的视频程序的结构设计。
作为计算机专业的老菜鸟,软件(工程)开发的新手,这篇博客用于讨论和记录如何学习和开展一个项目,包含一些个人的方法论,可能谈不上“正确指导作用”,欢迎交流。
Part 1 起草抽象化业务需求
首先,分析项目的需求,从需求入手。因为我们的目的是做项目和产品,对于整个开发来说,这是宇宙万法的源头(雾):
- 进行运动检测,更新录像帧率
- 进行视频录制,但是需要获得动态的录像帧率
- 进行实时推流,可能需要自动关闭以节约资源
为了降低代码的耦合性,方便对不同的业务进行后续的客制化,我们应该需要三个视频源,来分别应对这三个业务。举个例子,我们需要更改视频的帧率,但是如果随意更改帧率,会对实时运动检测造成影响。实际上,视频源的分离也是RK IPC在做的事情。这种设计可以在RKIPC文档(”/sdk_path/project/app/rkipc/rkipc”)找到:

我们将项目的需求进行解构,下一步应该是利用抽象和模型。(可能有天赋的人能自己设计或者想出来,但是我不知道也想不出来LOL, 于是就找示例程序和官方项目了)。
我们需要的“视频源”是能单独获得视频数据的东西,显然对此我们一无所知。这就回到我们之前的博客当中,需要自底向上学习单个视频源和视频输出的结构是怎样的。经过学习,根据rk的文档和源码,我们知道以下组件可以实现获得视频的流程:
- VI:这个东西是Video Input, 用来获取摄像头的视频(可以加ISP和AIQ处理获得人类想看的视频),输出的是原始视频的内容,比如RGB888原始格式。
- VENC:把输入的视频进行编码,编码成H264/5等格式,方便进行推流和储存,这一步调用的是硬件编码,很快。
- IVS: Intelligent Video Surveillance,智能监控模块,这是用来进行运动检测MD或者遮盖检测 OD的模块,基本由硬件执行,也很快。
- MB:内存池,基本是自动的,记得申请和销毁即可
- 其他模块:如OSD,VPSS,等项目迭代和优化再进行处理。
同时我们需要了解模块间的信息传递。根据MPI文档开头,常用的是,Bind,绑定,通过数据接收者绑定数据源来建立两者之间的关联关系,数据就会自动传输同步。
手册提供了绑定关系表,我们可以利用绑定传输图像进行绘制处理,也可以拿来控制帧率和编码。截图为部分绑定关系:

因此,结合我们自顶向下分析的业务抽象,我们就可设计这三条简单的视频传输通道 ,也就是如下视频源:
- VI 0 => bind VENC 0 => Storage 视频用于录制
- VI 1 => bind VENC 1 => RTSP/RTMP 视频用于推流
- VI 2 => bind IVS for OD MD 视频用于运动检测,更新帧率
需要的是有些模块也有自己的层级,比如VI 就分为vi设备(dev),输入 pipe、输出通道(channel)三个层级。
Part 2 学习实现抽象业务需求的实现
Part 2.0 学习项目结构,例程视频架构
根据简单的例程,比如sdk里面的例程(各路sample和example文件夹里)和luckfox的例程,我们应该已经学会怎样实现单条视频通道,并且熟悉视频的获取,下一步就是选择合适的代码结构,并且加入系统编程的元素。
我目前仅仅对Linux有了解,能熟练使用C语言(没错,就是大学和教材那种C语言基础),那么下一步要做的是看官方完整的工程。
我们先分析和学习Rockchip的IPC示例工程是怎么组织项目的。
“/your_sdk/project/app/rkipc/rkipc“ 里面有build,cmake,common,docs,lib,src这六个文件夹,还有.clang-format,CMakeLists.txt,format.sh,.gitignore, LICENSE这几个文件。
文件很简单,是编译规则,语法格式化,git需要忽略的文件(夹),许可证。
对于文件夹,build,cmake是编译相关,前文提到过docs是文档。
至于代码和库的本体,分别是:
common,用来储存各种通用模块与接口

lib,根据编译器存放的静态和动态库

src,源代码,不同的子项目使用不同的文件夹,本次借鉴(抄)的代码是rv1106_ipc

更多细节的规范可以借鉴Rockchip_Developer_Guide_Linux_RKIPC_CN,我们这次只是观摩学习。
现在我们已经学习了一种项目文件的组织方式,理明白了什么代码在哪里,就可以寻找对我们有用的部分了。
查看main.c来分析项目流程。
根据分析, main.c文件的作用有:
- 入口(废话)
- 信号处理和外部事件处理,例如sig_proc,用于终止程序,wait_key_event用来处理关键事件
- 主程序启动参数处理,比如变量short_options,long_options,函数usage_tip,rkipc_get_opt
- 功能和模块的初始化,你会发现一堆init函数
- 保证所有功能和模块一直执行,这也是while (g_main_run_)的意义,空转
- 循环过后是一堆去初始化deinit函数
这里我们其实就可以看出来g_main_run_是非常重要的控制变量,如果这个变量变成了false,程序马上就会终止while,去执行deinit部分,退出。而g_main_run_这个变量就是由信号处理函数sig_proc控制和写入的。很好,我们学习到了一种设计,来对程序进度进行控制。
这里插一个技巧,函数不一定就是“干实事”,函数也可以作为标志来使程序有更好的可读性和debug,比如void rk_system_init() { LOG_DEBUG(“%s\n”, __func__); }就只用来显示函数名。
下面我们就可以看视频部分的代码。在main.c当中,视频相关的单个功能初始化有这两步,ISP初始化不是必须的,就是不初始化颜色会发黄。可以参考例程和手册:rk_isp_init(自己封装,就是先初始化ISP设备)=>>调用RK_MPI_SYS_Init。然后我们可以研究rk_video_init();
这个函数在video 文件夹的video.c里面。
这个初始化函数同样给一个静态的全局变量g_video_run_赋值1,可能有和main.c类似的设计。
从ini文件读取一些必要的参数后,就开始进行初始化,
对于我们的设计,初始化部分我们临时需要的看的就是初始化设备,初始化通道,也就是rkipc_vi_dev_init()和rkipc_pipe_0_init()这两个函数。里面有很详细的注释,结合前文提到的视频架构,我反而觉得不算信息差,只要C语言的基础数据结构过关,学会找引用,视频配置部分可以自行解决,这里不多说。
其中rk_param_get_string(“video.0:aq_step_p”, NULL);这种函数用于从ini文件读取参数,我们的工程目前不需要这样做,直接用常量配置即可。项目完成后再进行反思和升级。
现在我们已经可以配置绑定我们需要的三个通道,录制,推流,运动检测。
配置完成,下面要做的是学习怎样各自从配置好的视频源获取数据或者结果。从Luckfox的demo,获取一帧的数据不是难事。但是,数据和结果的获取需要一直进行,另外在每一个rkipc_pipe_X_init后半部分我们发现有pthread_create函数,没错,我们要用到多线程函数。
Part 2.1 多线程
个人观点:针对这种单核的小小SoC,我们依然使用了多线程,因为多线程不只是常说的“充分利用大型设备的多核CPU” ,线程可以作为我们任务的最小的单位,是一个进程允许“同时”执行多个任务的抽象,而进程间的通讯和切换要付出比线程更大的成本,针对我们的使用场景,使用线程可以很完美的解决问题。快说感谢操作系统!XD
另外因为我们在Linux编写多线程,也没有特别复杂和定制化的要求,我们可以选择符合POSIX标准的方式,使用pthread头文件来解决问题。没错,pthread前面那个P就是POSIX的意思。
根据我们研究代码的路线,我们目前找到了线程的创建,没关系,那就把线程创建看一下学一下。
pthread_create(&venc_thread_0, NULL, rkipc_get_venc_0, NULL);
rk选择了最简单的方式创建线程,也就是给pthread_create一个线程对象venc_thread_0,(通过pthread_t venc_thread_0; 直接创建,没什么初始化的)然后传入函数指针rkipc_get_venc_0,函数指针指向的函数这样声明,前面也可以加static
void *rkipc_get_venc_0(void *arg) {
// lalala
return NULL;}
在对线程创建和管理有个大概思路之前,我们看看线程运行的函数rkipc_get_venc_0。定位到rkipc_get_venc_0,看一下这个线程怎样设计。
static void *rkipc_get_venc_0(void *arg) {
LOG_DEBUG("#Start %s thread, arg:%p\n", __func__, arg);
prctl(PR_SET_NAME, "RkipcVenc0", 0, 0, 0);
VENC_STREAM_S stFrame;
VI_CHN_STATUS_S stChnStatus;
int loopCount = 0;
int ret = 0;
stFrame.pstPack = malloc(sizeof(VENC_PACK_S));
while (g_video_run_) {
// 5.get the frame
ret = RK_MPI_VENC_GetStream(VIDEO_PIPE_0, &stFrame, 2500);
if (ret == RK_SUCCESS) {
void *data = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
// fwrite(data, 1, stFrame.pstPack->u32Len, fp);
// fflush(fp);
// LOG_DEBUG("Count:%d, Len:%d, PTS is %" PRId64", enH264EType is %d\n", loopCount,
// stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS,
// stFrame.pstPack->DataType.enH264EType);
rkipc_rtsp_write_video_frame(0, data, stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
if ((stFrame.pstPack->DataType.enH264EType == H264E_NALU_IDRSLICE) ||
(stFrame.pstPack->DataType.enH264EType == H264E_NALU_ISLICE) ||
(stFrame.pstPack->DataType.enH265EType == H265E_NALU_IDRSLICE) ||
(stFrame.pstPack->DataType.enH265EType == H265E_NALU_ISLICE)) {
rk_storage_write_video_frame(0, data, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS, 1);
if (enable_rtmp)
rk_rtmp_write_video_frame(0, data, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS, 1);
} else {
rk_storage_write_video_frame(0, data, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS, 0);
if (enable_rtmp)
rk_rtmp_write_video_frame(0, data, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS, 0);
}
// 7.release the frame
ret = RK_MPI_VENC_ReleaseStream(VIDEO_PIPE_0, &stFrame);
if (ret != RK_SUCCESS) {
LOG_ERROR("RK_MPI_VENC_ReleaseStream fail %x\n", ret);
}
loopCount++;
} else {
LOG_ERROR("RK_MPI_VENC_GetStream timeout %x\n", ret);
}
}
if (stFrame.pstPack)
free(stFrame.pstPack);
// if (fp)
// fclose(fp);
return 0;
}
我们可以看到这个函数和main.c的思路是很相似的,也是有个运行标志g_video_run_来控制主while循环,最后return个0或者NULL线程就结束了。注意,是线程结束,不是进程结束。
其实这样又是一个设计,我们创建线程的地方可以放在相应资源初始化的函数里面,这样就不需要全部初始化之后统一启动管理线程,也不用担心线程在不合时宜的时候启动访问不该访问的东西。
现在我们掌握了怎样创建一个线程,有目前的这些,已经足够我们把程序跑起来了。
可是程序有入口也有出口,对于c语言来讲,释放占用的资源和内存更是重中之重。 这就涉及到deinit函数。rk的deinit是有一个总的去初始化入口rk_video_deinit(),这个总入口再一步步去初始化各个通道和视频源。
前面我们已经对这些进程和资源了然于胸,去初始化就变得小菜一碟了,记住三步走:
- 更新全局控制变量为false,让相应的线程自行退出while循环,释放内存资源(比如一帧视频)。
- 使用pthread_join(venc_thread_0, NULL);加入线程,保证线程的彻底结束和资源释放。
- 调用RK_MPI的函数,该解绑的解绑,该停止的就停止,该销毁的就销毁。
其中对于视频组件的销毁,一定注意不要提前禁用底层依赖项目,比如IVS和VENC还活动着,你直接把视频设备VI摧毁了,会导致上层组件结果和缓存无法释放,直接卡死循环。要从上到下清除。最后在主函数main调用RK_MPI_SYS_Exit();彻底退出。
Part 2.2 线程之间的通讯和同步
其实对于线程来说,他们的变量都是共享的,因此通讯也并不是难事。在研究RKIPC的代码的同时,我们已经见到了最基本和裸奔的通信方式:g_video_run_ 变量作为标志,控制线程是否跳过循环。
这是非常简单的模型,只要有信号或者调用去初始化就刷新运行标志,而且我们可以看到,不管是main.c里面的g_main_run_还是video.c里面的g_video_run_,都只有一个修改标志的入口,而且只有程序即将结束的时候才写一次,直接裸奔不进行额外的保护也没问题。
但是,在抽象和理想的情况下,操作系统的调度可比一行c语言指令细致的多,多线程是“同时”进行的。尤其是在多次次读写甚至“同时”读写的情况(比如我们可能需要更新当前帧率),如果当前写入/读取文件或者变量被打断,会让程序变得不可靠,甚至有难以预料的结果(鬼知道编译器是怎么编译的,操作系统是怎么调度线程和分配内存的)。
所以,我们需要一些“科技”来保护一些资源的读取与写入,还需要确保核心的步骤不能被打断。这些不能被打断的步骤和操作,我们称之为“原子操作”,取原子不可再分之意。而实现这些操作的科技就是锁和信号量和条件变量。 好了你可以在网上或者书上找关于锁的内容了,下班!
说到打断,阻止打断就是让打断者暂停,就提到了一个重要的概念,阻塞。既然我们在两个线程竞争写入同一个资源的时候,我们选择保护一个,另一个就得等待,这个就是阻塞。
但是逻辑上会出现一个问题,A先占用资源1,再占用资源2;B先占用资源2,再占用资源1,等这俩卧龙凤雏都各自占完第一个资源,再申请第二个资源的时候,于是A会等资源2, B会等资源1,俩人都不松口(没解锁怎么松!),成了鹬蚌相争了,两个线程都卡了,主线程也一直等这俩,整个程序不能继续进行,便成了死锁(死了啦)。
常见的线程同步和控制需要的技术或者模型有:
- 互斥锁(使用者的角度来说,是最基本的保护变量和原子性的手段,开头一加锁结尾一解锁就行了)
- 读写锁(适合读取频率远远大于写入频率,或者有一个写入多个读取这种情况,我们的帧率控制就是符合这两个条件)
- 条件变量(配合互斥锁使用,合理使用条件变量可以实现线程休眠,唤醒,给CPU更多的时钟周期)
- 信号量(可以更高级控制多个线程的运行,并且由”是和否“升级到了“数值“,本次项目没遇到这种模型,先不管它)
- 消息队列和管道(对于C来说不够原生啊,得用其他队列来搓,咱临时也永不上,不管它)
- 抽象模型:状态机(用不到,咱的状态很简单,只有高帧率和低帧率,可以用状态机表示两个状态,但是能跑就不改了XD )
这次的视频组件目前用到的只有读写锁。
读写锁分为读锁和写锁,把加锁和解锁包裹你想保护或者读取的变量或者代码部分就行了。
Part 2.3 帧率控制和读帧的方法
目前我们已经更新了帧率变量,可以自由快乐地读取+写入帧了。
30帧和1帧这种速度对计算机太慢了,我们完全可以让它等,定时睡眠,便有了
- 计时
- 循环周期减去消耗时间得到休眠时间
- 休眠
- 新一轮开始
RK的帧率控制就是这样的:
vi_2_send
static void *rkipc_get_vi_2_send(void *arg) {
LOG_DEBUG("#Start %s thread, arg:%p\n", __func__, arg);
prctl(PR_SET_NAME, "RkipcGetVi2", 0, 0, 0);
int ret;
int32_t loopCount = 0;
VIDEO_FRAME_INFO_S stViFrame;
int npu_cycle_time_ms = 1000 / rk_param_get_int("video.source:npu_fps", 10);
long long before_time, cost_time;
while (g_video_run_) {
before_time = rkipc_get_curren_time_ms();
ret = RK_MPI_VI_GetChnFrame(pipe_id_, VIDEO_PIPE_2, &stViFrame, 1000);
if (ret == RK_SUCCESS) {
void *data = RK_MPI_MB_Handle2VirAddr(stViFrame.stVFrame.pMbBlk);
uint8_t *phy_addr = (uint8_t *)RK_MPI_MB_Handle2PhysAddr(stViFrame.stVFrame.pMbBlk);
rkipc_rockiva_write_nv12_frame_by_phy_addr(
stViFrame.stVFrame.u32Width, stViFrame.stVFrame.u32Height, loopCount, phy_addr);
ret = RK_MPI_VI_ReleaseChnFrame(pipe_id_, VIDEO_PIPE_2, &stViFrame);
if (ret != RK_SUCCESS)
LOG_ERROR("RK_MPI_VI_ReleaseChnFrame fail %x", ret);
loopCount++;
} else {
LOG_ERROR("RK_MPI_VI_GetChnFrame timeout %x", ret);
}
cost_time = rkipc_get_curren_time_ms() - before_time;
if ((cost_time > 0) && (cost_time < npu_cycle_time_ms))
usleep((npu_cycle_time_ms - cost_time) * 1000);
}
return NULL;
}
上面的方法简单粗暴,还有第二种就不是帧率控制了,是控制VENC或者VI 的帧率,来达到源头改变,再使用epoll来等待新的文件句柄。
大佬lyphotoes的代码
https://gitee.com/lyphotoes2022/rockit_test.git
#include <stdint.h>
#include <stdio.h>
#include <sys/poll.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <sys/epoll.h>
#include "libavcodec/codec.h"
#include "libavcodec/codec_id.h"
#include "libavcodec/packet.h"
#include "libavformat/avio.h"
#include "libavutil/avutil.h"
#include "libavutil/mathematics.h"
#include "libavutil/mem.h"
#include "libavutil/pixfmt.h"
#include "rk_comm_video.h"
#include "rk_defines.h"
#include "rk_debug.h"
#include "rk_mpi_vi.h"
#include "rk_mpi_venc.h"
#include "rk_mpi_mb.h"
#include "rk_common.h"
#include "rk_mpi_sys.h"
#include "rk_comm_vi.h"
#include "rk_comm_venc.h"
#include "rk_type.h"
// ffmpeg
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include <libavutil/imgutils.h>
#include <libavutil/timestamp.h>
#include <libavutil/opt.h>
#define MAX_EVENTS (5)
#define DEV_NAME "/dev/video11"
int run_ = 1;
static uint64_t first_pkt_pts = 0;
void signal_handler(int signo)
{
printf("signal %d(%s) received\n", signo, strsignal(signo));
run_ = 0;
}
AVFormatContext* create_mp4_file(const char* filename)
{
AVOutputFormat* fmt = av_guess_format(NULL, filename, NULL);
AVFormatContext* ofmt_ctx = NULL;
if (!fmt) {
fprintf(stderr, "Could not guess output format\n");
return NULL;
}
if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, filename) < 0) {
fprintf(stderr, "Could not create output context\n");
return NULL;
}
return ofmt_ctx;
}
int add_video_stream(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx)
{
AVStream* video_st = avformat_new_stream(ofmt_ctx, codec_ctx->codec);
if (!video_st) {
fprintf(stderr, "Failed to allocate stream\n");
return -1;
}
if (avcodec_parameters_from_context(video_st->codecpar, codec_ctx) < 0) {
fprintf(stderr, "Failed to copy codec parameters\n");
return -1;
}
video_st->codecpar->codec_tag = 0;
ofmt_ctx->streams[ofmt_ctx->nb_streams - 1]->time_base = (AVRational){1, 30}; // Assuming 30 fps
return 0;
}
int write_frame(AVFormatContext* ofmt_ctx,void *pdata, VENC_STREAM_S *pstFrame)
{
int ret;
int flag = 0;
AVStream *video_stream = ofmt_ctx->streams[0];
AVPacket packet = {0};
uint64_t pts;
if (pstFrame->pstPack->DataType.enH264EType == H264E_NALU_IDRSLICE || pstFrame->pstPack->DataType.enH264EType == H264E_NALU_ISLICE || pstFrame->pstPack->DataType.enH265EType == H265E_NALU_IDRSLICE || pstFrame->pstPack->DataType.enH265EType == H265E_NALU_ISLICE) {
flag |= AV_PKT_FLAG_KEY;
}
if (first_pkt_pts == 0) {
first_pkt_pts = pstFrame->pstPack->u64PTS;
}
pts = (pstFrame->pstPack->u64PTS - first_pkt_pts)/ 1000;
packet.data = pdata;
packet.size = pstFrame->pstPack->u32Len;
packet.pts = av_rescale_q(pts, (AVRational) {1,1000}, video_stream->time_base);
packet.dts = packet.pts;
packet.stream_index = video_stream->index;
packet.duration = av_rescale_q(1, (AVRational) {1,30}, video_stream->time_base);
packet.flags |= flag;
ret = av_interleaved_write_frame(ofmt_ctx,&packet);
av_packet_unref(&packet);
return ret;
}
int main(int argc, char **argv)
{
RK_S32 ret;
RK_S32 viId = 0;
RK_S32 channelId = 0;
RK_S32 width = 1920;
RK_S32 height = 1080;
RK_S32 maxWidth = 1920;
RK_S32 maxHeight = 1080;
RK_U32 gopSize = 60;
RK_U32 dstCodec = 8;// 8:H264, 12:H265
VI_DEV_ATTR_S stDevAttr;
VI_DEV_STATUS_S stDevStatus;
VI_CHN_ATTR_S stChnAttr;
VI_DEV_BIND_PIPE_S stBindPipe;
MPP_CHN_S stSrcChn, stDestChn;
VENC_CHN_ATTR_S stEncAttr;
RK_S32 loopCountSet = -1;
FILE *fp = NULL;
VENC_STREAM_S stFrame;
void *pData = RK_NULL;
VENC_RECV_PIC_PARAM_S stRecvParam;
char *out_filename = "test.mp4";
struct epoll_event ev, events[MAX_EVENTS];
int eventfd = -1 ;
int fd = 0;
int nfd = 0;
memset(&stRecvParam, 0, sizeof(stRecvParam));
memset(&stDevAttr, 0, sizeof(stDevAttr));
memset(&stBindPipe, 0, sizeof(stBindPipe));
memset(&stChnAttr, 0, sizeof(stChnAttr));
memset(&stEncAttr, 0, sizeof(stEncAttr));
memset(&stFrame, 0, sizeof(stFrame));
stChnAttr.stIspOpt.u32BufCount = 3;
stChnAttr.stIspOpt.enMemoryType = VI_V4L2_MEMORY_TYPE_DMABUF;
stChnAttr.stIspOpt.bNoUseLibV4L2 = RK_TRUE;
stChnAttr.u32Depth = 0;
stChnAttr.enPixelFormat = RK_FMT_YUV420SP;
stChnAttr.stFrameRate.s32DstFrameRate = -1;
stChnAttr.stFrameRate.s32SrcFrameRate = -1;
stChnAttr.stSize.u32Width = width;
stChnAttr.stSize.u32Height = height;
stChnAttr.enCompressMode = COMPRESS_MODE_NONE;
stChnAttr.stIspOpt.stMaxSize.u32Width = maxWidth;
stChnAttr.stIspOpt.stMaxSize.u32Height = maxHeight;
memcpy(stChnAttr.stIspOpt.aEntityName, DEV_NAME, strlen(DEV_NAME));
//初始化信号处理函数
signal(SIGINT, signal_handler);
signal(SIGPIPE, signal_handler);
signal(SIGTERM, signal_handler);
ret = RK_MPI_SYS_Init();
if (RK_SUCCESS != ret) {
RK_LOGE("rk mpi sys init failed!");
goto __FAILED;
}
// vi init begin
ret = RK_MPI_VI_QueryDevStatus(viId, &stDevStatus);
if (RK_SUCCESS == ret) {
if(!stDevStatus.bProbeOk){
RK_LOGE("sensor probe failed!");
goto __FAILED;
}
}
ret = RK_MPI_VI_GetDevAttr(viId, &stDevAttr);
if (RK_ERR_VI_NOT_CONFIG == ret) {
ret = RK_MPI_VI_SetDevAttr(viId, &stDevAttr);
if (RK_SUCCESS != ret) {
RK_LOGE("rk mpi vi set dev attr failed 0x%x!",ret);
goto __FAILED;
}
}else{
RK_LOGE("rk mpi vi set dev attr already!");
}
ret = RK_MPI_VI_GetDevIsEnable(viId);
if (RK_SUCCESS != ret) {
ret = RK_MPI_VI_EnableDev(viId);
if (RK_SUCCESS != ret) {
RK_LOGE("rk mpi vi enable dev failed 0x%x!",ret);
goto __FAILED;
}
stBindPipe.u32Num = 0;
stBindPipe.PipeId[0] = 0;
ret = RK_MPI_VI_SetDevBindPipe(viId, &stBindPipe);
if (ret != RK_SUCCESS) {
RK_LOGE("rk mpi vi set dev bind pipe failed 0x%x!",ret);
goto __FAILED;
}
}else{
RK_LOGE("rk mpi vi enable dev already!");
}
ret = RK_MPI_VI_SetChnAttr(0, channelId, &stChnAttr);
if (RK_SUCCESS != ret) {
RK_LOGE("rk mpi vi set chn attr failed 0x%x!",ret);
goto __FAILED;
}
ret = RK_MPI_VI_EnableChn(0, channelId);
if (RK_SUCCESS != ret) {
RK_LOGE("rk mpi vi enable chn failed 0x%x!",ret);
goto __FAILED;
}
// vi inited
// venc chn attr init
stEncAttr.stVencAttr.enType = dstCodec;
stEncAttr.stVencAttr.enPixelFormat = stChnAttr.enPixelFormat;
stEncAttr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
stEncAttr.stRcAttr.stH264Cbr.u32BitRate = 3000;
stEncAttr.stRcAttr.stH264Cbr.u32Gop = gopSize;
stEncAttr.stVencAttr.u32Profile = 100;
stEncAttr.stVencAttr.u32MaxPicWidth = maxWidth;
stEncAttr.stVencAttr.u32MaxPicHeight = maxHeight;
stEncAttr.stVencAttr.u32PicWidth = width;
stEncAttr.stVencAttr.u32PicHeight = height;
stEncAttr.stVencAttr.u32VirWidth = width;
stEncAttr.stVencAttr.u32VirHeight = height;
stEncAttr.stVencAttr.u32StreamBufCnt = 5;
stEncAttr.stVencAttr.u32BufSize = width * height / 2;
stEncAttr.stGopAttr.u32MaxLtrCount = 1;
// venc chn init
stRecvParam.s32RecvPicNum = loopCountSet;
printf("create venc\n");
ret = RK_MPI_VENC_CreateChn(0, &stEncAttr);
if (ret != RK_SUCCESS) {
RK_LOGE("rk mpi venc create chn failed 0x%x!\n",ret);
goto __FAILED;
}
ret = RK_MPI_VENC_StartRecvFrame(0, &stRecvParam);
if (ret != RK_SUCCESS) {
RK_LOGE("rk mpi venc start recv frame failed 0x%x!\n",ret);
goto __FAILED;
}
// bind
stSrcChn.enModId = RK_ID_VI;
stSrcChn.s32ChnId = viId;
stSrcChn.s32ChnId = channelId;
stDestChn.enModId = RK_ID_VENC;
stDestChn.s32DevId = 0;
stDestChn.s32ChnId = 0;
ret = RK_MPI_SYS_Bind(&stSrcChn, &stDestChn);
if (ret != RK_SUCCESS) {
RK_LOGE("rk mpi sys bind failed 0x%x!\n",ret);
goto __FAILED;
}
stFrame.pstPack = (VENC_PACK_S *) malloc(sizeof(VENC_PACK_S));
fd = RK_MPI_VENC_GetFd(0);
eventfd = epoll_create1(0);
if (eventfd == -1) {
printf("epoll create failed!\n");
goto __FAILED;
}
ev.data.fd = fd;
ev.events = EPOLLIN /* | EPOLLET */;
if (epoll_ctl(eventfd, EPOLL_CTL_ADD, fd,&ev) == -1) {
printf("epoll ctl failed!\n");
goto __FAILED;
}
// init ffmpeg
AVFormatContext * ofmt_ctx = create_mp4_file(out_filename);
if (!ofmt_ctx) {
fprintf(stderr, "Could not create output context\n");
goto __FAILED;
}
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
fprintf(stderr, "Codec not found.\n");
avformat_free_context(ofmt_ctx);
goto __FAILED;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
codec_ctx->codec_id = AV_CODEC_ID_H264;
codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
codec_ctx->bit_rate = 400000;
codec_ctx->width = 1920; // Assuming 1920x1080 resolution
codec_ctx->height = 1080;
codec_ctx->time_base = (AVRational){1, 30}; // Assuming 30 fps
codec_ctx->gop_size = 60;
codec_ctx->max_b_frames = 1;
codec_ctx->pix_fmt = AV_PIX_FMT_NV12;
if (add_video_stream(ofmt_ctx, codec_ctx) < 0) {
fprintf(stderr, "Could not add video stream\n");
avformat_free_context(ofmt_ctx);
goto __FAILED;
}
if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE) < 0) {
fprintf(stderr, "Could not open output file '%s'\n", out_filename);
avformat_free_context(ofmt_ctx);
goto __FAILED;
}
}
if (avformat_write_header(ofmt_ctx, NULL) < 0) {
fprintf(stderr, "Error occurred when opening output file\n");
avformat_free_context(ofmt_ctx);
goto __FAILED;
}
printf("begin loop\n");
while (run_) {
nfd = epoll_wait(eventfd, events, MAX_EVENTS, 20);
if (nfd >0) {
for(int i = 0; i < nfd; i++){
if (events[i].data.fd == fd) {
ret = RK_MPI_VENC_GetStream(0, &stFrame,500);
if (ret == RK_SUCCESS) {
pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
if (pData) {
ret = write_frame(ofmt_ctx,pData,&stFrame);
}
ret = RK_MPI_VENC_ReleaseStream(0, &stFrame);
if (ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VENC_ReleaseStream fail 0x%x", ret);
}
} else {
RK_LOGE("RK_MPI_VI_GetChnFrame fail 0x%x", ret);
}
}
}
}else if(nfd == 0){
// timeout
}else{
printf("epoll wait failed!\n");
break;
}
}
if(eventfd != -1){
close(eventfd);
}
if(fd){
RK_MPI_VENC_CloseFd(0);
}
av_write_trailer(ofmt_ctx);
avio_closep(&ofmt_ctx->pb);
if(fp)
fclose(fp);
avcodec_free_context(&codec_ctx);
avformat_free_context(ofmt_ctx);
RK_MPI_SYS_UnBind(&stSrcChn, &stDestChn);
RK_MPI_VI_DisableChn(viId,channelId );
RK_MPI_VENC_StopRecvFrame(0);
RK_MPI_VENC_DestroyChn(0);
RK_MPI_VI_DisableDev(viId);
if (stFrame.pstPack) {
free(stFrame.pstPack);
}
RK_MPI_SYS_Exit();
return 0;
__FAILED:
return -1;
}
注意文件句柄和epoll要查看RK_MPI手册的建议和实现。

Part 3 本项目的示例结构
3.2 文件结构
tree
有点懒,没弄通用模块,业务分文件放到src里面,然后库的源代码可以放到3rdparty拿来编译安装或者仅当头文件进行参考,我是当头文件参考去了,库手动编译成so和a文件放到lib里面,就没有设置成每次编译相应的库。
otal 60 drwxrwxr-x 10 range range 4096 Jan 31 22:48 . drwxrwxr-x 22 range range 4096 Jan 29 13:53 .. drwxr-xr-x 6 root root 4096 Jan 22 16:21 3rdparty drwxr-xr-x 4 root root 4096 Jan 31 22:48 build -rwxr-xr-x 1 root root 1537 Jan 26 21:20 build.sh -rw-r--r-- 1 root root 4670 Jan 22 16:21 CMakeLists.txt drwxrwxr-x 8 range range 4096 Jan 27 00:17 .git -rw-rw-r-- 1 range range 30 Jan 4 19:43 .gitignore drwxr-xr-x 5 range range 4096 Jan 22 16:21 include drwxr-xr-x 3 root root 4096 Jan 31 22:48 install drwxr-xr-x 2 range range 4096 Jan 22 16:21 lib -rw-r--r-- 1 root root 644 Jan 22 16:21 README.md drwxr-xr-x 2 root root 4096 Jan 22 16:21 src src ├── main.c ├── sqlite.c ├── storage.c ├── util_comm.c └── video.c ls -al 3rdparty total 28 drwxr-xr-x 6 root root 4096 Jan 22 16:21 . drwxrwxr-x 10 range range 4096 Jan 31 22:48 .. drwxr-xr-x 4 root root 4096 Jan 22 16:21 allocator -rw-r--r-- 1 root root 719 Jan 22 16:21 CMakeLists.txt drwxr-xr-x 5 root root 4096 Jan 22 16:21 librga drwxr-xr-x 10 root root 4096 Jan 22 16:21 media-server drwxr-xr-x 5 root root 4096 Jan 22 16:21 rknpu2
3.2 主函数
src/main.c
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/poll.h>
#include <time.h>
#include <unistd.h>
#include "sample_comm.h"
#include "pthread.h"
#include "video.h"
#include "sqlite_comm.h"
#include "util_comm.h"
static int g_main_run_ = 1;
static void sig_proc(int);
static void sig_proc(int signo)
{
printf("received signo %d \n", signo);
g_main_run_ = 0;
}
int main(int argc, char *argv[])
{
signal(SIGTERM, sig_proc);
signal(SIGINT, sig_proc);
system("RkLunch-stop.sh");
printf("System date: %s\n", get_date_string());
// init storage
storage_init();
//init mpi
RK_MPI_SYS_Init(); rk_video_init(); testSQLite();
while (g_main_run_ == 1)
{
sleep(1000);
}
sleep(2);
rk_video_deinit(); RK_MPI_SYS_Exit();storage_deinit();
return 0;
}
3.3 video.h
include/video.h
static void *rkipc_get_venc_2(void *arg); static void *rkipc_get_venc_0(void *arg); static void *rkipc_get_venc_1(void *arg); static int motion_detecter(int); static int rkipc_aiq_init(); static int rkipc_vi_dev_init(); static int rkipc_vi_dev_deinit(); static int rkipc_pipe_0_init(); static int rkipc_pipe_0_deinit(); static int rkipc_pipe_1_init(); static int rkipc_pipe_1_deinit(); static int rkipc_pipe_2_init(); static int rkipc_pipe_2_deinit(); static int rkipc_ivs_init(); static int rkipc_ivs_deinit(); static int frame_rate_setter(int, int); int rk_video_deinit(); int rk_video_init();
视频的架构暂时分析到这里,等demo完成后会加入osd来打日期,甚至标记运动的框框。
下一篇记录储存,主要想记录的东西有:引入第三方库media-server,文件处理,hls文件处理,日期切换和切片控制逻辑,线程条件锁。
Views: 86
