Paradigm Shift Design

ISHITOYA Kentaro's blog.

DirectShow.Net Libraryを使ってUSBカメラキャプチャしてiPadで閲覧可能なMP4を出力

USBカメラから撮った映像をiPadSafariで再生できるようにMP4にエンコードしたい。だけれども、AVIとかWMVとかで記録した後でmp4にエンコードするのは面倒くさい。


だからUSBカメラからそのままMP4に記録したい、というわけで頑張りました。久方ぶりに訳のわからないMicroSoftワールドに突入。ここんとこブックマークがDirectShowって出てたのは、これ作ってたからです。


COM嫌い!

はじめに

手順としては、

  1. GraphEditをインストール
  2. DirectShow Filterをいろいろ入れる
    1. DirectShow Filter Toolをインストールする
    2. 映像をh264にエンコードできるDirectShow Filterを探す
    3. 音声をAAC-LCにエンコードできるDirectShow Filterを探す
    4. mp4のMuxができるDirectShow Filterを探す
  3. GraphEdit上でGraphを作成してMP4エンコードをテストする
  4. iPadSafariで再生できるようにh264の設定を行う
  5. iPadSafariで再生テスト
  6. コード上でGraphを表現する

となります。


因みに最後のコードは、メモ書きなのでエラー処理とかしていません。使うときは適切にしてください。また、Filterも見つけられたもので構成してます。もっといいのがあれば教えてください。それから、WindowsSDKやDirectXSDKなんかのインストールが必要なのですが、その辺は限りなく端折ります。なので初期設定だのは他のサイトを見てください。あくまでもmp4にエンコードするための話です。


ライブラリは前に書いたエントリDirectShow.NETを使って音声キャプチャ - Paradigm Shift Designで使ったCode ProjectにあるDShowNETではなく、DirectShowNet libraryにあるDirectShow.NET Libraryを使います。前者はとりあえずキャプチャだけするという目的には向いているかもしれませんが、後者の方がラップしているインターフェースが多く、使い勝手がいいです。


以降、手順の説明をしていきます。

GraphEditをインストール

まず、GraphEditですが、このツールはDirectShowのFilterGraphを作成するためのプログラムです。キャプチャデバイスがあって、それを映像をh264に音声をAAC-LCにエンコードして、mp4にまとめて、ファイルに書き出す。という個々の処理を行うのがFilterで、それを繋いだのがGraphというもののようです。詳しくは

あたりを参考にしてください。


GraphEditそのものは何かのSDKに入っています。

DirectShow Filterをいろいろ入れる

手順にも書いたとおり、必要なものは

  1. h264 Encoder
  2. AAC-LC Encoder
  3. MP4 Muxer

です。


h264Encoderは、Download K-Lite Codec PackのMega*1をダウンロードしてインストール時に「Lots of staff」を選ぶか、x264 VFWにチェックを入れてインストールすれば「x264vfw - H.264/MPEG-4 AVC codec」というのが手に入ります。他にもRadScorpion’s blog » Blog Archive » MONOGRAM x264 Encoder 1.0.2.0とかありましたが、GraphEdit上でどうにもうまくFilterを繋げないので、これを選択しました。


AAC-LC Encoderは、RadScorpion’s blog » Blog Archive » MONOGRAM AAC Encoder v1.0.0.1を使いました。binaryをダウンロードしてきてregister.batを動かせば使えるようになります。AACのフィルタはいろいろ探しましたがあまり見つからず。


MP4 Muxerは、を使いました。ダウンロードしてきて解凍するとDLLが入っているので、DFTool.HTMなどを使ってフィルタを登録します。MP4 Muxerには他にもRadScorpion’s blog » Blog Archive » MONOGRAM MP4 Mux 0.9.3.0 Alphaなんかがありましたがやっぱりうまくつながらないので、これにしました。


因みに商用であれば、MPEG-4 Codec | LEADTOOLSThe 3ivx Filter Suite - DirectShow MPEG-4 Audio and Video Compressionというのがあって、3ivxなんかは試用期間を除去して使うとかごにょごにょというのがいろいろありましたが、$100なんだから買えよ的な。今回は、商用のものはできるだけ避けて、上記3つのフィルタを使ってGraphを作りました。

GraphEdit上でGraphを作成してMP4エンコードをテストする

これはもう、最初に紹介したGraphEditのページを見ればわかります。
一応、上記フィルタを使って構築したFilter Graphの図を貼っておきます。


