有了之前使用Emgu读取图片并显示在C#的PictureBox中的实践,今天使用相同的思路实现一个视频播放器。
任务
使用C#与Emgu实现一个简单的视频播放器,有以下功能:
可播放avi,rmvb等格式视频
有暂停、继续、停止、上一帧、下一帧等功能
有刻度条显示播放进度,并且可通过拖动刻度条来改来视频进度
要求:
VS 2012 RC,代码及库全部采用64位
使用C#的PictureBox,而不是Emgu的ImageBox :我想实际体验一下PictureBox与ImageBox之间的性能差距
采用System.Windows.Forms.Timer取帧:据说当间隔小于100ms时Timer会很不准确。在实际运行时,发现打开一个正常的视频文件(帧频为30/s)时,播放很慢,像在慢放一样,可证明的确有这个问题。不过我这么做是为了熟悉C#的API,下一次将换用更高效的方法。
使用自定义事件来控制按钮与刻度条的状态(如文字)
效果图
布局
布局还是比较简单的,主要是要用好以下几点:
AutoSize
Dock
Anchor
MinimumSize
就可以设计出一个窗口可随视频大小自动变化,按钮位置不会错位的布局出来。
Emgu相关API
使用Emgu封装好的方法读取视频文件,及获取相关信息:
// 读取视频文件(可以为AVI,rmvb等,只要系统安装了解码器)IntPtr capture = CvInvoke.cvCreateFileCapture(file);// 得到总帧数CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_COUNT);// 视频宽度CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_WIDTH);// 视频高度CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT);// 当前帧位置CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES);// 帧频CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FPS);
使用以下方法,将Emgu取得的图片变为PictureBox认识的Bitmap:
// 读取下一帧var frame = CvInvoke.cvQueryFrame(capture);// 生成一个新的Image容器Image dest =newImage(movieInfo.width, movieInfo.height);// 把数据复制过去CvInvoke.cvCopy(frame, dest, IntPtr.Zero);// 转换为Bitmapdest.ToBitmap();
如果想让视频定位到某一帧,使用以下方法:
intnewPos = 23;CvInvoke.cvSetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES, newPos);
Emgu的代码就是以上这些,其它的都是C#代码了。
视频信息
通过定义一个叫MovieInfo的struct,来保存与视频相关的信息供使用:
structMovieInfo {publicString filename;publicintframeCount;publicintwidth;publicintheight;publicintcurrentFrame;publicintfps;}
Timer
创建Timer,根据帧频设好Interval,以及Tick对应的操作。如下:
Timer myTimer =newTimer();myTimer.Interval = 1000 / Convert.ToInt32(movieInfo.fps);myTimer.Tick +=newEventHandler(MyTimer_Tick);myTimer.Start();
在MyTimer_Tick方法中读取下一帧画面,显示在PictureBox中
事件
各按钮与刻度条的状态,与视频的播放状态,是互相对应的。比如只有打开了一个视频文件,“开始/停止/前一帧/后一帧”按钮才可用,并且刻度条上的指针也随着播放向右移动;同时拖动刻度指针,视频也会随之变化。点击“前一帧/后一帧”时,画面也会跳动。它们之间是通过自定义的事件来协调的。
关于事件,可参考这篇文件:C#事件(event)解析
首先自定义了一个MovieEvent,它有多种不同的状态,用以区分不同的事件:
classMovieEvent : EventArgs {publicState EventState { get; set; }publicenumState { NewMovie, Started, Stopped, Paused, Playing, Scroll, PrevNext }publicMovieEvent(State state) :base() {this.EventState = state; }}
然后在Form1中定义了一个delegate和一个event handler:
delegatevoidMovieHandler(objectsender, MovieEvent e);eventMovieHandler MovieHandlers;
再写一个handler方法,用于捕获事件并处理。这里写的有点乱,不知道有没有更好的做法:
voidhandler(objectsender, MovieEvent e) {switch(e.EventState) {caseMovieEvent.State.NewMovie: btnStart.Enabled =true; btnStop.Enabled =true; btnPrev.Enabled =true; btnNext.Enabled =true; btnStart.Text ="暂停";break;caseMovieEvent.State.Paused: btnStart.Enabled =true; btnStop.Enabled =true; btnPrev.Enabled =true; btnNext.Enabled =true; btnStart.Text ="开始";break;// 更多}
在Form1的构造函数中,把handler注册上去:
this.MovieHandlers +=newMovieHandler(this.handler);
然后就是在程序的不同地方,执行了不同操作时,创建事件并散播出去:
MovieHandlers(this,newMovieEvent(MovieEvent.State.Stopped));MovieHandlers(this,newMovieEvent(MovieEvent.State.Playing));
项目源代码
放在了github上:https://github.com/freewind/opencv_emgu_learning/tree/master/AviPlayer_PictureBox
这是一个VS 2012 RC的项目,应该也可以在低版本运行。如果要运行,需要手动安装OpenCV/Emgu等,具体做法可参考我之前的文章。
改进
下一步将对该项目进行如下改进:
使用Emgu的ImageBox,测一下两者的性能差距
使用其它方式代替PictureBox
寻找进一步优化代码的方法
后记
(2012-08-09)
该程序有一个严重的问题:无法按帧频精准的播放视频。比如当帧频为30时,timer的间隔时间为1000/30=33,而准确的应该是33.3333,这样实际上就慢了一点。另外,如果视频比较大,解析一幅画面比较费时的时候,Timer就更加不准确了,播放起来像在慢放。
这应该是一个难以解决的问题,精准的控制时间很难做到,参见How to use c# to write a simple video player which plays the video with accurate fps?
由于我的目的是通过这个例子来学习OpenCV,所以就不往下钻了,做到现在的程度已经达到了目的。