2012年

12月

11日

DirectShowで動画編集②

結構時間掛かりましたが、ようやく記事を書き終えました。

 

今回はDirectShowで動画編集①からの続きです。前回から2ヶ月近くたっていますが、続きをやっていこうと思います。 

 

しかし、DirectShowはやっぱり難しいですね・・・

OpenCVがAVI2.0に対応してくれれば2度と使わないぞ!そんな日が来るかは知らないけど。

 

項目:

動画再生に必要なフィルタを自分で用意する

前回はIGraphBuilder.RenderFileメソッドを使うことで、aviファイル読み込み→映像・音声の再生をするフィルタを自動生成しました。

 

単純な動画再生だけならこれでいいですが、複雑な処理をさせたい場合自分でフィルタの結合・切り離しの作業がある程度必要となります。

 

慣れるためにも今回は簡単なところで、RenderFile関数が内部でやってくれてる一連のフィルタ処理を自分で用意してみましょう。

 

まずはAVIの映像と音声を再生するために必要なフィルタを確認します。

私の現環境では、下図のようになりました。

ではこの図のとおりにフィルタを用意していきましょう。

まずはフィルタグラフを作成し、AVIソースフィルタを追加します。

 

// フィルタグラフ作成

IFilterGraph graph = new FilterGraph() as IFilterGraph;

IGraphBuilder build = graph as IGraphBuilder;

 

// AVIソースフィルタの追加

string filename = "movie.avi";

string filtername = filename;

IBaseFilter source;

int hr = build.AddSourceFilter(filename, filtername, out source);

 

if(hr != 0)

    throw new Exception("ファイル読み込み失敗");


ソースフィルタ追加関数

public int DirectShowLib.IGraphBuilder.AddSourceFilter(

                    string lpwstrFileName,

                    string lpwstrFilterName,

                    out IBaseFilter ppFilter 

)

 

lpwstrFileName :読み込みたい動画ファイルの名前

lpwstrFilterName :追加するソースフィルタの名前

ppFilter:追加したソースフィルタを受け取る変数

戻り値::HRESULT(int)型

 


次に、スプリッターを追加してAVIストリームの映像と音声を分離させます。

 

IBaseFilter splitter = null; // スプリッタ

// ソースフィルタのメディアタイプ取得

AMMediaType media = new AMMediaType();
string filtername;
source.GetCurFile(out filtername, media);


// AVIファイルの場合(他のフォーマットが来た場合のために条件分け)
if (media.subType == MediaSubType.Avi)
    splitter = new AviSplitter() as IBaseFilter;

 

// メディア開放

DsUtils.FreeAMMediaType(media);

 

// スプリッタフィルタを追加
if (splitter != null)

{

    int hr = graph.AddFilter(splitter, "Splitter");

 

    if(hr != 0)

        throw new Exception("スプリッタの追加に失敗");

}

 

ここは読み込んだ動画ファイルのフォーマットによって追加すべきスプリッタが異なります。mp4の場合はもちろんMP4スプリッタ、flvならFLVスプリッタが必要です。

 

ちなみにwmvの場合、ソースフィルタの時点で映像と音声の分離が済んでいるのでスプリッタを新規に追加する必要はありません。

 

さて、今のフィルタグラフの状態はこんな感じです。

フィルタがそれぞれ独立した状態になっています。コレではフィルタは意味を成しません。フィルタ同士はピンで接続する必要があります。

 

ではmovie.aviソースフィルタからOutputピン、AVI SppitterフィルタからInputピンを取り出して、接続をしましょう。

 

フィルタからピンを取り出す処理は何度も行うので、関数化しておくと便利です。

 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
