前言

这是ffmpeg的第二篇,这篇主要实现一下使用ffmpeg的API实现解码,不使用和上篇的解析器做解析。

流程图

avformat_find_stream_info 用法 avcodec_find_encoder_#define


代码流程即如流程图所示,下面讲解一下当中部分函数的作用)。

  1. avformat_open_input
    打开输入文件,并读取文件头相关信息
  2. avformat_find_stream_info
    读取媒体文件信息。
  3. av_find_best_stream
    获取视频流序号(因为文件当中可能既有音频也有视频,字幕等流,我们这里使用这个函数获取视频流的序号)。
  4. avcodec_find_decoder
    获取解码器
  5. avcodec_parameters_to_context
    我们自己构建的解码器并没有设置一些解码相关的参数,此时我们拷贝视频流的参数到里面即可。
  6. av_read_frame
    从视频当中读取数据(一帧),不用和上篇一样,还需要我们使用解析器解析成一帧。
  7. avcodec_send_packet
    发送我们刚刚得到的解析数据到解码器做解码。
  8. avcodec_receive_frame
    获取解码之后的数据。

源码

#pragma once
#define __STDC_CONSTANT_MACROS
#define _CRT_SECURE_NO_WARNINGS

extern "C"
{
#include <libavformat/avformat.h>
#include "libavcodec/avcodec.h"
}

#define INBUF_SIZE 4096

using namespace std;

#define INPUT_FILE_NAME "lh_online.h264"
#define OUTPUT_FILE_NAME "lh_online.yuv"


static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt,
	FILE* ofile)
{
	int ret;
	int y_size;
	ret = avcodec_send_packet(dec_ctx, pkt);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "发送数据包到解码器出错。\n");
		exit(1);
	}

	while (ret >= 0) {
		ret = avcodec_receive_frame(dec_ctx, frame);
		if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
			return;
		else if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Error sending a packet for decoding.\n");
			exit(1);
		}
		//此时一帧视频已经保存到frame中了
		// 
		//打印输出的视频帧的帧数
		av_log(NULL, AV_LOG_INFO, "saving frame:%d , width: %d ,height: %d.\n", dec_ctx->frame_number, frame->width, frame->height);
		//获取一帧视频数据大小
		y_size = frame->width * frame->height;
		fwrite(frame->data[0], 1, y_size, ofile);    //Y
		fwrite(frame->data[1], 1, y_size / 4, ofile);  //U
		fwrite(frame->data[2], 1, y_size / 4, ofile);  //V
	}
}

int main(int argc, char* argv[])
{
	const AVCodec* codec;
	AVFormatContext* fmt_ctx = NULL;
	AVCodecContext* c = NULL;
	AVStream* st;
	AVFrame* frame;
	AVPacket* pkt;
	int ret;
	int stream_index;

	FILE* ofile;


	//打开输入文件,并为fmt_ctx分配空间
	if (avformat_open_input(&fmt_ctx, INPUT_FILE_NAME, NULL, NULL)) {
		av_log(NULL, AV_LOG_ERROR, "Codec not open source file.\n");
		exit(1);
	}

	//获取流信息
	if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
		av_log(NULL, AV_LOG_ERROR, "Could not find stream information.\n");
		exit(1);
	}

	//获取视频流序号(这里我们明确要解码的是视频,也只处理视频)
	stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
	if (stream_index < 0) {
		av_log(NULL, AV_LOG_ERROR, "Cannot find stream index\n");
		exit(1);
	}

	//获取文件流
	st = fmt_ctx->streams[stream_index];

	//获取解码器(这里不需要我们显示的指定了)
	codec = avcodec_find_decoder(st->codecpar->codec_id);
	if (!codec) {
		av_log(NULL, AV_LOG_ERROR, "Codec not found.\n");
		exit(1);
	}

	//分配解析器上下文
	c = avcodec_alloc_context3(codec);
	if (!c) {
		av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context.\n");
		exit(1);
	}

	//把输入流的编解码参数复制到我们的解码器上
	if (avcodec_parameters_to_context(c, st->codecpar) < 0) {
		av_log(NULL, AV_LOG_ERROR, "Failed to copy %s codec parameters to decoder context\n");
		exit(1);
	}

	//打开解码器
	if (avcodec_open2(c, codec, NULL) < 0) {
		av_log(NULL, AV_LOG_ERROR, "Could not open codec.\n");
		exit(1);
	}

	//分配AVPacket
	pkt = av_packet_alloc();
	if (!pkt) {
		exit(1);
	}

	//分配AVFrame
	frame = av_frame_alloc();
	if (!frame) {
		exit(1);
	}

	//打开输出文件
	ofile = fopen(OUTPUT_FILE_NAME, "wb+");
	if (!ofile) {
		av_log(NULL, AV_LOG_ERROR, "Could not open \s.\n", OUTPUT_FILE_NAME);
		exit(1);
	}


	//从文件读取帧
	while (av_read_frame(fmt_ctx, pkt) >= 0) {
		//只处理视频流
		if (pkt->stream_index==stream_index) {
			decode(c, frame, pkt, ofile);
		}
		av_packet_unref(pkt);
	}

	//flush 解码器
	decode(c, frame, NULL, ofile);

	//资源释放
	fclose(ofile);

	avcodec_free_context(&c);
	av_frame_free(&frame);
	av_packet_free(&pkt);

	return 0;
}

这个示例中和上篇文章一样,还是把一个H264编码的 lh_online.h264 的文件解码成原始YUV视频文件 lh_online.yuv
播放还是使用ffplay来实现,下面是播放的代码。

ffplay -i  D:\project\ffmpeg_demo\decode_video_by_api\lh_online.yuv -pix_fmt yuv420p -s 512*288

注意两点:

  1. 此时我们播放的数据格式是 yuv 的(此处可设置可不设置,ffplay默认可播放)。
  2. 我们需要知道视频的原始宽高,代码里有个位置获取了宽高的(dec_ctx->frame_number, frame->width, frame->height),我们需要把快其设置到命令里,否者是不能播放的(我这个文件是512*288的)。

下面看下效果。

avformat_find_stream_info 用法 avcodec_find_encoder_音视频_02


到此,视频解码相关的API即介绍完了。

相对于上篇文章基于parser的方式来的话,这篇文章的相对来说代码逻辑是比较直观的,也不需要我们自己去考虑分割视频帧相关的问题(平常我们使用最多的应该也是这种方式)。

需要注意的就是我们需要 avformat.h这个文件头,这个文件封装的里面即是媒体文件格式相关的API。