最近由于项目需求,要在界面中内嵌一个简单的视频播放器,能够打开视频,逐帧播放,进度条拖动等功能。
因此,首先尝试使用opencv编写。原因:1、便于后续处理;2、opencv提高的接口较完善。很快就动手写出了一个;基于OpenCV的视频播放器。
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <time.h>
#include <Windows.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
using namespace cv;
int g_slider_position = 0;
int n_position = 0;
VideoCapture pCap;
// 回调函数
void onTrackbarSlide( int pos, void * )
{
if (pos != n_position)
{
pCap.set(CV_CAP_PROP_POS_FRAMES, pos);
n_position = pos;
g_slider_position = pos;
}
}
int main( int argc, char *argv[] )
{
Mat pFrame;
int nFrames = 0;
double fps = 0; // 帧率
int spf = 0; // 1/fps
//char filename[200] = "768x576.avi";
if (argc == 2)
{
sprintf(filename, "%s", argv[1]);
}
namedWindow("VideoPlayer", 0); // 可改变窗口大小
pCap.open(filename);
if (!pCap.isOpened())
{
printf("Count not open video file/n");
exit(1);
}
// 获取视频帧率
fps = pCap.get(CV_CAP_PROP_FPS);
spf = (int)(1000/fps);
printf("fps = %.f\nspf = %d\n", fps, spf);
// 获取视频总帧数
nFrames = (int)(pCap.get(CV_CAP_PROP_FRAME_COUNT));
printf("Total frames = %d\n", nFrames);
// 如果由于编码方式问题获取不到帧数,则不创建滚动条
if (nFrames != 0)
{
createTrackbar(
"Position",
"VideoPlayer",
&g_slider_position,
nFrames,
onTrackbarSlide
);
}
while (1)
{
pCap >> pFrame;
if (pFrame.empty()) // 播放结束退出
{
break;
}
imshow("VideoPlayer", pFrame);
// 播放的过程中移动滚动条
printf("current pos %d\n", n_position);
n_position++;
g_slider_position = n_position;
setTrackbarPos("Position", "VideoPlayer", g_slider_position);
char c = waitKey((int)(spf)); // 按原来的帧率播放视频
if (c == 27) // 按下Esc退出播放
{
break;
}
}
pCap.release();
pFrame.release();
destroyWindow("VideoPlayer");
destroyWindow("VideoPlayerControl");
return 0;
}
在播放部分avi格式的视频时,拖动进度条会出现问题帧定位不准的问题,可能是定位到了关键帧。
google了一番,大家认为是从opencv2.x开始,开始使用ffmpeg。ffmpeg是一个开源的视频库,能够打开视频,转码等等。KMPlayer等等播放器都使用过这个库,只是因为违反了LGPL,被ffmpeg社区拉黑了。
下面给出的是X:\OpenCV\OpenCV231\modules\highgui\src\cap_ffmpeg_impl.hpp
bool CvCapture_FFMPEG::open( const char* _filename )
{
unsigned i;
bool valid = false;
close();
/* register all codecs, demux and protocols */
av_register_all();
#ifndef _DEBUG
// av_log_level = AV_LOG_QUIET;
#endif
int err = av_open_input_file(&ic, _filename, NULL, 0, NULL);
if (err < 0) {
CV_WARN("Error opening file");
goto exit_func;
}
err = av_find_stream_info(ic);
if (err < 0) {
CV_WARN("Could not find codec parameters");
goto exit_func;
}
for(i = 0; i < ic->nb_streams; i++) {
#if LIBAVFORMAT_BUILD > 4628
AVCodecContext *enc = ic->streams[i]->codec;
#else
AVCodecContext *enc = &ic->streams[i]->codec;
#endif
avcodec_thread_init(enc, get_number_of_cpus());
#if LIBAVFORMAT_BUILD < CALC_FFMPEG_VERSION(53, 4, 0)
#define AVMEDIA_TYPE_VIDEO CODEC_TYPE_VIDEO
#endif
if( AVMEDIA_TYPE_VIDEO == enc->codec_type && video_stream < 0) {
AVCodec *codec = avcodec_find_decoder(enc->codec_id);
if (!codec ||
avcodec_open(enc, codec) < 0)
goto exit_func;
video_stream = i;
video_st = ic->streams[i];
picture = avcodec_alloc_frame();
rgb_picture.data[0] = (uint8_t*)malloc(
avpicture_get_size( PIX_FMT_BGR24,
enc->width, enc->height ));
avpicture_fill( (AVPicture*)&rgb_picture, rgb_picture.data[0],
PIX_FMT_BGR24, enc->width, enc->height );
frame.width = enc->width;
frame.height = enc->height;
frame.cn = 3;
frame.step = rgb_picture.linesize[0];
frame.data = rgb_picture.data[0];
break;
}
}
if(video_stream >= 0) valid = true;
// perform check if source is seekable via ffmpeg's seek function av_seek_frame(...)
err = av_seek_frame(ic, video_stream, 10, 0);
if (err < 0)
{
filename=(char*)malloc(strlen(_filename)+1);
strcpy(filename, _filename);
// reopen videofile to 'seek' back to first frame
reopen();
}
else
{
// seek seems to work, so we don't need the filename,
// but we still need to seek back to filestart
filename=NULL;
int64_t ts = video_st->first_dts;
int flags = AVSEEK_FLAG_FRAME | AVSEEK_FLAG_BACKWARD;
av_seek_frame(ic, video_stream, ts, flags);
}
exit_func:
if( !valid )
close();
return valid;
}
err = av_seek_frame(ic, video_stream, 10, 0);
是大家认为opencv只能定位关键帧的原因所在。因此,大家推荐的做法是将0改成AVSEEK_FLAG_ANY,然后重新编译opencv。
第二个问题是,opencv能够打开并播放mp4,ts等格式的视频文件,但是不能提供帧定位。只有拖动进度条重新定位,视频就会从头开始播放。面对这个问题甚是苦恼。
既然opencv使用的是ffmpeg,那么我可以直接用ffmpeg的库进行视频文件的操作。因此从www.ffmpeg.org下载最新的static、shared和dev文件,解压出dev和shared,将shared中的bin文件夹拷贝到dev中。(我只是为了用起来方便,请原谅我的轻度强迫症)
我把dev放在了c盘根目录下,命名为ffmpeg。将c:\ffmpeg\bin加入到环境变量的path。然后注销一下。
在工程中加入ffmpeg\include和ffmpeg\lib,并将ffmpeg\lib中的*.lib加入到链接库中。在代码中加入对应的头文件就可以了。
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <time.h>
#include <Windows.h>
#include <iostream>
#include <stdlib.h>
#include <math.h>
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libavutil/common.h>
#include <libavutil/imgutils.h>
#include <libavutil/mathematics.h>
#include <libavutil/samplefmt.h>
#ifdef __cplusplus
}
#endif
#define N 3
#define INBUF_SIZE 4096
#define AUDIO_INBUF_SIZE 20480
#define AUDIO_REFILL_THRESH 4096
using namespace std;
using namespace cv;
int main( int argc, char *argv[] )
{
AVFormatContext *pFormatCtx;
int codec_id = CODEC_ID_MPEG4;
char filename[] = "Locust_Inspired_Jumping_Robot.flv";
// Open video file
AVCodec *codec;
AVCodecContext *c= NULL;
int i, out_size, x, y, outbuf_size;
FILE *f;
AVFrame *picture;
uint8_t *outbuf;
int had_output = 0;
av_register_all();
printf("Encode video file %s\n", filename);
codec = avcodec_find_encoder(CODEC_ID_H264);
if (!codec)
{
fprintf(stderr, "codec not found\n");
exit(1);
}
c = avcodec_alloc_context3(codec);
picture = avcodec_alloc_frame();
/* put sample parameters */
c->bit_rate = 40000;
//c->bit_rate_tolerance=30;
/* resolution must be a multiple of two */
c->width = 352;
c->height = 288;
/* frames per second */
c->time_base.den= 25;
c->time_base.num= 1;
c->gop_size = 10; /* emit one intra frame every ten frames */
c->max_b_frames=1;
c->pix_fmt = PIX_FMT_YUV420P;
if(codec_id == CODEC_ID_H264)
av_opt_set(c->priv_data, "preset", "slow", 0);
/* open it */
if (avcodec_open2(c, codec, NULL) < 0)
{
fprintf(stderr, "could not open codec\n");
exit(1);
}
f = fopen(filename, "wb");
if (!f)
{
fprintf(stderr, "could not open %s\n", filename);
exit(1);
}
/* alloc image and output buffer */
outbuf_size = 100000 + 12*c->width*c->height;
outbuf = (uint8_t*)malloc(outbuf_size); //CHANGED
/* the image can be allocated by any means and av_image_alloc() is
* just the most convenient way if av_malloc() is to be used */
av_image_alloc(picture->data, picture->linesize, c->width, c->height, c->pix_fmt, 1);
/* encode 1 second of video */
for(i = 0; i < 25; i++)
{
fflush(stdout);
/* prepare a dummy image */
/* Y */
for(y = 0; y < c->height; y++)
{
for(x = 0; x < c->width; x++)
{
picture->data[0][y * picture->linesize[0] + x] = x + y + i * 3;
}
}
/* Cb and Cr */
for(y = 0; y < c->height / 2; y++)
{
for(x = 0; x < c->width / 2; x++)
{
picture->data[1][y * picture->linesize[1] + x] = 128 + y + i * 2;
picture->data[2][y * picture->linesize[2] + x] = 64 + x + i * 5;
}
}
/* encode the image */
out_size = avcodec_encode_video(c, outbuf, outbuf_size, picture);
had_output |= out_size;
printf("encoding frame %3d (size=%5d)\n", i, out_size);
fwrite(outbuf, 1, out_size, f);
}
/* get the delayed frames */
for(; out_size || !had_output; i++)
{
fflush(stdout);
out_size = avcodec_encode_video(c, outbuf, outbuf_size, NULL);
had_output |= out_size;
printf("write frame %3d (size=%5d)\n", i, out_size);
fwrite(outbuf, 1, out_size, f);
}
/* add sequence end code to have a real mpeg file */
outbuf[0] = 0x00;
outbuf[1] = 0x00;
outbuf[2] = 0x01;
outbuf[3] = 0xb7;
fwrite(outbuf, 1, 4, f);
fclose(f);
free(outbuf);
avcodec_close(c);
av_free(c);
av_free(picture->data[0]);
av_free(picture);
printf("\n");
return 0;
}
值得注意的是,在包含ffmpeg的头文件时一定要定义在extern “C”中,同时这些头文件必须在#include <math.h>之后。
然后编译一下,就可以运行了。目前做到这一步了,现挖个坑,等把如何用ffmpeg读取编码输出视频的问题解决了再继续写。
未完待续