FFmpeg Visual Studio开发(三):视频解码

上一篇文章我们学习了如何把视频文件解封装,本篇文章我们来学习如何解码视频数据。文章分段讲解视频解码的各个步骤,接着会贴上完整代码,最后进行测试。

准备工作

首先创建一个新的控制台工程,把FFmpeg4的库配置好,不熟悉的朋友可以看看第一篇文章。接着跑一下测试程序看看配置是否成功。

#include "stdafx.h"
#include <iostream>

extern "C"
{
#include "libavformat/avformat.h"
};

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
	cout << "hello FFmpeg" << endl;
	cout << avcodec_configuration() << endl;
	return 0;
}

打印了配置信息,说明目前是没有问题的了。请添加图片描述
接着我们来认识几个结构体。

结构体说明
AVFormatContextIO相关的上下文结构体,用于获取音频流、视频流及文件相关操作
AVStream数据流结构体,可以是音频流或视频流
AVCodecContext解码器上下文结构体
AVPacket封装数据帧结构体,用于接收音视频的封装数据
AVFrame编解码数据帧结构体,用于接收视频编解码数据

最后,我们来了解一下全过程。
1.打开视频文件
2.创建输出文件
3.获取视频流
4.打开解码器
5.循环读取每一个封装视频帧
    5.1.解码视频帧
    5.2.输出yuv帧

打开视频文件

打开视频文件的同时把它的相关数据写入AVFormatContext。

static int openFile(const char* filePath, AVFormatContext **avFormatContext){
	//打开文件流,读取头信息
	int ret = avformat_open_input(avFormatContext, filePath, NULL, NULL);
	if (ret < 0){//文件打开失败
		char buff[1024];
		//把具体错误信息写入buff
		av_strerror(ret, buff, sizeof(buff)-1);
		cout << "can't open file" << endl;
		cout << buff << endl;
		//释放AVFormatContext的内存
		avformat_close_input(avFormatContext);
		return -1;
	}
	return 0;
}

创建输出文件

创建一个输出文件用于保存yuv输出数据。

FILE *outputFile = NULL;
fopen_s(&outputFile, outputFilePath, "wb");

获取视频流

视频文件里有音频流和视频流,FFmpeg是通过下标(index)来区分它们的。这里只获取视频流。

static int getVideoStream(int *videoIndex,AVFormatContext* avFormatContext,AVStream **avStream){
	*videoIndex = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
	
	if (videoIndex < 0){
		cout << av_get_media_type_string(AVMEDIA_TYPE_VIDEO) << endl;
		//释放AVFormatContext的内存
		avformat_close_input(&avFormatContext);
		return -1;
	}

	*avStream = avFormatContext->streams[*videoIndex];
	if (*avStream == NULL){
		cout << "can't get video stream" << endl;
		return -1;
	}

	return 0;
}

打开解码器

解码器分为音频解码器和视频解码器,这里获取的是视频解码器。

static int openVideoCodec(AVCodecContext **avCodecContext,AVStream *videoStream){
	int ret;

	//获取视频解码器
	AVCodec *avCodec = avcodec_find_decoder(videoStream->codecpar->codec_id);
	if (avCodec == NULL){
		cout << "can't find codec" << endl;
		return -1;
	}

	//获取视频解码器上下文
	*avCodecContext = avcodec_alloc_context3(avCodec);
	if (avCodecContext == NULL){
		cout << "can't alloc video codec context" << endl;
		return -1;
	}

	if (avcodec_parameters_to_context(*avCodecContext,videoStream->codecpar) < 0){
		cout << "can't copy input video codec parms to video decoder context" << endl;
		return -1;
	}

	AVDictionary *opts = NULL;

	//打开视频解码器
	if (avcodec_open2(*avCodecContext, avCodec, &opts) < 0){
		cout << "can't open video codec" << endl;
		return -1;
	}

	return 0;
}

循环读取每个封装视频帧

视频文件里封装了大量的数据帧,分为音频帧和视频帧。这里每找到一个视频帧就对其进行解码。

...
AVPacket *avPacket = NULL;
	if (createPacket(&avPacket) < 0){
		return -1;
	}
	while (av_read_frame(avFormatContext, avPacket) >= 0){
		if (avPacket->stream_index == videoIndex){
			cout << "--------------------video packet--------------------" << endl;
			//解码封装的视频数据
			decodeVideoPacket(avCodecContext, 
				avPacket, 
				outputPixelFormat, 
				outputFile,
				outputWidth,
				outputHeight);
		}
		av_packet_unref(avPacket);
	}
...

解码视频帧

