FFmpeg的封装模块涉及到对视频数据的封装,也设计到对音频数据的封装,将已经编码好的音频流和视频流封装到一个文件中去,对音视频数据进行封装有很多优点,最重要的优点我认为是方便音视频数据的同步,ffmpeg 的封装模块涉及到了音视频的编码,相对于前面的编码和解码来说,要更加的复杂一些。
音视频封装
- 从输入文件中读取未编码的视频数据和音频数据
- 创建编码视频数据和音频数据所需的AVFrame、AVPacket、AVCodecContext等结构体
- 写入文件头部信息
- 将未编码的音频数据和视频数据送到编码器编码
- 根据已经编码的音视频数据时间戳,将音视频数据写入输出文件中
- 写入文件尾部信息
avformat_alloc_output_context2(&oc, NULL, NULL, filename); //1、创建输出文件格式上下文,主要用于处理输入输出文件
if (fmt->video_codec != AV_CODEC_ID_NONE) {
add_stream(&video_st, oc, &video_codec, fmt->video_codec); //2、创建视频编码初始化流程,类似于前面文章的编码流程
}
if (fmt->audio_codec != AV_CODEC_ID_NONE) {
add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);//2、创建音频编码初始化流程,类似于前面上面的视频编码流程
}
open_video(oc, video_codec, &video_st, opt);// 2、初始化视频编码器上下文,拷贝编码器参数到封装器
open_audio(oc, audio_codec, &audio_st, opt);// 2、同上
av_dump_format(oc, 0, filename, 1);//打印关于输入文件详细的信息
ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);//创建AVIOContext,并挂在到pb上面,通过AVIOContext可以直接操作输出文件
ret = avformat_write_header(oc, &opt);//3、向输出文件中写入封装格式的信息头结构
av_compare_ts(video_st.next_pts, video_st.enc->time_base,audio_st.next_pts, audio_st.enc->time_base) <= 0)) {//比较音频数据和视频数据时间搓,先写音频数据还是视频数据
encode_video = !write_video_frame(oc, &video_st);//4&5、编码并写入视频数据到输出文件
} else {
encode_audio = !write_audio_frame(oc, &audio_st);//4&5、编码并写入音频数据到输出文件
}
av_write_trailer(oc);//6、写封装格式文件尾部信息
close_stream(oc, &video_st);//关闭视频流
close_stream(oc, &audio_st);//关闭音频流
avio_closep(&oc->pb);//关闭文件IO操作
avformat_free_context(oc);//释放格式上下文
上面这些代码音视频封装有了一个框架上的讲解,就是将音视频数据读起来,然后编码,再根据时间戳写到输出文件中去,就这么简单,但是实际上可能还涉及到音视频同步,封装格式首尾信息写入等。
下面还是详细解释一下每个模块具体调用了什么API接口
add_stream
/* find the encoder */
*codec = avcodec_find_encoder(codec_id);// 找到编码器
ost->st = avformat_new_stream(oc, NULL);//创建媒体流结构AVStream,主要用于输出文件中
c = avcodec_alloc_context3(*codec);//创建编码器上下文结构
case AVMEDIA_TYPE_AUDIO://设置音频相关参数
c->sample_fmt = (*codec)->sample_fmts ?
(*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
c->bit_rate = 64000;
c->sample_rate = 44100;
c->sample_rate = (*codec)->supported_samplerates[0];
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
c->channel_layout = AV_CH_LAYOUT_STEREO;
c->channel_layout = (*codec)->channel_layouts[0];
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
ost->st->time_base = (AVRational){ 1, c->sample_rate };
break;
case AVMEDIA_TYPE_VIDEO://设置视频相关参数
c->codec_id = codec_id;
c->bit_rate = 400000;
c->width = 352;
c->height = 288;
ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE };
c->time_base = ost->st->time_base;
c->gop_size = 12; /* emit one intra frame every twelve frames at most */
c->pix_fmt = STREAM_PIX_FMT;
c->max_b_frames = 2;
c->mb_decision = 2;
}
open_video
av_dict_copy(&opt, opt_arg, 0);// 拷贝输入命令时设置的字典key和value(常用于设置编码器的相关参数)
/* open the codec */
ret = avcodec_open2(c, codec, &opt);//初始化编码器上下文结构,申请编码器上下文数据成员空间
av_dict_free(&opt);//释放字典
/* allocate and init a re-usable frame */
av_frame_get_buffer(picture, 32);//申请编码所必须的buffer空间
/* copy the stream parameters to the muxer */
ret = avcodec_parameters_from_context(ost->st->codecpar, c);//将编码器的相关参数信息拷贝到输出流中
open_audio
av_dict_copy(&opt, opt_arg, 0); // 拷贝输入命令时设置的字典key和value(常用于设置编码器的相关参数)
ret = avcodec_open2(c, codec, &opt);// 初始化编码器上下文结构,申请编码器上下文数据成员空间
av_dict_free(&opt);//释放字典数据
/* init signal generator */
ost->t = 0;//初始化相关参数
ost->tincr = 2 * M_PI * 110.0 / c->sample_rate;
/* increment frequency by 110 Hz per second */
ost->tincr2 = 2 * M_PI * 110.0 / c->sample_rate / c->sample_rate;
nb_samples = 10000;
ost->frame = alloc_audio_frame(c->sample_fmt, c->channel_layout,c->sample_rate, nb_samples);//申请音频编码所必须的frame buffer空间
ost->tmp_frame = alloc_audio_frame(AV_SAMPLE_FMT_S16, c->channel_layout,c->sample_rate, nb_samples);
/* copy the stream parameters to the muxer */
ret = avcodec_parameters_from_context(ost->st->codecpar, c);//将编码器的相关参数信息拷贝到输出流中
ost->swr_ctx = swr_alloc();//申请重采样上下文结构
/* set options *///设置重采样参数
av_opt_set_int (ost->swr_ctx, "in_channel_count", c->channels, 0);
av_opt_set_int (ost->swr_ctx, "in_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
av_opt_set_int (ost->swr_ctx, "out_channel_count", c->channels, 0);
av_opt_set_int (ost->swr_ctx, "out_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "out_sample_fmt", c->sample_fmt, 0);
/* initialize the resampling context */
if ((ret = swr_init(ost->swr_ctx)) < 0) //初始化重采样上下文
write_video_frame
frame = get_video_frame(ost);//通过AVStream拿到AVFrame结构,
av_init_packet(&pkt);//初始化AVPacket结构 用于没有alloc packet
frame = get_video_frame(ost); //拿到未解码视频数据
av_init_packet(&pkt); //通过默认的值初始化packet模块
/* encode the image */
ret = avcodec_encode_video2(c, &pkt, frame, &got_packet); //得到编码后的视频数据
ret = write_frame(oc, &c->time_base, ost->st, &pkt);//将视频数据写入到输出文件
write_audio_frame
av_init_packet(&pkt); // 初始化AVacket结构
frame = get_audio_frame(ost);//通过AVStream得到AVFrame结构
dst_nb_samples = av_rescale_rnd(swr_get_delay(ost->swr_ctx, c->sample_rate) + frame->nb_samples,c->sample_rate, c->sample_rate, AV_ROUND_UP);//对音频采样率进行基准转换,输入采样率转换为输出采样率
ret = av_frame_make_writable(ost->frame);//确保frame是可写的状态
/* convert to destination format */
ret = swr_convert(ost->swr_ctx,ost->frame->data, dst_nb_samples, (const uint8_t **)frame->data, frame->nb_samples); //进行采样率的转换
frame = ost->frame;
frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base);//计算显示时间错
ost->samples_count += dst_nb_samples;
ret = avcodec_encode_audio2(c, &pkt, frame, &got_packet);//对音频数据进行编码
ret = write_frame(oc, &c->time_base, ost->st, &pkt);//将编码后的音频数据写到输出文件中
总结
ffmpeg中音视频数据的编码和封装比较的复杂,不仅设计到音视频的编码部分,还涉及到音视频编码数据的时间基转换和音视频数据的时间戳对比,但是设计到时间戳相关的计算也是封装格式的一个重要组成部分。