Skip to content

DirectShowVirtualCamera

HexagramNM edited this page Mar 1, 2025 · 3 revisions

DirectShowの仮想カメラ

DirectShowというWindows用のビデオや音声周りのフレームワークを利用することで、PC内の映像をカメラ扱いで配信する仮想カメラを実装することができます。以下のような特徴があります。

  • 古いフレームワークであるため、Windows10やそれ以前の古いWindowsでも動作する。

    • まだ大丈夫だが、MicrosoftとしてはMediaFoundationといった新しいAPIを使用するよう提案しており、DirectShowがいつか非推奨となる可能性はある。
  • ZoomやTeamsなど各アプリケーションが仮想カメラ用のdllファイルを読み込み、機能を利用する

    • そのため、32bitアプリケーション (x86) 、64bitアプリケーション (x64) の両方に対応しようとすると、x86版と x64版のdllを両方作る必要がある。
  • Windows標準のカメラアプリケーションではDirectShowの仮想カメラが使用できない(そもそも切り替えられない)

クラスの構成

まず前提として、DirectShowにはフィルタとピンという概念があります。

  • フィルタ: 映像を流す機能(ソースフィルタ)や映像を画面に移す機能(レンダリングフィルタ)など、1つの機能。仮想カメラはソースフィルタにあたります。

  • ピン: 映像信号などをやり取りする端子で、ピンを介してフィルタどうしがつながります。


NM_WCVCam_DSではフィルタとピンのそれぞれにクラスがあります。

  • NMVCamSource: ソースフィルタにあたるクラスで、CSourceクラスを継承している。内部でNMVCamPinのポインタをメンバにもつ。

  • NMVCamPin: ピンにあたるクラスで、主にCSourceStreamクラスを継承している。後述する仮想カメラとして認識されるためのインタフェースも継承している。映像サンプルを送る処理を実装している。

ソースフィルタの登録

ソースフィルタの登録周りはNM_WCVCam_DS/DllSetup.cppに実装を書いています。

最初にクラスIDからソースフィルタが生成されるように、CFactoryTemplateを作っておきます。

// NAME_NM_WCVCam_DSやCLSID_NM_WCVCam_DSはglobal_config.hで定義
// afFilterInfoはNM_WCVCam_DS/NMVCamFilter.hで定義
CFactoryTemplate g_Templates[] = {
    { NAME_NM_WCVCam_DS, &CLSID_NM_WCVCam_DS, NMVCamSource::CreateInstance, NULL, &afFilterInfo}
};

DirectShowのソースフィルタのサンプルでは、dllをレジストリに登録するDllRegisterServer関数でAMovieDllRegisterServer2(TRUE);のみを呼び出すものが多いです。しかし、これだけだとWebカメラなどが含まれるVideo Capture Sourcesのカテゴリに仮想カメラが登録されず、カメラとして認識されません。

以下のように、FilterMapper2のRegisterFilterメソッドを用いて、仮想カメラのクラスIDCLSID_NM_WCVCam_DSがカテゴリCLSID_VideoInputDeviceCategoryに紐づくよう登録する必要があります。

REGFILTER2 rf2FilterReg = {
    1,
    MERIT_NORMAL,
    1,
    &sudPins //NMVCamFilter.hで定義
};

STDAPI DllRegisterServer() 
{
    HRESULT hr;
    IFilterMapper2 *pFM2 = NULL;
    hr = AMovieDllRegisterServer2(TRUE);
    if (FAILED(hr)) 
    {
        return hr;
    }

    hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,
        IID_IFilterMapper2, (void **)&pFM2);
    if (FAILED(hr)) 
    {
        return hr;
    }

    hr = pFM2->RegisterFilter(
        CLSID_NM_WCVCam_DS,
        NAME_NM_WCVCam_DS,
        NULL,
        &CLSID_VideoInputDeviceCategory,
        NAME_NM_WCVCam_DS,
        &rf2FilterReg
    );

    if (pFM2) 
    {
        pFM2->Release();
    }
    return hr;
}

上記のDllRegisterServerに合わせて、dllをレジストリから登録解除するDllUnregisterServerは以下のようにする必要があります。

STDAPI DllUnregisterServer() 
{
    HRESULT hr;
    IFilterMapper2 *pFM2 = NULL;
    hr = AMovieDllRegisterServer2(FALSE);
    if (FAILED(hr)) 
    {
        return hr;
    }

    hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,
        IID_IFilterMapper2, (void **)&pFM2);
    if (FAILED(hr)) 
    {
        return hr;
    }

    hr = pFM2->UnregisterFilter(
        &CLSID_VideoInputDeviceCategory,
        NAME_NM_WCVCam_DS,
        CLSID_NM_WCVCam_DS
    );

    if (pFM2) 
    {
        pFM2->Release();
    }
    return hr;
}

