Index Topic Contents | |||
Previous Topic: Plug-in Distributors Next Topic: File Formats |
DirectShow and COM
Microsoft® DirectShow provides a framework that simplifies the creation of Component Object Model (COM) objects. This article describes this framework and most of what you need to know about COM to create a filter or plug-in distributor using the C++ class library. The article assumes the reader is familiar with C++. An understanding of COM would be helpful, but is not essential.
Contents of this article:
- COM Objects in DirectShow
- Reviewing the Instantiation Process
- Creating Filters
- Creating Plug-in Distributors
- Implementing the Class Factory
- Using an Object-Oriented Model
COM Objects in DirectShow
DirectShow filters, the filter graph manager, plug-in distributors, and enumerators are all COM objects. A general design has been adopted for the way in which DirectShow implements COM objects. This design is available to help you implement your own filters and plug-in distributors (or any COM object).
DirectShow components are supplied as in-process servers; that is, servers that run in the same address space as your application. They are packaged in a single dynamic-link library (DLL), Quartz.dll. Use the COM framework of DirectShow to build your own in-process COM servers, which you can package in your own DLL(s).
Typically, a single C++ class implements a single COM class. The DirectShow COM framework requires that C++ classes implementing COM objects conform to a few simple rules. One of these rules is that the developer provides a class factory template for each such class. The class factory template contains information about the class that is vital to the framework. Class factory templates are defined in the DLL using two global variables (g_Templates and g_cTemplates) as shown in the following example.
CFactoryTemplate g_Templates[]= { {L"My class name", &CLSID_MyClass, CMyClass::CreateInstance, CMyClass::Init}, {L"My class name2", &CLSID_MyClass2, CMyClass2::CreateInstance} }; int g_cTemplates = sizeof(g_Templates)/sizeof(g_Templates[0]);The names and types of these variables must be as they appear in the previous example. Because any DLL might contain several COM classes, each of which will require a class factory template, the factory templates are defined in an array and the number of elements in the array is recorded in another variable. Each element of the array contains the following fields.
- A textual description of the class (using wide characters, therefore the "L" prefix).
- A pointer to the class identifier of the class (CLSID).
- A pointer to a static method of the class that can create instances of the class (CFactoryTemplate::CreateInstance).
- A pointer to a static method of the class. This method is called when the DLL is loading or unloading and can perform one-time initialization and termination. If this method is not required, this can be omitted, will default to NULL, and will be ignored.
- A pointer to an AMOVIESETUP_FILTER structure. This is required when using filter self-registration services.
The DirectShow COM framework uses the information in these class factory templates to create instances of the specific class, and to register and unregister the COM classes.
The following example demonstrates a simple C++ class implementing a COM class using the DirectShow framework.
class CMyClass : public IMyInterface, public CUnknown { private: /* private attributes */ protected: ~CMyClass() { /* release private attributes */ } CMyClass(TCHAR *pName, LPUNKNOWN pUnk, HRESULT *phr) : CUnknown( pName, pUnk, phr ) { /* set up private attributes */ } public: DECLARE_IUNKNOWN static CUnknown *CreateInstance(LPUNKNOWN pUnk, HRESULT *phr) { CUnknown * result = 0; result = new CMyClass( NAME("CMyClass"), pUnk, phr ); if ( !result ) *phr = E_OUTOFMEMORY; return result; } STDMETHODIMP NonDelegatingQueryInterface(REFIID iid, void ** ppv) { if ( iid == IID_IMyInterface ) { return GetInterface(static_cast<IMyInterface *>(this), ppv ); } else { return CUnknown::NonDelegatingQueryInterface(iid, ppv); } } /* My interface methods */ };This is not a typical filter example, because filters will normally derive from more specialized base classes than CUnknown. However, because all base filter classes eventually derive from CUnknown, this example demonstrates what is essential in a more generic manner. (The example is probably more typical for a DirectShow plug-in distributor (PID), which extends the functionality of the filter graph manager, or for a framework for implementing an arbitrary COM object.)
In this example, the NonDelegatingQueryInterface method is implemented. The more specialized filter base classes that derive from CUnknown are responsible for implementing NonDelegatingQueryInterface for the required interfaces; this is only necessary in the derived filter class if it adds some interfaces that are not in the base classes. In this case, it adds its own interface, IMyInterface.
INonDelegatingUnknown::NonDelegatingQueryInterface is a method that allows other objects to access interfaces on the COM object. All COM objects support IUnknown::QueryInterface to do this, and the DirectShow class library supplies the DECLARE_IUNKNOWN macro to enable the IUnknown interface. The DirectShow framework goes one step further and makes it easy to aggregate objects (make them part of a larger COM object) by implementing an INonDelegatingUnknown interface. Even if your object is not aggregated, it uses the INonDelegatingUnknown interface, which is mapped to the IUnknown interface by the base classes.
Although aggregation is handled for all objects by the DirectShow class framework, it is typically not used by filters in current DirectShow filter graphs. Plug-in distributors do, however, require aggregation (as is described later in this article), and future filter graphs might incorporate filter objects that are composed of collections of aggregated filters.
With this in mind, it might be helpful to explore more of the details of the previous example. First, a brief review of some COM basics might be helpful. COM objects are created by their class factories, are reference counted during their lifetimes, and self-destruct when their reference counts drop to zero. COM objects can be created in isolation, or can be aggregated with an already existing COM object. In this second case, the existing object (referred to as the outer object) maintains the reference count. The created object (referred to as the inner object) is not reference counted, but will be destroyed by the outer object during the destruction of the outer object. The application cannot directly manipulate COM objects; an application can only invoke the methods, which the object chooses to expose through its interfaces. Typically, COM objects make several interfaces available. All COM objects must support the IUnknown interface.
All classes using the DirectShow framework must inherit from CUnknown either directly (as in the previous example) or indirectly, through one of the other supplied base classes. CUnknown, with the DECLARE_IUNKNOWN macro and the NonDelegatingQueryInterface method, provide the IUnknown interface with the required reference counting and support for COM aggregation.
NonDelegatingQueryInterface is a method on INonDelegatingUnknown, which is supported by CUnknown. NonDelegatingQueryInterface is overridden in derived classes that support new interfaces, such as IMyInterface in the previous example. The method should check for all the interfaces known to be implemented on the object and return appropriate pointers to these interfaces. Requests for unrecognized interfaces should be passed to the NonDelegatingQueryInterface of CUnknown. The call to the GetInterface method (of CUnknown) copies the interface into the ppv parameter and ensures that the correct reference count is incremented.
The methods in INonDelegatingUnknown mirror those in IUnknown. For more information about CUnknown, the INonDelegatingUnknown interface, and the NonDelegatingQueryInterface method, see the CUnknown section in the reference material. INonDelegatingUnknown is defined in Combase.h; CUnknown is implemented in Combase.cpp.
When an instance of the class is required, the framework, using the information in the class factory template, calls the derived class's CreateInstance member function. The framework passes a pointer to an outer unknown (if the object will be part of an aggregate object) through the pUnkparameter, and passes a pointer to an HRESULT value through the phr parameter. The constructor of an inherited class can set this value if an error occurs. The phr parameter should not be initialized; this is the calling application's responsibility. The CreateInstance member function constructs an instance of the class by calling the constructor. The name passed to the constructor is wrapped with the NAME macro supplied by DirectShow. When building debugging versions, NAME passes the textual name on to the constructor. When building nondebugging versions, NAME results in a null pointer, thus saving space in versions that are not for debugging purposes.
The class constructor and destructor are declared protected. This prohibits the creation of the object using C++ language constructs. Instances of this class can be created only by calling the CreateInstance member function.
The class constructor needs to construct the inherited CUnknown. The pName parameter points to a string that is available for debugging purposes. It is vital that the string referenced by pName is in static storage, because the constructor for CUnknown will not copy it.
Reviewing the Instantiation Process
It might be helpful at this point to consider the normal process of instantiating a COM object, and examine how the DirectShow COM framework supports this process. First, a look at the entry points required of an in-process server DLL (such as a filter or plug-in distributor) is in order.
In-process server DLLs must export certain standard functions so that COM can interact with them. The DirectShow framework provides these functions for you. The module definition file for the DLL must list these functions in its EXPORTS section, and link to Strmbase.lib. The functions are: DllGetClassObject and DllCanUnloadNow. (The source code for these functions is supplied in Dllentry.cpp.)
A DirectShow object can define DLL entry points that facilitate the automatic registration of COM classes. These entry points are DllRegisterServer and DllUnregisterServer. Although the framework does not directly provide these entry points, it does provide a function, called AMovieDllRegisterServer2, that can implement these entry point functions. These functions take care of registering and unregistering all COM objects for which you have provided class factory templates in the g_Templates array. You can add a DllRegisterServer function to your module that simply calls AMovieDllRegisterServer2, or you could do the same for DllUnregisterServer. For more information on self-registering DirectShow COM objects, see Register DirectShow Objects.
Registry entries are required to link the class identifier (CLSID) of the COM object to the DLL in which the class is implemented. The framework provides entry points in the DLL that support the automatic registration of class identifiers in the registry, using the information provided in the class factory templates.
Following are the steps that occur during initialization, which require the entry points mentioned previously.
- When the DLL is loaded, the DllMain entry point is called to perform any initialization. The framework provides this function. During its execution, any initialization routines referenced in the class factory templates will be called.
- When an application calls CoCreateInstance or CoGetClassObject, COM calls the DllGetClassObject function in the appropriate DLL to obtain a pointer to a class factory that can instantiate objects of the CLSID requested by the application. The framework supplies this function. Using the information in the class factory template, the framework creates a class factory. (If the requested CLSID cannot be found in the array of class factory templates, an error is returned to the application.)
- The class factory is called to instantiate an object that supports the interface identifier (IID) requested by the application. At this point, the class factory will call the static method referenced in the class factory template.
- During the DLL's lifetime, the QueryInterface method might be called on the IUnknown interface of the object (or owning object if aggregated), requesting some interface on that object. By deriving the object class from CUnknown, overriding NonDelegatingQueryInterface, and using the DEFINE_IUNKNOWN macro to declare the IUnknown interface, both COM aggregation and reference counting are addressed.
- During the life of the DLL, DllCanUnloadNow might be called to see if it is safe to unload the DLL. Typically, this returns S_FALSE if any class factory is locked, or if any of the objects that have been created still exist. The framework implements DllCanUnloadNow.
Creating Filters
When creating filters, you can take advantage of one of the richer classes that DirectShow provides, such as CTransformFilter or CBaseRenderer, instead of deriving from CUnknown. These supplied classes are derived from CUnknown, but provide additional functionality specific to various types of filters. However, building filters also requires an understanding of the DirectShow connection model (see Connection Model) and the pin classes. For more information about creating filters, see Creating a Transform Filter.
Creating Plug-in Distributors
The filter graph manager can perform operations at a high level, treating the filter graph as a single entity. These operations can be distributed across an entire filter graph, or perhaps confined to just a single filter in the filter graph. The filter graph manager, of itself, only exposes a few interfaces. A feature called a plug-in distributor allows the filter graph manager to be extended with additional interfaces. When the filter graph manager receives a request for an interface which it does not support, it tries to find a plug-in distributor (PID) that does support it. If it succeeds in finding such a PID, then that PID is instantiated as an aggregate object within the filter graph manager. By doing so, the filter graph manager appears to support many more interfaces. Plug-in distributors are aggregated with the filter graph manager, but all the aggregation logic is provided by CUnknown, allowing you to concentrate on the PID logic.
A PID is designed to be aggregated into a filter graph manager; it will call on the services of its owning filter graph manager. Because the PID is unlikely to function correctly without an owning filter graph, it checks for an outer unknown in the constructor of the PID. To make this determination, add the following line to the body of the constructor illustrated in the previous example.
if (!pUnk) *phr = VFW_E_NEED_OWNER;To be even more defensive against being used without an owner, the PID could also request an IFilterGraph or IGraphBuilder interface from the outer unknown during construction, because these interfaces are known to be only on the filter graph manager.
If the PID obtains any interface pointers from the filter graph manager, the pointers should be released immediately. Because the PID is an aggregate object, its lifetime is within the lifetime of its containing object, the filter graph manager, so there is no need to maintain a lock on it. Furthermore, maintaining a lock introduces a circular reference count that would not allow the destruction of the filter graph manager.
Implementing the Class Factory
The concept of a class factory is not specific to DirectShow; it is a common design that appears when the underlying type of the object being created is not known to the client that requests its creation. With COM objects, clients request interface pointers but know little about the underlying objects that implement that interface.
In C++, there are two means of implementing a class factory. One is to implement it as a genuine class, the other is to implement it as a static method on the class that the class factory will manage. The first method provides better separation of responsibilities and data hiding, and is the approach adopted by COM. The second method allows for a simpler implementation.
The DirectShow COM framework provides the best of both worlds. It exposes a genuine COM factory class to its clients while allowing the developer to implement the body of the class factory as a static method of your class. The bridge between these two approaches is two global variables, g_Templates and g_cTemplates, which were described previously.
The DirectShow framework defines two classes for implementing the class factory: CFactoryTemplate and CClassFactory. A CFactoryTemplate object holds information regarding a specific class, including a pointer to the static factory method of the class. When CClassFactory is instantiated, it must be given reference to a CFactoryTemplate instance. The CClassFactory instance will then act as a class factory for the class described in its associated CFactoryTemplate instance. The following illustration demonstrates the relationship between these classes, their instances, and the objects they create.
The DirectShow SDK includes a module, DllEntry.cpp, which provides the DllGetClassObject function. This function uses the process described previously to create a class factory that can produce instances of a class.
Using an Object-Oriented Model
All components of the DirectShow filter graph architecture are implemented as COM objects. This includes the filters through which data is passed, and filter components that serve as a connection between filters or allocate memory. Each object implements one or more interfaces, each of which contains a predefined set of functions, called methods. An application calls a method, or other component objects, to communicate with the object exposing the interface. For example, the application calls methods on the IMediaControl interface on the object of the filter graph manager, such as the Run method, which starts a media stream. The filter graph manager, in turn, calls the Run method on the IBaseFilter interface exposed by each of the filters.
Filter graph architecture uses COM interfaces because they have the following properties.
- COM interfaces are publicly defined. This means that any filter that implements the correct predefined interfaces will work in a filter graph without any knowledge about the other filters, because all filters are built with the same interface specifications.
- COM interfaces do not change after definition. A base set of interfaces are guaranteed to work; additional interfaces can be introduced to cover additional services. This definition prevents version problems.
- COM interfaces must have all methods implemented by any object that exposes them (even if the implemented method simply returns E_NOTIMPL). This assures that calling a method on the interface of an object will not generate an error.
- COM interfaces are discoverable. All COM objects support a method called QueryInterface that allows an external component to discover if an interface is present and retrieve a pointer to it.
- COM interfaces are implemented by the object that exposes the interface (they do not contain an implementation themselves). The interface is essentially a contract for the functionality. Objects like the filter graph manager, or Microsoft filters, have implemented interfaces that can be accessed. When you write a filter, you implement the interfaces.
To make filter development easier, DirectShow provides a set of C++ classes that help you implement the interfaces required by the objects you create.
© 1998 Microsoft Corporation. All rights reserved. Terms of Use.