連載2 画像転送システム サンプルプログラム
ダウンロード
ライセンスはフリーウエアとなっています。ソースファイルは変更の有無、コンテンツの有料/無料を問わず、ご自身の責任の元自由にご利用頂けます。
コーデックを使ったエンコード(画像の圧縮)及び、デコード(画像の復元)の参考になれば幸いです。
【使い方】
プログラムはサーバーとクライアントに分かれています。
設定は付属のiniファイルをメモ帳で編集して下さい。
サーバー側はポート番号の設定のみです。
クライアント側はポート番号とサーバーのアドレスを設定してください。
設定が終わったらサーバー側を先に起動します。次にクライアントを起動すると勝手に接続されサーバー側のデスクトップ画像がどんどん転送されてきて表示します。
なお、AMV3ビデオコーデックがインストールされていないと利用できません。他のコーデックを利用する場合はプログラム内のFourCCを書き換えて下さい。
【コンパイル】
普通にコンパイルできると思いますが、サーバー側のみマルチスレッドライブラリを利用しています。その辺が原因でコンパイルできない場合は31行目にある#define USE_THREADをコメントアウトして下さい。コメントアウトするとマルチスレッド系のAPIは使いません。
連載2 画像転送システム その11
固有ステータスと言うのは各コーデックにより内容が異なる為一概に言えませんが多くの場合はコーデックの設定内容を含んでいます。例えばAMVコーデックで言えばハーフサイズのON/OFFやマルチスレッドの設定など設定画面の内容が含まれています。これらのステータスをコーデックから取得したり、逆にアプリケーション側からコーデックへステータスを設定したりする事ができます。
コーデックからステータスを取得するにはICM_GETSTATEメッセージを使います。
第1パラメータにステータスを格納するバッファのアドレス、第2パラメータにバッファのサイズを設定しますが、第1パラメータをNULLにするとステータスを格納するのに必要なバッファサイズを返すので最初はバッファサイズの取得、次にバッファの確保、最後にステータスの取得と三段階で処理しましょう。
次に、ステータスの設定です。利用するのはICM_SETSTATEメッセージです。
こちらは第1パラメータにステータスが格納されたバッファのアドレス、第2パラメータにバッファサイズを指定するだけです。
BOOL GetSetState( void ) { HIC hic = ICOpen( ICTYPE_VIDEO, dwFcc, ICMODE_COMPRESS ); if (hic == NULL)return FALSE; // 1.バッファサイズの取得 DWORD dwSize = ICSendMessage( hic, ICM_GETSTATE, (DWORD)NULL, (DWORD)NULL ); if (dwSize) { // 2.バッファの確保 void *lpState = malloc( dwSize ); if (lpState == NULL) { ICClose( hic ); return FALSE; } // 3.ステータスの取得 ICSendMessage( hic, ICM_GETSTATE, (DWORD)lpState, (DWORD)dwSize ); // ステータスを設定 ICSendMessage( hic, ICM_SETSTATE, (DWORD)lpState, (DWORD)dwSize ); free( lpState ); } ICClose( hic ); return TRUE; } |
上の例では取得したステータスをそのまま設定するので意味ないですが、アマレココの場合には設定Aから設定Dまで別々にコーデックのステータスを取得し管理する事で同じコーデックに対しても異なる設定で録画できるようになっています。
また、ファンタジーリモートではクライアント側でコーデックの管理を一括しています。
その為、接続時にクライアント側のコーデックのステータスを取得しそれをサーバーへ送信、サーバー側は受信したステータスをコーデックに設定という流れになっています。
このようにステータスを上手く管理する事でアプリケーションの利便性が向上しますので是非とも活用したい機能です。但し全てのコーデックがステータスの取得、設定に対応しているわけではないのでその点は気を付けましょう。
連載2 画像転送システム その10
利用するAPIはICConfigure()となりますがこの関数は使わずにICSendMessage()を使って直接コーデックへメッセージを送る事で設定画面の表示を行ってみようと思います。
ICSendMessage()ではメッセージ(ICM~)と2つのパラメータを使ってコーデックを制御します。
今回利用するメッセージはICM_CONFIGUREで第1パラメータのみ利用します。第1パラメータには設定画面の親となるウインドウハンドル(hWnd)を指定しますが、良く分からないときはNULLを指定すれば良いでしょう。
また、この第1パラメータに-1を指定するとコーデックに設定画面を表示出来るかどうか問い合わせる事が出来ます。-1を指定して戻り値がICERR_OKであればコーデックは設定画面を表示できます。
ICERR_OK以外の場合はコーデックは設定画面を持たないのでそこで処理を終了します。予め調べておいてコーデックの設定ボタンを無効にするなどしておくと親切ですね。
BOOL ShowConfig( HWND hWnd ) { DWORD dwFcc = FCC('AMV3'); HIC hic = ICOpen( ICTYPE_VIDEO, dwFcc, ICMODE_COMPRESS ); if (hic == 0) return FALSE; // 設定画面があるか問い合わせる DWORD query = ICSendMessage( hic, ICM_CONFIGURE, (DWORD)-1, 0 ); if (query != ICERR_OK) { // 設定画面なし ICClose( hic ); return TRUE; } // 設定画面を表示 DWORD ret = ICSendMessage( hic, ICM_CONFIGURE, (DWORD)hWnd, 0 ); ICClose( hic ); if (ret != ICERR_OK) { // エラー return FALSE; } return TRUE; } |
なお、ICM_CONFIGUREの部分をICM_ABOUTと置き換える事でコーデックのAbout画面を表示する事が出来ます。
また、今回の様にICOpen()でコーデックのハンドルを取得し、そのハンドルへICSendMessage()でメッセージを送る事でコーデックに関する殆どの処理を行う事が出来ます。
今まで紹介してきたIC~と言う関数の殆どはこのメッセージ処理に置き換えるマクロとなっているので、最終的にはこのメッセージ処理を理解することでコーデックを自由自在に扱えるようになります。
連載2 画像転送システム その9
初期状態で特定のコーデックを選択する場合はCOMPVARS構造体のfccHandlerメンバーに選択したいコーデックのFourCCをセットしてICCompressorChoose()を実行します。
さらに、特定のフォーマットに対応したコーデックのみを表示する為にBITMAPINFOHEADER構造体を使い、主にbiCompressionとbiBitCountで利用可能なコーデックを絞ります。但し、画像サイズは大きくすると表示されなくなるコーデックがありますので、実際に使う画像サイズを正直に設定するのではなく小さ目の適当なサイズにしておきましょう。
ICCompressorChoose()はコーデックを選択してOKボタンが押されると戻り値としてTRUEを返します。
エラーまたはキャンセルボタンが押されるとFALSEが返ります。TRUEが返った場合はCOMPVARS構造体にコーデックの情報が詰まっています。主に必要なのはFourCCを示すfccHandlerだけなので自前の変数へコピーしておきましょう。
ICCompressorChoose()はコーデックを選択した後すぐに圧縮処理が開始できるように色々な準備(メモリの確保など)をしてくれますが、結局それらは使わないのでICCompressorChoose()のあとはすぐにICCompressorFree()でリソース(メモリー)を開放しておきましょう。
BOOL CodecChoose( DWORD *lpdwFcc ) { COMPVARS cv; ZeroMemory( &cv, sizeof(cv) ); cv.cbSize = sizeof(COMPVARS); cv.dwFlags = ICMF_COMPVARS_VALID; cv.fccType = ICTYPE_VIDEO; cv.fccHandler = *lpdwFcc; BITMAPINFOHEADER bi; ZeroMemory( &bi, sizeof( bi)); bi.biSize = sizeof(BITMAPINFOHEADER); bi.biBitCount = 32; //RGB32なら32、RGB24なら24を設定します bi.biPlanes = 1; bi.biCompression = BI_RGB; bi.biWidth = 320; // 画像サイズは適当に小さめにしておきます bi.biHeight = 240; bi.biSizeImage = bi.biWidth * bi.biHeight * bi.biBitCount / 8; HWND hWnd = NULL; // 親ウインドウのハンドル if (!ICCompressorChoose( hWnd, 0 | ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME, &bi, NULL, &cv, NULL )) { return FALSE; } *lpdwFcc = cv.fccHandler; ICCompressorFree( &cv ); return TRUE; } void main( void ) { // RGB32に対応しているコーデックの一覧を表示する、初期状態でAMV3コーデックを選択 DWORD dwFcc = FCC('AMV3'); if (CodecChoose( &dwFcc ) == TRUE) { // dwFccに選択されたコーデックのFourCCが格納されています。 // このFourCCを使ってAMV3以外の様々なコーデックで画像転送システムを実行できます。 }else{ // コーデックの選択はエラーまたはキャンセルされました。 } } |
次のようにBITMAPINFOHEADERを使わずにNULLを指定するとインストールされている全てのコーデックを表示する事ができます。
ICCompressorChoose( hWnd, 0 | ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME, NULL, NULL, &cv, NULL ) |
この場合、実際には使えないコーデックや、圧縮処理を持たない(復元専用の)コーデックなども表示されてしまうので不便ですね。
続く。
連載2 画像転送システム その8
サーバー側と殆ど同じですが、作成するDIBセクションの画像サイズはキャプチャしたときの画像サイズではなくコーデックの入力フォーマットを参照しています。コーデックのところでも書きましたが、これはコーデックのリサイズ処理に対応するためです。
後は、実際にウインドウへ画像を表示する部分は画像サイズが合わない場合が殆どなのでStretchBlt()を使って拡大または縮小表示しています。
// GDI処理で使う変数 HDC ghDC = NULL; // デスクトップのデバイスコンテキスト HDC ghOffDC = NULL; // オフスクリーンのデバイスコンテキスト(ここにデスクトップ画像を取り込む) LPVOID glpOff = NULL; // オフスクリーンのアドレス HBITMAP ghOffBmp = NULL; // オフスクリーンのビットマップハンドル HBITMAP ghOffBmpOld = NULL; // ビットマップハンドルの控え(不要?) // GDI処理の初期化 BOOL GDIInit( void ) { // デスクトップのデバイスコンテキスト取得 ghDC = GetDC( NULL ); // DIBSection作成 // ・圧縮された画像データはこのDIB(glpOff)に復元します。 // ・ココではRGB32のフォーマットで復元するためのDIBを作成します。 // ・DIBの画像サイズはコーデックの画像サイズに合わせます。 // ※コーデックの方でリサイズ(ハーフサイズ処理など)される場合がある為 BITMAPINFO bi; ZeroMemory( &bi, sizeof(bi) ); bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bi.bmiHeader.biBitCount = 32; // RGB32に復元するので32を指定する bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biWidth = glpbiIn->bmiHeader.biWidth; // コーデックの画像サイズを使う bi.bmiHeader.biHeight = glpbiIn->bmiHeader.biHeight; bi.bmiHeader.biSizeImage = glpbiIn->bmiHeader.biWidth*glpbiIn->bmiHeader.biHeight*4; // RGB32で計算 bi.bmiHeader.biCompression = BI_RGB; // フォーマットはRGB ghOffBmp = CreateDIBSection( NULL, &bi, DIB_RGB_COLORS, (void **)(&glpOff), NULL, 0); // 復元された画像を簡単に扱う為のデバイスコンテキストを作成 // ※復元された画像はデバイスコンテキスト(ghOffDC)を使って自在に加工できます。 ghOffDC = CreateCompatibleDC(ghDC); ghOffBmpOld = (HBITMAP)SelectObject( ghOffDC, ghOffBmp ); return TRUE; } // GDI処理 BOOL GDIMain( void ) { // ウインドウのデバイスコンテキストを取得 HDC hdc = GetDC( ghApp ); // ブリットモードを設定 SetStretchBltMode( hdc, COLORONCOLOR ); // ウインドウの大きさを取得 RECT rect; GetClientRect( ghApp, &rect ); // ウインドウいっぱいに復元した画像を表示 StretchBlt( hdc, 0, 0, rect.right, rect.bottom, ghOffDC, 0, 0, glpbiIn->bmiHeader.biWidth, glpbiIn->bmiHeader.biHeight, SRCCOPY ); // 後始末 ReleaseDC( ghApp, hdc ); return TRUE; } |
メイン処理で
// ブリットモードを設定 SetStretchBltMode( hdc, COLORONCOLOR ); |
としていますが、これを
SetStretchBltMode( hdc, HALFTONE ); |
とすると画質がよくなります。ただしメチャクチャ処理に時間がかかるので通常は使えません。画像サイズが小さい場合などに使いましょう。今は廃止になりましたが、アマレココのリサイズにあった「API早い」がCOLORONCOLORで、「API綺麗」がHALFTONEでした。
サンプルプログラムでは処理を簡略化するために一番最後に画像サイズを変更していますが、ファンタジーリモートでは画像サイズを縮小する場合はキャプチャー直後にサーバー側で行っています。
キャプチャー直後に縮小する事でコーデックの圧縮・復元処理やネットワークによる通信量を減らす事が出来て全体的なパフォーマンスが向上します。
逆に画像を拡大しなければならないときはサンプルと同じように表示する時にストレッチしています。
次回はサンプルには登場しなかったコーデックの選択方法や設定値の管理などを紹介したいと思います。
連載2 画像転送システム その7
利用するAPIはICDecompress()一つなので簡単です。注意すべきは入力フォーマットのメンバーに圧縮された画像データのバイト数を事前にセットしておく事だけです。
後は、入力フォーマットと、入力データが格納されたバッファ、出力フォーマットと復元後の画像を納めるバッファを指定してICDecompress()を呼び出します。
尚、サンプルプログラムではの第二引数を0としていますが、ここにはキーフレームである事を示すフラグ(キーフレームでない事を示すフラグ)や、NULLフレームである事を示すフラグをセットしてコーデックにコレラの情報を伝えます。
今回はNULLフレームは出てこない事とAMVコーデックはキーフレームの管理をコーデック側で行うので0となっています。
// コーデックを使って圧縮された画像データからDIBを復元する BOOL CodecMain( void ) { // 圧縮された画像データのバイト数を圧縮フォーマットのメンバーにセットします。 glpbiIn->bmiHeader.biSizeImage = dwDataSize; // デコンプレッサーでDIBを復元 DWORD ret = ICDecompress( ghIC, // デコンプレッサーのハンドル 0, // キーフレームやNULLフレームを示すフラグをセットする &glpbiIn->bmiHeader, // 圧縮された画像データのフォーマット(データサイズをbiSizeImageにセットする事) (void *)glpCompressBuff, // 圧縮された画像データへのポインタ &glpbiOut->bmiHeader, // 復元するDIBのフォーマット (void *)glpOff // DIBのデータアドレス ); if (ret != ICERR_OK ) return FALSE; return TRUE; } |
次回はクライアント側のGDI処理です。
連載2 画像転送システム その6
先ずはネットワーク処理からですが特別な処理は必要ないので省略して、コーデックの処理から始めます。
構造体やグローバル変数はサーバー側と同じモノを使い、最初にサーバー側で使ったコーデックの情報をネットワークから受信します。
受信は2回に分けて行い、一回目はコーデックのFourCCとサーバー側glpbiOutのバイト数を取得。次にバイト数分のバッファをglpbiInに確保してから二回目の通信でサーバー側のglpbiOutの内容をglpbiInへ受信します。
コレによりサーバー側のglpbiOutとクライアント側のglpbiInが同じ内容となりクライアントの入力フォーマットとします。
次に出力フォーマットglpbiOutです。基本的にはサーバー側のglpbiInと同じ設定になりますが完全に一致させる必要は無いのでネットワークは使わずに独自に設定しましょう。このとき画像サイズの設定だけ注意が必要です。画像サイズはコーデックにより変更される場合がありまして、例えばAMV3ビデオコーデックのハーフサイズオプションにより画像サイズが変更されます。こう言ったケースに対応するためにglpbiOutの画像サイズはキャプチャー時の画像サイズではなくglpbiInに設定されている画像サイズを使います。
入力/出力フォーマットが用意できたら、コーデックのデコンプレッサーのハンドルを取得します。このときコーデックのFourCCが必要になりますがこのFourCCはサーバー側から受信したFourCCを使います。
次にデコンプレッサーを初期化してデコンプレッサーの準備は完了です。
最後に圧縮されたデータを格納するためのバッファglpCompressBuffを確保して終わり。
#include <vfw.h> #include <aviriff.h> #pragma comment(lib,"vfw32") // コーデック情報の送信につけるヘッダー struct NETWORK_CODEC_HEADER { DWORD dwbiSize; // コーデック情報のバイト数 DWORD dwFcc; // コーデックのFourCC DWORD dwReserve[2]; // 未使用 }; // コーデック処理で使う変数 HIC ghIC = NULL; // コーデックのハンドル LPVOID glpCompressBuff = NULL; // 圧縮された画像を格納するバッファへのポインタ BITMAPINFO *glpbiIn = NULL; // 圧縮された画像フォーマット(実態はコーデックにより異なる) BITMAPINFO *glpbiOut = NULL; // 復元された画像フォーマット(ビットマップインフォで確定) // コーデック処理の初期化 BOOL CodecInit(SOCKET sock) { DWORD size; // コーデック情報のヘッダーを取得(コーデック情報のバイト数とFourCC) NETWORK_CODEC_HEADER network_codec_header; size = recv_all(sock, (char*)&network_codec_header, sizeof(network_codec_header), 0 ); if (size == SOCKET_ERROR) return FALSE; // コーデック情報(可変長)を格納する為のバッファを確保 glpbiIn = (BITMAPINFO*)malloc( network_codec_header.dwbiSize ); if (glpbiIn == NULL) return FALSE; // コーデック情報を取得(最終的な画像サイズや、どの様に圧縮したか等が記録されています) size = recv_all(sock, (char*)glpbiIn, network_codec_header.dwbiSize, 0 ); if (size == SOCKET_ERROR) return FALSE; // 復元に使うフォーマットを作成 glpbiOut = (BITMAPINFO*)malloc( sizeof(BITMAPINFO) ); if (glpbiOut == NULL) return FALSE; ZeroMemory(glpbiOut, sizeof(BITMAPINFO) ); glpbiOut->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); glpbiOut->bmiHeader.biBitCount = 32; glpbiOut->bmiHeader.biPlanes = 1; glpbiOut->bmiHeader.biWidth = glpbiIn->bmiHeader.biWidth; glpbiOut->bmiHeader.biHeight = glpbiIn->bmiHeader.biHeight; glpbiOut->bmiHeader.biSizeImage = glpbiIn->bmiHeader.biWidth* glpbiIn->bmiHeader.biHeight*4; glpbiOut->bmiHeader.biCompression = BI_RGB; // コーデックのデコンプレッサーのハンドルを取得 ghIC = ICOpen( ICTYPE_VIDEO, network_codec_header.dwFcc, // glpbiIn->biCompressionを使ってはダメ。 // 必ず、圧縮の時に使ったFourCCを使いましょう。 ICMODE_DECOMPRESS // ICMODE_DECOMPRESSとなります。圧縮時とは異なるので注意。 ); if (ghIC == NULL) return FALSE; // デコンプレッサーの初期化 if (ICERR_OK != ICDecompressBegin( ghIC, glpbiIn, // 圧縮された画像データのフォーマット(サーバー側のICCompressGetFormatで取得したlpbiOutをそのまま使う) glpbiOut // 復元する画像データのフォーマット(クライアント側で用意する) )) return FALSE; // 圧縮された画像データを格納するバッファを確保する。 // 確保するバイト数はサーバー側の初期化処理でICCompressGetSize等を使って取得する // 未圧縮よりもサイズが大きくなるコーデックも有りです。必ずコーデックから // 最大データサイズを取得しましょう。 glpCompressBuff = malloc(glpbiIn->bmiHeader.biSizeImage ); if (glpCompressBuff == NULL) return FALSE; return TRUE; } |
サーバー側のコンプレッサーの準備に比べると、クライアント側のデコンプレッサーの準備はその殆どをサーバ側から受信して済ませるので楽ですね。
レアなケースですが画像サイズが変更される可能性があることだけ注意しておけば大丈夫だと思います。次回は実際にデコンプレッサーを使って圧縮された画像の復元処理を紹介します。
連載2 画像転送システム その5
送信処理で重要なのが一度に送信するバイト数で、大きすぎても小さすぎてもパフォーマンスは低下します(なんだかハードディスクドライブのアクセスのコツと似ていますね)。
例えば1MBのデータを送信するのに
send( socket, (char *)lpbuff, 1024*1024, 0 ); |
とまとめて送信するよりも
for( DWORD i=0; i<1024*1024; i+=4096 ) send( socket, (char *)((DWORD)lpbuff + i), 4096, 0 ); |
の様に4096バイトずつこまめに転送する方が早く転送できるようです。私の場合には6kByteくらいで分割すると最も良い結果がでました。理由は不明ですがジャンボフレームなどが影響しているのかな?
と言う事で、次のような送信関数を使っています。
// 最大送信サイズ(これより大きいデータは、このサイズに分割して送信します。) #define PAKET_DATA_SIZE (2048*3) // 送信するデータが大きすぎるとパフォーマンスが低下するので // 一定の大きさ(PAKET_DATA_SIZE)に分割して送信します。 int send_all( SOCKET s, char *buf, int len, int flags ) { char *p = buf; int left = len; while(left) { int send_size = PAKET_DATA_SIZE; if (left < PAKET_DATA_SIZE) send_size = left; int size = send( s, p, send_size, 0 ); if (size == SOCKET_ERROR) return SOCKET_ERROR; left -= size; p += size; } return len; } |
連載2 画像転送システム その4
圧縮処理はICCompress()を一つ使うだけなので簡単。また、引数が多いですがAMVビデオコーデックをはじめ最近のコーデックの場合は殆どの引数を無視しますので0にしておきましょう(もちろんコーデックによっては個々の引数が意味を持つ場合もあるのでそういったコーデックを利用する場合はしっかり調べましょう)。
結局、重要な引数は入力フォーマットと、入力バッファのアドレス、出力フォーマットと出力バッファのアドレスと非常にシンプルです。
各フォーマットは準備編で用意したものを使いますが、ICCompress()の引数の型がなぜかLPBITMAPINFOHEADERとなっているので念のためヘッダーの方を指定しています。入力バッファのアドレスはDIBセクションのアドレスを指定します。出力バッファのアドレスは準備編で用意したものを使いましょう。
// コーデックを使ってDIBの画像を圧縮する BOOL CodecMain( void ) { DWORD dwFlagsIn = 0; DWORD dwFlagsOut = 0; DWORD dwckid = 0; long lFrameNum = 0; DWORD dwFrameSize = 0; // 目標ビットレート(指定なし) DWORD dwQuality = 0; DWORD ret = ICCompress( ghIC, // コーデックのハンドル dwFlagsIn, // 圧縮動作の制御フラグ(キーフレームなど) // AMVコーデックでは使いません。 &glpbiOut->bmiHeader, // 圧縮後のフォーマット(ビットマップインフォヘッダー) glpCompressBuff, // 圧縮後の画像データを格納するバッファ &glpbiIn->bmiHeader, // 圧縮元のフォーマット(ビットマップインフォヘッダー) glpOff, // 圧縮元の画像データを格納したバッファ(DIB) &dwckid, // 予約されています。使わないで下さい。 &dwFlagsOut, // 圧縮後のステータスが格納される(キーフレームの検出に利用) lFrameNum, // AMVコーデックでは使いません。 dwFrameSize, // 目標ビットレート、AMVコーデックでは使いません。 dwQuality, // 画質、AMVコーデックでは使いません。 NULL, // 前フレームのフォーマット、AMVコーデックでは使いません。 NULL // 前フレームを格納するバッファ、AMVコーデックでは使いません。 ); if (ret != ICERR_OK) return FALSE; DWORD dwDataSize = glpbiOut->bmiHeader.biSizeImage; // 圧縮後のバイト数はメンバー変数に格納されている DWORD dwKeyframe = FALSE; if (dwFlagsOut & AVIIF_KEYFRAME) dwKeyframe = TRUE; // キーフレームとして圧縮された return TRUE; } |
ICCompress()が正常に終わると戻り値としてICERR_OKがセットされ、出力バッファに圧縮後のデータがセットされています。
圧縮後のデータが何バイトになるかは出力フォーマットのメンバー変数glpbiOut->bmiHeader.biSizeImageで確認できます。
また、処理したフレームがキーフレームになる場合はdwFlagsOutにAVIIF_KEYFRAMEがセットされます。他のフラグと一緒にセットされることもあるようなので、イコールで判定するのではなく論理積(and)を使って該当するビットが立っているかどうかで確認するようにしましょう。
以上で、コーデックを使った圧縮処理は終わりとなりますが、APIの中には他にもICSeqCompressFrame()というコーデックを使って動画を圧縮出来るものがあります。こちらの方が細かい部分をAPIの方で管理してもらえるので扱いが楽なのですが、出力バッファを指定できないと言う欠点がありまして、処理速度が求められる場合などには向きません。
アマレココの3.00まではICSeqCompressFrame()を使っていたのでコーデックにより圧縮されたデータはAPIが管理するバッファへ格納されてしまい、それをワザワザ自分で用意した出力バッファ(ファイルバッファ)へコピーしていました。このコピー処理がまるまる無駄となりコーデックを使った場合のオーバーヘッドとなっていましたが、3.01からICCompress()を使うようにしたので直接ファイルバッファへ書き出すことが出来(コピー処理が無くなった分)高速化となっています。
次回はサーバー編の最終回、ネットワークの送信処理を紹介します。
連載2 画像転送システム その3
コーデックを使うにはvfw.hとaviriff.hの2つのヘッダーファイルをインクルードすることと、vfw32.libのリンクが必要です。
コーデックの処理はIC~というAPIを駆使して行きますが、ややっこしいのは入力フォーマット(圧縮前のフォーマット)と出力フォーマット(圧縮後のフォーマット)の扱いでしょうか。これらは基本的にBITMAPINFOという構造体を使いますが、コーデックによってはこの構造体が拡張される場合があるので構造体のサイズは不定となります。なので、変数の定義としてはポインタにしておき、必要に応じてその都度バッファを確保するようにします。
先ずは、入力フォーマット(圧縮前のフォーマット=DIBセクションと同じ)を用意します。これはBITMAPINFOでいいので簡単です。また設定内容はDIBセクションと同じにして下さい。
次に出力フォーマットを用意しますが、これはコーデックから取得する必要があり少々面倒です。先にコーデックへアクセスする為のハンドルを取得しましょう。
ハンドルの取得はICOpen()の二番目の引数に使いたいコーデックのFourCCを指定することで得られます。今回はAMV3ビデオコーデックを使うので、この部分はFCC('AMV3')と記述します。FCC()はマクロでaviriff.hの中で定義されています。これを使うと文字列を簡単にFourCCコードに変換してくれます。
ハンドルの取得が出来たら次の3段階に分けて出力フォーマットを取得します。
(1)出力フォーマットのサイズを得る。
出力フォーマットは拡張されている可能性があるので、構造体のバイト数が幾つになるか問い合わせましょう。ICCompressGetFormat()の3番目の引数に0を指定すると戻り値として必要なバイト数が返されます。
(2)出力フォーマットを格納するバッファを確保する。
1で取得したバイト数分だけバッファを確保します。
(3)出力フォーマットを得る。
ICCompressGetFormat()の3番目の引数に2のバッファを指定することで出力フォーマットを得ることが出来ます。
入力と出力のフォーマットが用意できたらICCompressBegin()で圧縮処理の初期化、ICCompress()で圧縮、ICCompressEnd()で圧縮終了となります。
また、これとは別に圧縮後のデータを格納するのに最大何バイトのバッファが必要か調べます。多くの場合ICCompressGetFormat()で得た出力フォーマットのglpbiOut->bmiHeader.biSizeImageに格納されていますが、この部分が0のコーデックもありますので、その場合はICCompressGetSize()で取得しましょう。私の場合は両方の値を比較して大きい方を使うことにしています。バッファサイズが決まったらすかさずバッファを確保してこれで準備完了です。
なお、FourCCと出力フォーマットはクライアント側の復元処理(デコンプレス)の際に必要となりますので、クライアント側へ通知しておきます。
#include <vfw.h> #include <aviriff.h> #pragma comment(lib,"vfw32") // コーデック情報の送信につけるヘッダー struct NETWORK_CODEC_HEADER { DWORD dwbiSize; // コーデック情報のバイト数 DWORD dwFcc; // コーデックのFourCC DWORD dwReserve[2]; // 未使用 }; // コーデック処理で使う変数 HIC ghIC = NULL; // コーデックのハンドル LPVOID glpCompressBuff = NULL; // 圧縮された画像を格納するバッファへのポインタ BITMAPINFO *glpbiIn = NULL; // 圧縮前の画像フォーマット(ビットマップインフォ) BITMAPINFO *glpbiOut = NULL; // 圧縮後の画像フォーマット(実態はコーデックにより異なる) // コーデック処理の初期化 BOOL CodecInit( SOCKET sock ) { // 圧縮元のフォーマットを設定(DIBと同じ設定にする) glpbiIn = (BITMAPINFO*)malloc( sizeof(BITMAPINFO) ); if (glpbiIn == NULL) return FALSE; ZeroMemory(glpbiIn, sizeof(BITMAPINFO) ); glpbiIn->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); glpbiIn->bmiHeader.biBitCount = 32; glpbiIn->bmiHeader.biPlanes = 1; glpbiIn->bmiHeader.biWidth = gdwSizeW; glpbiIn->bmiHeader.biHeight = gdwSizeH; glpbiIn->bmiHeader.biSizeImage = gdwSizeW*gdwSizeH*4; // コーデックのハンドルを取得(FourCCでコーデックの選択ができます) ghIC = ICOpen( ICTYPE_VIDEO, FCC('AMV3'), ICMODE_COMPRESS ); if (ghIC == NULL) return FALSE; // 圧縮後のフォーマットをコーデックから取得する // (1)圧縮後のフォーマットを格納するバイト数を取得する DWORD size = ICCompressGetFormat( ghIC, // コーデックのハンドル glpbiIn, // 圧縮元フォーマット 0 // 圧縮後のフォーマット部を0にすると、バイト数を戻り値として返す ); if (size == 0) return FALSE; // (2)圧縮後のフォーマットを格納するバッファを確保 glpbiOut = (BITMAPINFO*)malloc( size ); if (glpbiOut == NULL) return FALSE; // (3)圧縮後のフォーマットを取得 DWORD ret = ICCompressGetFormat( ghIC, glpbiIn, glpbiOut // このバッファへ圧縮後のフォーマットが格納される ); if (ret != ICERR_OK) return FALSE; // コーデックの圧縮処理を初期化 ret = ICCompressBegin( ghIC, glpbiIn, glpbiOut ); if (ret != ICERR_OK) return FALSE; // 圧縮後のデータの最大バイト数を取得する DWORD dwSizeImage = ICCompressGetSize( ghIC, glpbiIn, glpbiOut ); if (glpbiOut->bmiHeader.biSizeImage < dwSizeImage) glpbiOut->bmiHeader.biSizeImage = dwSizeImage; // 圧縮後のデータを格納するバッファを確保 glpCompressBuff = malloc(glpbiOut->bmiHeader.biSizeImage ); if (glpCompressBuff == NULL) return FALSE; // コーデック情報(圧縮後のフォーマット)をクライアント側へ送信(復元に使います) NETWORK_CODEC_HEADER network_codec_header; ZeroMemory( &network_codec_header, sizeof(network_codec_header) ); network_codec_header.dwbiSize = glpbiOut->bmiHeader.biSize; // コーデック情報のバイト数 network_codec_header.dwFcc = FCC('AMV3'); // コーデックのFourCC size = send_all( sock, (char*)&network_codec_header, sizeof(network_codec_header), 0 ); if (size == SOCKET_ERROR) return FALSE; // コーデック情報を送信 size = send_all( sock, (char*) glpbiOut, glpbiOut->bmiHeader.biSize, 0 ); if (size == SOCKET_ERROR) return FALSE; return TRUE; } |
この連載の中で一番シンドイ部分ですが、出力フォーマットと圧縮後の最大バイト数の扱いだけ気をつければ何とかなると思います。逆に出力フォーマットをBITMAPINFO構造体に固定してしまい上手く行かないケースが多そうですね。次回は圧縮処理の本編を紹介します。