Date

動的再接続とは?

動的再接続とは、再生またはポーズ状態のフィルタグラフを「停止せずに」別のフィルタへ繋ぎかえることである。例えばビデオエフェクトのトランスフォームフィルタを使うフィルタグラフであれば、停止状態にする必要がないのでスムーズなフィルタの抜き差しが可能になる。

動的再接続に必要な要件

動的再接続には「動的再接続する側」と「動的再接続される側」の両方のフィルタが対応している必要がある。

動的再接続の始端となる出力ピン IPinFlowControlインターフェイスが実装されていること。(*1)
動的再接続の終端となる入力ピン IPinConnectionインターフェイスが実装されていること。

*1 IPinFlowControlはフィルタグラフを制御するアプリケーション側から動的再接続を行う場合である。フィルタから開始する場合は実装は必ずしも必要ではない。

想定するフィルタグラフ

今回は、以下のフィルタグラフを想定する。フィルタグラフを再生を開始した約2秒後に、動的再接続により、途中のトランスフォームフィルタを取り外す。

My Source Filter 3は、CSourceを基底クラスとしたプッシュソースフィルタである。現在のストリーム時間を描画したビデオフレームを送信する。文字の色は緑、背景色は青である。

My Transform Filterは、RGBを入れ替えて出力するトランスフォームフィルタである。これにより下の図のように色が変化する。

My Renderer Filter 2は、CBaseVideoRendererを基底クラスとしたビデオレンダラで、受信したビデオフレームをウィンドウに描画する。

アプリケーション側の実装

フィルタグラフの構築

フィルタグラフを構築するコードを以下に示す。

 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