调用avcodec_send_packet函数就能解码视频帧。解码视频帧后,我们就能进一步的操作了。本例程将解码的视频帧以yuv格式写入输出文件。

static int decodeVideoPacket(AVCodecContext *avCodecContext,
	const AVPacket *avPacket,
	AVPixelFormat outputPixelFormat,
	FILE *outputFile,
	int outputWidth,
	int outputHeight){

	//解码视频帧数据
	if (avcodec_send_packet(avCodecContext,avPacket) < 0){
		cout << "error submitting a packet for decoding" << endl;
		return -1;
	}

	//将解码视频数据以yuv格式写入输出文件
	writeOutputFrame(outputWidth, outputHeight, outputPixelFormat, avCodecContext, outputFile);

	return 0;
}

输出yuv帧

输出YUV帧需要考虑以下几个设置。
1.yuv采样方式:YUV444、YUV422、YUV420
2.输出尺寸比例

YUV格式是原始格式,每一帧的数据存放方式是先写入该帧所有的Y值,再写所有的U值、最后再写所有的V值。因此在读写yuv文件时,采样方式和读写尺寸数据尤为重要。弄错了很有可能导致视频闪屏、花屏或色调偏暗。
在这里插入图片描述

static int writeOutputFrame(int outputWidth, int outputHeight, AVPixelFormat outputPixelFormat, AVCodecContext *avCodecContext, FILE *outputFile){
	AVFrame *avFrame = NULL;
	if (createFrame(&avFrame) < 0){
		return -1;
	}

	AVFrame *outputAvFrame = NULL;
	if (createFrame(&outputAvFrame) < 0){
		return -1;
	}

	//申请一个数组缓冲,用于存放每一帧的输出yuv数据
	uint8_t *outBuffer = (uint8_t *)av_malloc(av_image_get_buffer_size(outputPixelFormat,
		outputWidth, outputHeight, 1)*sizeof(uint8_t));

	av_image_fill_arrays(outputAvFrame->data, outputAvFrame->linesize,
		outBuffer, outputPixelFormat, outputWidth, outputHeight, 1);

	SwsContext *swsContext = NULL;
	if (getSwsContext(&swsContext, avCodecContext, outputPixelFormat, outputWidth, outputHeight) < 0){
		return -1;
	}

	//设置输出尺寸,如果和原尺寸不一致,则可以实现视频拉伸
	if (sws_scale(swsContext, avFrame->data, avFrame->linesize, 0,
		avCodecContext->height, outputAvFrame->data, outputAvFrame->linesize) <= 0){
		return -1;
	}

	int ret = 0;
	while (ret >= 0){
		//将解码好的一帧数据存放在AVFrame对象里
		ret = avcodec_receive_frame(avCodecContext, avFrame);

		if (ret < 0){
			//此处指解码帧数据读完,并不是错误
			if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
				return 0;

			//解码错误
			return -1;
		}

		cout << "read frame size:" << avFrame->width << "x" << avFrame->height << endl;

		int ySize = outputWidth * outputHeight;
		int uvSize;

		switch (outputPixelFormat)
		{
		case AV_PIX_FMT_YUV444P:
			uvSize = ySize;
			break;

		case AV_PIX_FMT_YUV422P:
			uvSize = ySize / 2;
			break;

		case AV_PIX_FMT_YUV420P:
			uvSize = ySize / 4;
			break;
		}

		//把一帧所有的Y值写进文件
		fwrite(outputAvFrame->data[0], 1, ySize, outputFile);

		//把一帧所有的U值写进文件
		fwrite(outputAvFrame->data[1], 1, uvSize, outputFile);

		//把一帧所有的V值写进文件
		fwrite(outputAvFrame->data[2], 1, uvSize, outputFile);

		//清空接收帧的数据,准备接收下一帧
		av_frame_unref(avFrame);
	}
}

完整代码

完整代码如下。当然其实还有一些地方是可以优化的。

#include "stdafx.h"
#include <iostream>

extern "C"
{
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}

using namespace std;

static int openFile(const char* fileName, AVFormatContext **avFormatContext){
	//打开文件流,读取头信息
	int ret = avformat_open_input(avFormatContext, fileName, NULL, NULL);
	if (ret < 0){//文件打开失败
		char buff[1024];
		//把具体错误信息写入buff
		av_strerror(ret, buff, sizeof(buff)-1);
		cout << "can't open file" << endl;
		cout << buff << endl;
		//释放AVFormatContext的内存
		avformat_close_input(avFormatContext);
		return -1;
	}
	return 0;
}

