赞
踩
Ubuntu系统版本:Ubuntu 16.04.7 LTS
参考博客 最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)、博客 海思HI3531D使用ffmpeg实时封装多路H264视频+AAC音频为MP4 以及博客 hisi3559A平台VENC获取H264裸流封装成mp4,完美贴合项目需求,确实有被帮助到,感谢几位博主的无私分享。
封装的流程可以从代码结构上很清楚地反映出来。
#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;
这一部分的代码很好理解,没有什么需要特别改动的地方。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; }
这里会调用到两个子函数,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; }
基本没有变化。
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; }
关于 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; }
基本没有变化。
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 的封装过程和 H264 类似,所以按理说这份封装代码是可以改造成 H265 视频封装为 mp4 文件的,只不过有几个要注意的点(可能包括但不限于,毕竟自己还没有实现):
PS:为什么最后写出来还是 H264 封装视频的博文?因为 libx265 的支持添加过于迷幻。咱摊牌了,x265 这东西咱驾驭不了,老老实实用 x264 吧。
经过对店家例程的分析发现,开发板在启动后会自动加载所有 .ko 文件并进行管脚复用配置。原因是在根文件系统下的 /etc/init.d/rcS 文件末尾加上了指令 cd /ko/ 以及 load3531d -i。这里涉及到三个地方的移植:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。