/// <summary>
/// ピン取得
/// </summary>
/// <param name="filter">ピンを取り出したいフィルタ</param>
/// <param name="pinDirection">ピンの種類(入力/出力)</param>
/// <param name="connected">接続済みのピンを取り出したい場合はtrue</param>
/// <returns>引数の条件にあったピン</returns>
public static IPin GetPin(
    IBaseFilter filter, PinDirection pinDirection, bool connected)
{
    IEnumPins enums;
    IPin[] pins = new IPin[1];
    PinDirection direct;

    filter.EnumPins(out enums);

    // フィルタ内のピンを全て列挙する
    while (enums.Next(pins.Length, pins, IntPtr.Zero) == 0)
    {
        // ピンの種類を取得
        pins[0].QueryDirection(out direct);

        // 指定した種類のピンが見つかった
        if (direct == pinDirection)
        {
            // 接続済みピンも取得する場合
            if (connected)
                break;
            else
            {
                // 接続先のピンがあるか確認する
                IPin pin;
                pins[0].ConnectedTo(out pin);

                // 未接続
                if (pin == null)
                    break;

                pin = null;
            }
        }

        // 未処理の参照が残るので、必ず開放する
        pins[0] = null;
    }

    enums = null;
    return pins[0];
}

使い方はこんな感じです。

 

// AVIソースフィルタから未接続の出力ピンを取得

IPin outputPin = GetPin(source, PinDirection.Output, false);


// AVIスプリッタから未接続の入力ピンを取得

IPin inputPin = GetPin(splitter, PinDirection.Input, false);

 

// それぞれのピン取得に成功

if(outputPin != null && inputPin != null)

{

    // ピンを繋げる

    int hr = build.Connect(outputPin, inputPin);

 

    if(hr != null)

        throw new Exception("フィルタの接続に失敗");

}

 

IGraphBuilder.Connectメソッドでピン同士を接続します。どちらかのピンが既に接続済みの場合は失敗します。このフィルタ同士の接続の処理も関数化しておくと便利でしょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// フィルター同士のピン接続
/// </summary>
/// <param name="builder">ビルダー</param>
/// <param name="output">出力側フィルター</param>
/// <param name="input">入力側フィルター</param>
/// <returns>HRESULT</returns>
public static int ConnectFilter(
    IGraphBuilder builder, IBaseFilter output, IBaseFilter input)
{
    IPin outputPin = GetPin(output, PinDirection.Output, false);
    IPin inputPin = GetPin(input, PinDirection.Input, false);
    int hr = 1; // S_FALSE
    
    if (inputPin != null && outputPin != null)
        hr = builder.Connect(outputPin, inputPin);

    outputPin = inputPin = null;
    return hr;
}

処理に成功すると、下図のようにフィルタ同士が繋がった状態になります。

2つのピンを接続する

public int DirectShowLib.IGraphBuilder.Connect(

        IPin ppinOut,

        IPin ppinIn 

)

 

ppinOut :出力ピン

ppinIn :入力ピン

戻り値::HRESULT(int)型


次に、スプリッタから映像ピンと音声ピンを取り出します。先ほどのGetPinメソッドだと同じ種類のピンを2つ以上取得することが出来ないので、また別の関数を用意します。 

 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
/// <summary>
/// ピン取得
/// </summary>
/// <param name="filter">ピンを取り出したいフィルタ</param>
/// <param name="pinDirection">ピンの種類(入力/出力)</param>
/// <param name="connected">接続済みのピンを取り出したい場合はtrue</param>
/// <returns>ピン配列</returns>
public static IPin[] GetPins(
    IBaseFilter filter, PinDirection pinDirection, bool connected)
{
    IEnumPins enums;
    filter.EnumPins(out enums);
    
    IPin[] pins = new IPin[1];
    List<IPin> results = new List<IPin>();

    // フィルタ内のピンを全て列挙する
    while (enums.Next(pins.Length, pins, IntPtr.Zero) == 0)
    {
        // ピンの種類を取得
        PinDirection direct;
        pins[0].QueryDirection(out direct);

        // 指定した種類のピンが見つかった
        if (direct == pinDirection)
        {
            // 接続先のピンがあるか確認する
            IPin pin;
            pins[0].ConnectedTo(out pin);
            
            // 有効ピンならリストに追加
            if (connected || pin == null)
                results.Add(pins[0]);
                
            pin = null;
        }

        // 未処理の参照が残るので、必ず開放する
        pins[0] = null;
    }

    enums = null;
    pins = null;

    // 配列化
    pins = results.ToArray();
    
    // リストは用済み
    results.Clear();
    results = null;
    
    return pins;
}

