EternalWindows
AVI / ビデオストリームの再生

ビデオストリームを再生するには、DrawDib関数を使う方法とVCM関数を使う方法があります。 DrawDib関数の場合は、圧縮されたDIBを関数が内部で解凍するため、 呼び出し方は比較的簡単といえます。 一方、VCM関数を利用する場合は、アプリケーションが明示的に解凍処理を行うことになりますが、 その解凍されたDIBを加工してから描画したい場合などは便利な存在となります。 今回は、DrawDib関数を利用する例を取り上げます。

DrawDib関数を利用するためには、最初にDrawDibOpenを呼び出すことになります。 これにより、DrawDib デバイスコンテキストのハンドルを取得することができます。

HDRAWDIB DrawDibOpen(VOID);

戻り値のHDRAWDIBが、DrawDib デバイスコンテキストのハンドルを表します。

DrawDib デバイスコンテキストのハンドルを取得すれば、 DrawDibDrawを呼び出してDIBを描画できるようになります。

BOOL DrawDibDraw(
  HDRAWDIB hdd,            
  HDC hdc,                 
  int xDst,                
  int yDst,                
  int dxDst,               
  int dyDst,               
  LPBITMAPINFOHEADER lpbi, 
  LPVOID lpBits,           
  int xSrc,                
  int ySrc,                
  int dxSrc,               
  int dySrc,               
  UINT wFlags              
);

hddは、DrawDib デバイスコンテキストのハンドルを指定します。 hdcは、デバイスコンテキストのハンドルを指定します。 このデバイスコンテキストにDIBが描画されることになります。 xDstは、送信先のx座標を指定します。 yDstは、送信先のy座標を指定します。 dxDstは、送信先の幅を指定します。 -1を指定すると、DIBの幅を指定したものと解釈されます。 dyDstは、送信先の高さを指定します。 -1を指定すると、DIBの高さを指定したものと解釈されます。 lpbiは、描画するDIBのフォーマットを指定します。 lpBitsは、DIBのビットイメージを指定します。 xSrcは、送信元のx座標を指定します。 ySrcは、送信元のy座標を指定します。 dxSrcは、送信元の幅を指定します。 -1を指定すると、DIBの幅を指定したものと解釈されます。 dySrcは、送信元の高さを指定します。 -1を指定すると、DIBの高さを指定したものと解釈されます。 wFlagsは、0で問題ありません。

不要になったDrawDib デバイスコンテキストのハンドルは、DrawDibCloseで開放することになります。

BOOL DrawDibClose(
  HDRAWDIB hdd
);

hddは、DrawDib デバイスコンテキストのハンドルを指定します。

ビデオストリームにおける1つのサンプルは、フレームと呼ばれることがあります。 このフレームを一定間隔でウインドウに描画することが映像の再生となるため、 AVIファイルから時間に関する情報を取得する必要があります。 これには、AVIStreamInfoを呼び出します。

STDAPI AVIStreamInfo(
  PAVISTREAM pavi,    
  AVISTREAMINFO * psi,
  LONG lSize          
);

paviは、ストリームのハンドルを指定します。 psiは、AVISTREAMINFO構造体のアドレスを指定します。 lSizeは、psiのサイズを指定します。 AVISTREAMINFO構造体は、次のように定義されています。

typedef struct { 
  DWORD fccType; 
  DWORD fccHandler; 
  DWORD dwFlags; 
  DWORD dwCaps; 
  WORD  wPriority; 
  WORD  wLanguage; 
  DWORD dwScale; 
  DWORD dwRate; 
  DWORD dwStart; 
  DWORD dwLength; 
  DWORD dwInitialFrames; 
  DWORD dwSuggestedBufferSize; 
  DWORD dwQuality; 
  DWORD dwSampleSize; 
  RECT  rcFrame; 
  DWORD dwEditCount; 
  DWORD dwFormatChangeCount; 
  TCHAR szName[64]; 
} AVISTREAMINFO

