例子
该例子的功能是将mp4文件转换成yuv数据以及h264裸流。
#include#include #include #include "config.h"extern "C" {#include #include #include #include #include #include };using namespace std;int main(void) { FILE *f_yuv = fopen("output.yuv", "wb+"); FILE *f_h264 = fopen("output.h264", "wb+"); //初始化avcodec avcodec_register_all(); //初始化 demuxer av_register_all(); //创建一个用于demuxer的结构体 AVFormatContext *av_format_context = avformat_alloc_context(); char source[] = "/Users/yxwang/Downloads/test.mp4"; if (avformat_open_input(&av_format_context, source, NULL, NULL) != 0) { cout << "打开文件失败" << endl; return -1; } //需要关闭尝试是否需要手动获取视频文件信息 if (avformat_find_stream_info(av_format_context, NULL) < 0) { //获取视频文件信息 cout << "Couldn't find stream information." << endl; return -1; } int videoindex = -1; for (int i = 0; i < av_format_context->nb_streams; i++) { if (av_format_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { videoindex = i; break; } } if (videoindex == -1) { cout << "Didn't find a video stream." << endl; return -1; } AVCodecContext *pCodecCtx = av_format_context->streams[videoindex]->codec; AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); //查找decoder if (pCodec == NULL) { printf("Codec not found.\n"); return -1; } if (avcodec_open2(pCodecCtx, pCodec, NULL) != 0) { printf("Can not open codec.\n"); return -1; } uint8_t *out_buffer = (uint8_t *) av_malloc( av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1)); AVFrame *decodeFrame = av_frame_alloc(); AVFrame *pFrameYUV = av_frame_alloc(); av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1); AVPacket* packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //Output Info----------------------------- printf("--------------- File Information ----------------\n"); av_dump_format(av_format_context, 0, source, 0); printf("-------------------------------------------------\n"); SwsContext *img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); while (av_read_frame(av_format_context, packet) >= 0) { //读取一帧压缩数据 if (packet->stream_index == videoindex) { fwrite(packet->data, 1, packet->size, f_h264); //把H264数据写入fp_h264文件 if (avcodec_send_packet(pCodecCtx, packet) < 0) { //解码一帧压缩数据 printf("Decode Error.\n"); return -1; } while (avcodec_receive_frame(pCodecCtx, decodeFrame) == 0) { sws_scale(img_convert_ctx, (const unsigned char* const *) decodeFrame->data, decodeFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); int y_size = pCodecCtx->width * pCodecCtx->height; fwrite(pFrameYUV->data[0], 1, y_size, f_yuv); //Y fwrite(pFrameYUV->data[1], 1, y_size / 4, f_yuv); //U fwrite(pFrameYUV->data[2], 1, y_size / 4, f_yuv); //V } } av_free_packet(packet); } fclose(f_yuv); fclose(f_h264); sws_freeContext(img_convert_ctx); av_frame_free(&pFrameYUV); av_frame_free(&decodeFrame); avcodec_close(pCodecCtx); //内部会调用avformat_free_context avformat_close_input(&av_format_context); return EXIT_SUCCESS;}
深入分析
那么正题来了,我们已经在前面的章节分析过了avcodec_register_all 以及 av_register_all两个函数。并且也已经知道使用avformat_alloc_context来创建一个AVFormatContext 是所有和解封装封装相关的基础操作()。
avformat_open_input
第一个需要研究的函数就是avformat_open_input了,该方法定义在了avformat.h中
/** * Open an input stream and read the header. The codecs are not opened. * The stream must be closed with avformat_close_input(). * 打开一个输入流,并读取它的头 * @param ps 可以传入空指针,这个时候方法会自动创建一个AVFormatContext并且放入ps中 * @param url URL of the stream to open. 流地址 * @param fmt 如果不为空,那么强制使用指定的输入格式,否则ffmpeg会去自动发现格式 * @param options A dictionary filled with AVFormatContext and demuxer-private options. * On return this parameter will be destroyed and replaced with a dict containing * options that were not found. May be NULL. * * @return 0 on success, a negative AVERROR on failure. * * @note If you want to use custom IO, preallocate the format context and set its pb field. */int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
该方法算是核心方法了,实现来说还是比较复杂的,这里有一张古时候的调用流程图,在当前版本的ffmpeg基本也是这个流程。
这里还有一张当前的调用流程图
实现写在了 avformat/utils.c中.
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options){ AVFormatContext *s = *ps; int i, ret = 0; AVDictionary *tmp = NULL; ID3v2ExtraMeta *id3v2_extra_meta = NULL; if (!s && !(s = avformat_alloc_context())) //如果AVFormatContext未空,那么新创建一个 return AVERROR(ENOMEM); if (!s->av_class) { av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n"); return AVERROR(EINVAL); } if (fmt) //如果fmt不为空,那么直接指定AVInputFormat s->iformat = fmt; if (options)//将option拷贝到 tmp中 av_dict_copy(&tmp, *options, 0); if (s->pb) // must be before any goto fail 设置flag 用户自己设置了AVIOContext s->flags |= AVFMT_FLAG_CUSTOM_IO; if ((ret = av_opt_set_dict(s, &tmp)) < 0) goto fail; if (!(s->url = av_strdup(filename ? filename : ""))) { ret = AVERROR(ENOMEM); goto fail; }#if FF_API_FORMAT_FILENAMEFF_DISABLE_DEPRECATION_WARNINGS av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename));FF_ENABLE_DEPRECATION_WARNINGS ...............
avformat_open_input方法的实现很长,不过其中包含了非常多的保护性代码,比如上面的代码,都是在做一些安全性保护,以及变量初始化。
init_input
这是avformat_open_input中核心方法,主要作用是打开输入的视频数据并且探测视频的格式.
/* Open input file and probe the format if necessary. */static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options){ int ret; AVProbeData pd = { filename, NULL, 0 }; int score = AVPROBE_SCORE_RETRY; //得分 if (s->pb) { //自定义AVIOContext的情况,一般发生在从内存中读取数据,这个时候需要自定义AVIOContext直接输入 s->flags |= AVFMT_FLAG_CUSTOM_IO; if (!s->iformat) //如果没有自己设置iformat,那么使用av_probe_input_buffer2推测AVInputFormat return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); else if (s->iformat->flags & AVFMT_NOFILE) av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and " "will be ignored with AVFMT_NOFILE format.\n"); return 0; //指定了iformat直接返回 } //如果没有设置AVInputFormat,那么使用av_probe_input_format2来判断文件格式。 //如果找到了大于预设值score的分数,那么直接返回分数 if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) || (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score)))) return score; //如果没有判断出来,那么就需要通过io_open真正打开文件,再去判断AVInputFormat,这个方法的实现我们后些说 if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0) return ret; if (s->iformat) return 0; return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize);}
在函数的开头的score变量是一个判决AVInputFormat的分数的门限值,如果最后得到的AVInputFormat的分数低于该门限值,就认为没有找到合适的AVInputFormat。FFmpeg内部判断封装格式的原理实际上是对每种AVInputFormat给出一个分数,满分是100分,越有可能正确的AVInputFormat给出的分数就越高。最后选择分数最高的AVInputFormat作为推测结果。
av_probe_input_format2
/** * Guess the file format. * * @param pd data to be probed 存储输入数据信息的AVProbeData结构体。 * @param is_opened Whether the file is already opened; determines whether * demuxers with or without AVFMT_NOFILE are probed. 文件是否打开。 * @param score_max A probe score larger that this is required to accept a * detection, the variable is set to the actual detection * score afterwards. * If the score is <= AVPROBE_SCORE_MAX / 4 it is recommended * to retry with a larger probe buffer.判决AVInputFormat的门限值。只有某格式判决分数大于该 * 门限值的时候,函数才会返回该封装格式,否则返回NULL。 */AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max);
该函数用于根据输入数据查找合适的AVInputFormat.
其中涉及到一个AVProbeData的结构体,从Init_input上我们可以找打它的构造
AVProbeData pd = { filename, NULL, 0 };
实际上就是用来存储视频数据信息的一个结构体,具体定义如下
/** * This structure contains the data a format has to probe a file. */typedef struct AVProbeData { const char *filename; //文件路径 unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. 用于存放推测的媒体数据,但是最后还需要填充AVPROBE_PADDING_SIZE个0(实际就是32个) */ int buf_size; /**< Size of buf except extra allocated bytes buffer长度,不包括填充的0的长度 */ const char *mime_type; /**< mime_type, when known. 存放的推测媒体数据的mime_type*/} AVProbeData;
回到 av_probe_input_format2 这个函数的定义如下
AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max){ int score_ret; AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret); if (score_ret > *score_max) { *score_max = score_ret; return fmt; } else return NULL;}
方法比较简单,实际上就是进一步去调用av_probe_input_format3去查找AVInputFormat,同时还要返回最大得分。通过和阈值得分比价,如果大于阈值得分,那么返回查找到的AVInputFormat会被返回,否则返回null。
av_probe_input_format3
层层递进,不愧是ffmpeg的核心方法,复杂程度也是杠杠得!在分析代码之前可以先了解一些知识:
ID3,一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息,ID3信息分为两个版本,v1和v2版。
定义没啥好说的,直接来看实现,代码还是比较长的。
AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened, int *score_ret/*最匹配格式的分数,需要改方法填入值*/){ AVProbeData lpd = *pd; AVInputFormat *fmt1 = NULL, *fmt; int score, score_max = 0; void *i = 0; const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; enum nodat { NO_ID3, ID3_ALMOST_GREATER_PROBE, ID3_GREATER_PROBE, ID3_GREATER_MAX_PROBE, } nodat = NO_ID3; if (!lpd.buf) lpd.buf = (unsigned char *) zerobuffer; //这一段是用来检查是否有ID3信息的,并且移动指针跳,使lpd.buf移动到数据地址 if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) { int id3len = ff_id3v2_tag_len(lpd.buf); if (lpd.buf_size > id3len + 16) { if (lpd.buf_size < 2LL*id3len + 16) nodat = ID3_ALMOST_GREATER_PROBE; lpd.buf += id3len; lpd.buf_size -= id3len; } else if (id3len >= PROBE_BUF_MAX) { nodat = ID3_GREATER_MAX_PROBE; } else nodat = ID3_GREATER_PROBE; } fmt = NULL; //遍历所有的 AVIputFormat while ((fmt1 = av_demuxer_iterate(&i))) { if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2")) continue; score = 0; if (fmt1->read_probe) { score = fmt1->read_probe(&lpd); if (score) av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size); if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) { switch (nodat) { case NO_ID3: score = FFMAX(score, 1); break; case ID3_GREATER_PROBE: case ID3_ALMOST_GREATER_PROBE: score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1); break; case ID3_GREATER_MAX_PROBE: score = FFMAX(score, AVPROBE_SCORE_EXTENSION); break; } } } else if (fmt1->extensions) { if (av_match_ext(lpd.filename, fmt1->extensions)) score = AVPROBE_SCORE_EXTENSION; } if (av_match_name(lpd.mime_type, fmt1->mime_type)) { if (AVPROBE_SCORE_MIME > score) { av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME); score = AVPROBE_SCORE_MIME; } } if (score > score_max) { score_max = score; fmt = fmt1; } else if (score == score_max) fmt = NULL; } if (nodat == ID3_GREATER_PROBE) score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max); *score_ret = score_max; return fmt;}
该方法最重要的就是循环中的内容,使用av_demuxer_iterate来遍历所有的demuxer(也就是AVInputFormat)。
如果当前AVInputFormat定义了read_probe方法,就是用该方法来匹配数据,并且返回一个所得分。这里我们不去分析每个AVInputFormat到底是如何去计算分数的(需要的时候可以自己去看相关的类型)。
av_match_ext用来比对文件的后缀名和AVInputFormat的后缀名是否相同。
另外还会使用av_match_name()比较输入媒体的mime_type,如果匹配,那么得分就是75分。
基本逻辑就是这样,其中av_match_ext 和 av_match_name其实是很基础的代码,实际上不涉及到多媒体逻辑,只是字符串比较而已,看一下代码一下就能明白,所以这里也不多介绍。
av_probe_input_buffer2
av_probe_input_buffer2(),它根据输入的媒体数据推测该媒体数据的AVInputFormat,声明位于libavformat\avformat.h
/** * Probe a bytestream to determine the input format. Each time a probe returns * with a score that is too low, the probe buffer size is increased and another * attempt is made. When the maximum probe size is reached, the input format * with the highest score is returned. * * @param pb the bytestream to probe 用于读取数据的AVIOContext * @param fmt the input format is put here 推测出来的AVInputFormat * @param url the url of the stream 输入媒体的路径 * @param logctx the log context 日志 * @param offset the offset within the bytestream to probe from 开始推测AVInputFormat的偏移量。 * @param max_probe_size the maximum probe buffer size (zero for default) 用于推测格式的媒体数据的最大值。0表示数据大长度 * @return the score in case of success, a negative value corresponding to an * the maximal score is AVPROBE_SCORE_MAX 推测后返回匹配分数 * AVERROR code otherwise */int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt, const char *url, void *logctx, unsigned int offset, unsigned int max_probe_size);
实现在avformat.c中
int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt, const char *filename, void *logctx, unsigned int offset, unsigned int max_probe_size){ AVProbeData pd = { filename ? filename : "" }; uint8_t *buf = NULL; int ret = 0, probe_size, buf_offset = 0; int score = 0; int ret2; //如果没有设置最大读取数据长度,那么设置成默认值,约1M if (!max_probe_size) max_probe_size = PROBE_BUF_MAX; else if (max_probe_size < PROBE_BUF_MIN) { av_log(logctx, AV_LOG_ERROR, "Specified probe size value %u cannot be < %u\n", max_probe_size, PROBE_BUF_MIN); return AVERROR(EINVAL); } if (offset >= max_probe_size) return AVERROR(EINVAL); if (pb->av_class) { uint8_t *mime_type_opt = NULL; char *semi; av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt); pd.mime_type = (const char *)mime_type_opt; semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL; if (semi) { *semi = '\0'; } }#if 0 if (!*fmt && pb->av_class && av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type) >= 0 && mime_type) { if (!av_strcasecmp(mime_type, "audio/aacp")) { *fmt = av_find_input_format("aac"); } av_freep(&mime_type); }#endif //这个for循环是精髓,它增量式读取媒体数据进行判断,如果判断出来了,那就直接返回,否则读取更多数据送入判断 for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt; probe_size = FFMIN(probe_size << 1, FFMAX(max_probe_size, probe_size + 1))) { score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0; /* Read probe data. */ //分配空间,用于读取数据 if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0) goto fail; //读取指定大小的数据 if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) { /* Fail if error was not end of file, otherwise, lower score. */ if (ret != AVERROR_EOF) goto fail; score = 0; ret = 0; /* error was end of file, nothing read */ } buf_offset += ret; if (buf_offset < offset) continue; pd.buf_size = buf_offset - offset; pd.buf = &buf[offset]; memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE); /* Guess file format. */ //最终的数据判断实际上还是调用我们上面介绍的av_probe_input_format2 *fmt = av_probe_input_format2(&pd, 1, &score); if (*fmt) { /* This can only be true in the last iteration. */ if (score <= AVPROBE_SCORE_RETRY) { av_log(logctx, AV_LOG_WARNING, "Format %s detected only with low score of %d, " "misdetection possible!\n", (*fmt)->name, score); } else av_log(logctx, AV_LOG_DEBUG, "Format %s probed with size=%d and score=%d\n", (*fmt)->name, probe_size, score);#if 0 FILE *f = fopen("probestat.tmp", "ab"); fprintf(f, "probe_size:%d format:%s score:%d filename:%s\n", probe_size, (*fmt)->name, score, filename); fclose(f);#endif } } if (!*fmt) ret = AVERROR_INVALIDDATA;fail: /* Rewind. Reuse probe buffer to avoid seeking. */ ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset); if (ret >= 0) ret = ret2; av_freep(&pd.mime_type); return ret < 0 ? ret : score;}
再回到前面一些,是不是已经忘了我们到底再分析什么了?我们正在分析 avformat_open_input 这个方法,并且还是刚说完第一步init_input而已。
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options){......//打开文件并判断数据类型if ((ret = init_input(s, filename, &tmp)) < 0) goto fail; s->probe_score = ret; //如果 AVIOContext设置了协议白名单,并且AVFormatContext自己没设置,那么拷贝过去 if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) { s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist); if (!s->protocol_whitelist) { ret = AVERROR(ENOMEM); goto fail; } } //黑名单,处理逻辑和白名单相同 if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) { s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist); if (!s->protocol_blacklist) { ret = AVERROR(ENOMEM); goto fail; } } //如果AVFormatContext设置了格式白名单,那么就用当前匹配出来的格式和白名单对比,如果不在白名单中,那么就报错 if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) { av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist); ret = AVERROR(EINVAL); goto fail; } //跳过打开文件时的初始字节,在encoding中没有效果,在decoding中用户自己设置 avio_skip(s->pb, s->skip_initial_bytes);..... //读取id3v2 if (s->pb) ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC,&id3v2_extra_meta);.....}
关于协议的白名单和协议黑名单,我们在之后讲解.
read_header()
该方法用于读取多媒体数据文件头,根据视音频流创建相应的AVStream,不同的AVInputFormat使用会用不同的读取方法,所以该方法会在每个自己的demuxer中自己定义。并且理论上需要调用avformat_new_stream来创建 AVStream(但是我看了flv格式的read_header方法,发现并没有调用avformat_new_stream来创建AVStream,而是被推迟了)。avformat_new_stream方法的作用就是初始化AVFormatContext中的AVSteam,分配空间,但是不会填入值。关于AVStream的创建会补充到中
参考文档
https://blog.csdn.net/leixiaohua1020/article/details/44064715