使用FFmpeg的SDK库实现将H.264流封装进MP4文件时全局SPS、PPS与流中SPS、PPS冲突的问题

一、问题
1. 使用FFmpeg的SDK库实现将H.264流封装进MP4文件的源码大致如下:

char* filename = "./test.mp4"

AVOutputFormat *fmt;
AVStream* video_st;
AVFormatContext *av_context;

/* 初始化资源 */
av_register_all();
int iret = avformat_alloc_output_context2(&av_context, NULL, NULL, filename);
if (!av_context) {
printf("Could not deduce output format from file extension: using MPEG.\n");
return -1;
}
av_context->oformat->video_codec = CODEC_ID_H264;
fmt = av_context->oformat;

/* Init AVCodecContext */
video_st = avformat_new_stream(av_context, NULL);
if (!video_st) {
fprintf(stderr, "Could not alloc stream\n");
return -1;
}

AVCodecContext *codec_context;
AVCodec *codec;
codec_context = video_st ->codec;
/* find the video encoder */
codec = avcodec_find_encoder(fmt->video_codec);
if (!codec) {
fprintf(stderr, "codec not found\n");
return -1;
}
avcodec_get_context_defaults3(codec_context, codec);
codec_context->codec_id = fmt->video_codec;
codec_context->bit_rate = 500000; /* put sample parameters */
codec_context->width = width; /* resolution must be a multiple of two */
codec_context->height = height;
codec_context->time_base.den = 1000;
codec_context->time_base.num = 1;
codec_contextc->gop_size = 25; /* emit one intra frame every twelve frames at most */
codec_context->pix_fmt = STREAM_PIX_FMT;

if (avcodec_open(video_st->codec, codec) < 0) {
fprintf(stderr, "video could not open codec\n");
ERROR_LOG("video codec not found!\n");
return -1 ;
}

av_dump_format(av_context, 0, filename, 1);

/* open the output file, if needed */
if (!(fmt->flags & AVFMT_NOFILE)) {
iret = avio_open(&av_context->pb, filename, AVIO_FLAG_WRITE);
if (iret < 0) {
fprintf(stderr, "Could not open '%s'\n", filename);
return -1;
}
}

/* write the stream header, if any */
avformat_write_header(av_context, NULL);

/* 将裸数据流写文件,并打上时间戳 */
for (i = 0; i < end ; i++) {
// 取得帧数据enc_data,enc_size,和pts

AVPacket pkt;
av_init_packet(&pkt);

if(is_key)
pkt.flags |= AV_PKT_FLAG_KEY;

pkt.pts = pts;
pkt.dts = pts;
pkt.stream_index = video_st->index;
pkt.data = enc_data;
pkt.size = enc_size;

/* write the compressed frame in the media file */
ret = av_interleaved_write_frame(av_context, &pkt);
}


/* 释放资源 */
av_write_trailer(av_context);
avcodec_close(video_st->codec);
avio_close(av_context->pb);
av_free(av_context);
video_st = NULL;
av_context = NULL;


2. 冲突问题
如果以上述方式进行封装时,FFmpeg默认写到流中的全局H.264 SPS、PPS默认是High profile;
如果流中的是baseline profile, 则会存在有两个SPS、PPS冲突问题,
如下图所示:

这时,对有些播放器,会出现花屏;

二、原因
FFmpeg 的API:
avcodec_open(video_st->codec, codec)
或 avcodec_open2()的定义位于libavcodec\utils.c,
它们将H.264的编码器初始化成了High profile;

具体过程如下:
avcodec_open2所做的工作,如下所列:
(1)为各种结构体分配内存(通过各种av_malloc()实现)。
(2)将输入的AVDictionary形式的选项设置到AVCodecContext。
(3)其他一些零零碎碎的检查,比如说检查编解码器是否处于“实验”阶段。
(4)如果是编码器,检查输入参数是否符合编码器的要求
(5)调用AVCodec的init()初始化具体的解码器。

它最关键的是第5步,即调用了一个关键的函数AVCodec的init()。
这个方法是初始化具体的编码器。
AVCodec的init()是一个函数指针,指向具体编解码器中的初始化函数。
这里我们以libx264为例,看一下它对应的AVCodec的定义。
libx264对应的AVCodec的定义位于libavcodec\libx264.c,
如下所示。