fccTypeは、ストリームの種類を表すFOURCCが格納されます。 fccHandlerは、圧縮に使用されたコーデックのFOURCCが格納されます。 dwFlagsは、ストリームに関する定数が格納されます。 dwCapsは、現在使用されていません。 wPriorityは、ストリームの優先順位が格納されます。 wLanguageは、ストリームの言語が格納されます。 dwScaleは、ストリームの時間単位が格納されます。 dwRateは、ストリームのレートが格納されます。 dwStartは、先頭のサンプル番号が格納されます。 dwInitialFramesは、インターリーブ形式のAVIファイルにおいて意味のある値が格納されます。 dwSuggestedBufferSizeは、ストリームの読み取りに最適なサイズが格納されます。 dwQualityは、ストリームの品質が0から10000までの値で格納されます。 -1の場合は、デフォルトの品質となります。 dwSampleSizeは、1サンプルにおけるバイトサイズが格納されます。 0の場合、サイズは可変になります。 rcFrameは、フレームを表示するための矩形が格納されます。 このメンバは、ビデオストリームの場合のみ有効です。 dwEditCountは、AVIファイルを編集した回数が格納されます。 dwFormatChangeCountは、フォーマットを変更した回数が格納されます。 szNameは、ストリームの説明文が格納されます。

dwRateをdwScaleで割れば、1秒間に何回フレームを描画すればよいかが分かります。 たとえば、dwRateが60でdwScaleが2ならば、1秒間に30回フレームを描画する必要があります。 このような値は、フレームレートと呼ばれています。 1000ミリ秒(1秒)をフレームレートで割れば、 1秒間にどれくらいの間隔でフレームを描画していけばよいかが分かるため、 この間隔を基に描画を行うことになります。

今回のプログラムは、AVIファイルからビデオストリームを取得し、各フレームを描画します。

#include <windows.h>
#include <vfw.h>

#pragma comment (lib, "winmm.lib")
#pragma comment (lib, "vfw32.lib")

struct THREADINFO {
	HWND       hwnd;
	BOOL       bExit;
	PAVISTREAM pavi;
};
typedef struct THREADINFO THREADINFO;
typedef THREADINFO *LPTHREADINFO;

DWORD WINAPI ThreadProc(LPVOID lpParamater);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HANDLE     hThread = NULL;
	static PAVISTREAM pavi = NULL;
	static THREADINFO threadInfo = {0};

	switch (uMsg) {

	case WM_CREATE: {
		DWORD         dwThreadId;
		ICINFO        icinfo;
		AVISTREAMINFO si;

		AVIFileInit();

		if (AVIStreamOpenFromFile(&pavi, TEXT("sample.avi"), streamtypeVIDEO, 0, OF_READ, NULL) != 0) {
			MessageBox(NULL, TEXT("ファイルまたはビデオストリームが存在しません。"), NULL, MB_ICONWARNING);
			return -1;
		}
		
		AVIStreamInfo(pavi, &si, sizeof(AVISTREAMINFO));
			
		if (si.fccHandler != comptypeDIB && !ICInfo(ICTYPE_VIDEO, si.fccHandler, &icinfo)) {
			TCHAR szBuf[256];
			LPSTR lp = (LPSTR)&si.fccHandler;
			wsprintf(szBuf, TEXT("%c%c%c%c"), lp[0], lp[1], lp[2], lp[3]);
			MessageBox(NULL, szBuf, TEXT("ビデオコーデックが存在しません。"), MB_ICONWARNING);
			return -1;
		}

		threadInfo.hwnd  = hwnd;
		threadInfo.pavi  = pavi;
		threadInfo.bExit = FALSE;

		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, &threadInfo, 0, &dwThreadId);
		SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST);

		return 0;
	}
	
	case WM_DESTROY:
		if (hThread != NULL) {
			threadInfo.bExit = TRUE;
			WaitForSingleObject(hThread, 1000);
			CloseHandle(hThread);
		}

		if (pavi != NULL)
			AVIStreamRelease(pavi);

		AVIFileExit();

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	HDC              hdc;
	HWND             hwnd;
	LONG             i;
	LONG             lStart, lEnd;
	LONG             lSize;
	LPBYTE           lpBits = NULL;
	HDRAWDIB         hdd;
	PAVISTREAM       pavi;
	LPTHREADINFO     lpThreadInfo = (LPTHREADINFO)lpParamater;
	AVISTREAMINFO    si;
	BITMAPINFOHEADER bmiHeader;
	double           dInterval;
	double           dCurTime, dNextTime;

	pavi = lpThreadInfo->pavi;
	hwnd = lpThreadInfo->hwnd;
	
	lStart = AVIStreamStart(pavi);
	lEnd  = lStart + AVIStreamLength(pavi);

	lSize = sizeof(BITMAPINFOHEADER);
	AVIStreamReadFormat(pavi, 0, &bmiHeader, &lSize);
	lpBits = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, bmiHeader.biSizeImage);

	AVIStreamInfo(pavi, &si, sizeof(AVISTREAMINFO));
	dInterval = 1000 / ((double)si.dwRate / si.dwScale);

	hdd = DrawDibOpen();
	
	for (; !lpThreadInfo->bExit;) {
		for (i = lStart; i < lEnd; i++) {
			dNextTime = (double)timeGetTime();
			dNextTime += dInterval;

			AVIStreamRead(pavi, i, 1, lpBits, bmiHeader.biSizeImage, NULL, NULL);
			hdc = GetDC(hwnd);
			DrawDibDraw(hdd, hdc, 0, 0, -1, -1, &bmiHeader, lpBits, 0, 0, -1, -1, 0);
			ReleaseDC(hwnd, hdc);
			ZeroMemory(lpBits, bmiHeader.biSizeImage);

			dCurTime = (double)timeGetTime();
			if (dNextTime < dCurTime)
				dNextTime = dCurTime + dInterval;
			
			Sleep((DWORD)(dNextTime - dCurTime));

			if (lpThreadInfo->bExit)
				break;
		}
	}

	HeapFree(GetProcessHeap(), 0, lpBits);
	DrawDibClose(hdd);

	return 0;
}

