前导知识
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)
需要用到的解码相关数据结构:
- AVFormatContext是统领全局的结构,包含一些视频文件名,视频时长,视频码率等信息。
- AVInputFormat:包含一些具体的视频格式信息,每种视频格式对应一个这种结构体。
- 一般视频文件有两个流:视频流和音频流。有几个流就有几个AVStream数据结构,一般视频流的index=0,AVstream在AVFormatContext中就是一个双重指针。
- AVCodecContext包含像素等编解码的相关信息(对于视频)。
- 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()函数裁剪完了之后,才能正常的播放视频。该函数效果图:
分析
首先准备所需材料:
- yuv播放器
- 一个视频文件(我用的是MP4视频文件)
- 环境: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;
}
设置分辨率:
文件解码前和解码后对比: