2012年

10月

04日

DirectShowで動画編集①

前回のOpenCVから続き、色々調べてきました。

今回はDirectShowで動画を読み込んで描画させます。

 

項目:

DirectShowの仕組み

DirectShowは、小さな処理を組み合わせて動画の再生や編集を行います。この小さな処理は一つだけでは成立せず、組み合わせることで初めて意味が出来ます。

 

動画を読み込む処理、動画を回転させる処理、圧縮する処理、画面に表示する処理など、たくさんの処理があります。この小さな処理のことを「フィルタ」といいます。

 

フィルタには主に「ソースフィルタ」、「変換フィルタ」、「レンダフィルタ」の3つに分けられます。これらのフィルタを組み合わせることで、動画のキャプチャ・再生・編集・出力等色々な処理が出来るようになります。

 

フィルタには入力端子と出力端子が付いていて、この端子同士を繋げる事でフィルタ間でデータのやり取りができるようになります。この端子のことを「ピン」といいます。読み込んだファイルデータ、画像、音声はこのピンを通じてフィルタに送られます。

 

この一連の処理のことを「フィルタグラフ」といい、DirectShowでメディアデータを扱う場合、必ずこのフィルタグラフを作る必要があります。

 

詳しい説明はwikipediaを参照されたし

http://ja.wikipedia.org/wiki/DirectShow

 

Windowsには、このフィルタグラフを視覚的に構築できる「GraphEdit」というツールが用意されています。

 

このソフトは現在最新版の「Windows SDK 7.1 」に同梱されています。Windows XPの場合、「Microsoft SDKs\WIndows\V7.1\Bin\graphedt.exe」にありました。

 

最新のWindows SDKは以下からダウンロードできます。

http://www.microsoft.com/en-us/download/details.aspx?id=8279

 

開くとこんな感じです。

このソフトに動画ファイルをドラッグ&ドロップすると、その動画形式を再生するフィルタグラフを自動的に構成してくれます。

 

フィルタグラフを構成するフィルタは環境によって変わります。私の環境の場合、AVIファイルを渡したらこのようになりました。

 

再生ボタンを押すと処理が始まります。

正しいフィルタグラフが構成されていれば、動画の再生が始まります。

 

以下のボタンを押すと、新たなフィルタを追加できます。

これでフィルタを繋ぎ変えて、あらたなフィルタグラフを構成することも出来ます。

 

AVI再生のフィルタグラフを編集してみました。これでも再生できます。

このように、ストリームデータのフォーマットに応じてフィルタを付け替えることで動画の再生や出力が可能になるわけです。

C#.NETとDirectShow

今回は、COMラッパーライブラリ「DirectShow。NET」を使用します。

 

一応C#にはデフォルトでDirectShowを扱うためのCOMライブラリに「ActiveMovie control type library」と言うものが用意されているのですが、このライブラリは個人的に使いにくかったので今回は使用しません。

 

以下からダウンロードできます。最新版はV2.1です。

directshow.net library

 

解凍してdllをプロジェクト参照に追加すれば導入完了です。

シンプルな動画再生

DirectShowには、メディアファイルを指定するだけで描画に必要な一連のフィルタを自動的にそろえてくれるメソッドが備わっています。コレを使えば単純な動画の再生は出来るようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.Diagnostics;
using DirectShowLib;

namespace DirectShowSample
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        static void Main()
        {
            // フィルタグラフCOMオブジェクトの作成
            IFilterGraph graph = new FilterGraph() as IFilterGraph;
            IMediaControl media = graph as IMediaControl;
            IMediaEventEx eventEx = media as IMediaEventEx;
            
            // 指定メディアファイルを描画できるフィルタを自動的に構成
            int hresult = media.RenderFile("movie.avi");
            Debug.WriteLine(string.Format("ソースフィルタ コード:{0}", hresult));

            // 実行
            hresult = media.Run();
            Debug.WriteLine(string.Format("処理開始 コード:{0}", hresult));

            // 再生終了まで待機
            EventCode code;
            eventEx.WaitForCompletion(-1, out code);

            // 停止
            media.Stop();

            // 開放
            eventEx = null;
            media = null;
            graph = null;
        }
    }
}