f:id:kent013:20110210022937p:image


日本語が文字化けしているのは面倒くさかった、ということで。
基本的には

メニュー -> Graph -> Insert Filter(Ctrl+F)

でフィルタ一覧画面からフィルタを選択してInsert Filter、そして、PINを繋いで行く、という感じです。

  1. Video Capture Sourcesから映像キャプチャデバイスを選ぶ
  2. Video Compressorsからx264cfw - H.264/MPEG-4 AVC codecを選ぶ
  3. Audio Capture Sourcesから音声キャプチャデバイスを選ぶ
  4. DirectShow FiltersからMONOGRAM AAC Encoderを選ぶ
  5. DirectShow FiltersからGDCL Mpeg-4 Multiplexorを選ぶ
  6. DirectShow FiltersからFile Writeを選び適当なファイル名を入力する

そして、1の出力を2の入力に…という感じで繋いでいって、繋ぎ終わったら

メニュー -> Graph -> Play

を選択すれば、キャプチャが行われるはずです。

iPadSafariで再生できるようにh264の設定を行う

iPadSafariで再生できる動画の仕様はLoading…を参考にしてください。

Safari on the desktop supports any media the installed version of QuickTime can play. This includes media encoded using codecs QuickTime does not natively support, provided the codecs are installed on the user’s computer as QuickTime codec components.

Safari on iOS (including iPad) currently supports uncompressed WAV and AIF audio, MP3 audio, and AAC-LC or HE-AAC audio. HE-AAC is the preferred format.

Safari on iOS (including iPad) currently supports MPEG-4 video (Baseline profile) and QuickTime movies encoded with H.264 video (Baseline profile) and one of the supported audio types.

iPad and iPhone 3G and later support H.264 Baseline profile 3.1. Earlier versions of iPhone support H.264 Baseline profile 3.0.

とあります。


GraphEdit上でそれぞれのフィルタを右クリックしてFilter Propertiesを選択すれば、プロパティページがあれば表示されます。
今回はK-Liteでh264エンコーダをインストールしましたので

スタートメニュー -> プログラム -> K-Lite Codec Pack -> Configuration -> x264 VFW

を選択すれば設定を行うことができます。


f:id:kent013:20110210023702p:image


ここで重要なのは「Basic」のProfileをBaselineに、Levelを3.1*2にすることです。でないとiPadでは再生されません。またfpsを30にするために*3、Extra command lineに「--fps 30」と入力します。そのほかの設定は、適宜いじってください。


AACに関してはデフォルトでAAC-LCで記録されますが、ビットレートなどをいじりたい場合は、GraphEditで編集してください。

iPadSafariで再生テスト

これで、正しいGraphができていてキャプチャに成功していれば、

 <html>
   <head></head>
   <body>
   <video width="352"  height="288" src="test.mp4"  controls autobuffer autoplay></video>
   </body>
 </html>

とかいうファイルを作って、ドメインのある場所に置いてキャプチャした動画ファイルをtest.mp4として同じディレクトリにおけば、iPadSafariで見られるはずです。
問題があるようなら、エンコードの設定を見直してください。

コード上でGraphを表現する

これが厄介で…
とりあえず、とにかくコードを載せます。
基本的なソースコードは、USBカメラをC#で使おうを参考にさせていただいています。サンプルコード中では

ICaptureGraphBuilder インターフェイス

を使っていますが、CaptureGraphBuilderは自動的にGraphを作るためのクラスなので*4、MediaSubTypeが定義されていないと、目的のグラフを作れません。なので無印の

IGraphBuilder インターフェイス

を使います。


