用ffmpeg从MP4提取H264裸流

MP4的格式封装比较复杂,想取出来H264裸流比较麻烦,因此借助ffmpeg工具比较方便。通常一般都是使用ffmpeg进行编解码较多,但是当我们视频是H264编码时候,直接提取比较合适。

提取H264

H264编码的MP4文件,使用ffmpeg提取相对比较方便,直接使用ffmpeg标准的媒体文件读取流程,通过读取AVPacket出来不需要解码,直接从其data数据域中即可获取到H264数据,通过观察就可以发现,这个H264并不是我们需要的,因为H264数据有两种方式

  • MP4编码方式,也就是开始四个字节表示数据长度
  • ANNEXB编码方式,也就是常见的 00 00 00 01 开头的方式

因此此处需要将MP4方式的数据转换成ANNEXB方式的,转换方式比较简单,一般ffmpeg读取一包是一帧数据,因此只需要自己写入 00 00 00 01 之后写入 AVPacket.data 偏移4位即可,相应的 AVPacket.size 也要减少4位。这样就可以了。当然ffmpeg也为我们准备了相对应的filter,我们直接利用即可,代码如下

AVBitStreamFilterContext* h264bsfc =  av_bitstream_filter_init("h264_mp4toannexb"); 

AVPacket packet;
while( av_read_frame(format_ctx_, &packet) >= 0 ) {
if( packet.stream_index == video_stream_index_ ) {
av_bitstream_filter_filter(h264bsfc, codec_ctx_, NULL, &packet.data, &packet.size, packet.data, packet.size, 0);
fwrite(packet.data, packet.size, 1, fp);
}

av_free_packet(&packet);
}

av_bitstream_filter_close(h264bsfc);

相比较简洁的多。

2019/03/28 更新:关于上面方法产生内存泄漏的问题

(为什么不直接修改前面内容呢?前事不忘后事之师,并且可以给他人和已经阅读过的人以提示)

最近在调试代码的时候,发现使用了上面的代码存在内存泄漏,然后查看使用版本的官方源码,其中各个函数的定义中很容易分析出问题,这其中 av_bitstream_filter_filter 的关键代码如下:

代码在 bitstream_filter.c 中,这里使用了 FFMPEG 3.3 分支代码分析。

int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
AVCodecContext *avctx, const char *args,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size, int keyframe)
{
...
pkt.data = buf;
pkt.size = buf_size;

ret = av_bsf_send_packet(priv->ctx, &pkt);
if (ret < 0)
return ret;

*poutbuf = NULL;
*poutbuf_size = 0;

ret = av_bsf_receive_packet(priv->ctx, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 0;
else if (ret < 0)
return ret;

*poutbuf = av_malloc(pkt.size + AV_INPUT_BUFFER_PADDING_SIZE);
if (!*poutbuf) {
av_packet_unref(&pkt);
return AVERROR(ENOMEM);
}

*poutbuf_size = pkt.size;
memcpy(*poutbuf, pkt.data, pkt.size);

av_packet_unref(&pkt);

...
}

里面的逻辑暂时先不管,关注 poutbuf 这个变量,为什么要关注这个变量呢,

  • 其一,整个循环流程只有2个处理函数,而且 av_read_frame 明确返回的包是要释放的
  • 其二, av_bitstream_filter_filter 函数入参只有 poutbuf 是指向指针的指针

其中 poutbuf 是传入的内存,也就是最终的处理结果,在源码内部明显可以看到为这个变量申请了新内存(注意变量类型),而这个变量却是传的 AVPacket 的变量地址,也就是说最终内部改变了 AVPacket 的内部内存,那么原来 AVPacket 的内存则丢掉了,也就是泄漏了。

根源找到了,修改起来也比较简单

AVBitStreamFilterContext* h264bsfc =  av_bitstream_filter_init("h264_mp4toannexb"); 

AVPacket packet;
while( av_read_frame(format_ctx_, &packet) >= 0 ) {
if( packet.stream_index == video_stream_index_ ) {
uint8_t* outbuf = nullptr;
int outlen = 0;
av_bitstream_filter_filter(h264bsfc, codec_ctx_, NULL,
&outbuf, &outlen, packet.data, packet.size, 0);
fwrite(packet.data, packet.size, 1, fp);
if(outbuf){
av_free(outbuf);
}
}

av_free_packet(&packet);
}

av_bitstream_filter_close(h264bsfc);

提取SPS和PPS

有时候需要取得H264的SPS和PPS,但是又不想去分析NALU去查找,毕竟相对操作起来比较麻烦。有个比较简单的办法是在视频的 AVCodecContext.extradata, 里面保存的是 avcC 类型的数据,其规范定义如下

aligned(8) class AVCDecoderConfigurationRecord {   
unsigned int(8) configurationVersion = 1;
unsigned int(8) AVCProfileIndication;
unsigned int(8) profile_compatibility;
unsigned int(8) AVCLevelIndication;
bit(6) reserved = '111111'b;
unsigned int(2) lengthSizeMinusOne;
bit(3) reserved = '111'b;
unsigned int(5) numOfSequenceParameterSets;
for (i=0; i< numOfSequenceParameterSetsispan>
unsigned int(16) sequenceParameterSetLength ;
bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
}
unsigned int(8) numOfPictureParameterSets;
for (i=0; i< numOfPictureParameterSetsispan>
unsigned int(16) pictureParameterSetLength;
bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
}
}

第7,8位表示SPS的长度,后续跟SPS数据,结束之后,首先是1位 numOfPictureParameterSets,跳过,接着是2位PPS的长度,然后跟着PPS数据,那么提取就相对比较简单。

char kNalStart[] = {0,0,0,1};
unsigned short sps_len = ntohs(*(unsigned short*)(codec_ctx_->extradata + 6));
fwrite(kNalStart, 4, 1, fp);
fwrite(codec_ctx_->extradata + 8, sps_len, 1, fp);
unsigned short pps_len = ntohs(*(unsigned short*)(codec_ctx_->extradata + 9 + sps_len));
fwrite(kNalStart, 4, 1, fp);
fwrite(codec_ctx_->extradata + 11 + sps_len, pps_len, 1, fp);
最近的文章

CentOS升级GCC

一般的Linux都是企业应用相对的发行版的软件包相对较旧,而现在C++11逐渐普及,支持C++11的编译器在Linux上的GCC稍微完整点的都至少是4.8版本,反观CentOS 6的GCC版本为4.4,即使是最新的CentOS 7也才仅为4.7。因此很多时候需要升级系统自带的编译器(注意,千万不要从 …

技术 继续阅读
更早的文章

CentOS7配置SRS服务及日志

最近将RTMP服务器转移到Linux平台下,同时也想使用下SRS服务器。但是SRS默认只提供了CentOS 6的RPM安装包,其他平台需自己编译,同时SRS也只提供了SysV方式的服务脚本,故在systemd架构下需要自己开发相对应的脚本,以下记录下本人在CentOS 7上安装配置SRS。 编译SR …

技术 继续阅读