以上です。コレだけの短いコードで簡単に動画を再生することが出来ます。

ビデオコーデックさえ揃っていればAVIだけでなく、MP4やFLVも同じコードで再生できます。

 

フィルタグラフに何のフィルタが追加されているか確認してみました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// フィルタグラフ内のフィルタを列挙
IEnumFilters enums;
graph.EnumFilters(out enums);
IBaseFilter[] filters = new IBaseFilter[1];

while (enums.Next(filters.Length, filters, IntPtr.Zero) == 0)
{
    FilterInfo info;
    filters[0].QueryFilterInfo(out info);

    Guid id;
    filters[0].GetClassID(out id);

    Debug.WriteLine(string.Format("Guid:{0} Name:{1}", id, info.achName));
    filters[0] = null;
}

filters = null;

出力結果:

Guid:79376820-07d0-11cf-a24d-0020afd79767 Name:Default DirectSound Device
Guid:b87beb7b-8d29-423f-ae4d-6582c10175ac Name:Video Renderer
Guid:04fe9017-f873-410e-871e-ab91661a4ef7 Name:ffdshow Video Decoder
Guid:cf49d4e0-1115-11ce-b03a-0020af0ba770 Name:AVI Decompressor
Guid:1b544c20-fd0b-11ce-8c63-00aa0044b51e Name:AVI Splitter
Guid:e436ebb5-524f-11ce-9f53-0020af0ba770 Name:../movie.avi

 

Default Direct Sound Deviceフィルタ ・・・ 音声再生フィルタ

Video Rendererフィルタ ・・・ 映像描画フィルタ

ffdshow Video Decoderフィルタ ・・・ ffdshow映像デコードフィルタ

AVI Decomporessorフィルタ ・・・ AVI映像圧縮解除フィルタ

AVI Splitterフィルタ ・・・ AVI映像・音声分離フィルタ

File Sourceフィルタ ・・・ ストリームファイルソースフィルタ

 

フィルタの接続は下図のようになっています。

自作のFormウィンドウに映像を表示

上記の方法の場合、IMediaControl.Runを実行すると勝手にウィンドウが表示されていました。しかしこれでは何かと困りものです。

 

任意のウィンドウに対して映像を描画させたいですよね?

次はFormウィンドウに対して動画を描画させます。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
using System;
using System.Diagnostics;
using System.Windows.Forms;
using DirectShowLib;

namespace DirectShowSample
{
    public partial class SimplePreviewForm : Form
    {
        IMediaControl media;
        IFilterGraph graph;

        IBaseFilter renderer;
        IVideoWindow window;
        IMediaEventEx eventEx;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SimplePreviewForm()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 初期化
        /// </summary>
        protected override void OnCreateControl()
        {
            base.OnCreateControl();

            // グラフビルダーの作成
            graph = new FilterGraph() as IFilterGraph;
            media = graph as IMediaControl;

            // ファイル読み込み
            int hresult = media.RenderFile("movie.avi");
            Debug.WriteLine(string.Format("ソースフィルタ コード:{0}", hresult));

            // レンダラーの取得
            hresult = graph.FindFilterByName("Video Renderer", out renderer);
            Debug.WriteLine(string.Format("レンダラー コード:{0}", hresult));

            if (renderer != null)
            {
                // レンダラーをウィンドウに登録
                window = renderer as IVideoWindow;
                
                // ウィンドウスタイルを変更
                hresult = window.put_WindowStyle(
                    WindowStyle.Child |        // 子ウィンドウ化
                    WindowStyle.ClipSiblings); // 兄弟関係の子ウィンドウをクリップ
                    
                // Formウィンドウを親ウィンドウに設定
                hresult = window.put_Owner(Handle);
                
                // その他設定
                window.put_Visible(OABool.True);
                window.put_AutoShow(OABool.False);
                window.put_FullScreenMode(OABool.False);
                window.SetWindowForeground(OABool.True);

                // ウィンドウサイズの取得
                int width, height;
                window.get_Width(out width);
                window.get_Height(out height);

                // 取得したサイズに変更
                this.Width = width;
                this.Height = height;
                window.SetWindowPosition(0, 0, ClientSize.Width, ClientSize.Height);
            }
        }