static int loadStreamInfo(AVFormatContext *avFormatContext){
	//读取流信息
	int ret = avformat_find_stream_info(avFormatContext, NULL);

	if (ret < 0){//读取流信息失败
		char buff[1024];
		//把具体错误信息写入buff
		av_strerror(ret, buff, sizeof(buff)-1);
		cout << "can't open stream" << endl;
		cout << buff << endl;
		//释放AVFormatContext的内存
		avformat_close_input(&avFormatContext);
		return -1;
	}
	return 0;
}

static int getVideoStream(int *videoIndex, AVFormatContext* avFormatContext, AVStream **avStream){
	*videoIndex = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
	if (videoIndex < 0){
		cout << av_get_media_type_string(AVMEDIA_TYPE_VIDEO) << endl;
		//释放AVFormatContext的内存
		avformat_close_input(&avFormatContext);
		return -1;
	}

	*avStream = avFormatContext->streams[*videoIndex];
	if (*avStream == NULL){
		cout << "can't get video stream" << endl;
		return -1;
	}

	return 0;
}

static int openVideoCodec(AVCodecContext **avCodecContext, AVFormatContext *avFormatContext, AVStream *videoStream){
	int ret;

	//获取视频解码器
	AVCodec *avCodec = avcodec_find_decoder(videoStream->codecpar->codec_id);
	if (avCodec == NULL){
		cout << "can't find codec" << endl;
		return -1;
	}

	//获取视频解码器上下文
	*avCodecContext = avcodec_alloc_context3(avCodec);
	if (avCodecContext == NULL){
		cout << "can't alloc video codec context" << endl;
		return -1;
	}

	if (avcodec_parameters_to_context(*avCodecContext, videoStream->codecpar) < 0){
		cout << "can't copy input video codec parms to video decoder context" << endl;
		return -1;
	}

	AVDictionary *opts = NULL;

	if (avcodec_open2(*avCodecContext, avCodec, &opts) < 0){
		cout << "can't open video codec" << endl;
		return -1;
	}

	return 0;
}

static int createFrame(AVFrame **avFrame){
	*avFrame = av_frame_alloc();
	if (*avFrame == NULL){
		cout << "can't alloc frame" << endl;
		return -1;
	}
	return 0;
}

static int createPacket(AVPacket **avPacket){
	*avPacket = av_packet_alloc();
	if (*avPacket == NULL){
		cout << "can't alloc packet" << endl;
		return -1;
	}
	return 0;
}

static int getSwsContext(SwsContext **swsContext,
	AVCodecContext *avCodecContext,
	AVPixelFormat desAVPixFormat,
	int outputWidth,
	int outputHeight){
	if ((*swsContext = sws_getContext(avCodecContext->width,
		avCodecContext->height,
		avCodecContext->pix_fmt,
		outputWidth,
		outputHeight,
		desAVPixFormat,
		NULL, NULL, NULL, NULL)) == NULL){

		cout << "can't get SwsContext" << endl;

		return -1;
	}
	return 0;
}

static int writeFrameToFile(int outputWidth, int outputHeight, AVPixelFormat outputPixelFormat, AVCodecContext *avCodecContext, FILE *outputFile){
	AVFrame *avFrame = NULL;
	if (createFrame(&avFrame) < 0){
		return -1;
	}

	AVFrame *outputAvFrame = NULL;
	if (createFrame(&outputAvFrame) < 0){
		return -1;
	}

	uint8_t *outBuffer = (uint8_t *)av_malloc(av_image_get_buffer_size(outputPixelFormat,
		outputWidth, outputHeight, 1)*sizeof(uint8_t));

	av_image_fill_arrays(outputAvFrame->data, outputAvFrame->linesize,
		outBuffer, outputPixelFormat, outputWidth, outputHeight, 1);

	SwsContext *swsContext = NULL;
	if (getSwsContext(&swsContext, avCodecContext, outputPixelFormat, outputWidth, outputHeight) < 0){
		return -1;
	}

	int ret = 0;
	while (ret >= 0){
		ret = avcodec_receive_frame(avCodecContext, avFrame);
		if (ret < 0){
			if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
				return 0;

			return -1;
		}

		cout << "frame size:" << avFrame->width << "x" << avFrame->height << endl;

		if (sws_scale(swsContext, avFrame->data, avFrame->linesize, 0,
			avCodecContext->height, outputAvFrame->data, outputAvFrame->linesize) <= 0){
			continue;
		}

		int ySize = outputWidth * outputHeight;
		int uvSize;

		switch (outputPixelFormat)
		{
		case AV_PIX_FMT_YUV444P:
			uvSize = ySize;
			break;

		case AV_PIX_FMT_YUV422P:
			uvSize = ySize / 2;
			break;

		case AV_PIX_FMT_YUV420P:
			uvSize = ySize / 4;
			break;
		}

		//把一帧所有的Y值写进文件
		fwrite(outputAvFrame->data[0], 1, ySize, outputFile);

		//把一帧所有的U值写进文件
		fwrite(outputAvFrame->data[1], 1, uvSize, outputFile);

		//把一帧所有的V值写进文件
		fwrite(outputAvFrame->data[2], 1, uvSize, outputFile);

		av_frame_unref(avFrame);
	}

	return 0;
}