この関数を使って、スプリッタから映像ピンと音声ピンを取り出します。

※動画によっては映像・音声のどちらかのピンが無いものもあります。

 

IPin videoPin = null; // 映像ピン

IPin audioPin = null; // 音声ピン

 

// 出力ピンの取得
IPin[] pins = GetPins(splitter, PinDirection.Output, false);

 

if(pins != null)
{
    AMMediaType media;

 

    for (int i = 0; i < pins.Length; i++)
    {
        // ピンのメディアタイプをチェック
        GetPinMediaType(pins[i], out media);

        // ビデオピン
        if (media.majorType == MediaType.Video)
            videoPin = pins[i];

        // オーディオピン
        if (media.majorType == MediaType.Audio)
            audioPin = pins[i];

 

        // 未参照が残るので開放

        DsUtils.FreeAMMediaType(media);
        pins[i] = null;
    }

    pins = null;
    media = null;

}

 


これでスプリッタから映像・音声ピンを取得できました。

では最後に、レンダラーフィルタを追加してそれぞれのピンと接続させます。

 

IPin rendererPin = null;

 

if(videoPin != null)

{

     // 映像レンダラーを作成してフィルタグラフに追加

    IBaseFilter video = new VideoRenderer() as IBaseFilter;

    graph.AddFilter(video, "Video Renderer");

 

    // ビデオレンダラーの入力ピンを取得

    rendererPin = GetPin(video, PinDirection.Input, false);

 

    // スプリッタの映像ピンとレンダラーを接続

    int hr = build.Connect(videoPin, rendererPin);

 

    if(hr != 0)

        throw new Exception("ビデオ接続に失敗");

}

 

if(audioPin != null)

{

     // 音声レンダラーを作成してフィルタグラフに追加

    IBaseFilter audio = new DSoundRender() as IBaseFilter;

    graph.AddFilter(audio, "DirectSound Device");

 

    // オーディオレンダラーの入力ピンを取得

    rendererPin = GetPin(audio, PinDirection.Input, false);

 

    // スプリッタの音声ピンとレンダラーを接続

    int hr = build.Connect(audioPin, rendererPin);

 

    if(hr != 0)

        throw new Exception("オーディオ接続に失敗");

}

 

とくに難しいことはありません。

これでIMediaControl.Runメソッドを呼び出せば動画の再生が始まります。

 

ちなみに上の図ではスプリッタの映像ピンとビデオレンダラーの間にAVI DecompressorフィルタとColor Space Converterフィルタが挟まってますが、自分で追加しなくてもスプリッタとレンダラを接続すれば、再生に最小限必要なフィルタをDirectShowの方で勝手に追加してくれます。

 

ただ、勝手に追加してしまうので目的の処理とは違うフィルタを用意される可能性もあります。例えばColor Space Converterは動画を32bitカラーに変換するフィルタなので、動画を16ビットや24ビットで扱いたい場合には必要のないフィルタです。

 

他にも動画を拡大・回転させたりなど、そういう処理をさせたい場合はスプリッタとビデオレンダラーをいきなり接続せずに、トランスフォームフィルタを別途用意して一つずつ接続してください。

 

まあ今回の処理ですが、動画を再生するだけならば初めからRenderFile関数を呼び出した方が良いです。動画フォーマットによって処理分けするのは結構面倒です・・・

AVI形式の動画ファイルを保存する

AVI形式の動画を保存するにはAVI MuxフィルタとFile Writerフィルタを使います。

 

