流程图
ffmpeg 的 Mux 主要分为 三步操作:
- avformat_write_header : 写⽂件头
- av_write_frame/av_interleaved_write_frame: 写packet
- av_write_trailer : 写⽂件尾
- avcodec_parameters_from_context:将AVCodecContext结构体中码流参数拷⻉到AVCodecParameters结构体中
- 和avcodec_parameters_to_context 将AVCodecParameters结构体中码流参数拷⻉到AVCodecContext结构体中
函数结构体梳理
avformat_alloc_output_context2
函数参数的介绍:
-
ctx:需要创建的context,返回NULL表示失败。
-
oformat:指定对应的AVOutputFormat,如果不指定,可以通过后⾯
-
format_name、filename两个参 数进⾏指定,让ffmpeg⾃⼰推断。
-
format_name: 指定⾳视频的格式,⽐如“flv”,“mpeg”等,如果设置为NULL,则由filename进⾏指 定,让ffmpeg⾃⼰推断。
-
filename: 指定⾳视频⽂件的路径,如果oformat、format_name为NULL,则ffmpeg内部根据 filename后缀名选择合适的复⽤器,⽐如xxx.flv则使⽤flv复⽤器。
AVOutputFormat
AVOutpufFormat表示输出⽂件容器格式,AVOutputFormat 结构主要包含的信息有:封装名称描述,编 码格式信息(video/audio 默认编码格式,⽀持的编码格式列表),⼀些对封装的操作函数。
ffmpeg⽀持各种各样的输出⽂件格式,MP4,FLV,3GP等等。⽽ AVOutputFormat 结构体则保存了这 些格式的信息和⼀些常规设置。 每⼀种封装对应⼀个 AVOutputFormat 结构,ffmpeg将AVOutputFormat 按照链表存储:
avformat_new_stream AVStream 即是流通道。
例如我们将 H264 和 AAC 码流存储为MP4⽂件的时候,就需要在 MP4⽂件中 增加两个流通道,⼀个存储Video:H264,⼀个存储Audio:AAC(假设H264和AAC只包含单个流通道).
av_interleaved_write_frame
将数据包写⼊输出媒体⽂件,并确保正确的交织(保持packet dts的增⻓性)。 该函数会在内部根据需要缓存packet,以确保输出⽂件中的packet按dts递增的顺序正确交织。
如果⾃⼰ 进⾏交织则应调⽤av_write_frame()
av_compare_ts
比较输入前后的
其他
- const char *name; // 复⽤器名称
- const char *long_name;//格式的描述性名称,易于阅读。
- enum AVCodecID audio_codec; //默认的⾳频编解码器
- enum AVCodecID video_codec; //默认的视频编解码器
- enum AVCodecID subtitle_codec; //默认的字幕编解码器
工程梳理
基本初始化 have 代表有几路流 其他很熟悉了
首先封装一个流结构体
有常用基本参数
// 封装单个输出AVStream
typedef struct OutputStream
{
AVStream *st;// 代表一个stream, 1路audio或1路video都代表独立的steam
AVCodecContext *enc; //编码器上下文
int64_t next_pts; //生成下一帧的pts player
int samples_count; //A——采样累计
AVFrame *frame; // 重采样后的frame, 视频叫scale
AVFrame *tmp_frame; // 重采样前
float t, tincr, tincr2; // 这几个参数用来生成PCM和YUV用的
struct SwsContext *sws_ctx; // 图像scale
struct SwrContext *swr_ctx; // 音频重采样
}OutputStream;
常规初始化
OutputStream video_st = { 0 }; // 封装视频编码相关的
OutputStream audio_st = { 0 }; // 封装音频编码相关的
const char *filename; // 输出文件
AVOutputFormat *fmt; // 输出文件容器格式, 封装了复用规则,AVInputFormat则是封装了解复用规则
AVFormatContext *oc;
AVCodec *audio_codec, *video_codec;
int ret;
int have_video = 0, have_audio = 0;
int encode_video = 0, encode_audio = 0;
AVDictionary *opt = NULL;
int i;
前面提过了通过名字获取outputformat
指定编码器
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
if (!oc)
{
// 如果不能根据文件后缀名找到合适的格式,那缺省使用flv格式
printf("Could not deduce output format from file extension: using flv.\n");
avformat_alloc_output_context2(&oc, NULL, "flv", filename);
}
fmt = oc->oformat; // 获取绑定的AVOutputFormat
fmt->video_codec = AV_CODEC_ID_H264; // 指定编码器
fmt->audio_codec = AV_CODEC_ID_AAC; // 指定编码器
初始化流和对应的编码器
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
增加输出流,返回AVStream,并给codec赋值,但此时codec并未打开
就是 根据前面的输入的参数 设定编码器 初始化流 在和ctx绑定
音频 视频都是这样
先定义一个 AVCodecContext *codec_ctx;
新建码流 绑定到 AVFormatContext stream
ost->st = avformat_new_stream(oc, NULL);
oc 是outputformatcontext
然后
codec_ctx = avcodec_alloc_context3(*codec); // 创建编码器上下文
最后复制给
ost->enc = codec_ctx
ost 就是输出流的封装格式
之后绑定基本参数
//增加输出流,返回AVStream,并给codec赋值,但此时codec并未打开
static void add_stream(OutputStream *ost, AVFormatContext *oc,
AVCodec **codec,
enum AVCodecID codec_id)
{
AVCodecContext *codec_ctx;
int i;
/* 查找编码器 */
*codec = avcodec_find_encoder(codec_id); //通过codec_id找到编码器
if (!(*codec))
{
fprintf(stderr, "Could not find encoder for '%s'\n",
avcodec_get_name(codec_id));
exit(1);
}
// 新建码流 绑定到 AVFormatContext stream->index 有设置
ost->st = avformat_new_stream(oc, NULL); // 创建一个流成分
if (!ost->st)
{
fprintf(stderr, "Could not allocate stream\n");
exit(1);
}
/* 为什么要 -1呢?每次调用avformat_new_stream的时候nb_streams+1
但id是从0开始, 比如第1个流:对应流id = nb_streams(1) -1 = 0
第2个流:对应流id = nb_streams(2) -1 = 1
*/
ost->st->id = oc->nb_streams - 1;
codec_ctx = avcodec_alloc_context3(*codec); // 创建编码器上下文
if (!codec_ctx)
{
fprintf(stderr, "Could not alloc an encoding context\n");
exit(1);
}
ost->enc = codec_ctx;
// 初始化编码器参数
switch ((*codec)->type)
{
case AVMEDIA_TYPE_AUDIO:
codec_ctx->codec_id = codec_id;
codec_ctx->sample_fmt = (*codec)->sample_fmts ? // 采样格式
(*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
codec_ctx->bit_rate = 64000; // 码率
codec_ctx->sample_rate = 44100; // 采样率
if ((*codec)->supported_samplerates)
{
codec_ctx->sample_rate = (*codec)->supported_samplerates[0];
for (i = 0; (*codec)->supported_samplerates[i]; i++)
{
if ((*codec)->supported_samplerates[i] == 44100)
codec_ctx->sample_rate = 44100;
}
}
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
if ((*codec)->channel_layouts)
{
codec_ctx->channel_layout = (*codec)->channel_layouts[0];
for (i = 0; (*codec)->channel_layouts[i]; i++) {
if ((*codec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
}
}
codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
// 设置timebase, 使用采样率
ost->st->time_base = (AVRational){ 1, codec_ctx->sample_rate };
break;
case AVMEDIA_TYPE_VIDEO:
codec_ctx->codec_id = codec_id;
codec_ctx->bit_rate = 400000;
/* Resolution must be a multiple of two. */
codec_ctx->width = 352; // 分辨率
codec_ctx->height = 288;
codec_ctx->max_b_frames = 1;
/* timebase: This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented. For fixed-fps content,
* timebase should be 1/framerate and timestamp increments should be
* identical to 1. */
ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE }; // 时基
codec_ctx->time_base = ost->st->time_base;
codec_ctx->gop_size = STREAM_FRAME_RATE; //
codec_ctx->pix_fmt = STREAM_PIX_FMT;
break;
default:
break;
}
/* 看看格式是否希望steam头是分开的 */
if (oc->oformat->flags & AVFMT_GLOBALHEADER)
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; //
}
创建各种buffer
if (have_video)
open_video(oc, video_codec, &video_st, opt);
if (have_audio)
open_audio(oc, audio_codec, &audio_st, opt);
open_video
先关联编码器
ret = avcodec_open2(codec_ctx, codec, &opt);
然后分配buffer 大小由传入的相关参数决定
ost->frame = alloc_picture(codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height);
如果格式不是YUV420P 如果输出格式不是YUV420P,则使用临时YUV420P则需要转换 然后将其转换为所需的输出格式。
if (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P)
{
// 编码器格式需要的数据不是 AV_PIX_FMT_YUV420P才需要 调用图像scale
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height);
if (!ost->tmp_frame)
{
fprintf(stderr, "Could not allocate temporary picture\n");
exit(1);
}
}
然后AVCodecContext结构体中码流参数拷⻉到AVCodecParameters结构体 也就是拷贝刚刚设定以及初始化的各种 codec_ctx 中参数拷贝到
流中ost->st->codecpar
ret = avcodec_parameters_from_context(ost->st->codecpar, codec_ctx);
static void open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
int ret;
AVCodecContext *codec_ctx = ost->enc;
AVDictionary *opt = NULL;
av_dict_copy(&opt, opt_arg, 0);
/* open the codec */
// 1. 关联编码器
ret = avcodec_open2(codec_ctx, codec, &opt);
av_dict_free(&opt);
if (ret < 0)
{
fprintf(stderr, "Could not open video codec: %s\n", av_err2str(ret));
exit(1);
}
// 2. 分配帧buffer
/* allocate and init a re-usable frame */
ost->frame = alloc_picture(codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->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 (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P)
{
// 编码器格式需要的数据不是 AV_PIX_FMT_YUV420P才需要 调用图像scale
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height);
if (!ost->tmp_frame)
{
fprintf(stderr, "Could not allocate temporary picture\n");
exit(1);
}
}
/* copy the stream parameters to the muxer */
ret = avcodec_parameters_from_context(ost->st->codecpar, codec_ctx);
if (ret < 0)
{
fprintf(stderr, "Could not copy the stream parameters\n");
exit(1);
}
}
alloc_picture
通过传入格式 创建对应的frame
static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
AVFrame *picture;
int ret;
picture = av_frame_alloc();
if (!picture)
return NULL;
picture->format = pix_fmt;
picture->width = width;
picture->height = height;
/* allocate the buffers for the frame data */
ret = av_frame_get_buffer(picture, 32);
if (ret < 0)
{
fprintf(stderr, "Could not allocate frame data.\n");
exit(1);
}
return picture;
}
判断输出文件
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;
}
}
写入header
audio AVstream->base_time = 1/44100, video AVstream->base_time = 1/25
/* 写头部. 到底做了什么操作呢? 对应steam的time_base被改写 和封装格式有关系*/
ret = avformat_write_header(oc, &opt);// 返回值 会改变 base_time base_time audio = 1/1000 video = 1/1000
####编码音频和视频
判断那个时间比较早 编码早的
while (encode_video || encode_audio)
{
/* select the stream to encode */
if (encode_video && // video_st.next_pts值 <= audio_st.next_pts时
(!encode_audio || av_compare_ts(video_st.next_pts, video_st.enc->time_base,
audio_st.next_pts, audio_st.enc->time_base) <= 0)) {
printf("\nwrite_video_frame\n");
encode_video = !write_video_frame(oc, &video_st);
}
else
{
printf("\nwrite_audio_frame\n");
encode_audio = !write_audio_frame(oc, &audio_st);
}
}
write_video_frame
就说编码+写入pkt中
oc 是formatctx 音频那边也是同样一个 这就保证了写入目标一致性
ret = write_frame(oc, &codec_ctx->time_base, ost->st, &pkt);
*
*
* encode one video frame and send it to the muxer
* return 1 when encoding is finished, 0 otherwise
*/
static int write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
int ret;
AVCodecContext *codec_ctx;
AVFrame *frame;
int got_packet = 0;
AVPacket pkt = { 0 };
codec_ctx = ost->enc;
frame = get_video_frame(ost);
av_init_packet(&pkt);
/* encode the image */
// 编码的另外一种方式 第二种方式罢了
ret = avcodec_encode_video2(codec_ctx, &pkt, frame, &got_packet);
if (ret < 0)
{
fprintf(stderr, "Error encoding video frame: %s\n", av_err2str(ret));
exit(1);
}
if (got_packet)
{
ret = write_frame(oc, &codec_ctx->time_base, ost->st, &pkt);
}
else
{
ret = 0;
}
if (ret < 0)
{
fprintf(stderr, "Error while writing video frame: %s\n", av_err2str(ret));
exit(1);
}
// 这里之所以有两个判断条件
// frame非NULL: 表示还在产生YUV数据帧
// got_packet为1: 编码器还有缓存的帧
return (frame || got_packet) ? 0 : 1;
}
get_video_frame
设定基础帧 和 重采样 然后产生数据 如果不一样就重采样 一样就不重采样返回数据
就这样简单
音频和视频逻辑类型 说一个就可以
产生一帧视频数据 如果大于需要的时间就不产生! 并且传入这一帧数据对应的PTS
// 我们测试时只产生STREAM_DURATION(这里是5.0秒)的视频数据 通过next_pts*time_base 得到结果不能大于5 因为只要五秒数据
if (av_compare_ts(ost->next_pts, codec_ctx->time_base,
STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
tatic AVFrame *get_video_frame(OutputStream *ost)
{
AVCodecContext *codec_ctx = ost->enc;
/* check if we want to generate more frames */
// 我们测试时只产生STREAM_DURATION(这里是5.0秒)的视频数据 如果数据比5秒还大那就有问题
if (av_compare_ts(ost->next_pts, codec_ctx->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 (codec_ctx->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(codec_ctx->width, codec_ctx->height,
AV_PIX_FMT_YUV420P,
codec_ctx->width, codec_ctx->height,
codec_ctx->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, codec_ctx->width, codec_ctx->height);
//视频重采样
sws_scale(ost->sws_ctx, (const uint8_t * const *) ost->tmp_frame->data,
ost->tmp_frame->linesize, 0, codec_ctx->height, ost->frame->data,
ost->frame->linesize);
} else {
fill_yuv_image(ost->frame, ost->next_pts, codec_ctx->width, codec_ctx->height);
}
ost->frame->pts = ost->next_pts++; // 为什么+1? 单位是 1/25 = 40ms
// 0 1 2 -> 0 40ms 80ms
return ost->frame;
}
源码地址
代码地址
https://github.com/zycccer/aac_mp3_pcm