// 使用ffmpeg取得视频的首个I帧,并转换为图片
// 图片格式可以为BMP、PPM、JPG
// 头文件CGetFirstIFrameToPic.h
#if !defined (_C_GET_FIRST_IFRAME_TO_PIC_H_)
#define _C_GET_FIRST_IFRAME_TO_PIC_H_
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "jpeglib.h"
}
// 取得视频的首个I帧,并转换为图片
// 图片格式可以为BMP、PPM、JPG
class CGetFirstIFrameToPic
{
public:
CGetFirstIFrameToPic(){};
virtual ~CGetFirstIFrameToPic(){};
// 取得视频的首个I帧,并转换为图片
virtual int GetFirstIFrameAndConvertToPic(char *pVideoFile, char *pPicFile);
// 保存帧数据为BMP图片
virtual int SaveFrameToBMP(char *pPicFile, uint8_t *pRGBBuffer, int nWidth, int nHeight, int nBitCount);
// 保存帧数据为PPM图片
virtual int SaveFrameToPPM(char *pPicFile, AVFrame *pFrame, int nWidth, int nHeight);
// 保存帧数据为JPG图片
virtual int SaveFrameToJPG(char *pPicFile, uint8_t *pRGBBuffer, int nWidth, int nHeight);
};
#endif //!defined(_C_GET_FIRST_IFRAME_TO_PIC_H_)
// 取得视频的首个I帧,并转换为图片
#include "stdafx.h"
#include "CGetFirstIFrameToPic.h"
// 取得视频的首个I帧,并转换为图片
int CGetFirstIFrameToPic::GetFirstIFrameAndConvertToPic(char *pVideoFile, char *pPicFile)
{
int nRet = 0;
if((NULL == pVideoFile) || (NULL == pPicFile))
{
return -1;
}
// 注册所有的文件格式和编解码器的库
av_register_all();
AVFormatContext *pFormatCtxDec = NULL; // 视频流的格式内容
// 读取文件的头部并且把信息保存到我们给的AVFormatContext结构
nRet = avformat_open_input(&pFormatCtxDec, pVideoFile, NULL, NULL);
if(nRet != 0)
{
printf("Couldn't open file %s.\n", pVideoFile);
return -1;
}
// 检查在文件中的流的信息
nRet = avformat_find_stream_info(pFormatCtxDec, NULL);
if(nRet < 0)
{
printf("Couldn't find stream information.\n");
return -1;
}
// 找到第一个视频流
int nVideoStream = -1;
for(unsigned int i = 0; i < pFormatCtxDec->nb_streams; i++)
{
if(pFormatCtxDec->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
nVideoStream = i;
break;
}
}
if(nVideoStream == -1)
{
printf("Didn't find a video stream.\n");
return -1;
}
printf("nVideoStream:%d.\n", nVideoStream);
// 流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)的东西。
// 这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一个指向他的指针。
// 但是我们必需要找到真正的编解码器并且打开它
AVCodecContext *pCodecCtxDec = NULL; // 编解码器上下文
// Get a pointer to the codec context for the video stream
pCodecCtxDec = pFormatCtxDec->streams[nVideoStream]->codec;
AVCodec *pCodecDec = NULL;
// Find the decoder for the video stream
pCodecDec = avcodec_find_decoder(pCodecCtxDec->codec_id);
if(pCodecDec==NULL)
{
printf("Codec not found.\n");
return -1;
}
// Open codec
nRet = avcodec_open2(pCodecCtxDec, pCodecDec, NULL);
if(nRet < 0)
{
printf("Could not open codec.\n");
return -1;
}
AVFrame *pFrameDec = NULL;
// Allocate video frame
pFrameDec = avcodec_alloc_frame();
if(pFrameDec == NULL)
{
printf("Allocate video frame error.\n");
return -1;
}
// 因为我们准备输出保存24位RGB色的PPM文件,我们必需把帧的格式从原来的转换为RGB。
// FFMPEG将为我们做这些转换。
// 在大多数项目中(包括我们的这个)我们都想把原始的帧转换成一个特定的格式。
// 让我们先为转换来申请一帧的内存
AVFrame *pFrameRGB = NULL;
// Allocate an AVFrame structure
pFrameRGB = avcodec_alloc_frame();
if(pFrameRGB == NULL)
{
printf("Allocate an AVFrame structure error.\n");
return -1;
}
// 即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始的数据。
// 我们使用avpicture_get_size来获得我们需要的大小,然后手工申请内存空间:
uint8_t *pBuffer;
int numBytes;
// Determine required buffer size and allocate buffer AV_PIX_FMT_RGB24
numBytes = avpicture_get_size(AV_PIX_FMT_RGB24, pCodecCtxDec->width, pCodecCtxDec->height);
pBuffer=(uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
// 现在我们使用avpicture_fill来把帧和我们新申请的内存来结合。
// 关于AVPicture的结成:AVPicture结构体是AVFrame结构体的子集
// ――AVFrame结构体的开始部分与AVPicture结构体是一样的。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, pBuffer, AV_PIX_FMT_RGB24,
pCodecCtxDec->width, pCodecCtxDec->height);
// 最后,我们已经准备好来从流中读取数据了。
// 读取数据
// 我们将要做的是通过读取包来读取整个视频流,
// 然后把它解码成帧,最好后转换格式并且保存。
int frameFinished = 0;
AVPacket packet;
static struct SwsContext *img_convert_ctx;
while(true)
{
av_init_packet(&packet);
nRet = av_read_frame(pFormatCtxDec, &packet);
if(nRet < 0)
{
break;
}
// Is this a packet from the video stream?
if(packet.stream_index == nVideoStream)
{
// Decode video frame
nRet = avcodec_decode_video2(pCodecCtxDec, pFrameDec, &frameFinished, &packet);
// Did we get a video frame?
if(frameFinished)
{
// 关键帧
if(pFrameDec->key_frame == 1)
{
char szPicFile[256];
// ------------frame to bmp start------------
memset(szPicFile, 0, sizeof(szPicFile));
strncpy(szPicFile, pPicFile, sizeof(szPicFile));
strncat(szPicFile, ".bmp", sizeof(szPicFile));
// Convert the image from its native format to RGB
img_convert_ctx = sws_getContext(
pCodecCtxDec->width, pCodecCtxDec->height, pCodecCtxDec->pix_fmt,
pCodecCtxDec->width, pCodecCtxDec->height,
AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
sws_scale(img_convert_ctx, pFrameDec->data, pFrameDec->linesize, 0,
pCodecCtxDec->height, pFrameRGB->data, pFrameRGB->linesize);
SaveFrameToBMP(szPicFile, pFrameRGB->data[0],
pCodecCtxDec->width, pCodecCtxDec->height, 24);
sws_freeContext(img_convert_ctx);
// ------------frame to bmp end------------
// ------------frame to ppm start------------
memset(szPicFile, 0, sizeof(szPicFile));
strncpy(szPicFile, pPicFile, sizeof(szPicFile));
strncat(szPicFile, ".ppm", sizeof(szPicFile));
// Convert the image from its native format to RGB
img_convert_ctx = sws_getContext(
pCodecCtxDec->width, pCodecCtxDec->height, pCodecCtxDec->pix_fmt,
pCodecCtxDec->width, pCodecCtxDec->height,
AV_PIX_FMT_RGB24, SWS_FAST_BILINEAR, NULL, NULL, NULL);
sws_scale(img_convert_ctx, pFrameDec->data, pFrameDec->linesize, 0,
pCodecCtxDec->height, pFrameRGB->data, pFrameRGB->linesize);
SaveFrameToPPM(szPicFile, pFrameRGB,
pCodecCtxDec->width, pCodecCtxDec->height);
sws_freeContext(img_convert_ctx);
// ------------frame to ppm end------------
// ------------frame to jpg start------------
memset(szPicFile, 0, sizeof(szPicFile));
strncpy(szPicFile, pPicFile, sizeof(szPicFile));
strncat(szPicFile, ".jpg", sizeof(szPicFile));
// Convert the image from its native format to RGB
img_convert_ctx = sws_getContext(
pCodecCtxDec->width, pCodecCtxDec->height, pCodecCtxDec->pix_fmt,
pCodecCtxDec->width, pCodecCtxDec->height,
AV_PIX_FMT_RGB24, SWS_FAST_BILINEAR, NULL, NULL, NULL);
sws_scale(img_convert_ctx, pFrameDec->data, pFrameDec->linesize, 0,
pCodecCtxDec->height, pFrameRGB->data, pFrameRGB->linesize);
// Save the frame to disk
SaveFrameToJPG(szPicFile, pFrameRGB->data[0],
pCodecCtxDec->width, pCodecCtxDec->height);
sws_freeContext(img_convert_ctx);
// ------------frame to jpg end------------
break;
}
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
av_free(pFrameRGB);
av_free(pBuffer);
av_free(pFrameDec);
return 0;
}
//typedef struct tagBITMAPFILEHEADER
//{
// unsigned short bfType; //2 位图文件的类型,必须为“BM”
// unsigned long bfSize; //4 位图文件的大小,以字节为单位
// unsigned short bfReserved1; //2 位图文件保留字,必须为0
// unsigned short bfReserved2; //2 位图文件保留字,必须为0
// unsigned long bfOffBits; //4 位图数据的起始位置,以相对于位图文件头的偏移量表示,以字节为单位
//} BITMAPFILEHEADER; //该结构占据14个字节。
//typedef struct tagBITMAPINFOHEADER{
// unsigned long biSize; //4 本结构所占用字节数
// long biWidth; //4 位图的宽度,以像素为单位
// long biHeight; //4 位图的高度,以像素为单位
// unsigned short biPlanes; //2 目标设备的平面数不清,必须为1
// unsigned short biBitCount; //2 每个像素所需的位数,必须是1(双色), 4(16色),8(256色)或24(真彩色)之一
// unsigned long biCompression;//4 位图压缩类型,必须是 0(不压缩),1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一
// unsigned long biSizeImage; //4 位图的大小,以字节为单位
// long biXPelsPerMeter; //4 位图水平分辨率,每米像素数
// long biYPelsPerMeter; //4 位图垂直分辨率,每米像素数
// unsigned long biClrUsed; //4 位图实际使用的颜色表中的颜色数
// unsigned long biClrImportant;//4 位图显示过程中重要的颜色数
//} BITMAPINFOHEADER; //该结构占据40个字节。
// 保存帧数据为BMP图片
// 保存帧数据为BMP图片
int CGetFirstIFrameToPic::SaveFrameToBMP(char *pPicFile, uint8_t *pRGBBuffer, int nWidth, int nHeight, int nBitCount)
{
BITMAPFILEHEADER bmpheader;
BITMAPINFOHEADER bmpinfo;
memset(&bmpheader, 0, sizeof(BITMAPFILEHEADER));
memset(&bmpinfo, 0, sizeof(BITMAPINFOHEADER));
FILE *fp = NULL;
fp = fopen(pPicFile, "wb");
if(NULL == fp)
{
printf("file open error %s\n", pPicFile);
return -1;
}
// set BITMAPFILEHEADER value
bmpheader.bfType = ('M' << 8) | 'B';
bmpheader.bfReserved1 = 0;
bmpheader.bfReserved2 = 0;
bmpheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
bmpheader.bfSize = bmpheader.bfOffBits + nWidth * nHeight * nBitCount / 8;
// set BITMAPINFOHEADER value
bmpinfo.biSize = sizeof(BITMAPINFOHEADER);
bmpinfo.biWidth = nWidth;
bmpinfo.biHeight = 0 - nHeight;
bmpinfo.biPlanes = 1;
bmpinfo.biBitCount = nBitCount;
bmpinfo.biCompression = BI_RGB;
bmpinfo.biSizeImage = 0;
bmpinfo.biXPelsPerMeter = 100;
bmpinfo.biYPelsPerMeter = 100;
bmpinfo.biClrUsed = 0;
bmpinfo.biClrImportant = 0;
// write pic file
fwrite(&bmpheader, sizeof(BITMAPFILEHEADER), 1, fp);
fwrite(&bmpinfo, sizeof(BITMAPINFOHEADER), 1, fp);
fwrite(pRGBBuffer, nWidth * nHeight * nBitCount / 8, 1, fp);
fclose(fp);
return 0;
}
// 我们做了一些标准的文件打开动作,然后写入RGB数据。
// 我们一次向文件写入一行数据。
// PPM格式文件的是一种包含一长串的RGB数据的文件。
// 如果你了解 HTML色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,
// 就像#ff0000#ff0000....就表示了了个红色的屏幕。
//(它被保存成二进制方式并且没有分隔符,但是你自己是知道如何分隔的)。
// 文件的头部表示了图像的宽度和高度以及最大的RGB值的大小。
// 保存帧数据为PPM图片
int CGetFirstIFrameToPic::SaveFrameToPPM(char *pPicFile, AVFrame *pFrame, int nWidth, int nHeight)
{
FILE *fp = fopen(pPicFile, "wb");
if(NULL == fp)
{
printf("file open error %s\n", pPicFile);
return -1;
}
// write header
fprintf(fp, "P6\n%d %d\n255\n", nWidth, nHeight);
// write pixel data
for(int y = 0; y < nHeight; y++)
{
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, nWidth * 3, fp);
}
fclose(fp);
return 0;
}
// 保存帧数据为JPG图片
// 该函数使用了jpeglib库进行图片压缩
// 保存帧数据为JPG图片
// 该函数使用了jpeglib库进行图片压缩
int CGetFirstIFrameToPic::SaveFrameToJPG(char *pPicFile, uint8_t *pRGBBuffer, int nWidth, int nHeight)
{
FILE *fp = fopen(pPicFile, "wb");
if(fp == NULL)
{
printf("file open error %s\n", pPicFile);
return -1;
}
struct jpeg_compress_struct jcs;
// 声明错误处理器,并赋值给jcs.err域
struct jpeg_error_mgr jem;
jcs.err = jpeg_std_error(&jem);
jpeg_create_compress(&jcs);
jpeg_stdio_dest(&jcs, fp);
// 为图的宽和高,单位为像素
jcs.image_width = nWidth;
jcs.image_height = nHeight;
// 在此为1,表示灰度图, 如果是彩色位图,则为3
jcs.input_components = 3;
//JCS_GRAYSCALE表示灰度图,JCS_RGB表示彩色图像
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
jpeg_set_quality (&jcs, 80, TRUE);
jpeg_start_compress(&jcs, TRUE);
// 一行位图
JSAMPROW row_pointer[1];
//每一行的字节数,如果不是索引图,此处需要乘以3
int row_stride = jcs.image_width * 3;
// 对每一行进行压缩
while (jcs.next_scanline < jcs.image_height)
{
row_pointer[0] = &(pRGBBuffer[jcs.next_scanline * row_stride]);
jpeg_write_scanlines(&jcs, row_pointer, 1);
}
jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(fp);
return 0;
}