Hi3531D调试手记(四):使用ffmpeg实时封装H264视频为MP4

本文详细解析了一段基于FFmpeg的H264视频封装为MP4的C代码,主要涵盖了封装流程、创建MP4文件、写入视频帧、处理SPS和PPS以及添加新流等步骤。代码针对实时视频封装,不涉及音频和视频编码。此外,还探讨了将代码改造为支持H265封装的注意事项。
摘要由CSDN通过智能技术生成

Ubuntu系统版本:Ubuntu 16.04.7 LTS

一、前言

  参考博客 最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)、博客 海思HI3531D使用ffmpeg实时封装多路H264视频+AAC音频为MP4 以及博客 hisi3559A平台VENC获取H264裸流封装成mp4,完美贴合项目需求,确实有被帮助到,感谢几位博主的无私分享。

  • 本文记录的代码为第二篇参考博客中贴出代码的个人需求向调整版,仅为实现实时视频封装,无视频编码需求以及音频封装需求,因此舍去了很多内容。
  • 代码中基本保留了参考博主的注释,同时也增加了部分自己在修改与调试过程中发现的问题与理解。
  • ffmpeg使用的版本为 4.3.2,交叉编译过程记录在 Hi3531D调试手记(一):Linux开发环境搭建 的第四节。
  • ffmpeg 这玩意儿确实很大,最直观的对比就是不加 ffmpeg 时编译出来的软件大小是 1MB,加上重新编译一下就是 74MB了。离谱。

二、封装流程概览

  封装的流程可以从代码结构上很清楚地反映出来。
在这里插入图片描述

三、实现代码

1. 头文件、自定义结构体及宏参数

#include "libavformat/avformat.h"
#define USING_SEQ   // USING_SEQ 和 USING_PTS 是原博主提供的两种计算 PTS、DTS 的方法
#define STREAM_FRAME_RATE 60		// 输出帧率

// 删除了音频相关以及后续并未使用的元素
typedef struct
{
	AVFormatContext* g_OutFmt_Ctx[VENC_MAX_CHN_NUM];	//每个通道的AVFormatContext
	int vi[VENC_MAX_CHN_NUM];							//视频流索引号
	HI_BOOL b_First_IDR_Find[VENC_MAX_CHN_NUM];			//第一帧是I帧标志
	long int VptsInc[VENC_MAX_CHN_NUM];					//用于视频帧递增计数
	HI_U64 Video_PTS[VENC_MAX_CHN_NUM];					//视频PTS
	HI_U64 Vfirst[VENC_MAX_CHN_NUM];					//视频第一帧标志
	char filename[VENC_MAX_CHN_NUM][1024];				//文件名
}FfmpegConf;

2. 创建mp4文件

  这一部分的代码很好理解,没有什么需要特别改动的地方。av_register_all() 在后面的函数中有可能会用上,这里可以暂时先放着。

//参数:通道号,文件名,fc
int HI_PDT_CreateMp4(VENC_CHN VeChn, char *pfile, FfmpegConf *fc)
{
    int ret = 0; 
    char pszFileName[256] = {0};			//保存文件名
    AVOutputFormat *pOutFmt = NULL;			//输出Format指针
	sprintf(pszFileName,"%s.mp4",pfile);

    // av_register_all();                      //注册所有编解码器、复用/解复用组件
  
	// 初始化输出视频码流的AVFormatContext
    avformat_alloc_output_context2(&(fc->g_OutFmt_Ctx[VeChn]), NULL, NULL, pszFileName);
	if (NULL == fc->g_OutFmt_Ctx[VeChn])	//失败处理
    {	
        printf("Could not deduce output format from file extension: using mp4. \n");
        avformat_alloc_output_context2(&(fc->g_OutFmt_Ctx[VeChn]), NULL, "mp4", pszFileName);
		if (NULL == fc->g_OutFmt_Ctx[VeChn])
    	{
    		printf("avformat_alloc_output_context2 failed\n");
        	return -1;
    	}
    }

    pOutFmt = fc->g_OutFmt_Ctx[VeChn]->oformat;		//获取输出Format指针
    if (pOutFmt->video_codec == AV_CODEC_ID_NONE)	//检查视频编码器
    {
        printf("add_video_stream ID failed\n"); 
		goto exit_outFmt_failed;
	}
    if (!(pOutFmt->flags & AVFMT_NOFILE))	        //应该是判断文件IO是否打开
    {
        ret = avio_open(&(fc->g_OutFmt_Ctx[VeChn]->pb), pszFileName, AVIO_FLAG_WRITE);	//创建并打开mp4文件
        if (ret < 0)
        {
            printf("could not create video file %s.\n",pszFileName);
            goto exit_vio_open_failed;
        }
    }

	//初始化一些参数
	fc->Video_PTS[VeChn] = 0;
	fc->Vfirst[VeChn] = 0;
	fc->vi[VeChn] = -1;
	fc->b_First_IDR_Find[VeChn] = 0;

	return HI_SUCCESS;
//错误处理
exit_vio_open_failed:	
	if (fc->g_OutFmt_Ctx[VeChn] && !(fc->g_OutFmt_Ctx[VeChn]->flags & AVFMT_NOFILE))
		avio_close(fc->g_OutFmt_Ctx[VeChn]->pb);
exit_outFmt_failed:
	if(NULL != fc->g_OutFmt_Ctx[VeChn])
		avformat_free_context(fc->g_OutFmt_Ctx[VeChn]);
	return -1;
}

