前导知识

1.VS2017–ffmpeg配置 2.ffmpeg入门–YUV格式 3.ffmpeg入门–结构体和类库 4.FFmpeg入门–函数 5.声明已否决

基础知识

1.容器(Container)——容器就是一种文件格式,比如flv,mkv等。包含下面5种流以及文件头信息。
2.流(Stream)——是一种视频数据信息的传输方式,5种流:音频,视频,字幕,附件,数据。
3.帧(Frame)——帧代表一幅静止的图像,分为I帧,P帧,B帧。
4.编解码器(Codec)——是对视频 进行压缩或者解压缩,CODEC =COde (编码) +DECode(解码)
5.复用/解复用(mux/demux)——把不同的流按照某种容器的规则放入容器,这种行为叫做复用(mux)/ 把不同的流从某种容器中解析出来,这种行为叫做解复用(demux)

需要用到的解码相关数据结构:

Android ffmpeg 硬解码 ffmpeg解码视频_ide

  1. AVFormatContext是统领全局的结构,包含一些视频文件名,视频时长,视频码率等信息。
  2. AVInputFormat:包含一些具体的视频格式信息,每种视频格式对应一个这种结构体。
  3. 一般视频文件有两个流:视频流和音频流。有几个流就有几个AVStream数据结构,一般视频流的index=0,AVstream在AVFormatContext中就是一个双重指针。
  4. AVCodecContext包含像素等编解码的相关信息(对于视频)。
  5. AVCodec:每种视/音频流对应于一个该结构图。

先来看看视频的解码过程:

基本函数

• av_register_all():注册所有组件
• avformat_open_input():打开输入视频文件
• avformat_find_stream_info():获取视频文件信息
• avcodec_find_decoder():查找解码器
• avcodec_open():打开解码器
• av_read_frame():从输入文件中读取一帧压缩数据
• avcodec_send_packet():解码一帧压缩数据
• avcodec_receive_frame():接收解码后的数据
• avcodec_send_frame():发送未编码的数据
• avcodec_receive_packet():接收编码后的数据
• avcodec_close():关闭解码器
• avformat_close_input():关闭输入视频文件

sws_scale()函数

解码后yuv格式的视频像素数据保存在AVFrame的data[0]、data[1]、data[2]中。但是这些像素值并不是连续存储的,每行有效像素之后存储了一些无效像素,以 高度Y数据为例,data[0]中一共包含了linesize[0]*height个数据。但是出于优化等方面的考虑,linesize[0]实际上并不等于宽度width,而是一个比宽度大一些的值。因此需要使用ses_scale()进行转换。转换后去除了无效数据,width和linesize[0]就取值相同了。
最后用大白话概括一下吧,就是说如果不用这个sws_scale()函数的话,直接播放的yuv数据是带有黑边的,只有经过sws_scale()函数裁剪完了之后,才能正常的播放视频。
该函数效果图:

Android ffmpeg 硬解码 ffmpeg解码视频_视频流_02

分析

首先准备所需材料:

  1. yuv播放器
  2. 一个视频文件(我用的是MP4视频文件)
  3. 环境:Windows10+vs17+ffmpeg4.1+ c++

/准备工作//

  • 调用av_register_all()

这一步注册库中含有的所有可用的文件格式和编码器,这样当打开一个文件时,它们才能够自动选择相应的文件格式和编码器。av_register_all()只需调用一次,所以,要放在初始化代码中。也可以仅仅注册个人的文件格式和编码。

  • 打开媒体文件
if (avformat_open_input(&pFormatContext, filepath, NULL, NULL) != 0)
	{
		cout << "open file error" << endl;
		return -1;
	}
  • 取出文件中的流信息并找到视频流的索引号
//获取视频流信息
	avformat_find_stream_info(pFormatContext, NULL
	//查找视频流编码索引,nb_streams表示视频文件中有几种流
	for (unsigned int i = 0; i < pFormatContext->nb_streams; i++)
	{    //遍历多媒体文件中的每一个流,判断是否为视频
		if (pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			videoIndex = i;
			break;
		}
	}
  • 获取视频流的编解码信息并通过编解码信息获取对应编解码器
pCodecContext = avcodec_alloc_context3(NULL);
	avcodec_parameters_to_context(pCodecContext, pFormatContext->streams[videoIndex]->codecpar);
	//找到一个和视频流对应的解码器
	pCodec = avcodec_find_decoder(pCodecContext->codec_id);
	//打开解码器
	avcodec_open2(pCodecContext, pCodec, NULL);

///开始解码

  • 开空间
//申请AVframe,存放原始帧
	pFrame = av_frame_alloc();
	//申请pFrameYUV,存放解码后的yuv
	pFrameYUV = av_frame_alloc(); //YUV帧
	//分配内存,用于图像格式转换
	out_buffer = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P,pCodecContext->width, pCodecContext->height, 1));
	//将pFrameYUV和out_buffer联系起来(pFrame指向一段内存);
	av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1);
	//存放编码后,解码前的数据,开空间
	packet = (AVPacket*)av_malloc(sizeof(AVPacket));
  • 打开视频流并读数据