フレームの描画処理は、メインスレッドとは別のスレッドで行うようにしています。 メインスレッドはウインドウメッセージの処理を行わなければならないため、 メインスレッドでフレームの描画を行った場合は、 ウインドウの操作時にフレームの描画が遅れる可能性があります。 また、フレームの描画処理をなるべく優先するために、 SetThreadPriorityでスレッドの優先順位を上げています。 THREADINFO構造体は、描画スレッドにストリームや終了検出フラグを渡すために存在しています。 終了検出フラグはWM_DESTROYでTRUEとなり、描画スレッドではこれがTRUEになっていないかを常に確認するようにしています。 TRUEにした直後に描画スレッドがタイミングよく終了するとは限りませんから、 WaitForSingleObjectでスレッドが終了するまで1秒間待機するようにしています。 ThreadProcの処理を順に見ていきます。

lStart = AVIStreamStart(pavi);
lEnd  = lStart + AVIStreamLength(pavi);

lSize = sizeof(BITMAPINFOHEADER);
AVIStreamReadFormat(pavi, 0, &bmiHeader, &lSize);
lpBits = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, bmiHeader.biSizeImage);

AVIStreamInfo(pavi, &si, sizeof(AVISTREAMINFO));
dInterval = 1000 / ((double)si.dwRate / si.dwScale);

フレームを描画していくにあたり、先頭のフレームと終端のフレームの番号が必要ですから、 AVIStreamStartとAVIStreamLengthでこれらを取得することになります。 また、フレームの描画時に呼び出すDrawDibDrawは、フレームのフォーマットを要求しているため、 AVIStreamReadFormatでこれを取得することになります。 また、AVIStreamReadで取得するフレームを格納できるだけのメモリも事前に確保しています。 dIntervalは、1000ミリ秒をフレームレートで割った値が格納されます。

フレームの描画は、二重ループの処理の中に実装されています。 1つ目のループは、全てのフレームを描画し終えても再び描画を開始させるためのループであり、 2つ目のループはフレームを順に描画していくためのループです。 ループ内の実装を具体的に見ていく前に、好ましくない実装について先に検討します。

for (; !lpThreadInfo->bExit;) {
	for (i = lStart; i < lEnd; i++) {
		AVIStreamRead(pavi, i, 1, lpBits, bmiHeader.biSizeImage, NULL, NULL);
		hdc = GetDC(hwnd);
		DrawDibDraw(hdd, hdc, 0, 0, -1, -1, &bmiHeader, lpBits, 0, 0, -1, -1, 0);
		ReleaseDC(hwnd, hdc);
		ZeroMemory(lpBits, bmiHeader.biSizeImage);
		
		Sleep((DWORD)dInterval);

		if (lpThreadInfo->bExit)
			break;
	}
}