        /// <summary>
        /// 描画開始
        /// </summary>
        /// <param name="e"></param>
        protected override void OnShown(EventArgs e)
        {
            // イベント通知設定
            eventEx = media as IMediaEventEx;
            eventEx.SetNotifyWindow(Handle, WM_DirectShow, IntPtr.Zero);

            // 実行開始
            Debug.WriteLine(string.Format("実行状態 コード:{0}", media.Run()));

            base.OnShown(e);
        }

        /// <summary>
        /// サイズ変更
        /// </summary>
        /// <param name="e"></param>
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);

            if (window != null)
            {
                window.put_Width(Width);
                window.put_Height(Height);
            }
        }

        /// <summary>
        /// 終了イベント
        /// </summary>
        /// <param name="e"></param>
        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);

            // メディア停止
            media.Stop();

            // イベントストップ
            if (eventEx != null)
            {
                eventEx.SetNotifyWindow(IntPtr.Zero, 0, IntPtr.Zero);
                eventEx = null;
            }

            // ウィンドの状態を戻す
            if (window != null)
            {
                window.put_Visible(OABool.False);
                window.put_Owner(IntPtr.Zero);
                window = null;
            }

            // 開放
            renderer = null;
            media = null;
            graph = null;
        }

        static const int WM_Application = 0x8000;
        static const int WM_DirectShow = WM_Application + 1;
        
        /// <summary>
        /// ウィンドウプロセス
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_DirectShow:
                    DirechShowEvent();
                    break;

                default:
                    base.WndProc(ref m);
                    break;
            }

            // 子ウィンドウにも同じイベントを送る
            if (window != null)
                window.NotifyOwnerMessage(m.HWnd, m.Msg, m.WParam, m.LParam);
        }

        /// <summary>
        /// DirectShowイベント
        /// </summary>
        void DirechShowEvent()
        {
            bool isComplete = false;
            EventCode eventCode;
            IntPtr param1, param2;
            int hresult = -1;

            do
            {
                // イベントを取得
                hresult = eventEx.GetEvent(
                    out eventCode,
                    out param1, 
                    out param2, 
                    0);

                if (hresult == 0)
                {
                    // 再生終了フラグ
                    isComplete = (eventCode == EventCode.Complete);

                    // イベントを削除
                    eventEx.FreeEventParams(eventCode, param1, param2);
                }
            } while (hresult == 0);

            // 再生終了
            if (isComplete)
                Close();
        }
    }
}

自前のFormクラスを用意して、OnCreateControlメソッド内で前回同様DirectShowの初期化&ファイル読み込みを行います。ここまでは一緒。


 

では変更点。まずは41行目。

 

登録フィルタ取得関数

public DirectShowLib.IFilterGraph.FindFilterByName(

                    string pName,

                    out IBaseFIlter ppFilter

)

 

pName:フィルタグラフに登録されてるフィルタの登録名

ppFilter:該当のフィルタを受け取るIBaseFilterインターフェースポインタ

戻り値::HRESULT(int)型

 

これでフィルタグラフからレンダラーフィルタを取り出します。

フィルタの登録名は「Video Renderer」です。

 

// レンダラーの取得

int hresult = graph.FindFilterByName("Video Renderer", out renderer);
Debug.WriteLine(string.Format("レンダラー コード:{0}", hresult));


 

次に47行目。レンダラーからIVideoWindowフィルタインターフェースを取り出します。

 

IVideoWindowインターフェースはウィンドウの情報を持つインターフェースで、ウィンドウサイズ、ウィンドウタイトル、ウィンドウスタイル等の取得・変更をすることが出来ます。

 

今回重要なメソッドは55行目。

 

親ウィンドウ登録関数

public DirectShowLib.IVideoWindow put_Owner(IntPtr owner)