繋ぐところあたりは、TSReader2MP4.csというソースを参考にしました。というかこのソースコードが一番役に立ちました。

 //画像のプレビューや動画キャプチャ用の設定を行う.
 bool SetupGraph()
 {
   int result;
 
   try
   {
     //captureFilter(ソースフィルタ)をgraphBuilder(フィルタグラフマネージャ)に追加.
     result = graphBuilder.AddFilter(captureFilter, "Video Capture Device");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
     //audioFilter(ソースフィルタ)をgraphBuilder(フィルタグラフマネージャ)に追加.
     result = graphBuilder.AddFilter(audioFilter, "Audio Capture Device");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     string monikerH264 = @"@device:cm:{33D9A760-90C8-11D0-BD43-00A0C911CE86}\x264";
     vfwFilter = Marshal.BindToMoniker(monikerH264) as IBaseFilter;
     result = graphBuilder.AddFilter(vfwFilter, "h264 video encoder");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     string monikerAAC = @"@device:sw:{083863F1-70DE-11D0-BD40-00A0C911CE86}\{88F36DB6-D898-40B5-B409-466A0EECC26A}";
     aacFilter = Marshal.BindToMoniker(monikerAAC) as IBaseFilter;
     result = graphBuilder.AddFilter(aacFilter, "aac audio encoder");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     string monikerMp4Mux = @"@device:sw:{083863F1-70DE-11D0-BD40-00A0C911CE86}\{5FD85181-E542-4E52-8D9D-5D613C30131B}";
     muxFilter = Marshal.BindToMoniker(monikerMp4Mux) as IBaseFilter;
     result = graphBuilder.AddFilter(muxFilter, "mp4 muxer");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     string monikerFileWrite = @"@device:sw:{083863F1-70DE-11D0-BD40-00A0C911CE86}\{8596E5F0-0DA5-11D0-BD21-00A0C911CE86}";
     IBaseFilter writerFilterBase = Marshal.BindToMoniker(monikerFileWrite) as IBaseFilter;
     writerFilter = writerFilterBase as IFileSinkFilter;
     result = graphBuilder.AddFilter(writerFilterBase, "file writer");
     if (result < 0) Marshal.ThrowExceptionForHR(result);
     writerFilter.SetFileName(this.FileName, null);
 
     //ピンを繋ぐ
     IPin pIn, pOut;
     pOut = DsFindPin.ByDirection(captureFilter, PinDirection.Output, 0);
     pIn = DsFindPin.ByDirection(vfwFilter, PinDirection.Input, 0);
     graphBuilder.Connect(pOut, pIn);
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     IAMStreamConfig videoStreamConfig = pOut as IAMStreamConfig;
     BitmapInfoHeader bmiHeader;
     bmiHeader = (BitmapInfoHeader)getStreamConfigSetting(videoStreamConfig, "BmiHeader");
     bmiHeader.Width = 352;
     bmiHeader.Height = 288;
     setStreamConfigSetting(videoStreamConfig, "BmiHeader", bmiHeader);
 
     pOut = DsFindPin.ByDirection(audioFilter, PinDirection.Output, 0);
     pIn = DsFindPin.ByDirection(aacFilter, PinDirection.Input, 0);
     graphBuilder.Connect(pOut, pIn);
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     pOut = DsFindPin.ByDirection(vfwFilter, PinDirection.Output, 0);
     pIn = DsFindPin.ByDirection(muxFilter, PinDirection.Input, 0);
     graphBuilder.Connect(pOut, pIn);
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     pOut = DsFindPin.ByDirection(aacFilter, PinDirection.Output, 0);
     pIn = DsFindPin.ByDirection(muxFilter, PinDirection.Input, 1);
     graphBuilder.Connect(pOut, pIn);
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
     pOut = DsFindPin.ByDirection(muxFilter, PinDirection.Output, 0);
     pIn = DsFindPin.ByDirection(writerFilter as IBaseFilter, PinDirection.Input, 0);
     graphBuilder.Connect(pOut, pIn);
     if (result < 0) Marshal.ThrowExceptionForHR(result);
 
 
     IBasicAudio audio = graphBuilder as IBasicAudio;
     audio.put_Volume(0);
 
     return true;
   }
   catch (Exception e)
   {
     MessageBox.Show("フィルターグラフの設定に失敗しました." + e.ToString());
     return false;
   }
   finally
   {
   }
 }

上記コード中のgetStreamConfigSettingとsetStreamConfigSettingは
DirectShowNet: how do you set the resolution of a stream?
にあったCapture Sample with DirectX and .NET - CodeProjectからダウンロードできるソース、Capture.csの下の方に定義があります。長いし引用にならないのでコピペしません。


ここで重要なのは、まずフィルタの読み込み方です。この読み込み方以外の方法が分かりません。もっといい方法があるような気もしますが、動いているのでOKということで。

 string monikerH264 = @"@device:cm:{33D9A760-90C8-11D0-BD43-00A0C911CE86}\x264";
 vfwFilter = Marshal.BindToMoniker(monikerH264) as IBaseFilter;
 result = graphBuilder.AddFilter(vfwFilter, "h264 video encoder");
 if (result < 0) Marshal.ThrowExceptionForHR(result);

