例子
该例子的功能是将mp4文件转换成yuv数据以及h264裸流。
#include<stdio.h>
#include <stdlib.h>
#include <iostream>
#include "config.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/mem.h>
#include <libavutil/imgutils.h>
#include <libavutil/pixfmt.h>
#include <libswscale/swscale.h>
}
;
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 是所有和解封装封装相关的基础操作(ffmpeg源码分析 (二))。
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_FILENAME
FF_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的创建会补充到ffmpeg源码分析 (二)中
参考文档
https://blog.csdn.net/leixiaohua1020/article/details/44064715