動画ファイルからビットマップを取得・IMediaDet版
動画ファイルのビデオ映像をビットマップとして取得したいと思いました。
参考サイトを頼りに割りとすんなりいけてしまいましたが、色々試していくうちにデコーダとスプリッタの組み合わせによってうまくいったりいかなかったりすることが分かりました。
参考サイト
DirectShowと戦うスレ
http://logsoku.com/thread/pc2.2ch.net/tech/1026666092/
469からIBasicVideoを使ったDelphiのソースコードがあります。
取得したバッファからビットマップを作成するコードがありがたい。
IMediaDet インターフェイス
http://msdn.microsoft.com/ja-jp/library/cc356940.aspx
IMediaDetインターフェースの説明。
IMediaDetインターフェース
IMediaDetインターフェースを利用してビットマップを取得する方法はIBasicVideoやSampleGrabberを利用する方法と違いフィルタグラフやレンダラーなどを用意することなくIMediaDetを作成するだけでいけます。
割とお手軽です。
ただ英語版のIMediaDetの説明を読むと将来なくなる可能性もあるようで非推奨なようです。
IMediaDetを利用する場合、ビットマップを取得するよりもファイルへ保存するだけの方が簡単です。
ビットマップを取得するためには面倒なビットマップファイルヘッダーを作る必要があるのですが、ファイルへの書き込みを行ってくれるWriteBitmapBitsメソッドではそのような面倒な手間はいりません。
欲しい映像の頭からの時間、ビデオの幅と高さ、そして保存するファイル名を与えて呼べばもうそれだけでOKです。
欲しい映像の時間の単位は秒です。
タイマーのようなミリ秒ではありません。
下図では60.5秒(1分と0.5秒。1分半ではありません)に設定しています。
procedure TForm1.Button1Click(Sender: TObject);
var
l_MediaDet : IMediaDet;
l_MediaType : TAMMediaType;
l_VideoInfo : TVideoInfoHeader;
li_Count : Integer;
i : Integer;
begin
if (OpenDialog1.Execute) then begin
// 1
CoCreateInstance(
CLSID_MediaDet,
nil,
CLSCTX_INPROC_SERVER,
IMediaDet,
l_MediaDet
);
// 2
if (Succeeded(l_MediaDet.put_Filename(OpenDialog1.FileName))) then begin
// 3
l_MediaDet.get_OutputStreams(li_Count);
for i := 0 to li_Count-1 do begin
// 4
//ストリーム指定
l_MediaDet.put_CurrentStream(i);
//メディアタイプ取得
l_MediaDet.get_StreamMediaType(l_MediaType);
if (IsEqualGUID(l_MediaType.majortype, MEDIATYPE_Video)) then begin
//ビデオ
if (IsEqualGUID(l_MediaType.formattype, FORMAT_VideoInfo)) then begin
// 5
l_VideoInfo := PVideoInfoHeader(l_MediaType.pbFormat)^;
// 6
l_MediaDet.WriteBitmapBits(
60.5,
//欲しい映像の頭からの時間(秒)
l_VideoInfo.bmiHeader.biWidth,
//ビデオの幅
l_VideoInfo.bmiHeader.biHeight, //ビデオの高さ
ChangeFileExt(Application.ExeName,
'.bmp') //保存するファイル名
);
Break;
end;
end;
end;
end;
end;
end;
uses節にActiveXとDirectShow9を付け足す必要があります。
DirectShow9ユニットはDSPackの中に入っています。
- IMediaDetはCoCreateInstanceで作成します。
- put_Filenameで動画ファイルを指定します。
IMediaDetのputFileNameメソッドの引数はWideStringです。
なのでD6のファイルダイアログのFileNameプロパティをそのまま渡してしまっても大丈夫です。
IGraphBuilderのRednerFileなどとはちょっと違っています。
- get_OutputStreamsで動画ファイルにある映像と音声のストリームの数を取得します。
通常の動画ファイルなら、映像1音声1の計2になります。
動画ファイルのストリームの順番は決まっていないようなので映像ストリームかどうかはループを使って総当りで調べます。
- put_CurrentStreamに0から始まるストリームの番号をセットしてget_StreamMediaTypeでストリームのメディアタイプを取得します。
取得したメディアタイプのメジャータイプがMEDIATYPE_Videoならそのストリームが映像ストリームであると判断できます。
- ビデオ画像の幅と高さを取得するためにl_MediaTypeのpbFormatをVIDEOINFOHEADERにキャストします。
ビデオの幅と高さが分かっているならこの処理は必要ありません。
念のためメディアタイプのフォーマットタイプがFORMAT_VideoInfoであることを確認してからにしてあります。
- WriteBitmapBitsメソッドを呼んで終わりです。
ビットマップを取得
次にビットマップを取得するサンプルです。
ファイルへ保存の場合と違いビットマップのファイルヘッダーを自分で計算しないといけない手間がかかります。
この辺りはお決まりのパターンのコード片のコピペでいけてしまうようなので難しくはないのですが煩わしくはあります。
ビットマップの取得を関数にしたものです。
ABitmapはこの関数を呼ぶ側で作成・破棄しなければなりません。
function gfnbBmpFromMediaDet(ABitmap : TBitmap; sFileName : WideString; fTime : Double = 0; iWidth : Integer = -1; iHeight : Integer = -1) : Boolean;
{
http://logsoku.com/thread/pc2.2ch.net/tech/1026666092/
http://msdn.microsoft.com/ja-jp/library/cc356940.aspx
http://msdn.microsoft.com/ja-jp/library/cc356932.aspx
http://msdn.microsoft.com/ja-jp/library/cc356944.aspx
}
var
l_IMediaDet : IMediaDet;
l_MediaType : TAMMediaType;
l_VideoInfo : TVideoInfoHeader;
i : Longint;
li_Count : Longint;
lp_Buff : PByte;
li_BuffSize : Longint;
l_Stream : TMemoryStream;
l_BmpHeader : TBitmapFileHeader;
begin
Result := False;
// 1
CoCreateInstance(
CLSID_MediaDet,
nil,
CLSCTX_INPROC_SERVER,
IMediaDet,
l_IMediaDet
);
try
// 2
if not(Succeeded(l_IMediaDet.put_Filename(sFileName))) then begin
Abort;
end;
// 3
l_IMediaDet.get_OutputStreams(li_Count);
for i := 0 to li_Count-1 do begin
// 4
//ストリーム指定
l_IMediaDet.put_CurrentStream(i);
l_IMediaDet.get_StreamMediaType(l_MediaType);
if (IsEqualGUID(l_MediaType.majortype, MEDIATYPE_Video)) then begin
//ビデオ
if (IsEqualGUID(l_MediaType.formattype, FORMAT_VideoInfo)) then begin
// 5
//VIDEOINFOHEADER
//ビデオの大きさを取得する必要がなければいらない
l_VideoInfo := PVideoInfoHeader(l_MediaType.pbFormat)^;
if (iWidth < 0) then begin
iWidth := l_VideoInfo.bmiHeader.biWidth;
end;
if (iHeight < 0) then begin
iHeight := l_VideoInfo.bmiHeader.biHeight;
end;
try
// 6
if not(Succeeded(l_IMediaDet.GetBitmapBits(fTime, @li_BuffSize, nil, iWidth, iHeight))) then begin
Abort;
end;
// 7
GetMem(lp_Buff, li_BuffSize);
try
// 8
//BitmapInfoHeader+ビットマップ本体がバッファにコピーされる。
ifnot(Succeeded(l_IMediaDet.GetBitmapBits(fTime, @li_BuffSize, lp_Buff, iWidth,
iHeight))) then begin
Abort;
end;
// 9
//BitmapFileHeader作成
FillChar(l_BmpHeader, SizeOf(l_BmpHeader), 0);
l_BmpHeader.bfType := $4d42;
l_BmpHeader.bfSize := SizeOf(l_BmpHeader) + li_BuffSize;
//IMediaDetで得られる画像はRGB24と決まっているのでパレットはない。
l_BmpHeader.bfOffBits := SizeOf(l_BmpHeader) + PBitmapInfoHeader(lp_Buff).biSize;
// 10
l_Stream := TMemoryStream.Create;
try
//ストリームにBitmapFileHeaderを書き込み。
l_Stream.Write(l_BmpHeader,
Sizeof(l_BmpHeader));
//BitmapInfoHeaderとビットマップ本体を書き込み。
l_Stream.Write(lp_Buff^,
li_BuffSize);
l_Stream.Position := 0;
ABitmap.LoadFromStream(l_Stream);
Result := True;
finally
l_Stream.Free;
end;
finally
FreeMem(lp_Buff);
end;
except
//キャプチャ失敗。
end;
end;
Break;
end;
end;
finally
l_IMediaDet := nil;
end;
end;
途中まではファイルへ保存と同じです。
WriteBitmapBitsの代わりにGetBitmapBitsを二回呼びます。
- 一回目では第三引数にnilを渡して呼んでいます。
こうすることで第二引数に必要なバッファの大きさ(バイト単位)がセットされます。
- GetMemでバッファにメモリを割り当てます。
- 第三引数にバッファを指定して二回目の呼び出しを行います。
成功ならこのバッファにはBitmapInfoHeaderとビットマップの本体がコピーされています。
- TBitmapに読み込ませるためにビットマップのファイルヘッダーを作成します。
GetBitmapBitsで取得できるビットマップは24ビットRGBであるのでパレットはありません。
- あとはStreamにデータを書き込み、更にBitmapに読み込ませて終わりです。
問題点
PCの構成にもよるのかも知れませんが、私の環境(Atomなネットブック)だとMP4スプリッタにHaaliメディアスプリッタを使うと一部のMP4ファイルがfTimeの指定にかかわらず最初のフレームしかキャプチャできません。
他のスプリッタに変えることで正常にキャプチャできるようになりました。