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);
|