owner:オーナーウィンドウのハンドル(HWND)

戻り値::HRESULT(int)型

 

ここにFormクラスのコントロールハンドルを渡せば、VideoRendererフィルタの描画結果を自前のFormやControlクラスに反映できるようになります。


 

最後に83行目。IMediaEventExインターフェースの処理です。「WaitForCompletion」から「SetNotifyWindow」に変更しています。

 

フィルタグラフが有効なレンダリングまで待機

pubic DirectShowib.IMediaEvent.WaitForCompletion(

                    int msTimeout,

                    out EventCode pEvCode

)


msTimeout:タイムアウト時間

pEvCode:イベントコードを受け取るポインタ

戻り値::HRESULT(int)型

 

 

イベント通知を処理するウィンドウの登録

pubic DirectShowib.IMediaEventEx.SetNofityWindow(

                    IntPtr hwnd,

                    int lMsg,

                    IntPtr lInstanceData  

)


hwnd:イベントメッセージを受け取るウィンドウのハンドル(HWND)

lMsg:ウィンドウに渡す通知メッセージ

lInstanceData:メッセージと共に渡すlParamの値

戻り値::HRESULT(int)型

 

前回はWaitForCompletionを使って再生が終わるまで待機していましたが、コレを使うと描画が全て終了するまでコントロールが帰ってきません。これではFormが表示される前の動画の再生が終了してしまいます。

 

今回はSetNofityWindowを使ってFormに対してDirectShowのメッセージを送り、再生が終了したかをウィンドウプロシージャで確認させています。

 

static const int WM_Application = 0x8000; // アプリケーション定義用メッセージ
static const int WM_DirectShow = WM_Application + 1; // DirectShow応答メッセージ

/// <summary>
/// ウィンドウプロセス
/// </summary>
/// <param name="m"></param>
protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_DirectShow:
            DirechShowEvent();
            break;

        default:
            base.WndProc(ref m);
            break;
    }

    // 子ウィンドウにも同じイベントを送る
    if (window != null)
        window.NotifyOwnerMessage(m.HWnd, m.Msg, m.WParam, m.LParam);
}

/// <summary>
/// DirectShowイベント
/// </summary>
void DirechShowEvent()
{
    bool isComplete = false;
    EventCode eventCode;
    IntPtr param1, param2;
    int hresult = -1;

    do
    {
        // イベントを取得
        hresult = eventEx.GetEvent(
            out eventCode,
            out param1,
            out param2,
            0);

        if (hresult == 0)
        {
            // 再生終了フラグ
            isComplete = (eventCode == EventCode.Complete);

            // イベントを削除
            eventEx.FreeEventParams(eventCode, param1, param2);
        }
    } while (hresult == 0);

    // 再生終了
    if (isComplete)
        Close();
}

 

最後に、Formが閉じる時にIVideoWindowのスタイルを元に戻し、親ウィンドウとイベントコード送り先のウィンドウを消します。

 

// イベントストップ
if (mediaEx != null)
{
    mediaEx.SetNotifyWindow(IntPtr.Zero, 0, IntPtr.Zero);
    mediaEx = null;
}

// ウィンドの状態を戻す
if (window != null)
{
    window.put_Visible(OABool.False);
    window.put_Owner(IntPtr.Zero);
    window = null;
}

今回はコレで終了。やってみれば案外簡単でした。フィルタも結構分かりやすかったです。

次回はIFiterGraph.RenderFieを使わずに、自分でフィルタを組み合わせて動画を再生する方法についてやっていこうと思います。

 

Next⇒DirectShowで動画編集②

コメントをお書きください

コメント: 1
  • #1

    ごまだれ (土曜日, 03 10月 2015 23:32)

    初めまして、c#で動画編集できないか悩んでいたところ、
    とてもよさげなタイトルが目に来たので来てみました。
    いままで標準ライブラリしか扱ってこなかった自分には
    理解できないものばかりだと思っていましたが、とても丁寧な説明ですね。
    自分にもわかりやすかったです。素晴らしい記事をありがとうございました。