Our final sample, ChessMen, does not use the VMR at all. DirectShow is about more than just file playback ” it can also be used to create video and audio files. The ChessMen application uses this feature to create an AVI file from a Direct3D animation. All that is required is a custom DirectShow source filter to capture the frame buffer and send it down the filter graph. The rest of the process ” encoding, muxing, and file writing ” is done using existing DirectShow filters.
Unfortunately, it is not possible to capture the frame buffer in real time. This is a fundamental limitation of modern graphics hardware. Writing to the graphics card is fast, but reading from the graphics card is exceedingly slow. If you copy every frame into CPU memory, you cannot possibly maintain a reasonable frame rate. Our solution is to run the animation from a script, so that it can run without human intervention.
When the ChessMen application first runs, it renders normally and accepts keyboard input to control the camera movement. At every frame, it takes a snapshot of the keyboard state and writes this information to a file. If you press the F1 key, the application restarts the animation from the beginning. This time, it does not accept keyboard input. Instead, it runs the animation from the saved key strokes and captures the animation into an AVI file.
Figure 12.2 shows the DirectShow filter graph used to capture the video. The source filter is our custom capture filter, which delivers uncompressed frames to the AVI Mux filter. The AVI Mux filter outputs an AVI byte stream. The byte stream is sent to the File Writer filter, which writes the file to disk.
The source filter is built with the DirectShow base class library. This library provides a set of classes for writing DirectShow filters. Although it is included in the Samples directory of the DirectShow SDK, it is meant for real-world use, and you should not hesitate to use it in your own applications.
The source filter is implemented by the CCapSource class, which derives from the CSource class. The filter has one output pin, implemented by the CCapPin pin, which is derived from the CSourceStream class. Most of the work is already done for us in the parent classes. Here are the main functions that we need to add:
GetMediaType. Proposes an output format.
CheckMediaType. Validates an output format.
DecideBufferSize. Sets the buffer size .
FillBuffer. Fills an output buffer with data.
The first three operations on this list occur when the filter connects to another filter. The last one, filling output buffers, happens on a worker thread while the filter graph runs. The parent classes implement the rest of the filter s functionality, including state transitions between running, paused , and stopped ; creating the worker thread; and delivering buffers downstream to the next filter.
In the following sections, we summarize the code without getting into the internal details of the base classes. For more information, see the DirectShow SDK documentation, especially the topic Writing DirectShow Filters . In addition, the book Programming Microsoft DirectShow for Digital Video and Television, by Mark Pesce (Microsoft Press), has two chapters on writing filters, including one on writing source filters.
The GetMediaType function returns a valid output format for the pin. For this filter, the allowable format is determined by the back buffer. The filter constructs the media type when the application sets the Direct3D device on the filter.
HRESULT CCapPin::SetDevice(IDirect3DDevice9 *pDevice) { CAutoLock cAutoLock(m_pFilter->pStateLock()); CheckPointer(pDevice, E_POINTER); m_pDevice.Release(); m_pDevice = pDevice; // Get the back buffer surface description. CComPtr<IDirect3DSurface9> pSurf; HRESULT hr = pDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pSurf); if (FAILED(hr)) { return hr; } D3DSURFACE_DESC desc; pSurf->GetDesc(&desc); // Convert the surface description to a media type. GUID subtype; SubtypeFromD3DFormat(desc.Format, &subtype); UINT width = desc.Width; UINT height = desc.Height; CreateRGBVideoType(&m_mtFormat, subtype, width, height, m_rtFrameLength); return hr; } Once the media type has been defined, a copy is returned in the GetMediaType function.
HRESULT CCapPin::GetMediaType(CMediaType *pMediaType) { CheckPointer(pMediaType, E_POINTER); CAutoLock cAutoLock(m_pFilter->pStateLock()); *pMediaType = m_mtFormat; return S_OK; }
The CMediaType class is a wrapper for the AM_MEDIA_TYPE structure. It overloads the assignment operator to copy the media type, including the format block.
The CheckMediaType function validates an output format. If the type is acceptable, the function returns S_OK. Otherwise, it returns a failure code.
HRESULT CCapPin::CheckMediaType(const CMediaType *pMediaType) { CAutoLock lock(m_pFilter->pStateLock()); CheckPointer(pMediaType, E_POINTER); if (*pMediaType == m_mtFormat) { return S_OK; } else { return VFW_E_TYPE_NOT_ACCEPTED; } }
Because the filter accepts only one media type, the function does a straight comparison with the m_mtFormat variable, using the overloaded equality operator on the CMediaType class. The CheckMediaType function may seem redundant, given the GetMediaType function, but pin connection in DirectShow involves a certain amount of negotiation between the pins. The process is generalized enough to handle the requirements of a broad range of filters.
The DecideBufferSize method sets the size of the buffers used to hold the video frames.
HRESULT CCapPin::DecideBufferSize( IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest) { CAutoLock cAutoLock(m_pFilter->pStateLock()); VIDEOINFOHEADER *pvi = (VIDEOINFOHEADER*) m_mt.Format(); // Image size is given in the video format. pRequest->cbBuffer = pvi->bmiHeader.biSizeImage; // We need at least one buffer. if (pRequest->cBuffers == 0) { pRequest->cBuffers = 1; } // Try to set the properties. ALLOCATOR_PROPERTIES ActualProps; HRESULT hr = pAlloc->SetProperties(pRequest, &ActualProps); if (FAILED(hr)) { return hr; } // Check what we actually got. We accept a larger buffer than // requested, but not a smaller buffer. if (ActualProps.cbBuffer < pRequest->cbBuffer) { return E_FAIL; } return S_OK; }
Buffer allocation is performed by an object called an allocator that supports the IMemAllocator interface. The allocator is specified in the pAlloc parameter. The pRequest parameter contains the buffer properties that the downstream filter has requested.
After filling in the number of buffers ( cBuffers ) and the required buffer size ( cbBuffer ), we call the IMemAllocator::SetProperties method on the allocator. The results are returned in the ActualProps parameter, and may differ slightly from the request even if the method succeeds. For example, the buffer size may get rounded up to some even multiple, such as 512 bytes.
The FillBuffer method is called from the source filter s worker thread in a continuous loop. In our filter, we must wait until the application has a new frame available. To synchronize these threads ” the application thread and the filter s worker thread ” we define a pair of event handles: m_hEventNewFrame , which is signaled by the application when it has a new frame, and m_hEventStop , which is signaled when the filter s Stop method is called. (The m_hEventStop event is needed so the worker thread doesn t block when the filter graph attempts to stop.) Here is the code for the FillBuffer method.
HRESULT CCapPin::FillBuffer(IMediaSample *pSample) { HANDLE objects[] = { m_hEventStop, m_hEventNewFrame }; DWORD result = WaitForMultipleObjects(2, objects, FALSE, INFINITE); if (result == WAIT_OBJECT_0) { return S_FALSE; // End of stream. } if (result != WAIT_OBJECT_0 + 1) { return E_FAIL; } // Time stamp the sample. REFERENCE_TIME rtStop = m_rtStreamTime + m_rtFrameLength; pSample->SetTime(&m_rtStreamTime, &rtStop); m_rtStreamTime += m_rtFrameLength; pSample->SetSyncPoint(TRUE); return CopyFrameToSample(pSample); }
If the stop event is signaled, the method returns S_FALSE . This informs the parent class to shut down the worker thread. Otherwise, we copy the contents of the back buffer into the sample buffer. The CopyFrameToSample function is straightforward, but we need to allow for the possibility that the buffer has a different pitch than the Direct3D surface, or a different image orientation.
HRESULT CCapPin::CopyFrameToSample(IMediaSample *pSample) { HRESULT hr; // Get a pointer to the buffer. BYTE *pData; pSample->GetPointer(&pData); // Get the back buffer surface. CComPtr<IDirect3DSurface9> pSurf; hr = m_pDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pSurf); if (FAILED(hr)) { return hr; } D3DLOCKED_RECT rect; hr = pSurf->LockRect(&rect, NULL, D3DLOCK_READONLY); if (SUCCEEDED(hr)) { // Copy each row of the surface into the buffer. // We cannot use memcpy, because the surface stride or // image orientation may not match. VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)m_mt.pbFormat; DWORD dwWidth, dwHeight; LONG lStride; BYTE *pTop; GetVideoInfoParameters(pVih, pData, &dwWidth, &dwHeight, &lStride, &pTop, false); DWORD cbRow = (pVih->bmiHeader.biBitCount / 8) * dwWidth; BYTE *pSource = (BYTE*)rect.pBits; for (DWORD row = 0; row < dwHeight; row++) { memcpy(pTop, pSource, cbRow); pTop += lStride; pSource += rect.Pitch; } pSurf->UnlockRect(); } return hr; }
The code used to build the capture graph is quite similar to the graph-building code that we ve shown in previous chapters. Some error-checking has been removed to make the code listing shorter.
HRESULT CGraph::BuildD3DCapGraph(IDirect3DDevice9 *pDevice) { InitializeFilterGraph(); // Add the source filter. m_pSource = new CCapSource(pDevice, &hr); m_pSource->AddRef(); hr = m_pGraph->AddFilter(m_pSource, L"CapFilter"); // Add the AVI Mux filter. CComPtr<IBaseFilter> pMux; hr = AddFilterByCLSID(m_pGraph, CLSID_AviDest, &pMux); // Add the File Writer filter and set the output file name. CComPtr<IBaseFilter> pWriter; hr = AddFilterByCLSID(m_pGraph, CLSID_FileWriter, &pWriter); CComQIPtr<IFileSinkFilter2> pSink(pWriter); pSink->SetFileName(OLESTR("d3dcap.avi"), NULL); // Hook up the filters. hr = ConnectFilters(m_pGraph, m_pSource, pMux); hr = ConnectFilters(m_pGraph, pMux, pWriter); return hr; }
Because the CCapSource class is compiled directly into the application, we can create it with new instead of going through the whole COM CoCreateInstance process. (You can also put a filter in a DLL and register it with a CLSID, which enables the filter to be created with CoCreateInstance , but that s not a requirement.)
Everything else in the ChessMen application is a variation on code that you ve seen before. We add the AVI Mux and File Writer filters to the graph, connect everything together, and we re ready to run the filter graph.
Note that the video files created by the ChessMen application can be quite large, because they contain uncompressed video. You could easily modify the application to insert an encoder filter before the AVI Mux filter, to reduce the file size, or else use the ASF Writer Filter to create Windows Media Video (WMV) files. Another option is to compress the file later, using a tool such as Windows Media Encoder.