Run the Mangler sample, which is included in the AVBook/bin directory. This application plays two video files at once, alpha-blending them in the same window. The application window also has several controls that can be used to change the video-mixing parameters (see Plate 5).
| Note |
Make sure that hardware acceleration is enabled in the DirectX control panel before you run the application. The VMR-9 requires hardware acceleration. |
The two sliders labeled Stream 1 and Stream 2 change the width, height, and position of the two video streams, independently of each other. (Stream 1 is the dog, Stream 2 is the car.) The sliders labeled Video Source control the portion of the final image that appears in the application window. If you reduce the width of the video source, it has the effect of stretching the image horizontally, because a smaller portion of the image fills the same area of the window. Reducing the height causes the image to stretch vertically.
The check box labeled Switch Z Order
The check box labeled Preserve Aspect Ratio controls whether the VMR-9 letterboxes the video. If this box is checked, the VMR-9 maintains the correct aspect ratio. If the box is unchecked, the image is
Most of the code in the Mangler application is Microsoft Win32 code for the UI, which we can ignore. We ll jump right into the DirectShow code, starting with building the filter graph.
In our previous DirectShow application, we called
RenderFile
, which built the entire filter graph all at once. However, as we noted earlier,
RenderFile
does not select the VMR-9. Therefore, we need to place the VMR-9 into the graph
The following code adds the VMR to the filter graph and initializes the VMR for windowless mode.
CComPtr<IGraphBuilder> m_pGraph; // Filter Graph Manager. CComPtr<IBaseFilter> m_pVMR; // VMR-9 filter. // Create the Filter Graph Manager. hr = m_pGraph.CoCreateInstance(CLSID_FilterGraph); // Add the VMR-9 filter to the graph. hr = AddFilterByCLSID(m_pGraph, CLSID_VideoMixingRenderer9, &m_pVMR); // Configure the VMR for windowless mode before we connect // any video streams to it. CComQIPtr<IVMRFilterConfig9> pConfig(m_pVMR); hr = pConfig->SetRenderingMode(VMR9Mode_Windowless); // Set the window where we want the VMR to paint the video. CComQIPtr<IVMRWindowlessControl9> pWC(m_pVMR); pWC->SetVideoClippingWindow(hVidWin);
To make the code more readable, some error-checking has been left out. Also, some code has been re-arranged to make the procedures more linear. The actual Mangler source code is organized into several C++ classes.
As in the previous application, the first step is to create the Filter Graph Manager. Then we create the VMR-9 and add it to the graph by calling an
To set up windowless mode, query the VMR filter for the IVMRFilterConfig9 interface and call IVMRFilterConfig9::SetRenderingMode with the flag VMR9Mode_Windowless . After windowless mode is established, we need to tell the VMR about the application window where it should draw the video. Query the VMR for the IVMRWindowlessControl9 interface, and call IVMRWindowlessControl9::SetVideoClippingWindow with the handle of the window. In the Mangler application, the video-clipping window is a static rectangle control that belongs to the dialog box.
The order of these method calls is important, because the VMR does not expose the IVMRWindowlessControl9 interface until you specify windowless mode. Before you do that, calling QueryInterface for IVMRWindowlessControl9 will fail.
Although it is not required in the Mangler application, at this point you may want to set the number of video streams. In both windowed mode and windowless mode, the VMR-9 defaults to four input pins, meaning you can render and mix four video streams
(Unlike the VMR-9, the VMR-7 creates only one input pin by default. If you are using the VMR-7 to mix multiple video streams, you must call IVMRFilterConfig::SetNumberOfStreams .)
The code for the AddFilterByCLISD function is shown in the following code. It calls CoCreateInstance to create the filter, and then calls IGraphBuilder::AddFilter on the Filter Graph Manager to insert the filter into the graph. (Creating a filter does not automatically add it to the graph.) The function returns the filter s IBaseFilter interface.
// Function to add a filter to the filter graph.
HRESULT AddFilterByCLSID(
IGraphBuilder *pGraph, // Pointer to the Filter Graph Manager.
const GUID& clsid, // CLSID of the filter to create.
IBaseFilter **ppF) // Receives a pointer to the filter.
{
if (!pGraph ! ppF)
{
return E_POINTER;
}
*ppF = 0;
CComPtr<IBaseFilter> pF;
HRESULT hr = pF.CoCreateInstance(clsid);
if (SUCCEEDED(hr))
{
hr = pGraph->AddFilter(pF, NULL);
if (SUCCEEDED(hr))
{
// Return the IBaseFilter pointer to the caller.
// The caller must release it.
*ppF = pF.Detach();
}
}
return hr;
}
Now that the VMR is set up for windowless mode, we can load the video files and render the streams.
WCHAR *wsFileName; // File name.
// Initialize wsFileName with the file name (not shown).
// Add a source filter for the source file.
CComPtr<IBaseFilter> pSource; // Source filter.
hr = m_pGraph->AddSourceFilter(wsFileName, L"Source Filter",
&pSource);
// Collect all the source filter's output pins into a list.
PinList pins;
hr = GetPinList(pSource, PINDIR_OUTPUT, pins);
// Try to render each pin, using only the renderers in the graph.
CComQIPtr<IFilterGraph2> pGraph2(m_pGraph);
for (PinIterator iter = pins.begin(); iter != pins.end(); ++iter)
{
// If any pin succeeds, treat it as a global success. Some pins
// may output non-video media types, so it's OK if they fail.
HRESULT hrTmp;
hrTmp = pGraph2->RenderEx(*iter,
AM_RENDEREX_RENDERTOEXISTINGRENDERERS, 0);
if (SUCCEEDED(hrTmp))
{
hr = S_OK;
}
}
// Release wsFileName (not shown).
We start by calling
IGraphBuilder::AddSourceFilter
to add a source filter for the video file. The first parameter is the file name, and the second parameter is a name for the filter. (The filter name is useful for debugging but
Next we hook up the source filter to the video renderer. For reasons that will become clear in a moment, we need to get a list of the source filter s output pins. The GetPinList function finds all of the pins on a filter that match a specified direction ” in this case, the output pins ” and collects them into an STL list object. We ll describe the code for GetPinList later in this section.
Finally, we iterate through the list of pins and call
IFilterGraph2::RenderEx
with each pin. The
RenderEx
method takes a pointer to an output pin s
IPin
interface. The
AM_RENDEREX_RENDERTOEXISTINGRENDERERS
flag
Figure 9.5:
AVI file playback using the VMR-9.
The source filter shown in this diagram has only one output pin, and this will be true in general for AVI files. So why go through the trouble of building a list of pins? The reason is that some source filters have multiple output pins ” for example, the source filter used to render Windows Media files creates one output pin per stream in the file.
Also, note that we did not put an audio renderer into the graph beforehand. Therefore, even if the video file has an audio stream, the audio won t be rendered, because we specified the AM_RENDEREX_RENDERTOEXISTINGRENDERERS flag. If you want to render the audio, you need to add the audio renderer to the graph first, as we did with the VMR. The CLSID for the audio renderer filter is CLSID_DSoundRender ” so named because it uses DirectSound to play the audio. You can easily modify the code in the Mangler application to include audio playback.
Here is the code for the GetPinList function.
typedef CComPtr<IPin> PinPtr;
typedef std::list<PinPtr> PinList;
typedef PinList::iterator PinIterator;
HRESULT GetPinList(
IBaseFilter *pFilter, // Pointer to the filter.
PIN_DIRECTION Direction, // The pin direction to search for.
PinList& list // A list to collect the pins.
)
{
if (!pFilter)
{
return E_POINTER;
}
CComPtr<IEnumPins> pEnum;
HRESULT hr = pFilter->EnumPins(&pEnum);
if (FAILED(hr))
{
return hr;
}
CComPtr<IPin> pPin;
while (S_OK == pEnum->Next(1, &pPin, NULL))
{
PIN_DIRECTION ThisDirection;
hr = pPin->QueryDirection(&ThisDirection);
if (FAILED(hr))
{
return hr;
}
if (ThisDirection == Direction)
{
list.push_back(pPin);
}
pPin.Release();
}
return S_OK;
}
To enumerate a filter s pins, we use the IEnumPins interface, which we get by calling IBaseFilter::EnumPins on the filter. To iterate through all of the pins, call IEnumPins::Next until the return value is something other than S_OK . (Don t use the SUCCEEDED macro, because in this case S_FALSE does not mean the same thing as S_OK .) The Next method returns an IPin pointer for the pin.
For each pin, the GetPinList function calls IPin::QueryDirection to find the direction of the pin, either input or output. If the pin direction matches the Direction parameter, the IPin pointer is stored in the list. The STL push_back function makes a copy of the CComPtr object, which calls AddRef on the raw IPin pointer, so the reference count remains correct.
Once the graph is built, we can configure how it
Figure 9.6:
Video-mixing preferences in the VMR-9.
Each stream has an output rectangle . The output rectangles control how the video streams are positioned relative to each other.
Internally, the VMR creates a Direct3D surface where is mixes the
By default, the VMR positions each video in the center of the surface, so that it fills the entire surface along one dimension, while
The output rectangle is measured relative to these coordinates, so the default position of each stream is {0.0, 0.0, 1.0, 1.0}. To place the image in the upper-left quadrant, you would set the output rectangle to {0.0, 0.0, 0.5, 0.5}. This also has the effect of shrinking the video to fit the smaller rectangle.
You are not limited to the range [0.0 ... 1.0]. Any part of the image that
To set the output rectangle for a stream, call IVMRMixerControl::SetOutputRect .
DWORD deStreamId = 0; // First video stream.
VMR9NormalizedRect rect;
// Set the rectangle boundaries to {0,0,1,1}.
rect.left = rect.top = 0.0f;
rect.right = rect.bottom = 1.0f;
m_pMixer->SetOutputRect(dwStreamId, &rect);
The first parameter ( dwStreamId ) specifies the video stream, indexed from zero. The index number is determined by the order in which the VMR pins are connected. The first pin to be connected is stream 0, the second is stream 1, and so on. In the Mangler application, this corresponds to the order in which the application s RenderVideoStream function is called.
You can apply a per-stream alpha value to make a video stream transparent. The alpha can range from 0.0, which is completely transparent, to 1.0, which is completely opaque. Set the alpha value for a stream with the IVMRMixerControl::SetAlpha method.
m_pMixer->SetAlpha(dwStreamId, 0.5f); // Semi-transparent (50% alpha).
The value is applied to the entire image area for that stream.
In the places where the video images do not cover the back-end surface, the surface is filled with a solid background color. This color is also visible behind the image, if the image is transparent. Set the background color with the IVMRMixerControl::SetBackgroundClr method, which takes a COLORREF value. The default color is black.
m_pMixer->SetBackgroundClr(RGB(0xFF, 0xFF, 0xC0)); // Pale yellow.
The z-order specifies the order in which the streams are composited onto the back-end surface. Higher z-orders are farther back ” in other words, the highest z-order is rendered first. If the stream s alpha is set to 1.0, the image obscures anything below it. Otherwise, the image is alpha-blended with layer below it. Set the z-order with the IVMRMixerControl::SetZOrder method.
m_pMixer->SetZOrder(dwStreamId, 0); // Move this stream to the front.
In the Mangler application, the Switch Z Order check box sets the z-order of stream number 1, switching it between the values 0 and 2. Stream number 0 is kept at z-order 1.
The source rectangle is a subrectangle within the back-end surface, and the destination rectangle is a subrectangle within the application window s client area. After the VMR composites the video streams to the back-end surface, it stretches the surface s source rectangle onto the window s destination rectangle. Thus if you reduce the destination rectangle, the video occupies a smaller portion of the application window. If you reduce the source rectangle, the VMR stretches a smaller portion of the composited image into the destination rectangle, with the effect of magnifying a section of the composited image. If the source rectangle is equal to the entire back-end surface, the entire image is displayed in the destination rectangle.
To set the source and destination rectangles, call IVMRWindowlessControl9::SetVideoPosition . To find the actual size of the back-end surface, call IVMRWindowlessControl9::GetNativeVideoSize . Always set the value of the source and destination rectangles before playback starts, or the video will not be visible, because both rectangles default to {0, 0, 0, 0}. The following code sets the source rectangle equal to the entire back-end surface and the destination rectangle equal to the entire window client area.
RECT rcSource;
RECT rcDest;
// Find the size of the composited video image.
ZeroMemory(&rcSource, sizeof(RECT));
m_pWC->GetNativeVideoSize(
&rcSource.right, // Width.
&rcSource.bottom, // Height.
NULL, NULL // Aspect ratio X and Y (optional).
);
// Use the entire client area for the destination rectangle.
GetClientArea(hwnd, &rcDest);
m_pWC->SetVideoPosition(&rcSource, &m_rcDest);
By default, the VMR does not preserve the aspect ratio of the image when it stretches the source rectangle to the destination rectangle. To preserve the aspect ratio, call IVMRWindowlessControl9::SetAspectRatioMode with the flag VMR9ARMode_LetterBox .
m_pWC->SetAspectRatioMode(VMR9ARMode_LetterBox);
Note that letterbox mode controls how the VMR stretches the back-end surface to the window, not how it draws the video streams onto the back-end surface.
If you enable letterbox mode, you can define the color of the border where the video is letterboxed. To do so, call IVMRWindowlessControl9::SetBorderColor with a COLORREF value. The Mangler application sets the border color to orange, which is quite ugly, but makes the effect very obvious.
m_pWC->SetBorderColor(RGB(0xFF, 0x80, 0x00)); // Orange.
The border color is not the same as the background color mentioned earlier. The background color is used to fill the areas of the back-end surface where the video does not appear, while the border color is used to fill the letterboxed edges of the client window, in letterbox mode only. The default border color is black.
The VMR has some additional features that are not demonstrated in the Mangler application:
You can alpha-blend a static bitmap onto the video. For example, you could use this to put a logo onto the video window. Call IVMRMixerBitmap9::SetAlphaBitmap .
You can grab a copy of the most recent video frame, in the form of a device-independent bitmap (DIB). Call IVMRWindowlessControl::GetCurrentImage .
If the graphics hardware supports it, the VMR can perform hardware-accelerated deinterlacing and image adjustment. This feature is described in detail in Chapter 12.
With our initial mixing preferences established, we are ready to start video playback by calling IMediaControl::Run on the Filter Graph Manager.
m_pControl->Run();
While the graph is running, each decoder in the graph delivers video frames to the VMR. The VMR runs a worker thread that picks up the decoded
Repaints.
When the application receives a
WM_PAINT
message, call
IVMRWindowlessControl::RepaintVideo
to inform the VMR to repaint the video window. Note that you do not have to call
RepaintVideo
just to get the VMR to draw each frame while the video plays; that happens automatically. The purpose of calling
RepaintVideo
is to make sure the VMR repaints the most recent frame after the application window is invalidated. This could happen between frames while the graph is running, or while the graph is
Window size changes. When the application receives a WM_SIZE message, you may need to recalculate the source and destination rectangles and call SetVideoPosition again. You can skip this call if you don t need to adjust the video position after the resize ” it will depend on your application. The Mangler application uses a non-resizable window, so it s not a issue.
Display changes. When the application receives a WM_DISPLAYCHANGE message, call IVMRWindowlessControl::DisplayModeChanged . This tells the VMR to take whatever actions are needed to respond to the display change.
As the Mangler application
DirectShow has an event mechanism that lets the Filter Graph Manager inform your application when interesting things happen inside the graph ” for example, when playback
At startup, we ask the Filter Graph Manager to notify our application whenever there is a new event. It will do so by posting a message to the application s message loop. We define a private Windows message for this purpose.
static const long WM_GRAPH_EVENT = WM_APP + 1;
To set up the notification, call IMediaEventEx::SetNotifyWindow .
m_pEvent->SetNotifyWindow((OAHWND)hwnd, WM_GRAPH_EVENT, 0);
The first argument is the window handle, cast to an OAHWND type. The second argument is the Windows message, and the third argument is an optional value that will be returned to the application in the lParam parameter of the message. The lParam value is not used in the Mangler application, but you could use it to track events from multiple filter graphs. Whenever the Filter Graph Manager queues a new event, the designated window receives the WM_GRAPH_EVENT message (defined as WM_APP + 1 ). In your message loop, respond to the message by calling IMediaEvent::GetEvent .
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam)
{
long EventCode, param1, param2; // Event parameters.
switch (msg)
{
case WM_GRAPH_EVENT:
// Loop until no messages are left in the queue.
while (SUCCEEDED(hr = m_pEvent->GetEvent(&EventCode, ¶m1,
¶m2, 0)))
{
// Decide what to do based on the event code.
switch (EventCode)
{
case EC_COMPLETE: // End of file.
// Handle the event (not shown).
break;
}
}
// Release any resources allocated for the event parameters.
m_pEvent->FreeEventParams(EventCode, param1, param2);
break;
// Handle other Windows messages as usual (not shown).
}
}
The
GetEvent
method returns an event code and two event parameters. The event code defines what kind of event
You may have noticed in the previous code example that we called GetEvent and FreeEventParams inside a while loop, which repeats for as long as GetEvent succeeds. This is done because several events might be queued by the time the application receives the WM_GRAPH_EVENT message and responds to it. Using a loop ensures that we get all the pending events.
When the Mangler application receives the EC_COMPLETE event, it stops the graph and seeks to the start of the file.
m_pControl->Stop(); LONGLONG rtPosition = 0; m_pSeek->SetPositions( &rtPosition, AM_SEEKING_AbsolutePositioning, // Current position. NULL, AM_SEEKING_NoPositioning // Stop position (no change). ); m_pControl->Run();
Seeking is performed by calling
IMediaSeeking::SetPositions
on the Filter Graph Manager. The first parameter is the time to seek to. The units are 100 nanoseconds (10
-7
seconds), so 1 second is 10,000,000 units. The
AM_SEEKING_AbsolutePositioning
flag means the seek time is
When the VMR mixes multiple video streams, it has limited support for seeking. You can stop the graph and seek all the streams back to the beginning, but you cannot seek around in the file during playback. In Chapter 12, we ll show a way to get around this limitation by using several VMR instances in the same application.