DirectShow For Media Playback In Windows - Part III: Customizing Graphs
by (13 September 2000)
|Return to The Archives|
|This installment of the DirectShow tutorial series will cover customizing filter graphs. There are many reasons why we might need to customize a filter graph. We may want to add a transform filter that will EQ our audio output, or add a color correction filter to our video output. Sometimes GraphBuilder just doesn't add the filter we want, so we need to fiddle with things a bit to get the desired filter graph. This is also good for learning what actually goes on during filter graph creation: how filters are added to a filter graph, how 2 filters are connected, etc.|
Here's a rundown of the interfaces we will be using. More in depth descriptions
can be found in the DirectShow documentation.|
In order to make changes to a filter graph, we need to know these interfaces. You'll see that connecting two filters is done at the pin level using the IPin interface. This means that we need to be able to get the IPin interface of any pins belonging to any filters in the filter graph.
There are a few different ways to build a suitable filter graph for any purpose. The first way is automatically build graphs using IGraphBUilder::RenderFile(), like we've done so far. Another way we can do it is by instantiating every filter we need and connecting them all one by one. It's not the easiest way, but possible, and a good learning exercise. The best way, though, is to let GraphBuilder do as much of the work as possible, while manually modifying the filter graph for the desired final graph. I'll call these semi-automatically created graphs. Let's look at each one separately.
Automatically Built Graphs
This is the way we've built all filter graphs so far. Quick Recap:|
Manually Built Graphs
Now let's say now that we have used GraphEdit to build a graph for a .WAV file,
so we've seen the completed graph and know exactly which filters we need and how
they are to be connected. We can build the filter graph from the ground up by
adding filters one at a time. Here's a quick look at what the filter graph
[file source filter]--[wave parser]--[sound renderer]
Remember that in COM we have interfaces, indicated by 'IID_xxx', and specific implementations of that interface, indicated by 'CLSID_xxx'. We need to know a class identifier (CLSID_xxx) for each filter in order to call CoCreateInstance to create that filter. Most of the common source and renderer filter CLSID's are listed in the uuids.h header. We can find CLSID_FileSource and CLSID_AudioRender in there (or CLSID_DSoundRender for the DirectSound renderer). Now how do we find a CLSID for the 'Wave Parser' filter that decodes the stream? Easiest way is to search through the registry for the string "Wave Parser" and use the CLSID that you find. There are other ways, like using IFilterMapper and IEnumRegFilters to get a list of registered filters, but for this I've just looked it up.
First, let's create the filters we know we need.
Notice I just used the DEFINE_GUID macro to define the CLSID (which is a GUID) for the wave parser. A GUID is just a structure, so all we need is to create it and put the right values in.
Now that we have the filters we want in our filter graph, we need to connect them. Connections between filters are done at the pin level, so we need to find the right pins we want to connect on each filter. There's a few different ways we can connect pins. IFilterGraph provides a ConnectDirect() method where we can specify a media type to use for the connection, and the 2 pins to be connected. This is fine if you know the media type that is going to be used, and you are confident that the 2 filters will be happy connecting to each other. A better way is to use IGraphBuilder::Connect(), which tries to connect 2 pins directly, and if that fails it tries to find combinations of filters that will go in between the 2 pins to complete the connection. This is the safest way, so it's what we'll use.
We use the IEnumPins interface to enumerate all the pins for the filters we want to connect. Since filters can have any number of pins, we need to pick the right ones to connect. We can use some of our extra knowledge we got from looking at the completed graph in GraphEdit. We know that the source filter and render filter both only have 1 pin, so no problem with pins there. We also know that the Wave Parser only has 1 input pin and 1 output pin. To make sure we have the pin we want, all we have to do is check the pin direction with IPin::QueryPinInfo().
Now we should have a fully functional filter graph exactly like the one created by calling IGraphBuilder::RenderFile(). This is alot of work for us compared to automatically generated graphs, so we normally don't use this approach. It's still good to see this and know it is possible to do. Now let's look at a more practical way to have control over our graph while letting GraphBuilder handle all the tedious work.
Semi-Automatically Built Graphs
When I say semi-automatically built graphs, this can mean a few things. We
could have a filter graph that is automatically generated, but we insert or
change some filters. We could also create a source filter, add it to the graph
(connect to some transform filters if we need to), and let graphbuilder
automatically create the rest of the graph. Finally, we could add some specific
filters we want to use to the graph, and let graphbuilder render the entire
graph and it will try to use the filters we've created. I'll quickly go over
each one now. |
Let's say we're still just playing a .WAV file. But this time, we want to run it through a transform filter to apply an effect to it. We'll use the Gargle filter since it's a sample provided with the DShow SDK. Note that sample filters need to be built and registered before they can be used, so make sure you build the Gargle sample and run 'regvsr32' on it before you try to create a Gargle filter. Our filter graph will look like so:
[source filters]--[wave parser]--[gargle effect]--[sound renderer]
We let graphbuilder create the default graph (without gargle filter). Then we enumerate filters and look for the renderer filter. We know that uncompressed sound is passed into the sound renderer from the filter it's connected to, and that the gargle effect input pin accepts uncompressed sound data also. So we can just slip the gargle filter in ahead of the renderer. For transform filters, this is a good way to insert them. You may want to insert an effect into a graph for mp3's, or AIFF files, and you can be pretty confident that the renderer's input is always going to be a format that the effect filter can take as input.
We can find the render filter either by looking for it by name, or by checking number of output pins. The only filter in a completed graph to have no output pins should be the renderer. In order to make our code work with different renderers (remember there are at least 2 different audio renderers that come with DirectShow), we don't want to look for it by name. So we'll find it by checking for output pins. Once we have found the renderer filter, we'll connect the gargle filters pins.
Here's the code to do this:
Well that was long and messy. At least we now have the graph the way we want it, and it's independant of which decoder and renderer are being used. Notice that in order to get the output pin of the filter that was previously connected to the renderer, we just call the renderer input pin's IPin::ConnectedTo().
The second way we can create semi-automatically built graphs makes use of some things we know about how graphbuilder creates graphs. We know that while GraphBuilder is attempting to create a graph, it tries to connect the current filter its working with, to any filter already in the graph before it starts adding new filters to the graph. This means that we can create a Gargle filter, add it to the graph, and call IGraphBuilder::RenderFile(), and graphbuilder will automatically stick the Gargle filter in the first place it will connect. Here's basically how we do this:
That's it. You can do this same thing in GraphEdit to see that it works. Some filters, for 1 reason or another, will not automatically be inserted. The best way to ensure that a filter ends up where you want it is to add it to the graph first, then enumerate it's pins and check if they are connected. If not, then go through the method of insertimg it manually before the renderer as we did earlier.
The last way to create a semi-automatically generated graph is to create the first filter (or filters) and render an output pin. Let's say this time we want to play a .WAV file from a website. Instead of the regular 'File Source (Async)' filter, we need the 'File Source (URL)'. What we need to do is create the source filter explicitly, then we can call IGraphBuilder::Render() on it's output pin. We're not restricted to calling Render() on source filters. Any time we have a partial graph (a source filter connected to any number of transform filters), we can call Render() on the unconnected output pin of the last filter in line, and graphbuilder will attempt to complete the graph for us. Here's the way that would go:
|Using these techniques, you should now be able build any filter graph you want. In future tutorials, we'll look at using IDDrawExclModeVideo to get DirectShow to play in a window owned by our app, and also look at Multimedia streaming using DirectShow. Multimedia streaming is very useful for games since it basically streams media in just like a file stream, while running it through a filter graph on the way. I'll also be covering building custom filters sometime too, so that you can make your own transforms or use custom media formats.|