通过读取包来读取整个视频流,然后把它解码成帧,最后转换格式并且保存。

fopen_s(&fp_yuv, "D:\\test\\fpyuv.yuv", "wb+");
	//读取码流中的视频一帧,放入编码后,解码前的数据到packet中
	av_read_frame(pFormatContext, packet)
	//如果读取的是视频流
	if (packet->stream_index == videoIndex){
		//把包转换为帧
	    ret = avcodec_send_packet(pCodecContext, packet);
		got_picture = avcodec_receive_frame(pCodecContext, pFrame);
		···
     }
  • 解码数据到yuv
//调整解码出来的图像,解码出来的可能含有填充的信息。
    sws_scale(img_convert_Context, (const uint8_t* const*)pFrame->data,
		               pFrame->linesize, 0, pCodecContext->height,
						pFrameYUV->data, pFrameYUV->linesize);
    // 已经解码,可以输出YUV的信息
	// 这个data是一个数组,需要注意!一般3个:代表Y、U、V
	y_size = (pCodecContext->height)*(pCodecContext->width);
	fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
	fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
	fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
	av_packet_unref(packet);

代码

#include "pch.h"
#include<iostream>
#include<string>

#define __STDC_CONSTANT_MACROS

extern "C" {
#include"libavcodec\avcodec.h"
#include "libavformat/avformat.h"
#include"libswscale/swscale.h" //用于视频像素数据的裁
#include "libavutil/avutil.h"
#include "libavutil/imgutils.h"
}

