今天学习
ffmpeg/doc/examples/muxing.c
程序编码10秒的音视频数据,写入参数指定的路径,文件后缀会决定文件的封装格式,使用的音视频编码器,调用的命令如下:
➜ examples git:(master) ✗ ./muxing_g /tmp/mux.mp4
➜ examples git:(master) ✗ ./muxing_g /tmp/mux.mov
创建AVFormatContext
//创建AVFormatContext, 根据文件后缀来推测output format
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
if (!oc) {
printf("Could not deduce output format from file extension: using MPEG.\n");
//无法推测output format, 使用mpeg
avformat_alloc_output_context2(&oc, NULL, "mpeg", filename);
给AVFormatContext添加 video / audio stream
if (fmt->video_codec != AV_CODEC_ID_NONE) {
add_stream(&video_st, oc, &video_codec, fmt->video_codec);
have_video = 1;
encode_video = 1;
if (fmt->audio_codec != AV_CODEC_ID_NONE) {
add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
have_audio = 1;
encode_audio = 1;
我们看看add_stream做了什么
/* Add an output stream. */
static void add_stream(OutputStream *ost, AVFormatContext *oc,
const AVCodec **codec,
enum AVCodecID codec_id)
AVCodecContext *c;
int i;
/* find the encoder */
//找到codec_id对应的编码器AVCodec
*codec = avcodec_find_encoder(codec_id);
//创建pkt
ost->tmp_pkt = av_packet_alloc();
//创建AVStream
ost->st = avformat_new_stream(oc, NULL);
//设置stream的index
ost->st->id = oc->nb_streams-1;
//创建编码器上下文
c = avcodec_alloc_context3(*codec);
//保存编码器上下文
ost->enc = c;
switch ((*codec)->type) {
case AVMEDIA_TYPE_AUDIO:
//设置音频编码器上下文参数(码率,采样率,位深,声道布局等)
//设置stream的时间基
ost->st->time_base = (AVRational){ 1, c->sample_rate };
break;
case AVMEDIA_TYPE_VIDEO:
//设置编码器上下文的参数
c->codec_id = codec_id;
//设置stream的时间基
ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE };
c->time_base = ost->st->time_base;
//编码器码率,帧率,gop,分辨率, 设置
break;
default:
break;
/* Some formats want stream headers to be separate. */
//处理一下stream header
if (oc->oformat->flags & AVFMT_GLOBALHEADER)
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
根据codec_id找到AVCodec
调用avformat_new_stream创建stream
设置stream的index
根据AVCodec创建编码器的上下文,配置编码器参数
设置stream的time_base
处理一下 stream headers 的标志位
音视频流创建好了,下一步为写入准备
上面创建好了stream, 创建了编码器上下文。open_video/open_audio继续为写入做准备
准备完成就打开文件准备写入
/* Now that all the parameters are set, we can open the audio and
* video codecs and allocate the necessary encode buffers. */
if (have_video)
open_video(oc, video_codec, &video_st, opt);
if (have_audio)
open_audio(oc, audio_codec, &audio_st, opt);
//打印oc的信息
av_dump_format(oc, 0, filename, 1);
/* open the output file, if needed */
if (!(fmt->flags & AVFMT_NOFILE)) {
//打开文件
ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "Could not open '%s': %s\n", filename,
av_err2str(ret));
return 1;
看看open_video 做了什么
static void open_video(AVFormatContext *oc, const AVCodec *codec,
OutputStream *ost, AVDictionary *opt_arg)
int ret;
AVCodecContext *c = ost->enc;
AVDictionary *opt = NULL;
//将opt_arg中的内容拷贝到opt中
av_dict_copy(&opt, opt_arg, 0);
/* open the codec */
//打开编码器
ret = avcodec_open2(c, codec, &opt);
//释放opt
av_dict_free(&opt);
if (ret < 0) {
fprintf(stderr, "Could not open video codec: %s\n", av_err2str(ret));
exit(1);
/* allocate and init a re-usable frame */
//根据格式,宽高,创建一个复用的AVFrame
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
if (!ost->frame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
/* If the output format is not YUV420P, then a temporary YUV420P
* picture is needed too. It is then converted to the required
* output format. */
ost->tmp_frame = NULL;
if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
//如果编码器对应的pix_fmt不是yuv420p, 创建一个yuv420p格式的AVFrame,保存在ost->tmp_frame中
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height);
if (!ost->tmp_frame) {
fprintf(stderr, "Could not allocate temporary picture\n");
exit(1);
/* copy the stream parameters to the muxer */
//将编码器的参数拷贝到stream对应的编码参数中
ret = avcodec_parameters_from_context(ost->st->codecpar, c);
if (ret < 0) {
fprintf(stderr, "Could not copy the stream parameters\n");
exit(1);
打开编码器
申请资源,创建一个AVFrame用于存储编码前数据
调用avcodec_parameters_from_context将编码器的编码参数拷贝到stream的codecpar中
写音视频数据到文件
/* Write the stream header, if any. */
//将流信息写文件头
ret = avformat_write_header(oc, &opt);
if (ret < 0) {
fprintf(stderr, "Error occurred when opening output file: %s\n",
av_err2str(ret));
return 1;
交替写音视频帧
while (encode_video || encode_audio) {
/* select the stream to encode */
交替写入编码后的音频和视频帧
视频写入结束或者video_st.next_pts <= audio_st.next_pts, 写视频
否则写音频
if (encode_video &&
(!encode_audio || 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);
} else {
//写入编码后的音频帧
encode_audio = !write_audio_frame(oc, &audio_st);
写trailer
/* Write the trailer, if any. The trailer must be written before you
* close the CodecContexts open when you wrote the header; otherwise
* av_write_trailer() may try to use memory that was freed on
* av_codec_close(). */
av_write_trailer(oc);
关闭编码器,关闭文件,释放资源
/* Close each codec. */
if (have_video)
close_stream(oc, &video_st);
if (have_audio)
close_stream(oc, &audio_st);
if (!(fmt->flags & AVFMT_NOFILE))
/* Close the output file. */
avio_closep(&oc->pb);
/* free the stream */
avformat_free_context(oc);
write_video_frame
static int write_video_frame(AVFormatContext *oc, OutputStream *ost)
return write_frame(oc, ost->enc, ost->st, get_video_frame(ost), ost->tmp_pkt);
只是简单调用了write_frame
static int write_frame(AVFormatContext *fmt_ctx, AVCodecContext *c,
AVStream *st, AVFrame *frame, AVPacket *pkt)
int ret;
// send the frame to the encoder
//将frame送给编码器去编码
ret = avcodec_send_frame(c, frame);
if (ret < 0) {
fprintf(stderr, "Error sending a frame to the encoder: %s\n",
av_err2str(ret));
exit(1);
while (ret >= 0) {
//从编码器中读出编码后的pkt
ret = avcodec_receive_packet(c, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
else if (ret < 0) {
fprintf(stderr, "Error encoding a frame: %s\n", av_err2str(ret));
exit(1);
/* rescale output packet timestamp values from codec to stream timebase */
//调整pkt的pts,pkt的时间基参编码器的时间基,将其转换为参考stream的时间基
av_packet_rescale_ts(pkt, c->time_base, st->time_base);
//设置pkt的stream_index和stream对应的一致,音视频分别对应于不同的stream_index
pkt->stream_index = st->index;
/* Write the compressed frame to the media file. */
log_packet(fmt_ctx, pkt);
//将pkt写入视频文件
ret = av_interleaved_write_frame(fmt_ctx, pkt);
/* pkt is now blank (av_interleaved_write_frame() takes ownership of
* its contents and resets pkt), so that no unreferencing is necessary.
* This would be different if one used av_write_frame(). */
if (ret < 0) {
fprintf(stderr, "Error while writing output packet: %s\n", av_err2str(ret));
exit(1);
return ret == AVERROR_EOF ? 1 : 0;
将AVFrame送给编码器去编码
读取pkt
调用av_packet_rescale_ts,调整pkt的时间戳,写入文件pkt的pts要以stream的time_base为基准
设置pkt的index
调用av_interleaved_write_frame将pkt写入文件
返回写入结果。当AVFrame为空时,会冲洗编码器,ret = AVERROR_EOF, 返回1, 结束写入
frame的每一帧数据,是来自于get_video_frame方法, 填充的假数据
static AVFrame *get_video_frame(OutputStream *ost)
AVCodecContext *c = ost->enc;
/* check if we want to generate more frames */
if (av_compare_ts(ost->next_pts, c->time_base,
STREAM_DURATION, (AVRational){ 1, 1 }) > 0)
return NULL;
/* when we pass a frame to the encoder, it may keep a reference to it
* internally; make sure we do not overwrite it here */
if (av_frame_make_writable(ost->frame) < 0)
exit(1);
if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
/* as we only generate a YUV420P picture, we must convert it
* to the codec pixel format if needed */
if (!ost->sws_ctx) {
ost->sws_ctx = sws_getContext(c->width, c->height,
AV_PIX_FMT_YUV420P,
c->width, c->height,
c->pix_fmt,
SCALE_FLAGS, NULL, NULL, NULL);
if (!ost->sws_ctx) {
fprintf(stderr,
"Could not initialize the conversion context\n");
exit(1);
fill_yuv_image(ost->tmp_frame, ost->next_pts, c->width, c->height);
sws_scale(ost->sws_ctx, (const uint8_t * const *) ost->tmp_frame->data,
ost->tmp_frame->linesize, 0, c->height, ost->frame->data,
ost->frame->linesize);
} else {
fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height);
ost->frame->pts = ost->next_pts++;
return ost->frame;
调用av_compare_ts 判断是否继续生成新的frame,一开始规定了只写10秒钟的数据,超过了就不写了
av_frame_make_writable使当前的frame可写,被编码器引用的frame不可写,调用该方法如果被引用,内部会创建新buf,变成可写的
填充数据yuv,还做了缩放处理,暂不讨论
更新frame的pts
返回生成的frame
write_audio_frame
* encode one audio frame and send it to the muxer
* return 1 when encoding is finished, 0 otherwise
static
int write_audio_frame(AVFormatContext *oc, OutputStream *ost)
AVCodecContext *c;
AVFrame *frame;
int ret;
int dst_nb_samples;
c = ost->enc;
//获取音频帧
frame = get_audio_frame(ost);
if (frame) {
/* convert samples from native format to destination codec format, using the resampler */
/* compute destination number of samples */
计算根据重采样后应该生成的采样个数
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);
//由于没有修改采样率,只是修改了位深,采样个数保持不变
av_assert0(dst_nb_samples == frame->nb_samples);
/* when we pass a frame to the encoder, it may keep a reference to it
* internally;
* make sure we do not overwrite it here
//使ost->frame可写
ret = av_frame_make_writable(ost->frame);
if (ret < 0)
exit(1);
/* convert to destination format */
//重采样
ret = swr_convert(ost->swr_ctx,
ost->frame->data, dst_nb_samples,
(const uint8_t **)frame->data, frame->nb_samples);
if (ret < 0) {
fprintf(stderr, "Error while converting\n");
exit(1);
frame = ost->frame;
//重新计算音频pts
frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base);
//计算samples_count
ost->samples_count += dst_nb_samples;
//将音频帧写入文件
return write_frame(oc, c, ost->st, frame, ost->tmp_pkt);
准备AVFrame,调用get_audio_frame获取一个AVFrame,内部根据编码格式,填充pcm假数据
调用av_rescale_rnd计算重采样个数,如果数据源和送入编码器的音频的采样率不同,需要转换采样率,示例程序只是变了采样格式,没有更改采样率,采样个数不变
做重采样,重采样数据保存在ost->frame中
重采样后,需要更新frame的pts
调用write_frame编码,将数据写入文件
学习了通过应用libavformat将音视频数据编码封装到文件中
创建AVFormatContext
添加stream
配置编解码器,stream 参数
编码frame,生成pkt,更新pts
交替写音视频pkt
关闭编解码器,结束写文件
某昆real
音视频开发