今天学习解析媒体文件。

写了一个用例,解析MP4文件得到视频帧和音频帧,并分别保存到不同的文件。

照惯例,先学习,再代码。

 

学习

av_register_all

/**
 * 初始化 libavformat,并且注册所有的合并器、解析器和协议。
 * 如果你不调用这个方法,你可以明确地选择你想要程序支持的格式。
 * 参照 av_register_input_format()
 * 参照 av_register_output_format()
 */
void av_register_all(void);

 

avformat_open_input

/**
 * 打开一个输入流并且读取它的头部数据,这个编解码器不会被打开。
 * 这个流必须使用avformat_close_input()关闭。
 * 
 * @参数 ps	指向一个用户提供的AVFormatContext(使用avformat_alloc_context分配的空间)。
 *			这个参数可以指向NULL,这样,该参数将会在方法内部被分配控件,并且写入ps。
 *
 *			注意,如果这个参数是用户提供的,方法调用失败后,AVFormatContext会被释放。
 *
 * @参数 url 被打开媒体流的url。对于媒体文件,它是媒体文件路径。
 * @参数 fmt 如果不为空,这个参数规定一个特定的输入格式。
 *           否则,输入格式将被自动检测。
 * @参数 options ...
 *
 * @成功返回0,失败返回负数。
 *
 * @注意,如果你想使用特定的IO,在分配格式上下文空间之前,设置它的pb字段
 */
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

avformat_find_stream_info

/* 读取媒体文件的包来获取流信息。这个方法对于没有头部的文件格式也是有用的,例如MPEG。
 * 对于MPEG-2这种帧模型重复的类型,这个方法也会计算真实帧率。
 *
 * 该逻辑文件地址不被这个方法更改。
 * 检查的数据包可能会被缓冲以用于以后处理。
 * 
 * @参数 ic 媒体文件句柄(格式上下文)
 * @参数 options ...
 * @成功返回值大于0
 * @注意:这个方法不保证打开所有的编解码器,所以非空的options参数将会返回一个完全合理的行为。	
 *
 * 为了让用户决定什么样的信息是他们需要的,我们不会浪费时间去获取用户不需要的东西。
 */
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

av_find_best_stream

/* 在媒体文件中寻找最合适的流结构体(AVStream)。
 * 如果参数decoder_ret为非空,av_find_best_stream将会流的编解码寻找默认解码器。
 * 对于那些不能找到解码器的流来说,它会被忽略。
 * @param ic 媒体文件句柄(格式上下文)
 * @param type 流类型,视频(AVMEDIA_TYPE_VIDEO),音频(AVMEDIA_TYPE_AUDIO)
 * @param wanted_stream_nb AVStream在AVFormatContext中streams的索引,调试证明最好填-1。
 * @param related_stream -1
 * @param decoder_ret null
 * @param flags 0
 * @return 成功返回值非负。
 */
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

/* 通过匹配的编解码器ID,找到一个注册的解码器.
 */
AVCodec *avcodec_find_decoder(enum AVCodecID id);

avcodec_alloc_context3

/* 分配一个AVCodecContext,并且设置它的字段为默认值。
 * 这个结构体应该使用avcodec_free_context()释放
 */
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

avcodec_parameters_to_context

/*根据AVCodecParameters的值填充AVCodecContext。
 *AVCodecParameters与AVCodecContext相对应的将会被替换,不对应不替换。
 */
int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

avcodec_open2

/* 使用提供的AVCodec来初始化AVCodecContext结构体。在使用这个功能之前,AVCodecContext必须通过
 * avcodec_alloc_context3()分配空间。
 * 这些方法 avcodec_find_decoder_by_name(),avcodec_find_encoder_by_name()... 提供一个简单的方法来检索到一个编解码器。
 *	这个方法不是线程安全的
 */
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

av_image_alloc

/* 通过参数w,h,pix_fmt,align来分配一个图像的空间,参数pointers和linesizes将会被赋值。
 * pointers必须通过av_freep(&pointers[0])释放。
 * @param 调整缓冲区大小对齐的值
 */
