我们接着分析幸狐官方的例程,看看怎样获得,打包,传输视频帧。
源码在这里
# VI 组件获取摄像头图像标注帧率例程 #git clone https://github.com/luckfox-eng29/luckfox_pico_rtsp_opencv.git
具体的视频架构官方网站已经很明显了,请自行浏览幸狐官方wiki,因此这里只讨论一些main文件中的细节和设计问题。感谢幸狐的封装,我们大概率将在实战的时候再会研究具体的封装。
幸狐封装的一些函数
/*****************************************************************************
* | Author : Luckfox team
* | Function :
* | Info :
*
*----------------
* | This version: V1.0
* | Date : 2024-04-07
* | Info : Basic version
*
******************************************************************************/
#include "luckfox_mpi.h"
RK_U64 TEST_COMM_GetNowUs() {
struct timespec time = {0, 0};
clock_gettime(CLOCK_MONOTONIC, &time);
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
}
int vi_dev_init() {
printf("%s\n", __func__);
int ret = 0;
int devId = 0;
int pipeId = devId;
VI_DEV_ATTR_S stDevAttr;
VI_DEV_BIND_PIPE_S stBindPipe;
memset(&stDevAttr, 0, sizeof(stDevAttr));
memset(&stBindPipe, 0, sizeof(stBindPipe));
// 0. get dev config status
ret = RK_MPI_VI_GetDevAttr(devId, &stDevAttr);
if (ret == RK_ERR_VI_NOT_CONFIG) {
// 0-1.config dev
ret = RK_MPI_VI_SetDevAttr(devId, &stDevAttr);
if (ret != RK_SUCCESS) {
printf("RK_MPI_VI_SetDevAttr %x\n", ret);
return -1;
}
} else {
printf("RK_MPI_VI_SetDevAttr already\n");
}
// 1.get dev enable status
ret = RK_MPI_VI_GetDevIsEnable(devId);
if (ret != RK_SUCCESS) {
// 1-2.enable dev
ret = RK_MPI_VI_EnableDev(devId);
if (ret != RK_SUCCESS) {
printf("RK_MPI_VI_EnableDev %x\n", ret);
return -1;
}
// 1-3.bind dev/pipe
stBindPipe.u32Num = pipeId;
stBindPipe.PipeId[0] = pipeId;
ret = RK_MPI_VI_SetDevBindPipe(devId, &stBindPipe);
if (ret != RK_SUCCESS) {
printf("RK_MPI_VI_SetDevBindPipe %x\n", ret);
return -1;
}
} else {
printf("RK_MPI_VI_EnableDev already\n");
}
return 0;
}
int vi_chn_init(int channelId, int width, int height) {
int ret;
int buf_cnt = 2;
// VI init
VI_CHN_ATTR_S vi_chn_attr;
memset(&vi_chn_attr, 0, sizeof(vi_chn_attr));
vi_chn_attr.stIspOpt.u32BufCount = buf_cnt;
vi_chn_attr.stIspOpt.enMemoryType =
VI_V4L2_MEMORY_TYPE_DMABUF; // VI_V4L2_MEMORY_TYPE_MMAP;
vi_chn_attr.stSize.u32Width = width;
vi_chn_attr.stSize.u32Height = height;
vi_chn_attr.enPixelFormat = RK_FMT_YUV420SP;
vi_chn_attr.enCompressMode = COMPRESS_MODE_NONE; // COMPRESS_AFBC_16x16;
vi_chn_attr.u32Depth = 2;
ret = RK_MPI_VI_SetChnAttr(0, channelId, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(0, channelId);
if (ret) {
printf("ERROR: create VI error! ret=%d\n", ret);
return ret;
}
return ret;
}
int vpss_init(int VpssChn, int width, int height) {
printf("%s\n",__func__);
int s32Ret;
VPSS_CHN_ATTR_S stVpssChnAttr;
VPSS_GRP_ATTR_S stGrpVpssAttr;
int s32Grp = 0;
stGrpVpssAttr.u32MaxW = 4096;
stGrpVpssAttr.u32MaxH = 4096;
stGrpVpssAttr.enPixelFormat = RK_FMT_YUV420SP;
stGrpVpssAttr.stFrameRate.s32SrcFrameRate = -1;
stGrpVpssAttr.stFrameRate.s32DstFrameRate = -1;
stGrpVpssAttr.enCompressMode = COMPRESS_MODE_NONE;
stVpssChnAttr.enChnMode = VPSS_CHN_MODE_USER;
stVpssChnAttr.enDynamicRange = DYNAMIC_RANGE_SDR8;
stVpssChnAttr.enPixelFormat = RK_FMT_RGB888;
stVpssChnAttr.stFrameRate.s32SrcFrameRate = -1;
stVpssChnAttr.stFrameRate.s32DstFrameRate = -1;
stVpssChnAttr.u32Width = width;
stVpssChnAttr.u32Height = height;
stVpssChnAttr.enCompressMode = COMPRESS_MODE_NONE;
s32Ret = RK_MPI_VPSS_CreateGrp(s32Grp, &stGrpVpssAttr);
if (s32Ret != RK_SUCCESS) {
return s32Ret;
}
s32Ret = RK_MPI_VPSS_SetChnAttr(s32Grp, VpssChn, &stVpssChnAttr);
if (s32Ret != RK_SUCCESS) {
return s32Ret;
}
s32Ret = RK_MPI_VPSS_EnableChn(s32Grp, VpssChn);
if (s32Ret != RK_SUCCESS) {
return s32Ret;
}
s32Ret = RK_MPI_VPSS_StartGrp(s32Grp);
if (s32Ret != RK_SUCCESS) {
return s32Ret;
}
return s32Ret;
}
int venc_init(int chnId, int width, int height, RK_CODEC_ID_E enType) {
printf("%s\n",__func__);
VENC_RECV_PIC_PARAM_S stRecvParam;
VENC_CHN_ATTR_S stAttr;
memset(&stAttr, 0, sizeof(VENC_CHN_ATTR_S));
// RTSP H264
stAttr.stVencAttr.enType = enType;
//stAttr.stVencAttr.enPixelFormat = RK_FMT_YUV420SP;
stAttr.stVencAttr.enPixelFormat = RK_FMT_RGB888;
stAttr.stVencAttr.u32Profile = H264E_PROFILE_MAIN;
stAttr.stVencAttr.u32PicWidth = width;
stAttr.stVencAttr.u32PicHeight = height;
stAttr.stVencAttr.u32VirWidth = width;
stAttr.stVencAttr.u32VirHeight = height;
stAttr.stVencAttr.u32StreamBufCnt = 2;
stAttr.stVencAttr.u32BufSize = width * height * 3 / 2;
stAttr.stVencAttr.enMirror = MIRROR_NONE;
stAttr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
stAttr.stRcAttr.stH264Cbr.u32BitRate = 3 * 1024;
stAttr.stRcAttr.stH264Cbr.u32Gop = 1;
RK_MPI_VENC_CreateChn(chnId, &stAttr);
memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
stRecvParam.s32RecvPicNum = -1;
RK_MPI_VENC_StartRecvFrame(chnId, &stRecvParam);
return 0;
}
以下是入口文件的入口函数
入口函数以及业务逻辑
int main(int argc, char *argv[]) {
RK_S32 s32Ret = 0;
int sX,sY,eX,eY;
int width = 2304;
int height = 1296;
char fps_text[16];
float fps = 0;
memset(fps_text,0,16);
//h264_frame
VENC_STREAM_S stFrame;
stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S));
VIDEO_FRAME_INFO_S h264_frame;
VIDEO_FRAME_INFO_S stVpssFrame;
// rkaiq init
RK_BOOL multi_sensor = RK_FALSE;
const char *iq_dir = "/etc/iqfiles";
rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL;
//hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2;
SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir);
SAMPLE_COMM_ISP_Run(0);
// rkmpi init
if (RK_MPI_SYS_Init() != RK_SUCCESS) {
RK_LOGE("rk mpi sys init fail!");
return -1;
}
// rtsp init
rtsp_demo_handle g_rtsplive = NULL;
rtsp_session_handle g_rtsp_session;
g_rtsplive = create_rtsp_demo(554);
g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0");
rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime());
// vi init
vi_dev_init();
vi_chn_init(0, width, height);
// vpss init
vpss_init(0, width, height);
// bind vi to vpss
MPP_CHN_S stSrcChn, stvpssChn;
stSrcChn.enModId = RK_ID_VI;
stSrcChn.s32DevId = 0;
stSrcChn.s32ChnId = 0;
stvpssChn.enModId = RK_ID_VPSS;
stvpssChn.s32DevId = 0;
stvpssChn.s32ChnId = 0;
printf("====RK_MPI_SYS_Bind vi0 to vpss0====\n");
s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvpssChn);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("bind 0 ch venc failed");
return -1;
}
// venc init
RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC;
venc_init(0, width, height, enCodecType);
while(1)
{
// get vpss frame
s32Ret = RK_MPI_VPSS_GetChnFrame(0,0, &stVpssFrame,-1);
if(s32Ret == RK_SUCCESS)
{
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
cv::Mat frame(height,width,CV_8UC3, data);
sprintf(fps_text,"fps = %.2f",fps);
cv::putText(frame,fps_text,
cv::Point(40, 40),
cv::FONT_HERSHEY_SIMPLEX,1,
cv::Scalar(0,255,0),2);
memcpy(data, frame.data, width * height * 3);
}
// send stream
// encode H264
RK_MPI_VENC_SendFrame(0, &stVpssFrame,-1);
// rtsp
s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1);
if(s32Ret == RK_SUCCESS)
{
if(g_rtsplive && g_rtsp_session)
{
//printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS);
rtsp_do_event(g_rtsplive);
}
RK_U64 nowUs = TEST_COMM_GetNowUs();
fps = (float) 1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS);
}
// release frame
s32Ret = RK_MPI_VPSS_ReleaseChnFrame(0, 0, &stVpssFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret);
}
s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
}
}
RK_MPI_SYS_UnBind(&stSrcChn, &stvpssChn);
RK_MPI_VI_DisableChn(0, 0);
RK_MPI_VI_DisableDev(0);
RK_MPI_VPSS_StopGrp(0);
RK_MPI_VPSS_DestroyGrp(0);
SAMPLE_COMM_ISP_Stop(0);
RK_MPI_VENC_StopRecvFrame(0);
RK_MPI_VENC_DestroyChn(0);
free(stFrame.pstPack);
if (g_rtsplive)
rtsp_del_demo(g_rtsplive);
RK_MPI_SYS_Exit();
return 0;
我们需要存放h264帧,需要以下操作
//h264_frame VENC_STREAM_S stFrame; //定义帧码流类型结构体。 stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S)); //pstPack是帧码流包结构,VENC_PACK_S是帧码流包结构体,显然储存了一个指针 VIDEO_FRAME_INFO_S h264_frame; // 这行没用到 VIDEO_FRAME_INFO_S stVpssFrame;// 定义视频图像帧信息结构体,这就是我们要的一帧的照片数据储存的地方
主要还是里面的while循环
s32Ret = RK_MPI_VPSS_GetChnFrame(0,0, &stVpssFrame,-1);//获取一帧的数据,写入到stVpssFrame里面
if(s32Ret == RK_SUCCESS) //获取成功
{
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
//用户获取YUV图像数据,可以通过RK_MPI_MB_Handle2VirAddr转换pMbBlk成虚拟地址来使用
//我们得到的就是YUV数据直接存就可以,RK官方甚至直接说这样存,也是没谁了
//data = RK_MPI_MB_Handle2VirAddr(sFrame.stVFrame.pMbBlk);
//fwrite(data, 1, sFrame.stVFrame.u32Width * sFrame.stVFrame.u32Height * 3 /2, fp);
//fflush(fp);
cv::Mat frame(height,width,CV_8UC3, data); //可见opencv读的是CV_8UC38 位无符号整型的 3 通道图像
sprintf(fps_text,"fps = %.2f",fps);
cv::putText(frame,fps_text,cv::Point(40, 40),cv::FONT_HERSHEY_SIMPLEX,1,cv::Scalar(0,255,0),2);
memcpy(data, frame.data, width * height * 3); //将打上文字的图像帧写回
}
// send stream这里开始编码加推流
// encode H264
RK_MPI_VENC_SendFrame(0, &stVpssFrame,-1); //将相应帧(也就是VPSS的stVpssFrame)推送到VENC进行编码
// rtsp
s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1); //从VENC读取压缩后的文件,然后写回到stFrame
if(s32Ret == RK_SUCCESS)
{
if(g_rtsplive && g_rtsp_session)
{
//printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
//上面这一行是官方留的调试信息
void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);//这里的pMbBlk
//void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);对比下
rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len,stFrame.pstPack->u64PTS);
rtsp_do_event(g_rtsplive);
}
RK_U64 nowUs = TEST_COMM_GetNowUs();
fps = (float) 1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS);
}
不过时间不早了,下次再分解我们需要仔细研究的结构体stFrame和stVpssFrame。
让我们看看它们有那些成员,以及怎样用这些结构体提取出保留视频所需的全部信息。我们目前不是很关注视频时怎样储存到VENC的,所以先研究一下H264帧。
//h264_frame VENC_STREAM_S stFrame;
先看看源代码。
//rk_comm_venc.h
/* Defines the features of an stream */
typedef struct rkVENC_STREAM_S {
VENC_PACK_S ATTRIBUTE *pstPack; /* R; stream pack attribute*/
RK_U32 ATTRIBUTE u32PackCount; /* R; the pack number of one frame stream*/
RK_U32 u32Seq; /* R; the list number of stream*/
union {
VENC_STREAM_INFO_H264_S stH264Info; /* R; the stream info of h264*/
VENC_STREAM_INFO_JPEG_S stJpegInfo; /* R; the stream info of jpeg*/
VENC_STREAM_INFO_H265_S stH265Info; /* R; the stream info of h265*/
VENC_STREAM_INFO_PRORES_S stProresInfo; /* R; the stream info of prores*/
};
union {
VENC_STREAM_ADVANCE_INFO_H264_S
stAdvanceH264Info; /* R; the stream info of h264*/
VENC_STREAM_ADVANCE_INFO_JPEG_S
stAdvanceJpegInfo; /* R; the stream info of jpeg*/
VENC_STREAM_ADVANCE_INFO_H265_S
stAdvanceH265Info; /* R; the stream info of h265*/
VENC_STREAM_ADVANCE_INFO_PRORES_S
stAdvanceProresInfo; /* R; the stream info of prores*/
};
} VENC_STREAM_S;
这里可以结合rkmpi文档来看。

