解説内容
DirectShowでの再生時、ウィンドウの透明度をいじってしまうと、不定期に点滅を繰り返す異常なウィンドウができてしまいます。そのためこのプログラムでは、ウィンドウに表示される直前の画像データをDIB(Device Independent Bitmap)として取得し、1秒間に30回程度自前で描画します。この事により、ウィンドウの透明度設定がしっかりと効くようになります。
プログラムの流れ
このプログラムの流れは次の通りです。
- ウィンドウを作る
- DirectShowを設定する
- フレームが切り替わった時に呼び出してもらう関数を登録する
- 再生開始
- 時間が進む
- フレームが変わった事が知らされる
- サンプリングされたフレームの情報をウィンドウに描画
- 5.に戻る(以下繰り返し)
使用するヘッダファイルとライブラリファイル
今回使用するヘッダとライブラリは、次のとおりです
- windows.h
- dshow.h――DirectShow用
- qedit.h――SampleGrabber用(後述します)
- strmiids.lib――DirectShow用
それと一つ注意です。透明なウィンドウの機能を利用するためには、_WIN32_WINNTを0x0500以上にdefineする必要があります。これを行わないとウィンドウを透明にできません。
まとめると次のとおりです。
//透明なウィンドウ機能を使えるようにするため #define _WIN32_WINNT 0x0500 #include <dshow.h> #include <windows.h> #include <qedit.h> #pragma comment(lib,"strmiids.lib") //ビデオのフルパス。各自変えて下さい const BSTR FileName = L"c:\\video\\ball.mpg"; //255で不透明 const byte WindowAlpha =150; //メディア情報 AM_MEDIA_TYPE am_media_type; VIDEOINFOHEADER *pVIH; //DIBビットマップへのポインタとサイズ。 long g_BufferSize; long *g_pImageBuffer; //メインのウィンドウのハンドル HWND hMainWnd;
ライブラリのリンクは好きな方法で構いません。でも、いちいち指定するのも面倒なのでソースコードに埋め込んでいます。後半部分は定数とグローバル変数です。ビデオへのフルパスはそれぞれ指定して下さい。
呼び出してもらう関数の作成
関数の作成なんですが、ここではクラスを作らなければなりません。そういう決まりです。と言う事でクラスを作るのですが、COMの知識が必要です。ですが、私はよく分からないので実装しません(それでも動くので)。気持ち悪さを感じたり、COMができたり、メモリリークが心配な人は実装して下さい。
//コールバックしてもらうための自前のクラス。 class mySampleGrabber: public ISampleGrabberCB{ public: //コンストラクタ mySampleGrabber(){ } //これが目的の関数 HRESULT STDMETHODCALLTYPE BufferCB( double SampleTime,BYTE *pBuffer,long BufferLen) { //これもめんどくさいのでグローバル関数を使っています //long g_BufferSize 画像データを受け取るバッファのサイズ //long *g_pImageBuffer 画像データを受け取るバッファのポインタ //HWND hMainWnd メインウィンドウのハンドル g_pImageBuffer = (long *)pBuffer; g_BufferSize = BufferLen; //メインウィンドウの再描画を要求 InvalidateRect(hMainWnd,NULL,FALSE); return S_OK; } //使わないので実装なし HRESULT STDMETHODCALLTYPE SampleCB( double SampleTime,IMediaSample *pSample) { return S_OK; } //COMのインターフェイス知識がないので実装なし。 //本当は実装するべきです。 STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { return S_OK; } STDMETHODIMP_(ULONG) AddRef() { return S_OK; } STDMETHODIMP_(ULONG) Release() { return S_OK; } };
している事は、次の2点です。
- 呼び出してもらった時に、その情報の入ったポインタをグローバル変数に代入する
- そして、ウィンドウの再描画をかける
ウィンドウ作成
とりあえず一つ山を越えました。もう一つ山がありますが、ここは一応谷ですので力を抜いていても大丈夫です。
//############################################### //ウィンドウプロシージャー //############################################### LRESULT CALLBACK WndProc(HWND hWnd,UINT msg,WPARAM wp,LPARAM lp){ PAINTSTRUCT ps; HDC hDC; switch(msg){ case WM_PAINT: hDC = BeginPaint(hWnd,&ps); //サンプルフィルターがサンプルしたDIBを表示する。 if ((am_media_type.formattype == FORMAT_VideoInfo) && (am_media_type.cbFormat >= sizeof(VIDEOINFOHEADER)) && (am_media_type.pbFormat != NULL) ) { SetDIBitsToDevice( hDC , 0 , 0 , pVIH->bmiHeader.biWidth , pVIH->bmiHeader.biHeight , 0 , 0 , 0,pVIH->bmiHeader.biHeight, g_pImageBuffer , (BITMAPINFO*)&pVIH->bmiHeader , DIB_RGB_COLORS ); } EndPaint(hWnd,&ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hWnd,msg,wp,lp); } //############################################### //自前のウィンドウクラスを登録する。 //############################################### int myRegisterWindow(HINSTANCE hInstance){ WNDCLASS winc; winc.style = CS_HREDRAW | CS_VREDRAW; winc.lpfnWndProc = WndProc; winc.cbClsExtra = winc.cbWndExtra = 0; winc.hInstance = hInstance; winc.hIcon = LoadIcon(NULL , IDI_APPLICATION); winc.hCursor = LoadCursor(NULL , IDC_ARROW); winc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); winc.lpszMenuName = NULL; winc.lpszClassName = TEXT("MY_DS"); return RegisterClass(&winc); } //############################################### //ウィンドウを作る //############################################### HWND myCreatWindow(HINSTANCE hInstance){ return CreateWindowEx( //当たり判定有り(後ろを操作できない) WS_EX_LAYERED |WS_EX_TOPMOST //当たり判定無し(後ろを操作できる) //WS_EX_LAYERED |WS_EX_TRANSPARENT|WS_EX_TOPMOST ,"MY_DS","DirectShowClear" //タイトルバー無し //,WS_POPUPWINDOW |WS_VISIBLE | WS_MAXIMIZEBOX | // WS_MINIMIZEBOX |WS_THICKFRAME //標準ウィンドウ ,WS_OVERLAPPEDWINDOW | WS_VISIBLE ,CW_USEDEFAULT,CW_USEDEFAULT ,640,480 ,NULL,NULL,hInstance,NULL); }
何度かウィンドウプログラミングをしている人はこれだけで分かると思いますが、何点か補足をします。
まず、am_media_typeですが、これはグローバル変数として宣言したものです。中にはサンプリングした画像に関するフォーマットデータが入っています。if文できちんと表示できる形式かを調べてからデータを渡します。ここで一つ、今回サンプリングされた画像のデータ形式はDIBとなっています。そのためSetDIBitsToDevice関数を使う事によって描画できます。DIBを扱うときは、座標に注意して下さい。
ウィンドウを透明化するためにはCreateWindowExを使い、拡張属性でWS_EX_LAYEREDを指定します。しかし、指定したままだと完全な透明のままなので、後でSetLayeredWindowAttributesという関数を使い設定します。
SetLayeredWindowAttributes(hWnd,RGB(0,0,0),WindowAlpha,LWA_ALPHA);
設定例です。これらの関数について分からない事は本やホームページで調べて下さい。
グラフ構築から再生
さていよいよ山場です。と言うわけで、まずソースコードを示します。
//DirectShowグラフ構築。 IGraphBuilder *pGB; //グラフを構築するために必要 IMediaControl *pMC; //ビデオの再生停止をコントロール IVideoWindow *pVW; //ビデオの出力ウィンドウを設定 //サンプリングフィルター ISampleGrabber *pSG; //サンプルガバラ IBaseFilter *pSGF; //そしてフィルター //COM関係 CoInitialize(NULL); CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC ,IID_IGraphBuilder,(LPVOID *)&pGB); // SampleGrabber(Filter)を生成する。 CoCreateInstance(CLSID_SampleGrabber,NULL,CLSCTX_INPROC ,IID_IBaseFilter,(LPVOID *)&pSGF); pGB->QueryInterface(IID_IMediaControl,(LPVOID *)&pMC); // FilterからISampleGrabberインターフェースを取得。 pSGF->QueryInterface(IID_ISampleGrabber,(LPVOID *)&pSG); // SampleGrabberを接続するフォーマットを指定する。 // 重要です。 ZeroMemory(&am_media_type,sizeof(am_media_type)); am_media_type.majortype = MEDIATYPE_Video; //取得したい画像の形式ここをいじるとおもしろい am_media_type.subtype = MEDIASUBTYPE_RGB24; am_media_type.formattype = FORMAT_VideoInfo; pSG->SetMediaType(&am_media_type); //グラフにSampleGrabberFilterを追加 pGB->AddFilter(pSGF,L"Sample Grabber"); //追加したフィルターを含めてグラフを作成。 pMC->RenderFile(FileName); //フィルターに入力されるビデオの情報を取得 pSG->GetConnectedMediaType(&am_media_type); //ビデオの高さや幅を取得 pVIH = (VIDEOINFOHEADER *)am_media_type.pbFormat; //呼び出してもらう関数の入ったクラスを作成 mySampleGrabber *mySGcall =new mySampleGrabber(); //コールバックに作成したクラスを登録。 //二番目の引数で「BufferCB」「SampleCB」どちらを呼び出すかを制御する。 //今回は「BufferCB」なので1 pSG->SetCallback((ISampleGrabberCB *)mySGcall,1); //標準だと自分でウィンドウを作って再生するので、 //このウィンドウで表示するようにする。 pGB->QueryInterface(IID_IVideoWindow,(LPVOID *)&pVW); pVW->put_Owner((OAHWND)hWnd); pVW->put_WindowStyle(WS_CHILD|WS_CLIPSIBLINGS); //表示サイズを0にする。 pVW->SetWindowPosition(0,0,0,0); //見えなくする。 pVW->put_Visible(OAFALSE); //再生開始。 pMC->Run();
コメントは入れていますが、コードを見てもわかりにくいと思うので流れを説明します。
- COMを使用するための準備
IGraphBuilderを作成(グラフ構築を行うもの)ISampleGrabberFilterを作成(ビデオのサンプリングを行うもの)ISampleGrabberをFilterから取得ISampleGrabberの入力フォーマットを設定- 構築されるグラフの中に
SampleGrabberFilterを入れるように設定 - グラフを構築
- サンプリングしたときにできる画像のフォーマットを取得
- フレームが変わったときに呼び出してもらう登録
- 標準で出力される映像を消す
- 再生する
と、以上のような流れになります。
ついでに、使ったものの処理もしなければならないので、そのコードも載せます。
//停止 pMC->Stop(); //使ったものはきちんと解放 //順番に注意しなきゃいけないときもあります pVW->Release(); pMC->Release(); pSG->Release(); pSGF->Release(); pGB->Release(); CoUninitialize(); //COMの実装をしていないので、自分でdelete delete(mySGcall);
エラー処理が全くありませんが、本来はするべきです。そうしないと、デバッカが無いとエラーを追えなくなってしまいます。
まとめ
色々と工夫の方向はあると思います。ビデオデータをリアルタイムでいじってみたいけどDirectXはちょっと……、なんてときぐらいだとは思いますが。工夫としては、ウィンドウの当たり判定を無くしてしまうとおもしろいと思います(ソースコード中のCreateWindowExをいじるだけですぐにできます)。「Windows Vista」のデモで行っていた、ビデオを使った16パズルもDirectXを使わずにできると思います。
細かい事については自分で調べてみるとおもしろいです。読んで頂きありがとうございました。