int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
                   int w, int h, enum AVPixelFormat pix_fmt, int align);

av_read_frame

/* 返回流的下一帧到AVPacket中。
 * 这个方法返回存储在文件中的数据,它并不确定这些有效帧有解码器。
 * 每次调用这个方法,它将分割文件中的数据到AVPacket里。它不会遗漏
 * 有效帧之间的数据,来给解码器最大的信息来解码。
 *
 * 如果返回的AVPacket有效,在不使用时,必须用av_packet_unref释放。
 * 对于视频,AVPacket包含一帧。对于音频,如果每一帧大小固定已知,那么
 * AVPacket包含多帧;如果每一帧大小可变,那么AVPacket包含一帧。
 *
 * 这几个值(pkt->pts, pkt->dts and pkt->duration)总是会被赋值。
 */
int av_read_frame(AVFormatContext *s, AVPacket *pkt);

struct AVPacket

/* 这个结构体存储压缩数据。它通过解析器输出,作为解码器的输入,或者通过编码器输出, 然后传给合成器。
 * 对于视频,它代表一个压缩帧,对于音频,它可能代表多个压缩帧,解码器允许输出一个空的packet,
 * 没有压缩数据,仅仅包含次要数据
 */
typedef struct AVPacket

avcodec_send_packet

/* 将数据包发送给解码器。(可以想象解码内部有个存放数据的容器)
 */
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

avcodec_receive_frame

/* 返回解码过后的一个帧,与avcodec_send_packet配套使用。
 * avcodec_send_packet传入的AVPacket可能包含多帧,所以avcodec_receive_frame可以调用
 * 多次,直到失败。
 */
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

 

源码

解析MP4文件得到视频帧和音频帧,并分别保存到不同的文件。(代码许多地方返回值未校验)

extern "C"
{
#include <libavutil/imgutils.h>
#include <libavutil/samplefmt.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
}

int get_format_from_sample_fmt(const char **fmt, enum AVSampleFormat sample_fmt)
{
	int i;
	struct sample_fmt_entry {
		enum AVSampleFormat sample_fmt; 
		const char *fmt_be, *fmt_le;
	} 
	
	sample_fmt_entries[] = {
		{ AV_SAMPLE_FMT_U8,  "u8",    "u8" },
		{ AV_SAMPLE_FMT_S16, "s16be", "s16le" },
		{ AV_SAMPLE_FMT_S32, "s32be", "s32le" },
		{ AV_SAMPLE_FMT_FLT, "f32be", "f32le" },
		{ AV_SAMPLE_FMT_DBL, "f64be", "f64le" },
	};

	*fmt = NULL;

	for (i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); i++) {
		struct sample_fmt_entry *entry = &sample_fmt_entries[i];
		if (sample_fmt == entry->sample_fmt) {
			*fmt = AV_NE(entry->fmt_be, entry->fmt_le);
			return 0;
		}
	}

	fprintf(stderr,
		"sample format %s is not supported as output format\n",
		av_get_sample_fmt_name(sample_fmt));
	return -1;
}

