写在前面:
之前使用avformat_open_input,只是打开文件流而已。
一个视频打开之后,不止一个流,有视频流、音频流、字幕流。
其中ic结构体里面的ic->nb_streams存放了是这个视频里面有几种流。
AVCodecContext *enc = ic->streams[i]->codec;
通过对ic结构体中的stream结构体中的codec进行判断,找到我们要的AVMEDIA_TYPE_VIDEO,也就是视频流
然后使用avcodec_find_decoder(enc->codec_id)函数来判断系统有没有这个编码器
接着再调用avcodec_open2函数将它打开这个解码器。AVFrame *yuv = av_frame_alloc();
定义一个帧空间AVFrame用来存放解码过的内容。ffmpeg中解码出来的都是yuv格式的。这个是将每一帧的色彩度和亮度存放起来的,而不是色彩信息。很多压缩格式都是用yuv这种方式来进行压缩的,这种算法压缩率更高,存储空间更小,存储一个像素点只要2个字节,而rgb需要3个字节。
av_frame_alloc();这个函数不是分配整个解码的空间,而是AVFrame这个对象的空间,主要是因为我们并没有传递视频宽高比例给它,而且ffmpeg可以不需要知道指定大小,也能解密。
av_frame_alloc()这个函数里面做了挺多初始化的,包括空间的申请。调用了一个动态库。在动态库申请的空间只能在动态库中释放,后面调用它的函数就能直接释放空间。
ffmpeg解码的调用用新老版本两种,先调用老版本avcodec_decode_video2函数,老版本中,对音视频的解码是分开解码的,新版的则是将它们合起来。
对于avcodec_decode_video2函数来说,它根据每次解码的结果来申请所需要的空间。
新版方式是使用avcodec_send_packet发送数据去解码,然后再用avcodec_receive_frame获取解码的结果,将编码后的帧存放到yuv帧空间中。
re = avcodec_send_packet(videoCtx, &pkt);
if (re != 0)
{
av_packet_unref(&pkt);
continue;
}
re = avcodec_receive_frame(videoCtx, yuv);
if (re != 0)
{
av_packet_unref(&pkt);
continue;
}
由于yuv格式没办法被播放器、显示器所接收,所以要被转换成RGB格式,需要打开一个转码器,还需要调用#include
转码器最好在一开始的时候打开。
通过sws_getCachedContext函数获取解码器的内容,并打开编码器。
struct SwsContext *sws_getCachedContext(struct SwsContext *context,
int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
context:转码器的内容,传null,就新创建一个转码器,如果传的不是null,会将传入的转码器context,跟缓冲里面的转码器进行比较(context后面的参数srcH等),是否一致,如果一致,就传回传入的这个context,如果不一致,就重新再创建一个。
srcW、srcH:视频源的长和宽度,这个我们从videoCtx->width,videoCtx->height中获取。
srcFormat:源的像素点格式,同样从这里获取videoCtx->pix_fmt,应该是yuv420
dstW、dstH:目标的长宽度。
dstFormat:输出的格式。这个要根据前端来设置的,我们使用的是QT,所以使用的是AV_PIX_FMT_BGRA
flags:是指定用什么算法。我们这里使用的是SWS_BICUBIC
后面的参数,暂时用不到。
打开转码器后,使用sws_scale函数进行转码。
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
c:这个也是传入刚刚打开的那个转码器cCtx。
srcSlice:原数据的data,也就是我们解码出来的yuv的data
srcStride:对应前一个参数data的一行数据的大小。
srcSliceY:位置,但是目前没用到。
srcSliceH:原数据的高度。
dst:目标数据的data,这个data要去new一个空间,空间大小则是我们输出一帧画面的大小。
dstStride:指的是目标数据,一行像素点的大小,即width*4,这个4就是RGBA(各占1个字节,所以要用到4字节)。
返回值则是转码后的高度。
首先一个视频的处理简单来说就是先将视频文件打开,avformat_open_input打开视频文件之后,提取出视频里面的信息,存放在AVFormatContext *ic里面。
那么一个视频文件被打开了,肯定先要被解码,那么就可以利用刚刚提取到的ic里面的视频格式信息,并使用avcodec_find_decoder函数来查找是否支持该文件格式的视频解码,如果有这个解码器,那么就使用avcodec_open2这个函数将解码器打开,解码器的信息可以使用AVCodecContext *videoCtx创建的容器里面。那么打开解码器之后呢,就可以开始视频解码,在代码中,就直接用一个死循环,做以下几个操作
1)使用av_read_frame函数读取一帧数据,数据内容存放在pkt里面。
2)通过avcodec_send_packet和avcodec_receive_frame函数来将该帧数据解码。
3)由于解码出来的内容是yuv格式的,那么我们还得将它转换成RGB格式的,所以使用sws_getCachedContext函数打开转码器,然后再用sws_scale函数进行转码。
ps:这里的sws_getCachedContext函数设计的很巧妙,第一次传入的context为NULL,所以会为我们创建一个转码器,后面再调用它的时候,由于context的参数和之前一致,所以sws_getCachedContext直接return context,这样就不会多次创建转码器了。
这次的代码如下:
#include "xplay.h"
#include
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swscale.lib")
extern "C"{
#include
#include
}
static double r2d(AVRational r)
{
return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}
#define OUT_WIDTH 800
#define OUT_HEIGHT 600
int main(int argc, char *argv[])
{
int totalSec = 0;
int pts = 0;
char re = 0;
av_register_all();
char *path = "my.mp4";
AVFormatContext *ic = NULL; //存放视频(流)的文件信息
AVPacket pkt; //存放读取视频的数据包
AVCodecContext *videoCtx = NULL;//创建解码器的容器
SwsContext *cCtx = NULL; //创建转码器的容器
int err;
int got_picture = 0; //在视频解码老版本函数avcodec_decode_video2中,传入的参数,如果函数获取到视频会返回1
int videoStream = 0; //记录视频流存放在streams[n]数组里的第几个。
int outwidth = OUT_WIDTH; //输出的宽度
int outheight = OUT_HEIGHT; //输出的高度
char *rgb = new char[outwidth*outheight * 4]; //图像的数据RGBA
re = avformat_open_input(&ic, path, 0, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf));
printf("open failed %s\n",path, buf);
getchar();
return -1;
}
totalSec = ic->duration / AV_TIME_BASE; //计算出进度条时间
printf("file totalSec is %d\n", totalSec);
for (int i = 0; i < ic->nb_streams;i++)
{
AVCodecContext *enc = ic->streams[i]->codec;
if (enc->codec_type == AVMEDIA_TYPE_VIDEO)//表示为一个视频
{
videoStream = i;
videoCtx = enc;
AVCodec *codec = avcodec_find_decoder(enc->codec_id); //判断系统有没有这个解码器
if (!codec)
{
printf("video code not find!!\n");
return -1;
}
err = avcodec_open2(enc, codec,NULL); //找到系统中这个解码器的话,就打开这个解码器。
if (err != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf));
printf("buf\n", path, buf);
return -2;
}
printf("open avcodec_open2 success!!!\n");
}
}
AVFrame *yuv = av_frame_alloc(); //分配的视频帧
for (;;) //解码
{
re = av_read_frame(ic, &pkt);
if (re != 0) break;
if (pkt.stream_index != videoStream)
{
av_packet_unref(&pkt); //如果不是视频格式的 就释放掉
continue;
}
pts = pkt.pts * r2d(ic->streams[pkt.stream_index]->time_base) *1000;
re = avcodec_send_packet(videoCtx, &pkt);
if (re != 0)
{
av_packet_unref(&pkt);
continue;
}
re = avcodec_receive_frame(videoCtx, yuv);
if (re != 0)
{
av_packet_unref(&pkt);
continue;
}
printf("[D]");
//打开转码器
cCtx = sws_getCachedContext(cCtx, videoCtx->width,
videoCtx->height,
videoCtx->pix_fmt,
outwidth, outheight,
AV_PIX_FMT_BGRA,
SWS_BICUBIC,
NULL, NULL, NULL);
if (!cCtx)
{
printf("sws_getCachedContext failed!!\n");
break;
}
uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 }; //转码的目标数据
data[0] = (uint8_t *)rgb;
int linesize[AV_NUM_DATA_POINTERS] = { 0 };
linesize[0] = outwidth * 4;
int h = sws_scale(cCtx,yuv->data,yuv->linesize,0,videoCtx->height,
data,
linesize);
if (h > 0)
printf("(%d)", h);
//re = avcodec_decode_video2(videoCtx, yuv, &got_picture, &pkt);
//if (got_picture)
//{
// printf("%d\n",re);
//}
printf("pts = %d\n", pts);
av_packet_unref(&pkt); //用于清理空间,防止内存泄露。
}
if (cCtx)
{
sws_freeContext(cCtx);
}
avformat_close_input(&ic);
QApplication a(argc, argv);
Xplay w;
w.show();
return a.exec();
}