上一篇文章我们学习了如何把视频文件解封装,本篇文章我们来学习如何解码视频数据。文章分段讲解视频解码的各个步骤,接着会贴上完整代码,最后进行测试。
准备工作
首先创建一个新的控制台工程,把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;
}
打印了配置信息,说明目前是没有问题的了。
接着我们来认识几个结构体。
结构体 | 说明 |
---|---|
AVFormatContext | IO相关的上下文结构体,用于获取音频流、视频流及文件相关操作 |
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仓库里,感兴趣的朋友可以看看。