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 就可以。