看到有人发了
嵌入式Linux通过帧缓存截图 - Embedded Linux Framebuffer Screenshot,我在前些时间也做了一些类似的工作,不过我截的是视频文件的帧画面。文章我在blog内发表了,现看到有人做了相似的工作,一起凑个热闹。文章写的有点乱,精力有限,不进行修改了。
2010/4/10
说起某科学的超电磁炮,从我漫长的动漫生涯来看,其实并不算一部顶尖之作。但是其OP动人心弦,确为绝佳。在片头动感的音乐中,首先出现的是经典的黑白场景,对炮姐从脚到头作一次扫描,接着停格在炮姐的拼发电流的手上,画面渐入彩 {MOD}。这短短的几秒钟,令我兴奋难以自抑。
之后,我就一直在纠结了。因为太过喜欢那几帧画面,想取来做桌面壁纸、屏保、手机屏保。但是在网上老找不到这些图片,或者质量太次。纠结了几个月,前两天忽然想起之前曾详细分析过ffmpeg的源码的,为什么不自己写个软件从stream中抓frame呢?于是立马动手做这件事。欲望才是创作的原动力啊,对炮姐的爱令我心中满怀热情。有人问我:为啥不播放时printscreen呢?但是printscreen可以实现某个时间段的连续帧获取吗?
开发环境:Debian /Etch
准备工作:
1、 svn checkout svn://svn.mplayerhq.hu/ffmpeg/trunk ffmpeg(下载最新版本的ffmpeg)
2、 cd ffmpeg
3、 ./configure --enable-gpl --enable-shared --prefix=/usr(编译为动态库,懒得改.bash_profile,prefix路径选/usr,默认为/usr/local)
4、 make(编译,如果要用到ffplay的话,先sudo apt-get install libsdl-dev,ffplay.c用到sdl的库)
5、 sudo make install(安装lib和include file,注意include file分几个目录存放的,不像之前版本那样统一放到/usr/include/ffmpeg下)
注:在进行这些准备工作前,最好将之前安装的ffmpeg的lib和include file全部清除干净。我一开始用的20071007的版本,再换成latest ffmpeg,编译的时候可以过去,但是运行时老出错。开始时我还以为新版本的ffmpeg的接口有非常非常大的改变,在我程序上找了很长时间无果。最后清除了20071007版的ffmpeg后,编译,顺利运行。
编写代码:
代码附录如后,现不对代码作详细说明,对于ffmpeg的架构及api使用,可参考《FFMpeg框架代码阅读》、《Using libavformat and libavcodec》。之后我应该会整理更详细一点的架构文档说明。这段代码我很大程度上参考了Using libavformat and libavcodec中的avcodec_sample.cpp,其实解码过程都是一样的,这点同样可以从ffmpeg里的例子如ffplay.c、seek_test.c中找到。
由于版本的更新,我修正了一些接口,主要是img_convert换成sws_scale,av_read_packet换成av_read_frame;实现了bmp的编码,话说bmp的标准也真是简单啊。
编译运行:
1、 gcc railgun.c -lavutil -lavformat -lavcodec –lswscale(文件名我都用railgun了,炮姐可以感受到我的爱了吧)
2、 ./a.out To_Aru_Kagaku_no_Railgun.mkv(哇啦啦,产生200张1980*1080的bmp图片,片源是1080P的BDrip,从哪一帧开始到哪一帧结束,可修改代码)
后续工作:可定位某个时间点的frame。事实上我用另外一个片源(avi格式,视频编码mpeg)已经实现了,但MKV的我死活搞不定,现在还不知道这个跟format还是跟codec有关。值得进一步研究。同时还有编解码、音视频同步、字幕显示等等很多东西,以后要先挑一个具体分析才行。
最后,我在相册发了一些炮姐的图片,全是我用这个工具提取出来的。不过CU相册有上传大小限制,我转为jpeg了。
2010/4/13
1、img_convert_ctx = sws_getContext(dec->width, dec->height, dec->pix_fmt, dec->width, dec->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);语句中PIX_FMT_RGB24应该为PIX_FMT_BGR24。否则BMP {MOD}彩变样。原来由于图片是黑白的,所以没及时发现。后来取彩 {MOD}图片时,发现青春靓丽的炮姐变整一个阿凡达了,失礼失礼。
2、总算实现了取某个时间段的frame。使用方法:./a.out air.mpg 000130 000132 (取air.mpg00:01:30--00:01:32的所有帧)
总体思路是:
timestamp = nSec * AV_TIME_BASE;
timestamp = av_rescale_q(timestamp, AV_TIME_BASE_Q, ic->streams[videoStream]->time_base);
avformat_seek_file(ic, videoStream, INT64_MIN, timestamp, timestamp, 0);
看avformat_seek_file的prototype说明,这个接口是新接口,之后可能会有新的变化,要留意这一点。
另外用这种方法来seek frame,似乎时间定位不准,存在一定的偏差(偏差貌似是按相等比例的,可以微调。也不打算继续改进了,以后的重点会转向编解码或图像处理)。
再有就是运行时会提示:[mpeg1video @ 0x804c300]warning: first frame is no keyframe,前面几帧图片失真。
// railgun.c
// A small sample program that shows how to use libavformat and libavcodec to
// read video from a file and write frame to a bmp file.
//
// Use
//
// gcc railgun.c -lavutil -lavformat -lavcodec -lswscale
//
// to build (assuming libavformat and libavcodec are correctly installed on
// your system).
//
// Run using
//
// ./a.out air.mpg 000130 000132
//
// to write frames(00:01:30--00:01:32) from "air.mpg" to disk in BMP
// format.
#include
#include
#include
#include
#include
#include
#undef sprintf
#undef uint8_t
#undef uint16_t
#undef uint32_t
#define uint8_t unsigned char
#define uint16_t unsigned short
#define uint32_t unsigned long
#pragma pack(2)
typedef struct BMPHeader
{
uint16_t identifier;
uint32_t file_size;
uint32_t reserved;
uint32_t data_offset;
} BMPHeader;
typedef struct BMPMapInfo
{
uint32_t header_size;
uint32_t width;
uint32_t height;
uint16_t n_planes;
uint16_t bits_per_pixel;
uint32_t compression;
uint32_t data_size;
uint32_t hresolution;
uint32_t vresolution;
uint32_t n_colors_used;
uint32_t n_important_colors;
}BMPMapInfo;
int CreateBmpImg(AVFrame *pFrame, int width, int height, int iFrame)
{
BMPHeader bmpheader;
BMPMapInfo bmpinfo;
FILE *fp;
int y;
char filename[32];
// Open file
memset(filename, 0x0, sizeof(filename));
sprintf(filename, "%d.bmp", iFrame);
fp = fopen(filename, "wb");
if(!fp)return -1;
bmpheader.identifier = ('M'<<8)|'B';
bmpheader.reserved = 0;
bmpheader.data_offset = sizeof(BMPHeader) + sizeof(BMPMapInfo);
bmpheader.file_size = bmpheader.data_offset + width*height*24/8;
bmpinfo.header_size = sizeof(BMPMapInfo);
bmpinfo.width = width;
bmpinfo.height = height;
bmpinfo.n_planes = 1;
bmpinfo.bits_per_pixel = 24;
bmpinfo.compression = 0;
bmpinfo.data_size = height*((width*3 + 3) & ~3);
bmpinfo.hresolution = 0;
bmpinfo.vresolution = 0;
bmpinfo.n_colors_used = 0;
bmpinfo.n_important_colors = 0;
fwrite(&bmpheader,sizeof(BMPHeader),1,fp);
fwrite(&bmpinfo,sizeof(BMPMapInfo),1,fp);
for(y=height-1; y>=0; y--)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, fp);
fclose(fp);
return 0;
}
//解码指定videostream,并保存frame数据到pFrame上
//返回: 0--成功,非0--失败
int DecodeVideoFrame(AVFormatContext *pFormatCtx, AVCodecContext *pCodecCtx,
int videoStream, int64_t endtime, AVFrame *pFrame)
{
static AVPacket packet;
static uint8_t *rawData;
static int bytesRemaining = 0;
int bytesDecoded;
int frameFinished;
static int firstTimeFlag = 1;
if (firstTimeFlag)
{
firstTimeFlag = 0;
packet.data = NULL;//第一次解frame,初始化packet.data为null
}
while (1)
{
do
{
if (packet.data == NULL) av_free_packet(&packet); //释放旧的packet
if (av_read_frame(pFormatCtx, &packet) < 0)
{
//从frame读取数据保存到packet上,<0表明到了stream end
printf("-->av_read_frame end
");
goto exit_decode;
}
} while (packet.stream_index != videoStream); //判断当前frame是否为指定的video stream
//判断当前帧是否到达了endtime,是则返回false,停止取下一帧
if (packet.pts >= endtime) return -1;
bytesRemaining = packet.size;
rawData = packet.data;
while (bytesRemaining > 0)
{
bytesDecoded = avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, rawData, bytesRemaining);
if (bytesDecoded < 0) return -1;
bytesRemaining -= bytesDecoded;
rawData += bytesDecoded;
if (frameFinished) return 0;
}
}
exit_decode:
bytesDecoded = avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, rawData, bytesRemaining);
if(packet.data != NULL) av_free_packet(&packet);
if (frameFinished != 0) return 0;
return -1;
}
void usage(const char *function)
{
printf("Usage: %s [File Name] [Start Time] [End Time]
", function);
printf("Ex: ./railgun panda.mpg 003005 003010
");
printf("Time Format: HrsMinsSecs. Ex 003005 means 00 hours 30 minutes 05 senconds
");
printf("
");
}
void ParseTime(char strStartTime[], int64_t *pStartSec,
char strEndTime[], int64_t *pEndSec)
{
int64_t starttime = 0, endtime = 0;
if (strStartTime && pStartSec)
{
starttime = atoi(strStartTime);
*pStartSec = (3600*starttime/10000) +
(60*(starttime%10000)/100) +
(starttime%100);
}
if (strEndTime && pEndSec)
{
endtime = atoi(strEndTime);
*pEndSec = (3600*endtime/10000) +
(60*(endtime%10000)/100) +
(endtime%100);
}
}
int main(int argc, char *argv[])
{
const char *filename;
AVFormatContext *ic = NULL;
AVCodecContext *dec = NULL;
AVCodec *codec = NULL;
AVFrame *frame = NULL;
AVFrame *frameRGB = NULL;
uint8_t *buffer = NULL;
int numBytes;
int i, videoStream;
int64_t startTime = 0;
int64_t endTime = 0;
static struct SwsContext *img_convert_ctx = NULL;
// Register all formats and codecs
av_register_all();
filename = argv[1];
// parse begin time and end time
if (argc == 3)
ParseTime(argv[2], &startTime, NULL, NULL);
else if (argc == 4)
ParseTime(argv[2], &startTime, argv[3], &endTime);
else
{
usage(argv[0]);
return -1;
}
startTime *= AV_TIME_BASE;
endTime *= AV_TIME_BASE;
// Open video file
if(av_open_input_file(&ic, filename, NULL, 0, NULL)!=0)
{
fprintf(stderr, "Cannt open input file
");
usage(argv[0]);
goto exit_err;
}
// Retrieve stream information
if(av_find_stream_info(ic)<0)
{
fprintf(stderr, "Cannt find stream info
");
goto exit_err;
}
// Dump information about file onto standard error
dump_format(ic, 0, filename, 0);
// Find the first video stream
videoStream=-1;
for(i=0; inb_streams; i++)
if(ic->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO)
{
videoStream=i;
break;
}
if(videoStream==-1)
{
fprintf(stderr, "No video stream
");
goto exit_err;
}
// Get a pointer to the codec context for the video stream
dec=ic->streams[videoStream]->codec;
// Find the decoder for the video stream
codec=avcodec_find_decoder(dec->codec_id);
if(codec==NULL)
{
fprintf(stderr, "Found no codec
");
goto exit_err;
}
// Open codec
if(avcodec_open(dec, codec)<0)
{
fprintf(stderr, "Cannt open avcodec
");
goto exit_err;
}
// Allocate video frame
frame=avcodec_alloc_frame();
// Allocate an AVFrame structure
frameRGB=avcodec_alloc_frame();
if(frameRGB==NULL)
{
av_free(frame);
fprintf(stderr, "Cannt alloc frame buffer for RGB
");
goto exit_err;
}
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, dec->width, dec->height);
buffer=(uint8_t *)av_malloc(numBytes);
if (!buffer)
{
av_free(frame);
av_free(frameRGB);
fprintf(stderr, "Cannt alloc picture buffer
");
goto exit_err;
}
// Assign appropriate parts of buffer to image planes in pFrameRGB
avpicture_fill((AVPicture *)frameRGB, buffer, PIX_FMT_RGB24, dec->width, dec->height);
img_convert_ctx = sws_getContext(dec->width, dec->height, dec->pix_fmt, dec->width, dec->height, PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
if (img_convert_ctx == NULL) {
fprintf(stderr, "Cannot initialize the conversion context
");
goto exit_err;
}
// Seek frame
startTime = av_rescale_q(startTime, AV_TIME_BASE_Q, ic->streams[videoStream]->time_base);
endTime = av_rescale_q(endTime, AV_TIME_BASE_Q, ic->streams[videoStream]->time_base);
avformat_seek_file(ic, videoStream, INT64_MIN, startTime, startTime, 0);
// Read frames and save first five frames to dist
i=0;
while(!DecodeVideoFrame(ic, dec, videoStream, endTime, frame))
{
// Save the frame to disk
i++;
sws_scale(img_convert_ctx, (AVPicture*)frame->data, (AVPicture*)frame->linesize,
0, dec->height, (AVPicture*)frameRGB->data, (AVPicture*)frameRGB->linesize);
CreateBmpImg(frameRGB, dec->width, dec->height, i);
}
exit_err:
// Free the RGB image
if (buffer)
av_free(buffer);
if (frameRGB)
av_free(frameRGB);
// Free the YUV frame
if (frame)
av_free(frame);
// Close the codec
if (dec)
avcodec_close(dec);
// Close the video file
if (ic)
av_close_input_file(ic);
if (img_convert_ctx)
sws_freeContext(img_convert_ctx);
return 0;
}