#define JIF(hr) if(FAILED(hr)) { HrToStrByAMGet(hr); return hr; }
HRESULT DoTest()
{
    HRESULT hr=NOERROR;
    CComPtr<ICaptureGraphBuilder2> cap_gb;
    CComPtr<IGraphBuilder> gb;
    CComPtr<IMediaControl> ctrl;
    CComPtr<IBaseFilter> renderer;
    CComPtr<IBaseFilter> transform;
    CComPtr<IBaseFilter> src;
    CComPtr<IGraphConfig> g_config;
    CComPtr<IPin> src_out;
    CComPtr<IPin> renderer_in;
    cap_gb.CoCreateInstance(CLSID_CaptureGraphBuilder2);
    gb.CoCreateInstance(CLSID_FilterGraph);
    cap_gb->SetFiltergraph(gb);
    gb.QueryInterface<IMediaControl>(&ctrl);
    JIF(hr);
    hr=src.CoCreateInstance(CLSID_MySource3);
    JIF(hr);
    hr=gb->AddFilter(src, L"Source");
    JIF(hr);
    hr=transform.CoCreateInstance(CLSID_MyTransform);
    JIF(hr);
    hr=gb->AddFilter(transform, L"Transform");
    JIF(hr);
    hr=renderer.CoCreateInstance(CLSID_MyRenderer2);
    JIF(hr);
    hr=gb->AddFilter(renderer, L"Renderer");
    JIF(hr);
    hr=cap_gb->RenderStream(NULL, NULL, src, transform, renderer);
    JIF(hr);

始端と終端ピンの取得

動的再接続の始端と終端ピンを取得する。ソースフィルタ、レンダラーフィルタともにピンが1つなのでシンプルなコードである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// get an out pin connected to transform.
    ULONG u;
    PIN_DIRECTION dir;
    {
        CComPtr<IEnumPins> enum_pins;
        hr=src->EnumPins(&enum_pins);
        JIF(hr);
        hr=enum_pins->Next(1, &src_out, &u);
        src_out->QueryDirection(&dir);
        JIF(hr);
    }
    // get an input pin
    {
        CComPtr<IEnumPins> enum_pins;
        hr=renderer->EnumPins(&enum_pins);
        JIF(hr);
        hr=enum_pins->Next(1, &renderer_in, &u);
        renderer_in->QueryDirection(&dir);
        JIF(hr);
    }

プッシュソースフィルタを動的再接続対応に拡張する

当ブログで公開している My Source Filter を動的再接続対応に拡張して My Source Filter3 として実装していく。My Source Filterの出力ピンクラスCPushPinCSourceStreamを基底クラスとしている。この基底クラスは動的再接続に対応していない。そこでCSourceStreamを動的再接続に対応するように拡張する。

クラス定義の修正

CPushPinクラスに対して、追加でIPinFlowControlインターフェイスを実装する。さらに基底クラスで実装されているCSourceStream::DoBufferProcessingLoopをこのクラスでオーバーライドする。

 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
// プッシュピンクラス (MySourceFilter.h)
class CPushPin : CSourceStream , IPinFlowControl {
public:
    DECLARE_IUNKNOWN
    CPushPin(HRESULT *phr, CSource *pFilter);
    ~CPushPin();
    STDMETHODIMP    NonDelegatingQueryInterface(REFIID riid, void **ppv);// (追加)
    STDMETHODIMP    Notify(IBaseFilter *pSelf, Quality q);
    // CSourceStreamの実装
    HRESULT            GetMediaType(CMediaType *pMediaType);
    HRESULT            CheckMediaType(const CMediaType *pMediaType);
    HRESULT            DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest);
    HRESULT            DoBufferProcessingLoop(void);// (追加)
    HRESULT            FillBuffer(IMediaSample *pSample);
    // IPinFlowControl (追加)
    HRESULT STDMETHODCALLTYPE Block(DWORD dwBlockFlags, HANDLE hEvent);
    // CBasePin (追加)
    STDMETHODIMP Disconnect(void);
private:
    CSource*        m_pFilter;            //このピンが所属しているフィルタへのポインタ
    unsigned __int64 m_Count;            //フレームカウンタ
    REFERENCE_TIME    m_rtFrameLength;    //1フレームあたりの時間
    HBITMAP m_Bitmap;
    LPDWORD m_BmpData;
    HDC     m_Hdc;
    // ブロックに使用 (追加)
    HANDLE m_BlockEvent;
    CCritSec m_BlockLock;
    DWORD m_BlockFlags;
};

動的再接続可能フラグの設定

ピンが動的再接続に対応している場合、CBasePin::SetReconnectWhenActiveを呼び出す必要がある。引数にtrueを渡すとピンがアクティブな状態(つまり再生またはポーズ状態)においても再接続が可能となる。

1
2
3
4
5
6
7
CPushPin::CPushPin(HRESULT *phr, CSource *pFilter) :
CSourceStream(NAME("CPushPin"), phr, pFilter, OUTPUT_PIN_NAME)
, m_pFilter(pFilter) ,m_Count(0), m_rtFrameLength(1000000)
, m_BmpData(NULL), m_Hdc(NULL), m_Bitmap(NULL), m_BlockFlags(0), m_BlockEvent(NULL)
{
    SetReconnectWhenActive(true);
}

ブロックの開始・終了

動的再接続が始まると、まず出力ピンからのデータ送信をブロックするよう要求される。