using namespace std;
int main() {
	/*
	描述媒体文件的构成及基本信息,封装格式上下文结构体,是统领全局的基本结构体,
	很多函数需要使用它作为参数,保存了视频文件封装格式相关信息。
	*/
	AVFormatContext *pFormatContext;   //统筹结构,保存封装格式相关信息
	/*
	描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息。
	*/
	AVCodecContext *pCodecContext;    //保存视/音频编解码相关信息
	/*
	编解码器对象,每种编解码格式对应一个该结构体,每一个AVCodecContext都包含一个AVCodec;
	*/
	AVCodec *pCodec = NULL;     //每种编解码器对应一个结构体
	/*
	该结构描述了解码的(原始)音频或视频数据。
	存放编码前,解码后的原始数据,如YUV格式的视频数据或PCM格式的音频数据等.
	*/
	AVFrame *pFrame;    //解码后的数据
	AVFrame *pFrameYUV; //转码之后的数据


	uint8_t *out_buffer; //缓冲数组

	/*
	存储一帧压缩编码数据。存放编码后,解码前的压缩数据,即ES数据。
	*/
	AVPacket *packet;
	struct SwsContext *img_convert_Context; //图像转换格式上下文信息

	/*
	编解码参数,每个AVStream中都含有一个AVcodecParameters,用于存放当前流的编解码参数。
	*/
	//AVCodecParameters *pCodecParam = NULL;  //码流的属性结构体
	int videoIndex = -1;   //为了检查哪个流是视频流,保存流数组下标
	int ret = -1;
	int y_size = 0;        //像素的宽度
	int got_picture;
	int frame_context = 0; //总的帧数
	const char* filepath = "D:\\wait.mp4";//输入文件路径
	const char* output = "D:\\test\\myvideo.yuv";
	//FILE *fp_frame = fopen("D:\\test\\output.yuv", "wb+");
	FILE *fp_yuv = NULL;



	//注册所有的相关组件

	av_register_all();
	//初始化网络
	//avformat_network_init();

	//格式上下文结构体指针开空间,分配空间
	pFormatContext = avformat_alloc_context();

	//打开多媒体文件,注意第一个参数是指针的指针
	if (avformat_open_input(&pFormatContext, filepath, NULL, NULL) != 0)
	{
		cout << "open file error" << endl;
		return -1;
	}
	//获取视频流信息
	if (avformat_find_stream_info(pFormatContext, NULL) < 0)
	{
		cout << "find stream information error" << endl;
		return -1;
	}

	//查找视频流编码索引,nb_streams表示视频文件中有几种流
	for (unsigned int i = 0; i < pFormatContext->nb_streams; i++)
	{    //遍历多媒体文件中的每一个流,判断是否为视频
		if (pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			videoIndex = i;
			break;
		}
	}

	//如果没有找到视频的索引,说明并不是一个视频文件
	if (videoIndex == -1)
	{
		cout << "don`t find a video stream." << endl;
		return -1;
	}


	//获取该视频流对应的编解码的信息
	//复制编解码的信息到编解码的结构AVCodecContext结构中,
	//一方面为了操作结构中数据方便(不需要每次都从AVFormatContext结构开始一个一个指向)
	//另一方面方便函数的调用
	//pCodecContext = pFormatContext->streams[videoIndex]->codec;

	pCodecContext = avcodec_alloc_context3(NULL);
	if (pCodecContext == NULL) {

		printf("Could not allocate AVCodecContext\n");

		return -1;

	}
	avcodec_parameters_to_context(pCodecContext, pFormatContext->streams[videoIndex]->codecpar);

	//找到一个和视频流对应的解码器
	pCodec = avcodec_find_decoder(pCodecContext->codec_id);
	if (pCodec == NULL)
	{
		cout << "DeCodec not found." << endl;
		return -1;
	}

	//打开解码器
	if (avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
		cout << "can not open codec." << endl;
		return -1;
	}

	//初始化yuv容器,并且初始化内存空间
    //申请AVframe,存放原始帧
	pFrame = av_frame_alloc();
	//申请pFrameYUV,存放yuv视频
	pFrameYUV = av_frame_alloc(); //YUV帧


	//分配内存,用于图像格式转换,宏AV_PIX_FMT_YUV420P 代替宏PIX_FMT_YUV420P
	out_buffer = (uint8_t *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
		pCodecContext->width, pCodecContext->height, 1));
	//将pFrameYUV和out_buffer联系起来(pFrame指向一段内存);宏AV_PIX_FMT_YUV420P 代替宏PIX_FMT_YUV420P
	av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
		AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1);

	//存放编码后的数据,开空间
	packet = (AVPacket*)av_malloc(sizeof(AVPacket));


	cout << "----------file information-----------------" << endl;
	//调试函数,输出文件的音、视频流的基本信息
	av_dump_format(pFormatContext, 0, filepath, 0);
	cout << "-------------------------------------------" << endl;


	//初始化SWS,图片格式转换
	//经过验证,输出YUV不需要格式转换,但需要调整尺寸
	//把帧从原始格式(pCodecCtx->pix_fmt)转换成为yuv格式。
	img_convert_Context = sws_getContext(pCodecContext->width, pCodecContext->height,
		pCodecContext->pix_fmt, pCodecContext->width, pCodecContext->height,
		AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);

	//打开存放解码后的视频像素yuv数据
	//fp_yuv=fopen("D:\\test\\output_sws.yuv", "wb+");
	fopen_s(&fp_yuv, "D:\\test\\fpyuv.yuv", "wb+");
	if (fp_yuv == NULL) {
		cout << "open yuvfile error" << endl;
		return -1;
	}
	//读取码流中的音频若干帧或者视频一帧,放入编码后,解码前的数据到packet中
	while (av_read_frame(pFormatContext, packet) >= 0)
	{
		//如果读取的是视频流
		if (packet->stream_index == videoIndex)
		{
			//提供原始数据到解码器中
			ret = avcodec_send_packet(pCodecContext, packet);
			//一帧解码后的数据从解码器中返回
			got_picture = avcodec_receive_frame(pCodecContext, pFrame);
			if (ret < 0)
			{
				cout << "decode error" << endl;
				return -1;
			}
			//成功解码一帧数据
			if (got_picture == 0)
			{
				//调整解码出来的图像,解码出来的可能含有填充的信息。
				sws_scale(img_convert_Context, (const uint8_t* const*)pFrame->data,
					pFrame->linesize, 0, pCodecContext->height,
					pFrameYUV->data, pFrameYUV->linesize);

				// 已经解码,可以输出YUV的信息
													// 这个data是一个数组,需要注意!一般3个:代表Y、U、V
				y_size = (pCodecContext->height)*(pCodecContext->width);
				fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
				fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
				fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
				cout << "success decode " << frame_context << " frame." << endl;
				frame_context++;
			}
			else {
				cout << "return decode output data error" << endl;
			}
		}
		av_packet_unref(packet);
	}

	cout << "decode the total number of frames are :" << frame_context << endl;
	
	/*while (true)
	{
		if (!(pCodec->capabilities& AV_CODEC_CAP_DELAY))
			return 0;
		ret = avcodec_decode_video2(pCodecContext, pFrame, &got_picture, packet);
		ret = avcodec_send_packet(pCodecContext, packet);
		got_picture = avcodec_receive_frame(pCodecContext, pFrame);
		if (ret < 0)
			break;
		if (got_picture != 0)
			break;
		sws_scale(img_convert_Context, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecContext->height, pFrameYUV->data, pFrameYUV->linesize);
		y_size = pCodecContext->height*pCodecContext->width;
		fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
		fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
		fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
	}*/
	
	fclose(fp_yuv);
	sws_freeContext(img_convert_Context);
	av_frame_free(&pFrameYUV);
	av_frame_free(&pFrame);
	avcodec_close(pCodecContext);
	avformat_close_input(&pFormatContext);

	return 0;
}

Android ffmpeg 硬解码 ffmpeg解码视频_视频流_03


设置分辨率:

Android ffmpeg 硬解码 ffmpeg解码视频_Android ffmpeg 硬解码_04


文件解码前和解码后对比:

Android ffmpeg 硬解码 ffmpeg解码视频_ide_05