この例では、DrawDibDrawによる描画を終えた後、Sleepを呼び出すことによってdIntervalの値だけ待機しています。 フレームレートが仮に60であった場合、dIntervalには16が格納されることになり、 これが意味することは「1秒間に60回フレームを描画するには、16ミリ秒置きに描画すればよい」というものです。 したがって、Sleepで16ミリ秒待機するのは問題ないように思えますが、実際にはこれは誤りです。 たとえば、Sleepから制御が返ったときの現在の時間が100とします。 この時点で、次にSleepから制御が返ったときの現在の時間は、116になっていなければならないことになります。 そうでなければ、16ミリ秒置きの描画実現できないからです。 しかし、次のSleepを実行するまでの処理(フレームの描画)に2ミリ秒が要したとすれば、 Sleepを実行する直前の時間は102となります。 そして、Sleepで16ミリ秒待機したとなると、Sleepから制御が返ったときの現在の時間は118となり、 本来の116とは時間が異なっています。 つまり、16ミリ秒置きに描画できないことになります。 問題の根底となっているのは、Sleepで無条件に16ミリ秒待機した点であり、 Sleepを実行する直前の時間は102であれば、 Sleepに指定するべき時間は、116(本来望む時間)から102(現在の時間)を引いた14でなければならなかったのです。 下記のコードは、この点を踏まえて実装されています。

for (; !lpThreadInfo->bExit;) {
	for (i = lStart; i < lEnd; i++) {
		dNextTime = (double)timeGetTime();
		dNextTime += dInterval;
		
		AVIStreamRead(pavi, i, 1, lpBits, bmiHeader.biSizeImage, NULL, NULL);
		hdc = GetDC(hwnd);
		DrawDibDraw(hdd, hdc, 0, 0, -1, -1, &bmiHeader, lpBits, 0, 0, -1, -1, 0);
		ReleaseDC(hwnd, hdc);
		ZeroMemory(lpBits, bmiHeader.biSizeImage);

		dCurTime = (double)timeGetTime();
		if (dNextTime < dCurTime)
			dNextTime = dCurTime + dInterval;
		
		Sleep((DWORD)(dNextTime - dCurTime));

		if (lpThreadInfo->bExit)
			break;
	}
}

まず、timeGetTimeで現在の時間を取得します。 そして、この値にdIntervalを足せば、描画を開始すべき次の時間が分かります。 描画処理の流れについては、AVIStreamReadでフレームを取得し、 それをDrawDibDrawでデバイスコンテキストに描画するだけです。 AVIStreamReadの第2引数は取得するフレームの番号であり、 iを指定することで順にインクリメントされていきます。 また、1つのフレームを取得するため、第3引数には1を指定します。 ZeroMemoryでビットイメージをクリアしているのは、 若干生じるフレームの乱れを消去するためです。 描画処理が終了したら、timeGetTimeで現在の時間を取得します。 そして、次の時間から現在の時間を引いた値をSleepに指定します。 if文の処理は、現在の時間が次の時間を超えてしまった場合の対策です。 これは、何らかの原因で描画処理があまりにも時間がかかった場合に生じます。

画像が何も表示されない場合は、AVIStreamReadが失敗している可能性が考えられます。 適切にバッファとサイズを指定して失敗する場合はAVIERR_FILEREADが返ることがありますが、 原因はよく分かっていません。 AVIStreamReadの失敗は、オーディオストリームに対しても発生することがあります。

再生スレッドの調整

実際に今回のプログラムを実行すると分かることですが、 ループ再生時ではない初回再生時において、 ストリームの先頭のフレームが確認できない場合があります。 これは、スレッドの作成がWinMainにおけるShowWindowの前に実行されるからであり、 先頭のフレームの描画された時点ではまだウインドウが表示されていないことが原因です。 これを防ぐためには、ShowWindowの後にCreateThreadを呼び出せばよいのですが、 初期化コードを一個所で管理するためにも、できればWM_CREATEで呼び出したいものです。 そこで、次のような方法を利用します。

hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, &threadInfo, CREATE_SUSPENDED, &dwThreadId);
SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST);

SetWindowLong(hwnd, GWL_USERDATA, (DWORD)hThread);

WM_CREATEにおけるCreateThreadの呼び出し時にCREATE_SUSPENDEDを指定するようにします。 これにより、スレッドはサスペンド状態となり、 ResumeThreadを呼び出さない限り処理を開始しないことになります。 よって、WinMainのShowWindowの後にResumeThreadを呼び出す方法が成立します。 ResumeThreadを呼び出すにはスレッドのハンドルが必要ですから、 SetWindowLongでこれをウインドウに関連付けています。 そして、WinMainではGetWindowLongでこれを取得します。

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

ResumeThread((HANDLE)GetWindowLong(hwnd, GWL_USERDATA));

ResumeThreadはShowWindowの後に呼び出しても問題ありませんが、 UpdateWindowによるクライアント領域の更新が終了してからのほうが好ましいといえます。



戻る