インタフェースの実装

ソースフィルタが仮想カメラとして認識されるためには、ピンクラスNMVCamPinで以下の3つのインタフェースを継承する必要があります。

  • IAMStreamConfig
  • IKsPropertySet
  • IAMFilterMiscFlags

これらを継承する際、以下のメソッドを実装する必要があります。(IUnknownIAMStreamConfigIKsPropertySetに継承されています。)

  • IUnknown
    • QueryInterface
    • AddRef
    • Release
  • IAMStreamConfig
    • GetFormat
    • GetNumberOfCapabilities
    • GetStreamCaps
    • SetFormat
  • IKsPropertySet
    • Get
    • Set
    • QuerySupported
  • IAMFilterMiscFlags
    • GetMiscFlags

基本的にはNM_WCVCam_DS/NMVCamPinImpl.cppにあるように実装します。IAMStreamConfigGetStreamCapsはこのピンが送る映像の形式を取得するメソッドで、ここで設定するパラメタはどういう映像を配信するかで変更すべき可能性があります。以下のコードのコメントにどういうパラメタか記載します。

HRESULT NMVCamPin::GetStreamCaps(
    int           iIndex,
    AM_MEDIA_TYPE **ppmt,
    BYTE          *pSCC
)
{
    *ppmt = CreateMediaType(&m_mt);
    VIDEOINFOHEADER *pvi = (VIDEOINFOHEADER *)(*ppmt)->pbFormat;

    pvi->bmiHeader.biCompression = BI_RGB; //圧縮形式。ここでは非圧縮のRGBで指定。
    pvi->bmiHeader.biBitCount = PIXEL_BIT; //1ピクセル当たりのbit数
    pvi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); //BITMAPINFOHEADER構造体のサイズ(バイト単位)
    pvi->bmiHeader.biWidth = VCAM_VIDEO_WIDTH; //画面の幅(ピクセル単位)
    pvi->bmiHeader.biHeight = VCAM_VIDEO_HEIGHT; //画面の高さ(ピクセル単位)
    pvi->bmiHeader.biPlanes = 1; //ここは必ず1。
    pvi->bmiHeader.biSizeImage = GetBitmapSize(&pvi->bmiHeader); //1フレームあたりの画像のサイズ(バイト単位)
    pvi->bmiHeader.biClrImportant = 0; //重要な色の数。0にするとすべての色が重要という扱いになる。とりあえず0でよい。

    SetRectEmpty(&(pvi->rcSource));
    SetRectEmpty(&(pvi->rcTarget));

    (*ppmt)->majortype = (const GUID)(*sudPinTypes.clsMajorType); //ピンのメジャータイプ(仮想カメラの場合は MEDIATYPE_Video)
    (*ppmt)->subtype = (const GUID)(*sudPinTypes.clsMinorType); //ピンのサブタイプ(仮想カメラの場合は MEDIASUBTYPE_RGB24)
    (*ppmt)->formattype = FORMAT_VideoInfo; //ppmtの設定で使用するフォーマット形式。仮想カメラの場合はFORMAT_VideoInfo
    (*ppmt)->bTemporalCompression = FALSE; //フレームの時間的な圧縮があるか(中間フレームを生成するか)
    (*ppmt)->bFixedSizeSamples = TRUE; //1フレームあたりのデータサイズは固定か(非圧縮ならTRUE)
    (*ppmt)->lSampleSize = pvi->bmiHeader.biSizeImage; //1フレームあたりの画像のサイズ(バイト単位)
    (*ppmt)->cbFormat = sizeof(VIDEOINFOHEADER); //1フレームあたりの画像のサイズ(バイト単位)

    VIDEO_STREAM_CONFIG_CAPS *pvscc = (VIDEO_STREAM_CONFIG_CAPS *)pSCC;
    pvscc->guid = FORMAT_VideoInfo; //pvsccの設定で使用するフォーマット形式。仮想カメラの場合はFORMAT_VideoInfo
    pvscc->VideoStandard = AnalogVideo_None; //アナログ形式のビデオをサポートするか。通常はAnalogVideo_None

    //ここからの指定はMicrosoftのドキュメント上だとDeprecatedとなっているので、指定しなくても良さそう。
	//以下は入力画像について
    pvscc->InputSize.cx = VCAM_VIDEO_WIDTH; //画像の最大サイズの幅(ピクセル単位)
    pvscc->InputSize.cy = VCAM_VIDEO_HEIGHT; //画像の最大サイズの高さ(ピクセル単位)
    pvscc->MinCroppingSize.cx = VCAM_VIDEO_WIDTH; //拡大縮小した際の画像の最小の幅(ピクセル単位)
    pvscc->MinCroppingSize.cy = VCAM_VIDEO_HEIGHT; //拡大縮小した際の画像の最小の高さ(ピクセル単位)
    pvscc->MaxCroppingSize.cx = VCAM_VIDEO_WIDTH; //拡大縮小した際の画像の最大の幅(ピクセル単位)
    pvscc->MaxCroppingSize.cy = VCAM_VIDEO_HEIGHT; //拡大縮小した際の画像の最大の高さ(ピクセル単位)
    pvscc->CropGranularityX = 80; //拡大縮小する際の幅の変化量(ピクセル単位)
    pvscc->CropGranularityY = 60; //拡大縮小する際の高さの変化量(ピクセル単位)
    pvscc->CropAlignX = 0; //入力画像の矩形について、水平方向で合わせるべき位置?
    pvscc->CropAlignY = 0; //入力画像の矩形について、垂直方向で合わせるべき位置?

    pvscc->MinOutputSize.cx = VCAM_VIDEO_WIDTH; //拡大縮小した際の画像の最小の幅(ピクセル単位)
    pvscc->MinOutputSize.cy = VCAM_VIDEO_HEIGHT; //拡大縮小した際の画像の最小の高さ(ピクセル単位)
    pvscc->MaxOutputSize.cx = VCAM_VIDEO_WIDTH; //拡大縮小した際の画像の最大の幅(ピクセル単位)
    pvscc->MaxOutputSize.cy = VCAM_VIDEO_HEIGHT; //拡大縮小した際の画像の最大の高さ(ピクセル単位)
    pvscc->OutputGranularityX = 0; //拡大縮小する際の幅の変化量(ピクセル単位)
    pvscc->OutputGranularityY = 0; //拡大縮小する際の高さの変化量(ピクセル単位)
    pvscc->StretchTapsX = 0; //横方向に拡大する際に、どうピクセルを処理するか
    pvscc->StretchTapsY = 0; //縦方向に拡大する際に、どうピクセルを処理するか
    pvscc->ShrinkTapsX = 0; //横方向に縮小する際に、どうピクセルを処理するか
    pvscc->ShrinkTapsY = 0; //縦方向に縮小する際に、どうピクセルを処理するか
    //Deprecatedとなっているのはここまで
    
    pvscc->MinFrameInterval = 200000; //最小のフレーム間の時間(100nsec単位)。この指定だと50fps
    pvscc->MaxFrameInterval = 50000000; //最大のフレーム間の時間(100nsec単位)。この指定だと0.2fps
    pvscc->MinBitsPerSecond = (VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BIT) / 5; //1秒あたりの最小ビットレート
    pvscc->MaxBitsPerSecond = (VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BIT) * 50; //1秒あたりの最大ビットレート

    return S_OK;
}