3. 写视频帧

  这里会调用到两个子函数,HI_PDT_WriteVideo 调用 HI_ADD_SPS_PPS 向视频流中写入起始的 SPS 和 PPS,而在 HI_ADD_SPS_PPS 的一开始又会调用 HI_PDT_Add_Stream 在之前创建的 mp4 文件中添加一个新流,之后才能开始写入文件头、视频数据和文件尾。
  PTS、DTS 计算这一块实际上并没有过多理会,因为不涉及音频流写入,也就与音画不同步这个问题没有关系了。

HI_S32 HI_PDT_WriteVideo(VENC_CHN VeChn, VENC_STREAM_S *pstStream, FfmpegConf *fc)
{
	unsigned int i = 0;
	unsigned char* pPackVirtAddr = NULL;	//码流首地址
	unsigned int u32PackLen = 0;			//码流长度
    int ret = 0;
    AVStream *Vpst = NULL; 					//视频流指针
    AVPacket pkt;			//音视频包结构体,这个包不是海思的包,填充之后,用于最终写入数据
    uint8_t sps_buf[32];
    uint8_t pps_buf[32];
    uint8_t sps_pps_buf[64];
    unsigned int pps_len=0;
    unsigned int sps_len=0;
	if(NULL == pstStream)	//裸码流有效判断
	{
		return HI_SUCCESS;
	}
	//u32PackCount是海思中记录此码流结构体中码流包数量,一般含I帧的是4个包,P帧1个
    for (i = 0 ; i < pstStream->u32PackCount; i++)	
    {
    	//从海思码流包中获取数据地址,长度
        pPackVirtAddr = pstStream->pstPack[i].pu8Addr + pstStream->pstPack[i].u32Offset;	
        u32PackLen = pstStream->pstPack[i].u32Len - pstStream->pstPack[i].u32Offset;	
		av_init_packet(&pkt);			//初始化AVpack包
		pkt.flags = AV_PKT_FLAG_KEY;	//默认是关键帧,关不关键好像都没问题
		switch(pstStream->pstPack[i].DataType.enH264EType)
		{
			case H264E_NALU_SPS:	//如果这个包是SPS
				pkt.flags =  0;	    //不是关键帧
				if(fc->b_First_IDR_Find[VeChn] == 2)	//如果不是第一个SPS帧
				{
					continue;	//不处理,丢弃
					//我只要新建文件之后的第一个SPS PPS信息,后面都是一样的,只要第一个即可
				}
				else //如果是第一个SPS帧
				{
					sps_len = u32PackLen;
					memcpy(sps_buf, pPackVirtAddr, sps_len);
					if(fc->b_First_IDR_Find[VeChn] == 1)	//如果PPS帧已经收到
					{
						memcpy(sps_pps_buf, sps_buf, sps_len);	//复制sps
						memcpy(sps_pps_buf+sps_len, pps_buf, pps_len);	//加上pps
						//去添加视频流,和SPS PPS信息,这步之后才开始写入视频帧
						ret = HI_ADD_SPS_PPS(VeChn, sps_pps_buf, sps_len+pps_len, fc);
						if(ret<0)return HI_FAILURE;
					}
					fc->b_First_IDR_Find[VeChn]++;
				}
				continue; //继续
			case H264E_NALU_PPS:
				pkt.flags = 0;	//不是关键帧
				if(fc->b_First_IDR_Find[VeChn] == 2)	//如果不是第一个PPS帧
				{
					continue;
				}
				else //是第一个PPS帧
				{
					pps_len = u32PackLen;
					memcpy(pps_buf, pPackVirtAddr, pps_len);	//复制
					if(fc->b_First_IDR_Find[VeChn] == 1)	//如果SPS帧已经收到
					{
						memcpy(sps_pps_buf, sps_buf, sps_len);
						memcpy(sps_pps_buf+sps_len, pps_buf, pps_len);
						//这里和SPS那里互斥,只有一个会执行,主要是看SPS和PPS包谁排在后面
						ret = HI_ADD_SPS_PPS(VeChn, sps_pps_buf, sps_len+pps_len, fc); 
						if(ret<0)return HI_FAILURE;
					}
					fc->b_First_IDR_Find[VeChn]++;
				}
				continue;
			case H264E_NALU_SEI:	//增强帧,不含图像数据信息,只是对图像数据信息和视频流的补充
				continue;	//不稀罕这个帧
			case H264E_NALU_PSLICE:		//P帧
			case H264E_NALU_IDRSLICE:	//I帧
				if(fc->b_First_IDR_Find[VeChn] != 2)	//如果这个文件还没有收到过sps和pps帧
				{
					continue;	//跳过,不处理这帧
				}
				break;
			default:
				break;
		}
		
		if(fc->vi[VeChn] < 0)	//流索引号,如果g_OutFmt_Ctx里面还没有新建视频流,也就是说还没收到I帧
		{
			#ifdef DEBUG
			printf("vi less than 0 \n");
			#endif
			return HI_SUCCESS;
		}

		if(fc->Vfirst[VeChn] == 0)	//如果是文件的第一帧视频
		{
			fc->Vfirst[VeChn] = 1;	
			#ifdef USING_SEQ	//使用帧序号计算PTS
			fc->Video_PTS[VeChn] = pstStream->u32Seq; //记录初始序号
			#endif
			#ifdef USING_PTS	//直接使用海思的PTS
			fc->Video_PTS[VeChn] = pstStream->pstPack[i].u64PTS;	//记录开始时间戳
			#endif
		}
		
		Vpst = fc->g_OutFmt_Ctx[VeChn]->streams[fc->vi[VeChn]];	//根据索引号获取视频流地址
	    pkt.stream_index = Vpst->index;	//视频流的索引号 赋给 包里面的流索引号,表示这个包属于视频流
		
		//以下,时间基转换,PTS很重要,涉及音视频同步问题
		#if 0	//原博主的,可以用
			pkt.pts = av_rescale_q_rnd((fc->VptsInc[VeChn]++), Vpst->codec->time_base,Vpst->time_base,(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
			pkt.dts = av_rescale_q_rnd(pkt.pts, Vpst->time_base,Vpst->time_base,(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
		#endif
		#if 1	//我用的
			#ifdef USING_SEQ	
				//跟原博主差不多,我怕中间丢帧,导致不同步,所以用序号来计算
				pkt.pts = av_rescale_q_rnd(pstStream->u32Seq - fc->Video_PTS[VeChn], (AVRational){1, STREAM_FRAME_RATE},Vpst->time_base,(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
				pkt.dts = pkt.pts;	//只有I、P帧,相等就行了
			#endif
			#ifdef USING_PTS
				//海思的PTS是us单位,所以将真实世界的1000000us转成90000Hz频率的时间
				pkt.pts = pkt.dts =(int64_t)((pstStream->pstPack[i].u64PTS - fc->Video_PTS[VeChn]) *0.09+0.5);
			#endif
		#endif
		// 这个位置的数值设置是否合理(与输入帧率、输出帧率匹配)将直接影响最终封装出来的 mp4 播放效果,
		// 有兴趣可以自己改一改能有个直观的感受
		pkt.duration = 16;      // 60帧 16ms
		pkt.duration = av_rescale_q(pkt.duration, Vpst->time_base, Vpst->time_base);
		pkt.pos = -1;	//默认
		
		//最重要的数据要给AVpack包
		pkt.data = pPackVirtAddr ;	//接受视频数据NAUL
   		pkt.size = u32PackLen;		//视频数据长度
		//把AVpack包写入mp4
		ret = av_interleaved_write_frame(fc->g_OutFmt_Ctx[VeChn], &pkt);
		if (ret < 0)
		{
		    printf("cannot write video frame\n");
		    return HI_FAILURE;
		}
    }
	return HI_SUCCESS;
}

3.1 写SPS和PPS

  基本没有变化。

HI_S32 HI_ADD_SPS_PPS(VENC_CHN VeChn, uint8_t *buf, uint32_t size, FfmpegConf *fc)
{
	HI_S32 ret;
	ret = HI_PDT_Add_Stream(VeChn,fc);	//创建一个新流并添加到当前AVFormatContext中
	if(ret < 0)
	{
		printf("HI_PDT_Add_Stream faild\n");
		goto Add_Stream_faild;
	}
	
	fc->g_OutFmt_Ctx[VeChn]->streams[fc->vi[VeChn]]->codecpar->extradata_size = size;
	fc->g_OutFmt_Ctx[VeChn]->streams[fc->vi[VeChn]]->codecpar->extradata = (uint8_t*)av_malloc(size + AV_INPUT_BUFFER_PADDING_SIZE);
	memcpy(fc->g_OutFmt_Ctx[VeChn]->streams[fc->vi[VeChn]]->codecpar->extradata, buf, size);	//写入SPS和PPS

	ret = avformat_write_header(fc->g_OutFmt_Ctx[VeChn], NULL);		//写文件头
	if(ret < 0)
	{
		printf("avformat_write_header faild\n");
		goto write_header_faild;
	}

    return HI_SUCCESS;
    
write_header_faild:
	if (fc->g_OutFmt_Ctx[VeChn] && !(fc->g_OutFmt_Ctx[VeChn]->flags & AVFMT_NOFILE))
		avio_close(fc->g_OutFmt_Ctx[VeChn]->pb);
	
Add_Stream_faild:
    if(NULL != fc->g_OutFmt_Ctx[VeChn])
		avformat_free_context(fc->g_OutFmt_Ctx[VeChn]);
	fc->vi[VeChn] = -1;
	fc->VptsInc[VeChn] = 0;
	fc->b_First_IDR_Find[VeChn] = 0;	//sps,pps帧标志清除
	return HI_FAILURE;
}

3.2 添加新流

  关于 vcodec = avcodec_find_encoder(pOutFmt->video_codec); 这一句,参考博主附上的注释是默认使用的视频编码器就是 H264,但是自己在实际调试,检查变量的值的时候输出发现 pOutFmt->video_codec 的值是 12,对应的枚举变量名是 AV_CODEC_ID_MPEG4,而非 AV_CODEC_ID_H264(27),且手动设置查找 AV_CODEC_ID_H264 的时候发现找不到编码器,因为编译 ffmpeg 库的时候没有添加 libx264 支持。
  后来博主在网上找了找,发现 MPEG-4 包含 H264,这或许是代码仍然能运行的原因。

static int HI_PDT_Add_Stream(VENC_CHN VeChn, FfmpegConf *fc)
{
    AVOutputFormat *pOutFmt = NULL;			//用于获取AVFormatContext->Format
    AVCodecParameters *vAVCodecPar = NULL;	//新替代参数AVStream->CodecPar
    AVStream *vAVStream = NULL;				//用于指向新建的视频流
	AVCodec *vcodec = NULL;					//用于指向视频编码器

	pOutFmt = fc->g_OutFmt_Ctx[VeChn]->oformat;	//输出Format
	
    //查找视频编码器,MP4格式输出时默认值为 AV_CODEC_ID_MPEG4
    vcodec = avcodec_find_encoder(pOutFmt->video_codec);
    // vcodec = avcodec_find_encoder(AV_CODEC_ID_H264); 
    if (NULL == vcodec)
    {
        printf("could not find video encoder H264\n");
        return -1;
    }
	
	//根据视频编码器信息(H264),在AVFormatContext里新建视频流通道
    vAVStream = avformat_new_stream(fc->g_OutFmt_Ctx[VeChn], vcodec);	
    if (NULL == vAVStream)
    {
       printf("could not allocate vcodec stream\n");
       return -1;
    }
    //给新建的视频流一个ID,0
    vAVStream->id = fc->g_OutFmt_Ctx[VeChn]->nb_streams-1;	//nb_streams是当前AVFormatContext里面流的数量
    vAVCodecPar = vAVStream->codecpar;
	fc->vi[VeChn] = vAVStream->index;	//获取视频流的索引号
    
    //对视频流的参数设置
    vAVCodecPar->codec_type = AVMEDIA_TYPE_VIDEO;
    vAVCodecPar->codec_id = AV_CODEC_ID_H264;
    vAVCodecPar->bit_rate = 0;			            //kbps,好像不需要
    vAVCodecPar->width = 1920;			            //像素
    vAVCodecPar->height = 1080;
    vAVStream->time_base = (AVRational){1, STREAM_FRAME_RATE};	//时间基:1/STREAM_FRAME_RATE
    vAVCodecPar->format = AV_PIX_FMT_YUV420P;

    return HI_SUCCESS;
}

4.关闭并保存mp4文件

  基本没有变化。

void HI_PDT_CloseMp4(VENC_CHN VeChn, FfmpegConf *fc)
{
	int ret;
    if (fc->g_OutFmt_Ctx[VeChn])
    {
       ret = av_write_trailer(fc->g_OutFmt_Ctx[VeChn]);	//写文件尾
       if(ret<0)
       {
       		#ifdef DEBUG
       		printf("av_write_trailer faild\n");
       		#endif
       }
    }
    if (fc->g_OutFmt_Ctx[VeChn] && !(fc->g_OutFmt_Ctx[VeChn]->oformat->flags & AVFMT_NOFILE)) //文件状态检测
	{
		ret = avio_close(fc->g_OutFmt_Ctx[VeChn]->pb);	//关闭文件
		if(ret < 0)
       {
       		#ifdef DEBUG
       		printf("avio_close faild\n");
       		#endif
       }
	}
	if (fc->g_OutFmt_Ctx[VeChn])
    {
        avformat_free_context(fc->g_OutFmt_Ctx[VeChn]);	//释放结构体
        fc->g_OutFmt_Ctx[VeChn] = NULL;
    }
    //清除相关标志
	fc->vi[VeChn] = -1;	
	fc->VptsInc[VeChn] = 0;
	fc->Vfirst[VeChn] = 0;
	fc->b_First_IDR_Find[VeChn] = 0;
}

四、H265相关讨论

  事实上 H265 的封装过程和 H264 类似,所以按理说这份封装代码是可以改造成 H265 视频封装为 mp4 文件的,只不过有几个要注意的点(可能包括但不限于,毕竟自己还没有实现):

  • 其一就是添加新流函数中的 vcodec = avcodec_find_encoder(pOutFmt->video_codec); 这一句。H265 是 H264 的改版,但是否仍然兼容 mpeg-4 编码器设置还未可知。这个问题不容忽视,毕竟添加新流通道时 vcodec 作为参数传入了。如果需要另外设置 AV_CODEC_ID_H265,就可能还需要向 ffmpeg 添加 libx265 支持,并启用创建mp4函数中的 av_register_all() 函数。
  • 其二就是写 SPS 和 PPS 函数。H265 相比 H264 在 SPS 之前多了一个 VPS,因此要沿用当前的编程思路也需要增加 VPS 的整合与写入。这里提出一种看上去可行(自己脑测的并没有上手试验)的修改方法:增加 switch 里 VPS 包的情况处理,并将 switch 里判断其他包是否到达的数量从 1 改到 2,如果条件成立,就一次性把三个包整合到一起写文件头。

PS:为什么最后写出来还是 H264 封装视频的博文?因为 libx265 的支持添加过于迷幻。咱摊牌了,x265 这东西咱驾驭不了,老老实实用 x264 吧。

五、杂记(个人向)

  经过对店家例程的分析发现,开发板在启动后会自动加载所有 .ko 文件并进行管脚复用配置。原因是在根文件系统下的 /etc/init.d/rcS 文件末尾加上了指令 cd /ko/ 以及 load3531d -i。这里涉及到三个地方的移植:

  • 将 mpp 下的 ko 文件夹整个拷贝到根文件系统的根目录下,里面自带所有的 .ko 文件以及 load3531d;
  • 将管脚复用配置脚本 pinmux.sh 拷贝进 /ko/ 下,然后在 load3531d 中加载所有 .ko 文件的函数末尾加上 ./pinmux.sh 指令执行管脚复用配置(具体的配置参数含义可以在 SDK 包中 ReleaseDoc/00.hardware/chip 目录下提供的引脚复用寄存器 Excel 表查找);
  • pinmux.sh 是使用 himm 工具进行管脚复用配置的,所以还要把 himm 工具拷贝到板载 linux 的 $PATH 下,而 himm 实际上就在 SDK包的 osdrv/tools/board/reg-tools-1.0.0/ 下,在该目录下 make 就会在 bin 子目录下生成 btools 以及包括 himm 在内的一众链接文件(其实都是指向 btools),所以把 btools 移植过去然后重命名为 himm 就可以。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值