directshow 的文档比较详细,这里我们其实是要实现一个DirectShow 的 SourceFilter,在DirectShow提供的sdk包中,有实例代码(目录:Extras\DirectShow\Samples\C++\DirectShow\),我们的工程是拷贝Filters\PushSource然后做修改。
主要修改如下:
原来的PushSource注册的三个filter,每个filter一个pin,我们改成注册一个filter,包含两个pin,音视频各一个
const AMOVIESETUP_MEDIATYPE sudOpPinTypes[] = { { &MEDIATYPE_Video, // Major type &MEDIASUBTYPE_NULL // Minor type }, { &MEDIATYPE_Audio, // Major type &MEDIASUBTYPE_NULL // Minor type } }; const AMOVIESETUP_PIN sudMylibPin[] = { { L"Output", // Obsolete, not used. FALSE, // Is this pin rendered? TRUE, // Is it an output pin? FALSE, // Can the filter create zero instances? TRUE, // Does the filter create multiple instances? &CLSID_NULL, // Obsolete. NULL, // Obsolete. 1, // Number of media types. &sudOpPinTypes[0] // Pointer to media types. }, { L"Output", // Obsolete, not used. FALSE, // Is this pin rendered? TRUE, // Is it an output pin? FALSE, // Can the filter create zero instances? TRUE, // Does the filter create multiple instances? &CLSID_NULL, // Obsolete. NULL, // Obsolete. 1, // Number of media types. &sudOpPinTypes[1] // Pointer to media types. } }; const AMOVIESETUP_FILTER sudMylibSource = { &CLSID_MylibSource, // Filter CLSID g_wszMylib, // String name MERIT_DO_NOT_USE, // Filter merit 2, // Number pins sudMylibPin // Pin details }; // List of class IDs and creator functions for the class factory. This // provides the link between the OLE entry point in the DLL and an object // being created. The class factory will call the static CreateInstance. // We provide a set of filters in this one DLL. CFactoryTemplate g_Templates[] = { { g_wszMylib, // Name &CLSID_MylibSource, // CLSID CMylibSource::CreateInstance, // Method to create an instance of MyComponent NULL, // Initialization function &sudMylibSource // Set-up information (for filters) }, }; int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
注意将原来的CFactoryTemplate g_Templates[3]改成CFactoryTemplate g_Templates[],我一开始忘了修改,在将我们的filter注册到系统时间老是失败,后来一步步跟踪发现提供的CFactoryTemplate无效,才找到这个BUG
我们的SourceFilter需要打开一个URL,这一点实例代码里面没有相关内容。通过查文档发现需要暴露一个 IFileSourceFilter 接口,这样GraphBuilder才会将URL传递过来。
STDMETHODIMP CMylibSource::Load(LPCOLESTR pszFileName, const AM_MEDIA_TYPE *pmt) { Mylib_Open(pszFileName); return S_OK } STDMETHODIMP CMylibSource::GetCurFile(LPOLESTR *ppszFileName, AM_MEDIA_TYPE *pmt) { return E_FAIL; } STDMETHODIMP CMylibSource::NonDelegatingQueryInterface(REFIID riid, void **ppv) { /* Do we have this interface */ if (riid == IID_IFileSourceFilter) { return GetInterface((IFileSourceFilter *) this, ppv); } else { return CSource::NonDelegatingQueryInterface(riid, ppv); } }
IFileSourceFilter有两个函数:Load、GetCurFile,load直接调用我们的open,GetCurFile直接返回错误。然后在NonDelegatingQueryInterface查询接口时,把新加的接口暴露出去。
在增加继承IFileSourceFilter接口后,编译会报IUnkown的一些函数没有实现,只需要在头文件类的定义里面增加“DECLARE_IUNKNOWN”就可以解决问题。
因为我们的媒体SDK库输出的音视频是统一处理的,这里要实现的Pin用一个类就可以了,运行时会生成两个实例,音视频各一个。
如果不需要拖动的话,实现GetMediaType、DecideBufferSize、FillBuffer就可以了,支持拖动比较复杂,我们先看没有拖动的情况
HRESULT CMylibPin::GetMediaType(CMediaType *pMediaType) { CAutoLock cAutoLock(m_pFilter->pStateLock()); CheckPointer(pMediaType, E_POINTER); return FillMediaType(pMediaType); } HRESULT CMylibPin::DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest) { HRESULT hr; CAutoLock cAutoLock(m_pFilter->pStateLock()); CheckPointer(pAlloc, E_POINTER); CheckPointer(pRequest, E_POINTER); // Ensure a minimum number of buffers if (pRequest->cBuffers == 0) { pRequest->cBuffers = 20; } if (m_info->type == mylib_video) pRequest->cbBuffer = 2 * 1024 * 1024; else pRequest->cbBuffer = 2 * 1024; ALLOCATOR_PROPERTIES Actual; hr = pAlloc->SetProperties(pRequest, &Actual); if (FAILED(hr)) { return hr; } // Is this allocator unsuitable? if (Actual.cbBuffer < pRequest->cbBuffer) { return E_FAIL; } return S_OK; } // This is where we insert the DIB bits into the video stream. // FillBuffer is called once for every sample in the stream. HRESULT CMylibPin::FillBuffer(IMediaSample *pSample) { CheckPointer(pSample, E_POINTER); // If the bitmap file was not loaded, just fail here. CAutoLock cAutoLockShared(&m_cSharedState); Mylib_SampleEx2 sample; sample.stream_index = m_nIndex; HRESULT ret = m_SampleCache->ReadSample(sample, &m_bCancel); if (ret == S_OK) { BYTE *pData; long cbData; pSample->GetPointer(&pData); cbData = pSample->GetSize(); if (cbData > sample.buffer_length) cbData = sample.buffer_length; memcpy(pData, sample.buffer, cbData); pSample->SetActualDataLength(cbData); if (sample.start_time * UINTS_MICROSECOND < m_rtStart) m_rtStart = sample.start_time * UINTS_MICROSECOND; REFERENCE_TIME rtStart = sample.start_time * UINTS_MICROSECOND - m_rtStart; REFERENCE_TIME rtStop = rtStart + m_nSampleDuration; pSample->SetTime(&rtStart, &rtStop); pSample->SetSyncPoint(sample.is_sync); pSample->SetDiscontinuity(m_bDiscontinuty); m_bDiscontinuty = FALSE; } return ret; }
GetMediaType需要设置pMediaType,格式DShow我们的媒体编码格式,函数FillMediaType根据当前流是音频还是视频分别调用FillAAC1、FillAVC1。AAC、AVC都是编码格式,具体在DShow中的设置方式如下:
static HRESULT FillAVC1(CMediaType *pMediaType, Mylib_StreamInfoEx const * m_info) { MPEG2VIDEOINFO * pmvi = // maybe sizeof(MPEG2VIDEOINFO) + m_info->format_size - 7 - 4 (MPEG2VIDEOINFO *)pMediaType->AllocFormatBuffer(sizeof(MPEG2VIDEOINFO) + m_info->format_size - 11); if (pmvi == 0) return(E_OUTOFMEMORY); ZeroMemory(pmvi, pMediaType->cbFormat); pMediaType->SetSubtype(&MEDIASUBTYPE_AVC1); pMediaType->SetFormatType(&FORMAT_MPEG2Video); pMediaType->SetTemporalCompression(TRUE); pMediaType->SetVariableSize(); VIDEOINFOHEADER2 * pvi = &pmvi->hdr; SetRectEmpty(&(pvi->rcSource)); SetRectEmpty(&(pvi->rcTarget)); pvi->AvgTimePerFrame = UNITS / m_info->video_format.frame_rate; BITMAPINFOHEADER * bmi = &pvi->bmiHeader; bmi->biSize = sizeof(BITMAPINFOHEADER); bmi->biWidth = m_info->video_format.width; bmi->biHeight = m_info->video_format.height; bmi->biBitCount = 0; bmi->biPlanes = 1; bmi->biCompression = 0x31435641; // AVC1 //pmvi->dwStartTimeCode = 0; pmvi->cbSequenceHeader = m_info->format_size - 7; BYTE * s = (BYTE *)pmvi->dwSequenceHeader; My_uchar const * p = m_info->format_buffer; //My_uchar const * e = p + stream_info.format_size; My_uchar Version = *p++; My_uchar Profile = *p++; My_uchar Profile_Compatibility = *p++; My_uchar Level = *p++; My_uchar Nalu_Length = 1 + ((*p++) & 3); size_t n = (*p++) & 31; My_uchar const * q = p; for (size_t i = 0; i < n; ++i) { size_t l = (*p++); l = (l << 8) + (*p++); p += l; } memcpy(s, q, p - q); s += p - q; n = (*p++) & 31; q = p; for (size_t i = 0; i < n; ++i) { size_t l = (*p++); l = (l << 8) + (*p++); p += l; } memcpy(s, q, p - q); s += p - q; pmvi->dwProfile = Profile; pmvi->dwLevel = Level; pmvi->dwFlags = Nalu_Length; return S_OK; } static HRESULT FillAAC1(CMediaType *pMediaType, Mylib_StreamInfoEx const * m_info) { WAVEFORMATEX * wf = (WAVEFORMATEX *)pMediaType->AllocFormatBuffer(sizeof(WAVEFORMATEX) + m_info->format_size); if (wf == 0) return(E_OUTOFMEMORY); ZeroMemory(wf, pMediaType->cbFormat); pMediaType->SetSubtype(&MEDIASUBTYPE_RAW_AAC1); pMediaType->SetFormatType(&FORMAT_WaveFormatEx); pMediaType->SetTemporalCompression(TRUE); pMediaType->SetVariableSize(); wf->cbSize = sizeof(WAVEFORMATEX); wf->nChannels = m_info->audio_format.channel_count; wf->nSamplesPerSec = m_info->audio_format.sample_rate; wf->wBitsPerSample = m_info->audio_format.sample_size; wf->wFormatTag = WAVE_FORMAT_RAW_AAC1; memcpy(wf + 1, m_info->format_buffer, m_info->format_size); return S_OK; }
DecideBufferSize就比较简单了,我们针对音视频分别采用2M和2K的帧缓存大小,缓存数量都用20
HRESULT CMylibPin::DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest) { HRESULT hr; CAutoLock cAutoLock(m_pFilter->pStateLock()); CheckPointer(pAlloc, E_POINTER); CheckPointer(pRequest, E_POINTER); // Ensure a minimum number of buffers if (pRequest->cBuffers == 0) { pRequest->cBuffers = 20; } if (m_info->type == mylib_video) pRequest->cbBuffer = 2 * 1024 * 1024; else pRequest->cbBuffer = 2 * 1024; ALLOCATOR_PROPERTIES Actual; hr = pAlloc->SetProperties(pRequest, &Actual); if (FAILED(hr)) { return hr; } // Is this allocator unsuitable? if (Actual.cbBuffer < pRequest->cbBuffer) { return E_FAIL; } return S_OK; }
FillBuffer有点小麻烦,因为我们的媒体SDK库输出的音视频是混在一起的,都是Pin上的FillBuffer是分开的,所以这里做的一个Sample缓存,比如需要一个视频帧时,如果读出来的是音频帧,就缓存起来,知道读到一个视频帧。下次需要音频帧时,可以从缓冲里直接给。这里只给出FillBuffer的实现,没有缓存相关的代码。
HRESULT CMylibPin::FillBuffer(IMediaSample *pSample) { CheckPointer(pSample, E_POINTER); // If the bitmap file was not loaded, just fail here. CAutoLock cAutoLockShared(&m_cSharedState); Mylib_Sample sample; sample.stream_index = m_nIndex; HRESULT ret = m_SampleCache->ReadSample(sample, &m_bCancel); if (ret == S_OK) { BYTE *pData; long cbData; pSample->GetPointer(&pData); cbData = pSample->GetSize(); if (cbData > sample.buffer_length) cbData = sample.buffer_length; memcpy(pData, sample.buffer, cbData); pSample->SetActualDataLength(cbData); if (sample.start_time * UINTS_MICROSECOND < m_rtStart) m_rtStart = sample.start_time * UINTS_MICROSECOND; REFERENCE_TIME rtStart = sample.start_time * UINTS_MICROSECOND - m_rtStart; REFERENCE_TIME rtStop = rtStart + m_nSampleDuration; pSample->SetTime(&rtStart, &rtStop); pSample->SetSyncPoint(sample.is_sync); pSample->SetDiscontinuity(m_bDiscontinuty); m_bDiscontinuty = FALSE; } return ret; }
要支持拖动,Pin(注意是Pin,不是SourceFilter)需要继承CSourceSeeking,实现下面三个虚函数:
virtual HRESULT ChangeStart(); virtual HRESULT ChangeStop() {return S_OK;}; virtual HRESULT ChangeRate() {return E_FAIL;};
另外别忘了暴露新的接口:
STDMETHODIMP CMylibPin::NonDelegatingQueryInterface(REFIID riid, void **ppv) { if( riid == IID_IMediaSeeking && m_bSeekable) { return CSourceSeeking::NonDelegatingQueryInterface( riid, ppv ); } return CSourceStream::NonDelegatingQueryInterface(riid, ppv); }
ChangeRate是改变播放速度的,不支持,直接返回错误。ChangeStop是改变结束位置,这个对正常观看来说没有什么意义,返回OK好了。按照DShow的要求,改变位置后需要清空前面的音视频缓存(要通知到各个Filter),然后停止,再从新的位置开始:
void CMylibPin::ChangeStart() { if (ThreadExists()) { OutputDebugString(_T("UpdateFromSeek Cancel\r\n")); m_bCancel = TRUE; DeliverBeginFlush(); // Shut down the thread and stop pushing data. Stop(); m_SampleCache->Seek(m_rtStart); m_bDiscontinuty = TRUE; OutputDebugString(_T("UpdateFromSeek Resume\r\n")); m_bCancel = FALSE; DeliverEndFlush(); // Restart the thread and start pushing data again. Pause(); } }
代码写完了,编译脚本里面已经有自动注册到系统了。但是还有一件事情,要让系统知道我们的vod://协议用这个SourceFilter打开。方法是修改注册表,导入下面的内容:
[HKEY_CLASSES_ROOT\vod]
"Source Filter"="{6A881765-07FA-404b-B9B8-6ED429385ECC}"
现在可以通过graphedit测试了,打开url:vod://xxxxxxx。当然其他基于directShow的播放器都是可以播放这样的url的哦,试一下windows media player吧,哈哈,竟然也能播放呢。