有了之前使用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<Bgr, byte> dest = new Image<Bgr, byte>(movieInfo.width, movieInfo.height); // 把数据复制过去 CvInvoke.cvCopy(frame, dest, IntPtr.Zero); // 转换为Bitmap dest.ToBitmap();
如果想让视频定位到某一帧,使用以下方法:
int newPos = 23;
CvInvoke.cvSetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES, newPos);
Emgu的代码就是以上这些,其它的都是C#代码了。
视频信息
通过定义一个叫MovieInfo的struct,来保存与视频相关的信息供使用:
struct MovieInfo { public String filename; public int frameCount; public int width; public int height; public int currentFrame; public int fps; }
Timer
创建Timer,根据帧频设好Interval,以及Tick对应的操作。如下:
Timer myTimer = new Timer(); myTimer.Interval = 1000 / Convert.ToInt32(movieInfo.fps); myTimer.Tick += new EventHandler(MyTimer_Tick); myTimer.Start();
在MyTimer_Tick方法中读取下一帧画面,显示在PictureBox中
事件
各按钮与刻度条的状态,与视频的播放状态,是互相对应的。比如只有打开了一个视频文件,“开始/停止/前一帧/后一帧”按钮才可用,并且刻度条上的指针也随着播放向右移动;同时拖动刻度指针,视频也会随之变化。点击“前一帧/后一帧”时,画面也会跳动。它们之间是通过自定义的事件来协调的。
关于事件,可参考这篇文件:C#事件(event)解析
首先自定义了一个MovieEvent,它有多种不同的状态,用以区分不同的事件:
class MovieEvent : EventArgs { public State EventState { get; set; } public enum State { NewMovie, Started, Stopped, Paused, Playing, Scroll, PrevNext } public MovieEvent(State state) : base() { this.EventState = state; } }
然后在Form1中定义了一个delegate和一个event handler:
delegate void MovieHandler(object sender, MovieEvent e); event MovieHandler MovieHandlers;
再写一个handler方法,用于捕获事件并处理。这里写的有点乱,不知道有没有更好的做法:
void handler(object sender, MovieEvent e) { switch (e.EventState) { case MovieEvent.State.NewMovie: btnStart.Enabled = true; btnStop.Enabled = true; btnPrev.Enabled = true; btnNext.Enabled = true; btnStart.Text = "暂停"; break; case MovieEvent.State.Paused: btnStart.Enabled = true; btnStop.Enabled = true; btnPrev.Enabled = true; btnNext.Enabled = true; btnStart.Text = "开始"; break; // 更多 }
在Form1的构造函数中,把handler注册上去:
this.MovieHandlers += new MovieHandler(this.handler);
然后就是在程序的不同地方,执行了不同操作时,创建事件并散播出去:
MovieHandlers(this, new MovieEvent(MovieEvent.State.Stopped)); MovieHandlers(this, new MovieEvent(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,所以就不往下钻了,做到现在的程度已经达到了目的。