我们接着分析幸狐官方的例程,看看怎样获得,打包,传输视频帧。

源码在这里

# 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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.