很好,不过根据后面推流的函数所需的东西是pstPack来看,我们先研究一下这个pstPack指针。这个指针对应的类型是VENC_PACK_S,VENC_PACK_S ATTRIBUTE *pstPack;其中ATTRIBUTE 是一个宏,用来进行结构体的内存对齐,比如以下代码段可以确定使用该宏的变量会被16字节对齐,用来优化内存或者适配特定的硬件框架。
//rk_common.h #define ALIGN_NUM 16 #define ATTRIBUTE __attribute__((aligned(ALIGN_NUM)))
//rk_comm_venc.h
/* Defines a stream packet */
typedef struct rkVENC_PACK_S {
MB_BLK pMbBlk; /* R; the virtual address of stream */
RK_U32 u32Len; /* R; the length of stream */
RK_U64 u64PTS; /* R; PTS */
RK_BOOL bFrameEnd; /* R; frame end */
RK_BOOL bStreamEnd; /* R; stream end */
VENC_DATA_TYPE_U DataType; /* R; the type of stream */
RK_U32 u32Offset; /* R; the offset between the Valid data and the start
address */
RK_U32 u32DataNum; /* R; the stream packets num */
VENC_PACK_INFO_S stPackInfo[8]; /* R; the stream packet Information */
} VENC_PACK_S;
继续寻找,分析pstPack。
不错,注释很详细,我想我们是可以利用这些信息进行视频封装的。在开始学习视频封装成文件之前,先看看推流用了什么东西。从效果来看,推流也是实现视频了,既然有现成的,那就分析分析可不可以直接使用推流所需的信息,毕竟我对视频文件格式还一无所知。
推流方面rtsp函数使用了这些视频帧信息作为参数
(uint8_t *)pData, stFrame.pstPack->u32Len,stFrame.pstPack->u64PTS
根据AI的回答
在RTSP推流过程中,这三个参数是非常关键的,它们分别代表了视频帧的数据、长度和时间戳。具体来说:
*(uint8_t )pData:这是指向视频帧数据的指针。它包含了实际的视频数据,需要被编码并发送到RTSP服务器。
stFrame.pstPack->u32Len:这是视频帧数据的长度。它告诉RTSP服务器当前帧的数据大小,以便正确地处理和传输数据。
stFrame.pstPack->u64PTS:这是视频帧的时间戳(Presentation Timestamp)。时间戳用于同步音视频流,确保视频帧在正确的时间被播放。
应该离答案很接近了,我们下一步就是根据这些帧信息写视频文件。
Views: 97