void demuxing_decoding(const char* filename)
{
	av_register_all();
	puts("注册...");

	AVFormatContext* formatContext = nullptr;
	if (0 != avformat_open_input(&formatContext, filename, NULL, NULL))
	{
		puts("avformat_open_input失败.");
		return;
	}
	puts("avformat_open_input...");
	
	if (avformat_find_stream_info(formatContext, NULL) < 0)
	{
		puts("avformat_find_stream_info失败.");
		return;
	}
	puts("avformat_find_stream_info...");

	//视频
	int video_stream_idx = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, 0, -1, nullptr, 0);
	if (video_stream_idx < 0)
	{
		puts("avformat_find_stream_info AVMEDIA_TYPE_VIDEO失败.");
		return;
	}

	AVStream* video_st = formatContext->streams[video_stream_idx];
	AVCodec *videCodec = avcodec_find_decoder(video_st->codecpar->codec_id);
	AVCodecContext* videoContext = avcodec_alloc_context3(videCodec);
	avcodec_parameters_to_context(videoContext, video_st->codecpar);
	avcodec_open2(videoContext, videCodec, nullptr);

	uint8_t *video_dst_data[4] = { NULL };
	int video_dst_linesize[4];
	int video_dst_bufsize = av_image_alloc(video_dst_data, video_dst_linesize, videoContext->width, videoContext->height, videoContext->pix_fmt, 1);

	//音频
	int audio_stream_idx = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
	AVStream* audio_st = formatContext->streams[audio_stream_idx];
	AVCodec* audioCodec = avcodec_find_decoder(audio_st->codecpar->codec_id);
	AVCodecContext* audioContext = avcodec_alloc_context3(audioCodec);
	avcodec_parameters_to_context(audioContext, audio_st->codecpar);
	avcodec_open2(audioContext, audioCodec, nullptr);

	av_dump_format(formatContext, 0, filename, 0);

	AVFrame* frame = av_frame_alloc();

	AVPacket pkt;
	av_init_packet(&pkt);
	pkt.data = NULL;
	pkt.size = 0;

	FILE *videofile = nullptr;
	char video_name[128] = { 0 };
	sprintf_s(video_name, 128, "output\\demuxing_video.%s", videCodec->name);
	fopen_s(&videofile, video_name, "wb");
	FILE *audiofile = nullptr;
	char audio_name[128] = { 0 };
	sprintf_s(audio_name, 128, "output\\demuxing_audio.%s", audioCodec->name);
	fopen_s(&audiofile, audio_name, "wb");

	while (av_read_frame(formatContext, &pkt) >= 0) 
	{
		if (pkt.stream_index == video_stream_idx)
		{
			avcodec_send_packet(videoContext, &pkt);
			while (0 == avcodec_receive_frame(videoContext, frame))
			{
				av_image_copy(video_dst_data, video_dst_linesize, (const uint8_t **)frame->data, frame->linesize, videoContext->pix_fmt, videoContext->width, videoContext->height);
				fwrite(video_dst_data[0], 1, video_dst_bufsize, videofile);
				puts("写入视频");
			}
		}
		else if (pkt.stream_index == audio_stream_idx)
		{
			avcodec_send_packet(audioContext, &pkt);
			while (0 == avcodec_receive_frame(audioContext, frame))
			{
				size_t unpadded_linesize = frame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)frame->format);
				fwrite(frame->extended_data[0], 1, unpadded_linesize, audiofile);
				puts("写入音频");
			}
		}

		av_packet_unref(&pkt);
	}

	printf("Play the output video file with the command:\n"
		"ffplay -f rawvideo -pix_fmt %s -video_size %dx%d %s\n",
		av_get_pix_fmt_name(videoContext->pix_fmt), videoContext->width, videoContext->height,
		video_name);

	enum AVSampleFormat sfmt = audioContext->sample_fmt;
	int n_channels = audioContext->channels;
	const char *fmt;

	if (av_sample_fmt_is_planar(sfmt)) {
		const char *packed = av_get_sample_fmt_name(sfmt);
		printf("Warning: the sample format the decoder produced is planar "
			"(%s). This example will output the first channel only.\n",
			packed ? packed : "?");
		sfmt = av_get_packed_sample_fmt(sfmt);
		n_channels = 1;
	}

	if (get_format_from_sample_fmt(&fmt, sfmt) < 0)
	{

	}

	printf("Play the output audio file with the command:\n"
		"ffplay -f %s -ac %d -ar %d %s\n",
		fmt, n_channels, audioContext->sample_rate,
		audio_name);

	fclose(videofile);
	fclose(audiofile);
	avcodec_free_context(&videoContext);
	avcodec_free_context(&audioContext);
	avformat_close_input(&formatContext);
	av_frame_free(&frame);
	av_free(video_dst_data[0]);

	getchar();
}