ここで出てくる構造体の詳細については以下のドキュメントをご覧ください。

ピンクラスの処理の実装

ピンクラスであるNMVCamPinCSourceStreamクラスを継承しているため、以下のメソッドを実装する必要があります。

  • Notify (CSourceStreamが継承しているCBasePinにあるメソッド)
  • GetMediaType
  • CheckMediaType
  • DecideBufferSize
  • FillBuffer

特に仮想カメラを自作する際に修正しうるGetMediaTypeFillBufferについて以下で解説してきます。それ以外はNM_WCVCam_DS/NMVCamPin.cppのコードをご覧ください。

映像形式の取得: GetMediaType

このメソッドは、ピンクラスが送信する映像の形式を取得するためのメソッドです。仮想カメラの映像サイズを変更する場合はこの関数を修正する必要があります。

HRESULT NMVCamPin::GetMediaType(CMediaType *pMediaType) 
{
    HRESULT hr=NOERROR;
    VIDEOINFO *pvi=(VIDEOINFO *)pMediaType->AllocFormatBuffer(sizeof(VIDEOINFO));
    ZeroMemory(pvi, sizeof(VIDEOINFO));

    pvi->AvgTimePerFrame=_rtFrameLength;

    //仮想カメラの映像サイズを変更する場合は、このpBmiで設定する値を変更する必要がある。
    BITMAPINFOHEADER *pBmi=&(pvi->bmiHeader);
    pBmi->biSize=sizeof(BITMAPINFOHEADER);
    pBmi->biWidth = VCAM_VIDEO_WIDTH;
    pBmi->biHeight = VCAM_VIDEO_HEIGHT;
    pBmi->biPlanes=1;
    pBmi->biBitCount=PIXEL_BIT;
    pBmi->biCompression=BI_RGB;
    pvi->bmiHeader.biSizeImage=DIBSIZE(pvi->bmiHeader);

    SetRectEmpty(&(pvi->rcSource));
    SetRectEmpty(&(pvi->rcTarget));

    pMediaType->SetType(&MEDIATYPE_Video);
    pMediaType->SetFormatType(&FORMAT_VideoInfo);

    const GUID subtype=GetBitmapSubtype(&pvi->bmiHeader);
    pMediaType->SetSubtype(&subtype);

    pMediaType->SetTemporalCompression(FALSE);
    const int bmpsize=DIBSIZE(*pBmi);
    pMediaType->SetSampleSize(bmpsize);
    
    return hr;
}