monikerの文字列はGraphEditのInsert Filterからとってくるといいです。


f:id:kent013:20110210022936p:image


の一番下にある、Filter Monikerの文字列です。これができるということを知るのに6時間ぐらいかかりました…orz


次に、PINの繋ぎ方です。
GraphEditで繋いだのと同じように

 IPin pIn, pOut;
 pOut = DsFindPin.ByDirection(captureFilter, PinDirection.Output, 0);
 pIn = DsFindPin.ByDirection(vfwFilter, PinDirection.Input, 0);
 graphBuilder.Connect(pOut, pIn);
 if (result < 0) Marshal.ThrowExceptionForHR(result);

として、これを繰り返して繋いで行きます。
繋ぎ終われば、Graphの設定は完了です。


そして、カメラの解像度の設定ですが、

 IAMStreamConfig videoStreamConfig = pOut as IAMStreamConfig;
 BitmapInfoHeader bmiHeader;
 bmiHeader = (BitmapInfoHeader)getStreamConfigSetting(videoStreamConfig, "BmiHeader");
 bmiHeader.Width = 352;
 bmiHeader.Height = 288;
 setStreamConfigSetting(videoStreamConfig, "BmiHeader", bmiHeader);

のように、captureFilter*5の出力ピンからIAMStreamConfigを得て、それにBitmapInfoHeaderをセットします。ここで指定できる解像度は、カメラで設定できるものでなければなりません。カメラのドライバについているプログラムで確認するか、

 IAMStreamConfig videoStreamConfig = pOut as IAMStreamConfig;
 int iCount, iSize;
 videoStreamConfig.GetNumberOfCapabilities(out iCount, out iSize);
 for (int iFormat = 0; iFormat < 40; iFormat++)
 {
   AMMediaType type;
   VideoStreamConfigCaps vsc = new VideoStreamConfigCaps();
   VideoInfoHeader vih = new VideoInfoHeader();
   IntPtr scc = Marshal.AllocHGlobal(iSize);
   result = videoStreamConfig.GetStreamCaps(iFormat, out type, scc);
 
   Marshal.PtrToStructure(scc, vsc);
   Marshal.PtrToStructure(type.formatPtr, vih);
 
   Console.WriteLine("{0} x {1} Min:{2:0.00}fps Max:{3:0.00}fps", 
                     vih.BmiHeader.Width, vih.BmiHeader.Height, 
                     10000000 / vsc.MaxFrameInterval, 
                     10000000 / vsc.MinFrameInterval);
 }

のようにしてデバッグ出力するといいかと思います。上記のコードはフォーマットが40もないのでエラーになりますが。
上記コードは、カメラ映像を表示するにはを参考にしています。


オーディオのボリュームについては

 IBasicAudio audio = graphBuilder as IBasicAudio;
 audio.put_Volume(0);

とすればいいようです。
ボリュームの値は、

IBasicAudio::put_Volume

によれば、

[in] ボリュームを -10,000 〜 0 の数値で指定する。最大ボリュームは 0、無音は -10,000。必要なデシベル値を 100 倍する。たとえば、-10,000 = -100 dB。

だそうです。


もう一つだけ注意するべきことがあります。
それは、必ずInterfaceを開放してDisposeするまえにキャプチャと関連付けられたmediaControlのStopを呼び出してから終了することです。

mediaControl.Stop();

こうしないとお尻のフレームあたりが尻切れトンボになって、プレーヤーで再生するときに秒数がマイナス表示になったり、エラー表示が出て再生できなかったりします。


これでUSBカメラからmp4を出力するプログラムができました。プログラムを動かしてできたmp4を、先ほどのhtmlを使ってiPad上でmp4が正しく再生できるかどうか確認してみてください。


というわけで、長くなりましたが以上です!
COMがよくわかってないからホント疲れた…


せんでん

1タップで写真共有tottepost
カメラのついたiPad/iPhoneで撮った写真を、その場でFacebook/Mixi/DropboxなどのサービスにアップロードできるtottepostというiOSアプリを開発しています!詳しくは、iTunes App Storeをご覧ください。


ご購入はこちら!
1タップで写真共有 - tottepost - ISHITOYA Kentaro

*1:MegaとかFullとか作る必要あるのかと…

*2:iOS4.2のSafariではProfileがMainでも動作しました

*3:カメラが30fps出ることが前提ですが

*4:たぶん

*5:映像デバイスのソースフィルタ