本例子实现的是将视频数据和音频数据,按照一定的格式封装为特定的容器,比如FLV、MKV、MP4、AVI等等。
实现的过程,可以大致用如下图表示:
从图中可以大致看出视频封装的流程:
根据流程可以推倒出大致的代码实现:
对于由 YUV 数据得到编码的视频数据部分,不再重复。
直接看与 Muxer 相关的部分,与特定的 Muxer 相关的信息,FFMpeg 提供了一个 AVFormatContext 的结构体描述,并用avformat_alloc_output_context2()函数来分配它。
该函数的声明如下:
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);
其中:
代码如下:
AVOutputFormat *fmt; AVFormatContext *oc; /* allocate the output media context */ avformat_alloc_output_context2(&oc, NULL, NULL, filename); if (!oc) { printf("Could not deduce output format from file extension: using MPEG.\n"); avformat_alloc_output_context2(&oc, NULL, "mpeg", filename); } if (!oc) return 1; fmt = oc->oformat;
有了表示媒体文件格式的 AVFormatContext 结构后,就需要根据媒体格式来判断是否需要往媒体文件中添加视频流、音频流(有的媒体文件,这两种流并不是必须的);
以 MP4 格式的媒体文件为例,我们需要一路视频流、一路音频流。
因此需要创建一路流,FFMpeg 提供的创建流的函数为avformat_new_stream(),该函数完成向 AVFormatContext 结构体中所代码的媒体文件中添加数据流,函数声明如下:
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
其中:
注意:对于 Muxer,该函数必须在调用avformat_write_header()前调用。
使用完成后,需要调用avcodec_close()和avformat_free_context()来清理由它分配的内容。
该函数调用完成后,一个新的 AVStream 便已经加入到输出文件中,下面就需要设置 stream 的 id 和 codec 等参数。
以视频流为例,代码如下:
OutputStream *ost; AVFormatContext *oc; AVCodec **codec; AVCodecContext *c; AVStream *st; st = avformat_new_stream(oc, *codec); if(!st){ fprintf(stderr, "Could not allocate stream\n"); exit(1); } st->id = oc->nb_streams-1; c = st->codec;
参数设置完成后,就可以打开编码器并为编码器分配必要的内存。
步骤跟之前的类似,以视频为例,示例代码如下:
//open the codec ret = avcodec_open(c, codec, &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 ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
接下来进行真正的封装:首先,为媒体文件添加头部信息,FFMpeg 为此提供的函数为avformat_write_header()。
其次,将编码好的音视频 AVPacket 包添加到媒体文件中去,FFMpeg 为此提供的函数为av_interleaved_write_frame()。
最后,写入文件尾的数据,FFMpeg 为此提供的函数为av_write_trailer()。
封装的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等。
本例子实现的是将音视频分离,例如将封装格式为 FLV、MKV、MP4、AVI 等封装格式的文件,将音频、视频分离开来。
实现的过程,可以大致用如下图表示:
从图中可以看出大致的节封装流程:
根据流程可以推到出大致的代码流程:
在音视频分离后,需要将分离出的音视频分别放到不同的输出文件中,为此,需要打开文件以备后用。
static const char *video_dst_filename = NULL; static const char *audio_dst_filename = NULL; static FILE *video_dst_file = NULL; static FILE *audio_dst_file = NULL; video_dst_filename = argv[2]; audio_dst_filename = argv[3]; video_dst_file = fopen(video_dst_filename, "wb+"); audio_dst_file = fopen(audio_dst_filename, "wb+");
对于给定的需要 AV 分离的输入文件,使用avformat_open_input打开输入文件,并分配AVFormatContext结构。
该函数的声明如下:
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);
其中:
实现代码如下:
//open input file, and allocate format context if(avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) < 0){ fprintf(stderr, "Could not open source file %s\n", src_filename); exit(1); } //retrive stream information if(avformat_find_stream_info(fmt_ctx, NULL) < 0){ fprintf(stderr, "Could not find stream information\n"); exit(1); }
通过输入文件分配好AVFormatContext后,需要找到里面的音频流和视频流,此处需要用到的函数为av_find_best_stream;
之后要根据找到的不同的流(如H264流、HEVC流等)找到特定的编解码器,此处使用avcodec_find_decoder;
找到了解码器后, 就需要打开解码器,此处使用avcodec_open2函数完成。
下面分别介绍这几个函数的使用:
av_find_best_stream函数定义如下:
int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);
其中:
avcodec_find_decoder函数定义如下:
AVCodec *avcodec_find_decoder(enum AVCodecID id);
该函数参数为AVCodecID指定了请求的解码器,成功返回解码器,否则返回 NULL。
avcodec_open2函数定义如下:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
其中:
该函数的主要作用是根据给定的AVCodec初始化AVCodecContext,在使用该函数之前,待初始化的AVCodecContext结构需要先使用avcodec_alloc_context3分配好。
其中的参数 AVCodec可以通过avcodec_find_decoder_by_nameavcodec_find_encoder_by_nameavcodec_find_decoder或avcodec_find_endcoder来获取。
在进行真正的解码之前,必须调用该函数。
下面给出使用的示例:
avcodec_register_all(); av_dict_set(&opts, "b", "2.5M", 0); codec = avcodec_find_decoder(AV_CODEC_ID_H264); if(!codec) exit(1); context = avcodec_alloc_context3(codec); if(avcodec_open2(context, codec, opts) < 0) exit(1);
对于上面分析的部分,我们将其封装在一个函数里,代码如下:
static int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type) { int ret, stream_index; AVStream *pStream; AVCodecContext *codec_ctx = NULL; AVCodec *codec; ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0); if(ret < 0){ fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), src_filename); }else{ stream_index = ret; pStream = fmt_ctx->streams[stream_index]; //find decoder for the stream codec_ctx = pStream->codec; codec = avcodec_find_decoder(codec_ctx->codec_id); if(!codec){ fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type)); return AVERROR(EINVAL); } //open the decoder if((ret = avcodec_open2(codec_ctx, codec, NULL))< 0){ fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type)); return ret; } } *stream_idx = stream_index; }
针对音频、视频,分别调用该函数,示例代码如下:
if(open_codec_context(&video_stream_idx, fmt_ctx, AVMEDIA_TYPE_VIDEO) >= 0){ video_stream = fmt_ctx->streams[video_stream_idx]; video_codec_ctx = video_stream->codec; //allocate image where the decoded image will be put width = video_codec_ctx->width; height = video_codec_ctx->height; pix_fmt = video_codec_ctx->pix_fmt; ret = av_image_alloc(video_dst_data, video_dst_linesize, width, height, pix_fmt, 1); if(ret < 0){ fprintf(stderr, "Could not allocate raw video buffer\n"); exit(1); } video_dst_bufsize = ret; } if(open_codec_context(&audio_stream_idx, fmt_ctx, AVMEDIA_TYPE_AUDIO) >= 0){ audio_stream = fmt_ctx->streams[audio_stream_idx]; audio_codec_ctx = audio_stream->codec; }
上面的一些准备工作完成后,就需要从输入文件中一帧一帧读取数据,并进行解码了。
从这里可以看出,需要找到一个 一帧视频存放的地方,为此需要使用av_init_packet初始化一个AVPacket。
之后就可以使用av_read_frame来从输入 文件中读取一个 frame。
示例代码如下:
static int decode_packet(int *got_frame, int cached) { int ret = 0; int decoded = pkt.size; *got_frame = 0; if(pkt.stream_index == video_stream_idx){ //decode video frame ret = avcodec_decode_video2(video_codec_ctx, frame, got_frame, &pkt); if(ret < 0){ fprintf(stderr, "Error decoding video frame (%s) \n", av_err2str(ret)); return ret; } printf("num %d got_frame %d\n", num++, *got_frame); if(*got_frame){ av_image_copy(video_dst_data, video_dst_linesize, (const uint8_t **)(frame->data), frame->linesize, pix_fmt, width, height); //write to raw video file fwrite(video_dst_data[0], 1, video_dst_bufsize, video_dst_file); } }else if(pkt.stream_index == audio_stream_idx){ //decode audio frame ret = avcodec_decode_audio4(audio_codec_ctx, frame, got_frame, &pkt); if(ret < 0){ fprintf(stderr, "Error decoding audio frame (%s)\n", av_err2str(ret)); return ret; } if(*got_frame){ size_t unpadded_linesize = frame->nb_samples * av_get_bytes_per_sample(frame->format); fwrite(frame->extended_data[0], 1, unpadded_linesize, audio_dst_file); } } return FFMIN(ret, pkt.size); } //allocate frame frame = av_frame_alloc(); if(!frame){ fprintf(stderr, "Could not allocate frame\n"); exit(1); } av_init_packet(&pkt); pkt.data = NULL; pkt.size = 0; //read frames from the file int got_frame; while(av_read_frame(fmt_ctx, &pkt) >= 0){ AVPacket orig_pkt = pkt; do{ ret = decode_packet(&got_frame, 0); if(ret < 0) break; pkt.data += ret; pkt.size -= ret; }while(pkt.size > 0); av_free_packet(&orig_pkt); }
解封装大致流程已经完成了,剩余的是一些收尾工作,例如释放刚刚分配的内存等。
作者:赖人李冰
http://lazybing.github.io/blog/2017/01/01/ffmpeg-sdk-learning/
本文分享自微信公众号 - 音视频开发进阶(glumes_blog)
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2020-07-23
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句