有了之前使用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,所以就不往下鑽了,做到現在的程度已經達到了目的。