File Writerフィルタは文字通りファイル出力をするフィルタで、AVI Muxフィルタは映像と音声を結合してAVI形式にするフィルタです。このフィルタで作成されたAVIはAVI2.0に対応しています。

 

例えばmp4をaviへ変換出力するフィルタグラフはこんな感じになります。

mp4ソースフィルタをスプリッタで分離→それぞれデコーダーにかける→AVI Muxで再結合→ファイル出力」という流れです。

 

さて、またフィルタを一つずつ作成していくのもいいのですが、前の項目を見てもらえば分かる通りかなり長くなってしまいます。なので今回はICaptureGraphBuilder2インターフェースを使ってサクっとやっちゃいましょう。

 

ICaptureGraphBuilder2は文字通りキャプチャを行うための機能で、コレを使うとWebカメラやDVDのキャプチャができるようになります。

 

やり方しだいでは、ウィンドウやデスクトップのキャプチャも可能だと思うんですけど・・・散々調べたけど一切分かりません。それを一番やりたいんだけど出来ないのかな?

 

自作のフィルタを作ればいいんだろうけど、そこまでしてやりたくない・・・デフォルトのものだけで何とか出来ないものか・・・・・・

 


ではコードにおこしてみましょう。

フィルタグラフを作成して、mp4ソースフィルタを追加します。

 

// フィルタグラフ作成

IFilterGraph graph = new FilterGraph() as IFilterGraph;

IGraphBuilder build = graph as IGraphBuilder;

 

// MP4ソースフィルタの追加

string filename = "movie.mp4";

string filtername = filename;

IBaseFilter source;

build.AddSourceFilter(filename, filtername, out source);

 

 

次にキャプチャグラフを作成して、出力ファイルを指定します。

 

// キャプチャグラフ作成

ICaptureGraphBuilder2 capture =

        new CaptureGraphBuilder2() as ICaptureGraphBuilder2;

 

// キャプチャグラフが使用するグラフビルダーを指定

capture.SetFiltergraph(build);

 

// 出力ファイルの設定

IBaseFilter aviMux;

IFileSinkFilter fileWriter;


capture.SetOutputFileName(

        MediaSubType.Avi, // 出力する動画形式 AVIを指定

        "result.avi", // 出力ファイル名

        out aviMux, // 出力に必要なAvi Muxフィルタ

        out fileWriter); // 出力に必要なFile Writerフィルタ

 

 

最後にAVI Muxへ送るソースフィルタを設定してフィルタを実行します。

 

// ソースの描画先を設定

capture.RenderStream(DsGuid.Empty, DsGuid.Empty, source, null, aviMux);

 

// フィルタの実行

IMediaControl media = graph as IMediaControl;

media.Run();

// 処理終了まで待機
IMediaEventEx eventEx = media as IMediaEventEx;

EventCode code;
eventEx.WaitForCompletion(-1, out code);

// フィルタの停止
media.Stop();



フィルタグラフのファイル書き込みセクションの作成

public int ICaptureGraphBuilder2.SetOutputFileName(

                Duid pType,

                string lpstrFile,

                out IBaseFilter ppbf,

                out IFileSinkFilter ppSink

)

 

pType :出力する動画のメディアタイプ

lpstrFilter:出力する動画のファイル名

ppbf:マルチプレクサ(Mux)フィルタを受け取る変数

ppSink:File Writerフィルタを受け取る変数

戻り値::HRESULT(int)型

 

ppfの型はpTypeで指定したメディアによって変わります。

今回はAVI形式なので、AVI Muxが返ってきます。

 

ソースフィルタとシンクフィルタの接続

public int ICaptureGraphBuilder2.RenderStream(

                DsDuid PinCategory,

                DsDuid  MediaType,

                object pSource,

                IBaseFilter pfCompressor,

                IBaseFilter pfRenderer

)

 

PinCategory  :ピンの種類 PinCategory列挙体の値 任意のピンを使う場合はnull

