DirectShow 过滤器开发第二部分:Live Source Filter






4.83/5 (19投票s)
一个通用的源滤镜,它暴露一个接口,用于向下游推送预定义大小和帧率的 RGB 样本,并可用于任何自定义帧输入场景。
引言
在数字视频领域工作的绝大多数软件和硬件供应商都提供了一种源滤镜,能够从视频采集卡或 IP 摄像机等硬件设备获取帧。因此,我们作为软件开发者的任务很简单,就是构建一个滤镜图,添加这个源滤镜,然后渲染它。然而,并非所有应用程序和硬件设备都是为了 DirectShow 而设计的,当您需要使用它们时,您可能会自己构建一个源滤镜来包装该应用程序或硬件提供商的 API。
为了使这项任务变得容易,我决定创建一个通用的源滤镜,它暴露一个接口,用于向下游推送预定义大小和帧率的 RGB 样本,并可用于任何自定义帧输入场景(我希望如此!)。
在继续之前,请参阅本系列文章的第一部分,其中涵盖了滤镜开发的先决条件、滤镜注册和调试,这些内容对于所有类型的滤镜都是相同的。
源滤镜
源滤镜是滤镜图中最重要的滤镜,因为它决定了媒体的大小、帧率和媒体格式。每个滤镜图至少包含一个源滤镜,它可以产生多个媒体流,不一定是相同类型的。例如,一个媒体拆分器源滤镜可以产生视频、音频和文本(字幕)流,其中每个流都通过其自己的引脚被推送到下游滤镜。通常,关于流的信息包含在媒体文件中。当没有媒体文件时,这些信息就驻留在源滤镜内部,或者驻留在其输出引脚内部。
构建源滤镜时,您基本上有两种选择:
- 构建一个继承自
CSource
基类的源滤镜,并添加一个继承自CSourceStream
的嵌套引脚类,该类负责实际的帧创建和传递。CSourceStream
继承自CAMThread
类,该类封装了负责将媒体样本传递到下游滤镜的后台线程。这些类型的滤镜称为推送源滤镜,它们在 MSDN 上有详细的文档,并且有许多示例,例如PushSource 和Bouncing Ball 示例。此外,CodeProject 上也有一些文章描述了如何构建这些滤镜。因此,我将不涵盖这个主题。 - 相反,我将介绍文档较少的实时源滤镜。要创建实时源滤镜,请使用
CBaseFilter
作为滤镜基类,其引脚类应继承自CBaseOutputPin
。
class CLiveSourceStream;
class CLiveSource : public CBaseFilter, public IAMFilterMiscFlags
{
public:
DECLARE_IUNKNOWN;
CLiveSource(LPUNKNOWN pUnk, HRESULT* phr);
virtual ~CLiveSource(void);
static CUnknown* WINAPI CreateInstance(LPUNKNOWN pUnk, HRESULT *phr);
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv);
int GetPinCount();
CBasePin* GetPin(int n);
virtual ULONG STDMETHODCALLTYPE GetMiscFlags( void)
{
return AM_FILTER_MISC_FLAGS_IS_SOURCE;
}
private:
CLiveSourceStream* m_pOutputPin;
CCritSec m_critSec;
};
CBaseFilter
是一个抽象类,您需要实现它的两个纯虚函数:
int CLiveSource::GetPinCount()
{
CAutoLock cAutoLock(&m_critSec);
return 1;
}
CBasePin* CLiveSource::GetPin(int n)
{
CAutoLock cAutoLock(&m_critSec);
return m_pOutputPin;
}
GetPinCount
返回 1,因为只有一个输出引脚,该引脚通过 ILiveSource
接口从另一个上下文推送视频样本。GetPin
返回指向该唯一引脚的指针。
根据 MSDN 文档,实时源滤镜以不恒定的速率传递媒体样本。如果滤镜实现了 IAMFilterMiscFlags
接口并且滤镜的输出引脚实现了 IAMPushSource
接口,则该滤镜被视为实时源滤镜。
class CLiveSourceStream : public CBaseOutputPin,
public ILiveSource, public IAMPushSource
{
public:
DECLARE_IUNKNOWN;
CLiveSourceStream(CBaseFilter *pFilter, CCritSec *pLock, HRESULT *phr);
virtual ~CLiveSourceStream();
// CBaseOutputPin overrides
virtual HRESULT GetMediaType(int iPosition, CMediaType* pmt);
virtual HRESULT CheckMediaType(const CMediaType *pmt);
virtual HRESULT DecideBufferSize(IMemAllocator *pAlloc,
ALLOCATOR_PROPERTIES *ppropInputRequest);
// ILiveSource members
virtual HRESULT AddFrame(HBITMAP hBmp);
virtual HRESULT AddFrame(BYTE* pBuffer, DWORD size);
virtual HRESULT SetFrameRate(int frameRate);
virtual HRESULT SetBitmapInfo(BITMAPINFOHEADER& bInfo);
// IAMPushSource members
virtual STDMETHODIMP GetPushSourceFlags(ULONG *pFlags);
virtual STDMETHODIMP SetPushSourceFlags(ULONG Flags);
virtual STDMETHODIMP SetStreamOffset(REFERENCE_TIME rtOffset);
virtual STDMETHODIMP GetStreamOffset(REFERENCE_TIME *prtOffset);
virtual STDMETHODIMP GetMaxStreamOffset(REFERENCE_TIME *prtMaxOffset);
virtual STDMETHODIMP SetMaxStreamOffset(REFERENCE_TIME rtMaxOffset);
virtual STDMETHODIMP GetLatency(REFERENCE_TIME *prtLatency);
virtual STDMETHODIMP Notify(IBaseFilter * pSender, Quality q);
private:
HRESULT GetPixelData(HBITMAP hBmp, BYTE** ppData, int* pSize);
HRESULT GetMediaSample(IMediaSample** ppSample);
private:
BITMAPINFOHEADER m_bmpInfo;
int m_frameRate;
REFERENCE_TIME m_rtFrameRate;
REFERENCE_TIME m_lastFrame;
};
尽管这里实现了 IAMPushSource
,但由于只有一个输出引脚且不需要同步,所以它几乎没有用处。例如,如果您想要一个具有不同样本率的第二个引脚,那么您应该按照此处的描述来实现该接口。
ILiveSource
滤镜有四个方法:
struct ILiveSource : IUnknown
{
// Adds bitmap to the video sequence
virtual HRESULT AddFrame(HBITMAP hBmp) PURE;
// Adds pixel data buffer to the video sequence
virtual HRESULT AddFrame(BYTE* pBuffer, DWORD size) PURE;
// Set the video frame info.
// Default value is width = 704,
// height = 576 (4CIF) and 32 bits per pixel
virtual HRESULT SetBitmapInfo(BITMAPINFOHEADER& bInfo) PURE;
// Set the expected frame rate of the video.
// Value should be in range of [0,30]
// Default value is 0
virtual HRESULT SetFrameRate(int frameRate) PURE;
};
前两个方法负责将帧传递到正在运行的滤镜图中。第一个方法使用 GetObject
API(下面描述)来获取像素数据和其他位图参数。第二个 AddFrame
重载函数接受一个指向像素数据缓冲区和缓冲区大小的指针,以防止溢出。大小会与 BITMAPINFOHEADER
结构中的 biSizeImage
成员进行比较,以确定其是否有效。
默认情况下,BITMAPINFOHEADER
被设置为 4CIF 帧(704 x 576)和每像素 32 位;但是,您可以使用 SetBitmapInfo
方法进行设置。
SetFrameRate
用于设置每个媒体样本的开始和结束时间。使用这些值,渲染器滤镜会调度每个样本进行显示并监控质量控制。默认情况下,帧率设置为 0,这意味着不会进行调度,每个媒体样本将在到达渲染器的输入引脚后立即渲染。
媒体样本
IMediaSample
是 DirectShow 滤镜之间使用的传输单元。源滤镜负责创建它们,填充数据,并将它们传递到下游。转换滤镜对这些数据执行一些操作,而渲染器滤镜则将它们渲染到屏幕、网络或其他位置。每个媒体样本都包含一个内存缓冲区,其中包含实际的样本数据——在视频的情况下,它通常包含原始或编码的像素数据。每个样本还具有控制播放的参考时间和从渲染器滤镜到源滤镜或其他可以处理此类消息的组件反向发送的质量控制消息。
当使用 CBaseOutputPin
类时,您有责任从内存分配器获取媒体样本,用数据填充它,设置时间戳,设置样本大小,并将样本传递到连接的下游滤镜的输入引脚。因此,AddFrame
方法的实现如下:
HRESULT CLiveSourceStream::AddFrame(HBITMAP hBmp)
{
CAutoLock cAutoLock(m_pLock);
IMediaSample* pSample = NULL;
BYTE* pData = NULL;
HRESULT hr = GetMediaSample(&pSample);
if(FAILED(hr))
{
return hr;
}
hr = pSample->GetPointer(&pData);
if(FAILED(hr))
{
pSample->Release();
return hr;
}
hr = GetPixelData(hBmp, &pData, &iSize);
if(FAILED(hr))
{
pSample->Release();
return hr;
}
hr = pSample->SetActualDataLength(iSize);
if(FAILED(hr))
{
pSample->Release();
return hr;
}
hr = pSample->SetSyncPoint(TRUE);
if(FAILED(hr))
{
pSample->Release();
return hr;
}
hr = this->Deliver(pSample);
pSample->Release();
return hr;
}
GetMediaSample
从基类 CBaseOutputPin
的内存分配器中分配一个样本,并传递开始和结束时间,这些时间也可以为零。
HRESULT CLiveSourceStream::GetMediaSample(IMediaSample** ppSample)
{
REFERENCE_TIME rtStart = m_lastFrame;
m_lastFrame += m_rtFrameRate;
return this->GetDeliveryBuffer(ppSample, &rtStart, &m_lastFrame, 0);
}
GetPixelData
将位图参数与先前设置(或默认)的参数进行验证,如果参数 OK,则返回指向像素数据和数据大小的指针。
HRESULT CLiveSourceStream::GetPixelData(HBITMAP hBmp, BYTE** ppData, int* pSize)
{
ASSERT(hBmp);
BITMAP bmp = {0};
int res = ::GetObject(hBmp, sizeof(BITMAP), &bmp);
if(res != sizeof(BITMAP))
{
return E_FAIL;
}
if(bmp.bmBitsPixel != m_bmpInfo.biBitCount ||
bmp.bmHeight != m_bmpInfo.biHeight ||
bmp.bmWidth != m_bmpInfo.biWidth)
{
return E_INVALIDARG;
}
*pSize = bmp.bmWidthBytes * bmp.bmHeight;
memcpy(*ppData, bmp.bmBits, *pSize);
return S_OK;
}
成功传递媒体样本后,您需要释放它,因为它是一个简单的 COM 对象,并使用引用计数进行内存管理。请注意,需要样本超出 Deliver
方法调用范围的下游滤镜需要在该样本上调用 AddRef
。
COM 机制
正如您已经知道的,滤镜是 COM 容器,这意味着您必须实现三个基本的 COM 结构:
- 用于创建滤镜类的静态工厂方法
CUnknown* WINAPI CLiveSource::CreateInstance(LPUNKNOWN pUnk, HRESULT *phr) { CUnknown* pNewFilter = new CLiveSource(pUnk, phr); if (phr) { if (pNewFilter == NULL) *phr = E_OUTOFMEMORY; else *phr = S_OK; } return pNewFilter; }
- 当您的滤镜实现某些接口时,您应该重写
QueryInterface
方法,并通过调用GetInterface
方法来增加引用计数。STDMETHODIMP CLiveSource::NonDelegatingQueryInterface(REFIID riid, void **ppv) { CheckPointer(ppv, E_POINTER); if(riid == IID_ILiveSource) { return GetInterface((ILiveSource*) m_pOutputPin, ppv); } else if(riid == IID_IAMPushSource) { return GetInterface((IAMPushSource*) m_pOutputPin, ppv); } else if(riid == IID_IAMFilterMiscFlags) { return GetInterface((IAMFilterMiscFlags*) this, ppv); } else { return CBaseFilter::NonDelegatingQueryInterface(riid, ppv); } }
- 当实现像本示例代码中的
ILiveSource
这样的接口时,您应该使用DECLARE_IUNKNOWN
宏来实现IUnknown
。
使用代码
成功构建和注册滤镜后,您就可以在代码中使用它了。
::CoInitialize(NULL);
graph.CoCreateInstance(CLSID_FilterGraph);
graph->QueryInterface(IID_IMediaControl, (void**)&media);
graph->QueryInterface(IID_IFilterGraph2, (void**)&m_filterGraph2);
pSource.CoCreateInstance(CLSID_CLiveSource);
pSource->QueryInterface(IID_ILiveSource, (void**)&pLiveSource);
m_filterGraph2->AddFilter(pSource, LIVE_FILTER_NAME);
IPin* outpin = NULL;
pSource->FindPin(LIVE_OUTPIN_NAME, &outpin);
m_filterGraph2->Render(outpin);
outpin->Release();
RECT area;
m_videoWnd.GetClientRect(&area);
CComPtr<IVideoWindow> pWnd = NULL;
graph->QueryInterface(IID_IVideoWindow, (void**)&pWnd);
pWnd->put_Owner((OAHWND)m_videoWnd.m_hWnd);
pWnd->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);
pWnd->SetWindowPosition(area.left, area.top,
area.right - area.left, area.bottom - area.top);
pWnd.Release();
media->Run();
在其他地方,可能在另一个线程中,由于滤镜是线程安全的,您可以调用 AddFrame
。
HBITMAP hBmp = GetFrame();
HRESULT hr = pLiveSource->AddFrame(hBmp);
if(FAILED(hr))
{
// Handle error here !
}
::DeleteObject(hBmp);
参考文献
历史
- 15.2.2011
- 13.3.2011
初始版本。
IAddFrame
接口中的固定调用约定。