IPinFlowControl::Blockメソッドは、ブロックの開始・終了を要求するときに呼び出される。ブロックが完了した時、引数で渡されるhEventのイベントをシグナルにする必要があるためm_hBlockEventにイベントハンドルを保存している。(*2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
HRESULT CPushPin::Block(DWORD dwBlockFlags, HANDLE hEvent) {
    CAutoLock lock(&m_BlockLock);
    if(dwBlockFlags==0) {
        if(hEvent!=NULL) {
            return E_INVALIDARG;
        }
        if(m_BlockEvent==NULL) {
            return S_FALSE;
        }
    }
    m_BlockFlags=dwBlockFlags;
    m_BlockEvent=hEvent; // イベントハンドルの保存
    return S_OK;
}

*2 つまりブロック完了までは非同期的に実行される。但しhEvent==NULLのときは同期的に動作することが規定されている。(今回は未実装。)

ピンの切断

ブロック完了後、ピンは一旦切断されるのでCBasePin::Disconnectが呼ばれる。ただし、既存の実装ではサポートされない。CBasePin::DisconnectInternalを呼び出すようにオーバーライドする。

1
2
3
4
STDMETHODIMP CPushPin::Disconnect(void) {
    CAutoLock cObjectLock(m_pLock);
    return __super::DisconnectInternal();
}

CBasePin::DisconnectInternalはピンの切断を行うメソッドである。CBasePin::Disconnectとの違って、ピンがアクティブ状態であっても切断処理を行うことができる。

送信の制御

CSourceStreamでは、基底クラスでストリーミングスレッドを起動してサンプルの送信を行っている。ストリーミングスレッドは、外部からのポーズや停止状態への遷移要求があるかどうかポーリングしつつ、FillBufferを呼び出してサンプルの送信を行っている。しかし動的再接続の実装に必要なブロックをサポートしていないので、このメソッドをオーバーライドしなければならない。

 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
HRESULT CPushPin::DoBufferProcessingLoop(void) {
    Command com;
    OnThreadStartPlay();
    do {
        while (!CheckRequest(&com)) {
            HRESULT hr;
            bool block=false;
            {
                CAutoLock lock(&m_BlockLock);
                if(m_BlockEvent!=NULL) {
                    SetEvent(m_BlockEvent);
                    NOTE("Blocked!!");
                    block=true;
                }
            }
            if(block) {
                while(m_BlockFlags!=0) { //ブロック解除されるまでループする
                    Sleep(1);
                }
                NOTE("Unblocked!!");
                // You must commit a new allocator.
                hr=m_pAllocator->Commit();
                if(FAILED(hr)) {
                    HrToStrByAMGet(hr);
                    return S_FALSE;
                }
            }
            CAutoLock lock(&m_BlockLock);
            IMediaSample *pSample;
            hr = GetDeliveryBuffer(&pSample,NULL,NULL,0);
(以下略)

whileループの冒頭で、m_BlockEventが設定されていないか確認し、もし設定されていればイベントをシグナル状態にする。

ブロックが解除されるとm_BlockFlagsが0に設定される。ここでピンは再接続を完了しているので、サンプルの送信ができる。

ここで注意してほしいのが、ピンを再接続されるとアロケータは初期化されているということである。従って、そのままではGetDeliveryBufferが失敗する。必ずIMemAllocator::Commitを呼び出してアロケータをコミットしておく必要がある。

レンダラフィルタを動的再接続対応に拡張する

当ブログで公開している My Renderer Filter を動的再接続対応に拡張して My Renderer Filter2 として実装していく。

入力ピンクラスの拡張

CBaseVideoRendererを基底クラスとした場合、入力ピンのインスタンスはCRendererInputPinクラスであり、CBaseRenderer::m_pInputPinにそのポインタが代入されている。動的再接続に対応するにはIPinConnectionを実装している必要がありますがCRendererInputPinには実装がない。そこで、独自の入力ピンクラスを定義する必要がある。

入力ピンクラスのインスタンス作成

CMyRenderer2のコンストラクタで、入力ピンクラスのインスタンスを作成する。ここで作成しておくと、基底クラスで作成されない。

1
2
3
4
5
6
7
8
CMyRenderer2::CMyRenderer2(IUnknown *pUnk, HRESULT *phr) :
CBaseVideoRenderer(CLSID_MyRenderer2, FILTER_NAME , pUnk, phr)
, m_Hwnd(NULL)
{
    // at line 616, renbase.cpp, do new an input pin if m_pInputPin == NULL.
    m_pInputPin=new CDynReconnInputPin(this, phr, L"Dynamic Reconn");
    m_pInputPin->SetReconnectWhenActive(true);
}

ピンの切断

IPinConnection::DynamicDisconnectを実装する。動的再接続時にピンが一時的に切断されるときはCBasePin::Disconnectではなく、このメソッドが呼び出される。ここでもCBasePin::DisconnectInternalを呼び出す。

1
2
3
4
5
6
HRESULT STDMETHODCALLTYPE CDynReconnInputPin::DynamicDisconnect(void) {
    HRESULT hr=NOERROR;
    CAutoLock cObjectLock(m_pLock);
    hr=DisconnectInternal();
    return hr;
}

メディアタイプの調整

グラフの実行中に、ピンがあるメディアタイプを受け入れられるか確認するためにIPinConnection::DynamicQueryAcceptが呼び出される。今回は面倒なので単にS_OKを返している。(動的フォーマット対応する場合に実装する必要があるが、今回は不要。)

ストリーム終了通知

ピンが切断される前、途中のフィルタに処理が保留されているサンプルを送信するためにストリーム終了通知が呼び出される。動的再接続の場合、IPinConnection::NotifyEndOfStreamが呼び出される。引数にイベントハンドルが渡されるので、ストリーム終了通知をこのフィルタで処理したらシグナル状態にする。

動的再接続時のストリーム終了通知は、EC_COMPLETEを送信してはいけない。そこでEndOfStreamをオーバーライドし、もしイベントハンドルが設定されていたらEC_COMPLETEを送信しない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
HRESULT STDMETHODCALLTYPE CDynReconnInputPin::NotifyEndOfStream(HANDLE hNotifyEvent) {
    if(hNotifyEvent==NULL) {
        if(m_Event==NULL) {
            return S_FALSE;
        }
    }
    m_Event=hNotifyEvent;
    return S_OK;
}
HRESULT STDMETHODCALLTYPE CDynReconnInputPin::IsEndPin(void) {
    return S_FALSE;
}
STDMETHODIMP CDynReconnInputPin::EndOfStream() {
    if(m_Event!=NULL) {
        SetEvent(m_Event);
        // DO NOT notify EC_COMPLETE.
        NOTE("EndOfStream");
        return S_OK;
    }
    return __super::EndOfStream();
}

再び、アプリケーション

フィルタの動的再接続対応ができたので、あとはフィルタグラフを制御するだけである。再生状態にしたあと、IPinFlowControl::Blockを呼び、一時的にサンプルの送信をブロックする。そのあとIGraphConfig::ReConnectを呼び、再接続を行う。再度IPinFlowControl::Blockでブロックを解除すると、サンプルの送信が再開される。

 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
// run my graph
    hr=ctrl->Run();
    JIF(hr);

    Sleep(2000); // wait 2 sec

    // Query IPinFlowControl
    CComPtr<IPinFlowControl> flow_ctrl;
    hr=src_out.QueryInterface<IPinFlowControl>(&flow_ctrl);
    JIF(hr);
    // Block
    HANDLE block_event=CreateEvent(NULL, FALSE, FALSE, NULL);
    hr=flow_ctrl->Block(AM_PIN_FLOW_CONTROL_BLOCK, block_event);
    JIF(hr);
    WaitForSingleObject(block_event, INFINITE);
    // Reconnect
    gb.QueryInterface<IGraphConfig>(&g_config);
    JIF(hr);
    hr=g_config->Reconnect(src_out, renderer_in
        , NULL, NULL, NULL
        , AM_GRAPH_CONFIG_RECONNECT_DIRECTCONNECT);
    JIF(hr);
    // Unblock
    hr=flow_ctrl->Block(0, NULL);
    JIF(hr);
    Sleep(2000); // wait 2 sec
    // stop my graph
    ctrl->Stop();
    CloseHandle(block_event);

https://github.com/mahorigahama/DynReConn


Comments

comments powered by Disqus