MediaType:出力ピンのメディアタイプ 任意のピンを使う場合はnull

pSource:pfRendererへ情報を送る出力ソース フィルタでもピンでも可能

 pfCompressor:圧縮・中間フィルタ これを経由してpfRendererへ渡す 何もしないならnull

 pfRenderer:psSourceの情報を受け取るフィルタ

戻り値::HRESULT(int)型

 

 

サンプルコードです。

wmv、mp4、aviを読み込んで32bitカラーに圧縮してavi出力します。

wmvの場合、映像と音声の同期が取れない場合があります。

32bitAVI出力サンプル.rar
圧縮ファイル アーカイブ 102.0 KB

さて、実はやりたいことがあるのでDirectShow編はあと1回やります。ネタはもうできているのであとは記事にするだけです。

 

というわけで、DirectShowで動画編集③へと続きます。次回はavi→連番bmp出力、連番bmp→avi再生・出力をやっていきます。

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

コメント: 9
  • #1

    納豆 (水曜日, 03 7月 2013 07:46)

    初めまして。
    DirectShowの情報が少ない中、大変参考になりました。
    ③も楽しみにしています。

    自分も③に関する処理は書きました。
    SampleGrabberにISampleGrabberでコールバックで画像取得していますが
    パフォーマンスに不満があり、もっと高速に処理できないものか…と考えたりしてます。

    複数の動画をできる限りシームレスに連続再生する方法も知りたいです。

  • #2

    ze10 (木曜日, 04 7月 2013 23:18)

    >>納豆様 ありがとうございます。

    一応ネタもプログラムも出来ているんですが、中々記事がまとまらずに半年以上経ってしまいました・・・

    でもなるべく早く投稿するつもりです。

    >>SampleGrabberにISampleGrabberでコールバック...
    ワタシはフィルタグラフを1フレームずつ動かし、処理を抜けてからSampleGrabberから画像を取得するようにしています。

  • #3

    納豆 (月曜日, 08 7月 2013 11:20)

    >ワタシはフィルタグラフを1フレームずつ動かし、処理を

    なるほど。
    リアルタイムで動画&音声を再生しながら取得する必要があるのでコールバック内で処理してます…。

  • #4

    mm (土曜日, 02 11月 2013 18:15)

    はじめまして。
    ③の連番bmp→avi出力の手法を知りたいです。
    投稿お待ちしております。

  • #5

    ze10 (水曜日, 06 11月 2013 20:41)

    >>4 mm様

    この記事書いてからもうすぐで一年が経過してしまいますね・・・
    申し訳ないです。

    今年までには記事に纏めるようにします。

  • #6

    shut (日曜日, 16 3月 2014 14:44)

    ③の連番bmp→avi出力の手法についてなのですが
    此方で紹介予定なのはDESCombineを用いて
    bmpを結合する方法でしょうか。

    色々調べてみてこの方法を知ったのですが、
    他に方法があるのであればぜひお聞きしたいです。

  • #7

    ze10 (土曜日, 19 4月 2014 07:50)

    >>6 shut様
    コメントの反映が遅れて申し訳ありません。

    DESCombineというものは初めて聞きました。そのような手法もあるんですね。
    プログラムも書く内容も完成はしてるんですけど、如何せん長くなるので中々書けずにもう2014年になっちゃいました・・・すみません。

    時間がある時にゆっくりとまとめちゃいたいですね。

  • #8

    mm (木曜日, 29 10月 2015 16:25)

    お父様のご冥福、心からお祈り申し上げます。
    ③の投稿を楽しみにしています。

  • #9

    ze10 (日曜日, 08 11月 2015 21:35)

    >>8 mm様 温かい言葉をありがとうございます
    XPが主流じゃなくなった今、DirectShowの需要があるのかどうか分かりかねますが、期待されてる以上いつか記事に興したいと思います。

    一応プログラムも完成してるんですけどね、いかんせん記事に興すのが難しくて・・・