AVCodec ff_libx264_encoder = {
.name = "libx264",
.long_name = NULL_IF_CONFIG_SMALL("libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.priv_data_size = sizeof(X264Context),
.init = X264_init,
.encode2 = X264_frame,
.close = X264_close,
.capabilities = CODEC_CAP_DELAY | CODEC_CAP_AUTO_THREADS,
.priv_class = &x264_class,
.defaults = x264_defaults,
.init_static_data = X264_init_static,
};

可以看出在ff_libx264_encoder中init()指向X264_init()。
X264_init()的定义同样位于libavcodec\libx264.c,
X264_init()做了两项工作:
(1)设置X264Context的参数。X264Context主要完成了libx264和FFmpeg对接的功能。
可以看出代码主要在设置一个params结构体变量,该变量的类型即是x264中存储参数的结构体x264_param_t。
(2)调用libx264的API进行编码器的初始化工作。
例如调用x264_param_default()设置默认参数,调用x264_param_apply_profile()设置profile,调用x264_encoder_open()打开编码器等等。

X264Context的定义,位于libavcodec\libx264.c,如下所示:
typedef struct X264Context {
AVClass *class;
x264_param_t params;
x264_t *enc;
x264_picture_t pic;
uint8_t *sei;
int sei_size;
char *preset;
char *tune;
char *profile;
char *level;
int fastfirstpass;
char *wpredp;
char *x264opts;
float crf;
float crf_max;
int cqp;
int aq_mode;
float aq_strength;
char *psy_rd;
int psy;
int rc_lookahead;
int weightp;
int weightb;
int ssim;
int intra_refresh;
int bluray_compat;
int b_bias;
int b_pyramid;
int mixed_refs;
int dct8x8;
int fast_pskip;
int aud;
int mbtree;
char *deblock;
float cplxblur;
char *partitions;
int direct_pred;
int slice_max_size;
char *stats;
int nal_hrd;
int avcintra_class;
char *x264_params;
} X264Context;


初始化完成后,FFmpeg 的全局SPS,PPS存储在 AVCodecContext->extradata 和 AVCodecContext->extrdata_size 中,
通过GDB跟踪显示如下:
378 if (avcodec_open(video_st->codec, codec) < 0){
(gdb) p video_st->codec
$11 = (AVCodecContext *) 0x7fffe0006ce0
(gdb) p *video_st->codec
$12 = {av_class = 0xf3e580, bit_rate = 500000, bit_rate_tolerance = 4000000, flags = 4194304, sub_id = 0, me_method = -1, extradata = 0x0, extradata_size = 0,....
(gdb) n
(gdb) p *video_st->codec
$13 = {av_class = 0xf3e580, bit_rate = 500000, bit_rate_tolerance = 4000000, flags = 4194304, sub_id = 0, me_method = -1, extradata = 0x7fffe049c700 "",
extradata_size = 34, time_base = {num = 1, den = 1000}, width = 320, height = 240, gop_size = 1000, pix_fmt = PIX_FMT_YUV420P, draw_horiz_band = 0, sample_rate = 0,...

三、解决方案
1. 方案一:将全局的SPS、PPS设置成和流中同样的baseline属性
代码如下所示:

AVDictionary *opts = NULL;
av_dict_set(&opts, "profile", "baseline", 0);

/* open the codec */
if (avcodec_open2(c, codec, &opts) < 0){
fprintf(stderr, "video could not open codec\n");
return -1;
}

2. 方案二:使用流的SPS、PPS覆盖全局的SPS、PPS;
即,
Step1: 获得流中的SPS,PPS数据,及它们合在一起的size;
Step2:释放原有的 video_st->codec->extradata 数据;
Step3:申请新的 video_st->codec->extradata 空间,并将流的SPS+PPS复制进去,
再置 video_st->codec->extradata_size 为新的size;
具体的的源码不在些处列出。
需要的可以去此下载,
我从FFmpeg中抽取出来,
读取文件中数据流,快速找到buffer中的SPS 和 PPS,并解析它们的代码;

猜你喜欢

转载自blog.csdn.net/fireroll/article/details/80711424