static int decodeVideoPacket(AVCodecContext *avCodecContext,
	const AVPacket *avPacket,
	AVPixelFormat outputPixelFormat,
	FILE *outputFile,
	int outputWidth,
	int outputHeight){

	if (avcodec_send_packet(avCodecContext, avPacket) < 0){
		cout << "error submitting a packet for decoding" << endl;
		return -1;
	}

	if (writeFrameToFile(outputWidth, outputHeight, outputPixelFormat, avCodecContext, outputFile) < 0){
		return -1;
	}

	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	char inputFileName[100];
	char outputFileName[100];

	FILE *outputFile = NULL;

	cout << "input file name: ";
	//输入文件名,如C://WorkZone//Res//video.mp4
	cin >> inputFileName;

	AVFormatContext *avFormatContext = NULL;

	if (openFile(inputFileName, &avFormatContext) < 0){
		return -1;
	}

	cout << "output file name: ";
	//输入文件名,如C://WorkZone//Res//result.yuv
	cin >> outputFileName;

	fopen_s(&outputFile, outputFileName, "wb");

	int pixelType;

	cout << "select pixel type num" << endl;
	cout << "1.yuv444  2.yuv422  3.yuv420" << endl;

	cin >> pixelType;

	AVPixelFormat outputPixelFormat;

	switch (pixelType){
	case 1:
		outputPixelFormat = AV_PIX_FMT_YUV444P;
		break;
	case 2:
		outputPixelFormat = AV_PIX_FMT_YUV422P;
		break;
	case 3:
		outputPixelFormat = AV_PIX_FMT_YUV420P;
		break;

	default:
		cout << "invalid type" << endl;
		return -1;
	}

	int outputWidth;
	int outputHeight;

	cout << "output width:";
	cin >> outputWidth;

	cout << "output height:";
	cin >> outputHeight;

	if (outputWidth <= 0 || outputHeight <= 0){
		cout << "invalid output size" << endl;
		return -1;
	}

	if (loadStreamInfo(avFormatContext) < 0){
		return -1;
	}

	//打印格式信息
	av_dump_format(avFormatContext, 0, inputFileName, 0);

	int videoIndex;
	//根据videoIndex获取视频流
	AVStream *videoStream = NULL;
	if (getVideoStream(&videoIndex, avFormatContext, &videoStream) < 0){
		return -1;
	}

	AVCodecContext *avCodecContext = NULL;

	if (openVideoCodec(&avCodecContext, avFormatContext, videoStream) < 0){
		return -1;
	}

	AVPacket *avPacket = NULL;
	if (createPacket(&avPacket) < 0){
		return -1;
	}

	while (av_read_frame(avFormatContext, avPacket) >= 0){
		if (avPacket->stream_index == videoIndex){
			cout << "--------------------video packet--------------------" << endl;
			//cout << "remain packet num:" << packetRemain << endl;
			decodeVideoPacket(avCodecContext,
				avPacket,
				outputPixelFormat,
				outputFile,
				outputWidth,
				outputHeight);
		}
		av_packet_unref(avPacket);
	}

	//释放AVFormatContext的内存
	avformat_close_input(&avFormatContext);
	return 0;
}

测试

准备一个1分钟左右的mp4视频。注意,源视频最好不要超过1分钟,因为转换出来的yuv文件巨大!

启动程序,输入源视频文件路径、输出文件路径、采样方式(1.yuv444)、输出尺寸(320x240)。
在这里插入图片描述
可以看到输出的yuv文件相当大。
在这里插入图片描述
通过FFplay命令播放,注意输入的尺寸和yuv采样方式。

ffplay -s 320x240 -pix_fmt yuv444p -i result.yuv

在这里插入图片描述

最后

本篇文章讲述了如何通过FFmpeg进行解码,并输出yuv原始数据文件。下一篇文章我们来学习FFmpeg音频解码。

项目工程在我的Gitee仓库里,感兴趣的朋友可以看看。

参考文章

《ffmpeg 视频解码h264和yuv》
《FFMPEG4.1 - 视频解码与解码(上)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值