映像サンプルの送信: FillBuffer

仮想カメラが映像の1フレームを取得する度に呼び出されるメソッドです。引数で受け取るサンプルに1フレームの画像のピクセルデータや時間情報を渡します。そのため、毎フレームの画像を作り、ピクセルデータを渡す処理はここに書くことになります。

処理の最後にstd::this_thread::sleep_forを呼び出すとCPUの使用率を抑え、アプリケーションが反応しやすくなります。逆にないと、この処理の呼び出しが多くなりすぎて、アプリケーションが反応しなくなることがありました。

HRESULT NMVCamPin::FillBuffer(IMediaSample *pSample) 
{
    HRESULT hr=E_FAIL;
    CheckPointer(pSample,E_POINTER);
    // ダウンストリームフィルタが
    // フォーマットを動的に変えていないかチェック
    ASSERT(m_mt.formattype == FORMAT_VideoInfo);
    ASSERT(m_mt.cbFormat >= sizeof(VIDEOINFOHEADER));
    // フレームに書き込み
    LPBYTE pSampleData=NULL;
    const long size=pSample->GetSize();
    pSample->GetPointer(&pSampleData);

    CRefTime ref;
    m_pFilter->StreamTime(ref);

    // ここでpSampleDataにピクセルデータを入れることで、
    // 映像の1フレーム分の画像が送られ、仮想カメラ上で表示されます。
    if (_sharedCaptureWindowTexture) {
        GetSampleOnCaptureWindow(pSampleData);
    }
    else {
        CopyMemory((PVOID)pSampleData, (PVOID)_placeholderBitmapData,
            VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BYTE);
    }

    const REFERENCE_TIME delta=_rtFrameLength;
    REFERENCE_TIME start_time=ref;
    FILTER_STATE state;
    m_pFilter->GetState(0, &state);
    if(state==State_Paused)
        start_time=0;
    REFERENCE_TIME end_time=(start_time+delta);
    pSample->SetTime(&start_time, &end_time);
    pSample->SetActualDataLength(size);
    pSample->SetSyncPoint(TRUE);

    //CPU使用率を抑えて、ZoomなどのUIの反応をしやすくするために適度にSleepする。
    std::this_thread::sleep_for(std::chrono::milliseconds(5));

    return S_OK;
}

注意点

ピンのサブタイプについて

NM_WCVCam_DS/NMVCamFilter.hでピンのタイプを定義しており、サブタイプをMEDIASUBTYPE_RGB24としています。ここをMEDIASUBTYPE_RGB32にしてしまうと、一部のアプリケーションが形式に対応していないためか、映像が表示されないことがあります。

const AMOVIESETUP_MEDIATYPE sudPinTypes =
{
    &MEDIATYPE_Video,
    &MEDIASUBTYPE_RGB24
};

FillBufferで渡すピクセルデータについて

映像サンプルとしてピクセルデータを渡す際、一番下に位置するピクセルデータから渡す必要があります。普通に上のピクセルからデータを渡してしまうと、上下が反転した状態で表示されることになります。

また、1ピクセルごとにBGRの順でデータを渡す必要があります。これは共有テクスチャの方でもフォーマットをBGRAに設定することで、アルファ成分だけスキップすればよいという状態にできます。


戻る

Clone this wiki locally