Making Windows Media Player Plug-ins Work Together
Jim Travis
Microsoft Corporation
Updated September 2004
Applies to:
Microsoft® Windows Media® Player 9 Series SDK
Microsoft Windows Media Player 10 SDK
Summary: Describes techniques you can use to enable Microsoft Windows Media Player plug-ins to interoperate. The article covers two common scenarios: connecting a UI plug-in to a DSP plug-in and connecting a rendering plug-in to a DSP plug-in.
Contents
Introduction
About the Samples
Modifying the Sample Projects
Connection Framework Overview
About the Plug-in Agent
About the Plug-in Communication Interfaces
Connecting a UI Plug-in to a DSP Plug-in
Connecting a Rendering Plug-in to a DSP Plug-in
For More Information
Introduction
The Microsoft® Windows Media® Player 9 Series Software Development Kit (SDK) introduced a new plug-in architecture. The new architecture supports the following new plug-in types:
- Digital signal processing (DSP) plug-ins, which enable you to process audio and video before the Player renders them
- User interface (UI) plug-ins, which enable you to extend the Player UI
- Rendering plug-ins, which enable you to use and render data from a Windows Media Format SDK arbitrary data stream
Each of these three plug-in types provides a powerful way to customize Windows Media Player.
By combining different types of plug-ins, you can provide the user with additional functionality. For example, DSP plug-ins can provide a property page that you can configure to enable the user to change settings for the plug-in. However, you might want to create a design that instead displays UI elements in Windows Media Player. Such elements could be manipulated by the user to change plug-in settings, much like the Graphic Equalizer that the Player provides. You can create such a design by combining a UI plug-in with a DSP plug-in. In this design, a settings area UI plug-in provides the controls and connects to a DSP plug-in that handles the DSP tasks. From the user's perspective, the two plug-ins appear to interoperate seamlessly.
The Player SDK does not provide a way to establish this sort of communication between plug-ins. Because Windows Media Player plug-ins are Microsoft Component Object Model (COM) objects, you can create your own mechanism to connect plug-ins by using COM technology.
This article shows you how to connect Player plug-ins in two different configurations:
- A UI plug-in connected to an audio DSP plug-in. This is the scenario described previously.
- A rendering plug-in connected to a video DSP plug-in. In this scenario, you use a rendering plug-in to receive data from an .asf file arbitrary data stream and then forward that data to the DSP plug-in. The DSP plug-in uses that data to change its settings.
Understanding these two scenarios will give you the information you need to create your own plug-in connection architecture based on the examples in this article.
This article assumes that you have a working knowledge of Microsoft Visual C++® and COM, and that you have some understanding of the Windows Media Player SDK plug-in architecture and plug-in wizard. The examples in this article were created using Microsoft Visual Studio® .NET 2002, but this version of Visual Studio is not a minimum requirement to do the tasks described.
Note The techniques described in this article are available for use with Windows Media Player 9 Series and Windows Media Player 10 in the operating systems on which they are supported. They may be unavailable in subsequent versions.
This document contains the following topics:
About the Samples
The following five sample projects accompany this article:
- WMPlayerPluginAgent. This project creates a COM object that helps plug-ins locate and connect to each other.
- UI2DSP. This project creates a UI plug-in designed to connect to an audio DSP plug-in partner.
- DSP2UI. This project creates a partner audio DSP plug-in to which the UI2DSP plug-in connects.
- Render2DSP. This project creates a rendering plug-in that connects to a video DSP plug-in partner.
- DSP2Render. This project creates a partner video DSP plug-in that connects to the Render2DSP plug-in.
To download the samples, click the following link: Samples.
These projects contain working code, but they do not compile in Visual Studio because placeholders replace globally unique identifiers (GUIDs). For example, where a class identifier (ID) is required in a .reg file, the token "### CLSID" is used. To complete the projects, you must generate new GUIDs and replace tokens with the GUIDs you generate.
When creating your own projects, you should start by using the Windows Media Player Plug-in Wizard. The plug-in wizard generates new GUIDs for you automatically when it creates a project. You should also define your own interfaces, rather than using the sample ones in your projects. If you would like to modify the sample projects to make them compile, the following section will guide you. Otherwise, you can skip ahead to Connection Framework Overview.
Note Compiling the sample projects requires Visual Studio .NET 2002 or later.
Modifying the Sample Projects
The root sample projects folder contains a command-line utility, named ReplaceGUIDs.exe, that generates new GUIDs for you and places them into the sample code. To run the utility, simply browse to the folder named Connect_Plugins_Samples and then double-click the executable file. This utility requires Microsoft .NET Framework 1.0 or later. You must run the utility from the Connect_Plugins_Samples folder.
Note Be sure to point your development environment to the path for the WMPlayerPluginAgent folder as a directory for included files because each of the plug-in projects has WMPlayerPluginAgent.h as a dependency. WMPlayerPluginAgent.h is created when you compile WMPlayerPluginAgent.idl.
To modify the sample projects manually, follow these instructions:
Create the GUIDs
To create the GUIDs, you need to generate a series of GUIDs and label them. Then you must copy the GUIDs you generated into the correct places in the code. Follow these steps to create the GUIDs:
- Open the Create GUID utility (guidgen.exe). In Visual Studio .NET 2002, this utility can be run from the Tools menu.
- Choose DEFINE_GUID. This format creates text for a GUID in DEFINE_GUID format. It also provides the same GUID in registry format in a commented line just above the DEFINE_GUID format. You will need to use both of these formats depending on the usage in the code.
- Open a text editor, such as Notepad.
- Copy and paste the following labels into Notepad:
- IID1
- IID2
- IID3
- LIBID
- CLSID_Agent
- APPID_Agent
- CLSID_UI2DSP
- CLSID_DSP2UI
- IDSP2UI
- CLSID_DSP2UIPropPage
- CLSID_Render2DSP
- CLSID_DSP2Render
- IDSP2Render
- CLSID_DSP2RenderPropPage
- Below each label, copy and paste a unique GUID. Remember to click New GUID each time. When done, you have 14 different GUIDs, each having a unique label.
Modify WMPlayerPluginAgent
- Open WMPlayerPluginAgent.idl. Replace each of the three "### IID" tokens with the three GUIDs you labeled IID1, IID2, and IID3. Use registry format, but remove the curly braces.
- Using registry format (without curly braces), replace the "### LIBID" token in WMPlayerPluginAgent.idl with the GUID you labeled LIBID. Using registry format, replace the "### LIBID" token in WMPluginAgent.rgs with this GUID as well. (The sample .rgs files provide curly braces with the tokens. Do not use two sets of curly braces.)
- Replace the "### CLSID" tokens in WMPlayerPluginAgent.idl and WMPluginAgent.rgs with the GUID you labeled CLSID_Agent. Use registry format, but remove the curly braces.
- In Solution Explorer, right-click WMPlayerPluginAgent.idl and choose Compile to compile the file. This generates intermediate files for the proxy/stub .dll file.
- Open WMPlayerPluginAgent.cpp. Replace the "### APPID" token with the GUID you labeled APPID_Agent.
- Build the solution.
Modify UI2DSP
- Open UI2DSP.h. Using DEFINE_GUID format, replace the "### CLSID1" token in the definition for CLSID_UI2DSP with the GUID you labeled CLSID_UI2DSP. Using registry format, replace the "### CLSID" tokens in UI2DSP.rgs with this GUID as well.
- Replace the "### CLSID2" token in the definition for CLSID_WMPPluginAgent with the GUID you labeled CLSID_Agent. Use DEFINE_GUID format.
- Replace the "### CLSID3" token in the definition for CLSID_DSP2UI with the GUID you labeled CLSID_DSP2UI. Use DEFINE_GUID format.
- Build the solution. No .sln file is included with the sample, so you must save the one created for you when prompted.
Modify DSP2UI
- Open DSP2UI.h. Using DEFINE_GUID format, replace the "### CLSID1" token in the definition for CLSID_DSP2UI with the GUID you labeled CLSID_DSP2UI. Using registry format, replace the "### CLSID" tokens in DSP2UI.rgs with this GUID as well.
- Replace the "### CLSID2" token in the definition for CLSID_UI2DSP with the GUID you labeled CLSID_UI2DSP. Use DEFINE_GUID format.
- Replace the "### CLSID3" token in the definition for CLSID_WMPPluginAgent with the GUID you labeled CLSID_Agent. Use DEFINE_GUID format.
- Replace the "### IID" token with the GUID you labeled IDSP2UI. Use registry format.
- Open DSP2UIPropPage.h. Using DEFINE_GUID format, replace the "### CLSID" token in the definition for CLSID_DSP2UIPropPage with the GUID you labeled CLSID_DSP2UIPropPage. Using registry format, replace the "### CLSID" token in DSP2UIPropPage.rgs with this GUID as well.
- Build the solution. No .sln file is included with the sample, so you must save the one created for you when prompted.
Modify Render2DSP
- Open Render2DSP.h. Using DEFINE_GUID format, replace the "### CLSID1" token in the definition for CLSID_Render2DSP with the GUID you labeled CLSID_Render2DSP. Using registry format, replace the "### CLSID" tokens in Render2DSP.rgs with this GUID as well.
- Using DEFINE_GUID format, replace the "### CLSID2" token in the definition for CLSID_DSP2Render with the GUID you labeled CLSID_DSP2Render.
- Replace the "### CLSID3" token in the definition for CLSID_WMPPluginAgent with the GUID you labeled CLSID_Agent. Use DEFINE_GUID format.
- Build the solution. No .sln file is included with the sample, so you must save the one created for you when prompted.
Modify DSP2Render
- Open DSP2Render.h. Using DEFINE_GUID format, replace the "### CLSID1" token in the definition for CLSID_DSP2Render with the GUID you labeled CLSID_DSP2Render. Using registry format, replace the "### CLSID" tokens in DSP2Render.rgs with this GUID as well.
- Replace the "### CLSID2" token in the definition for CLSID_Render2DSP with the GUID you labeled CLSID_Render2DSP. Use DEFINE_GUID format.
- Replace the "### CLSID3" token in the definition for CLSID_WMPPluginAgent with the GUID you labeled CLSID_Agent. Use DEFINE_GUID format.
- Using registry format, replace the "### IID" token with the GUID you labeled IDSP2Render.
- Open DSP2RenderPropPage.h. Using DEFINE_GUID format, replace the "### CLSID" token in the definition for CLSID_DSP2Render with the GUID you labeled CLSID_DSP2RenderPropPage. Using registry format, replace the "### CLSID" token in DSP2RenderPropPage.rgs with this GUID as well.
- Build the solution. No .sln file is included with the sample, so you must save the one created for you when prompted.
Note DSP plug-ins require that you install and reference the Microsoft DirectX® SDK. Refer to the Windows Media Player 10 SDK for more information.
Sample Digital Media File
The download also includes a sample digital media file, named Render2DSP.asf, which works with the Render2DSP sample rendering plug-in. The file plays a video accompanied by an audio track. Note that this is an actual video, not a Windows Media Player visualization.
Connection Framework Overview
Windows Media Player instantiates plug-ins that the user has chosen to install and use. At any given moment, any combination of installed plug-ins may be active in the Player. The Player creates and destroys DSP plug-ins as it plays various types of digital media content. Rendering plug-ins are only created when a file is being played that contains a stream of the media type that the plug-in supports. Users can disable UI plug-ins at any time, and some UI plug-ins display only when the Windows Media Player Now Playing feature is visible. This raises a question when we want to enable two plug-ins to interoperate: How does any particular plug-in find and connect to a partner plug-in?
Because you are familiar with COM, you might be tempted to suggest that one or both of the plug-ins could be created as a COM singleton object. As you might recall, a singleton is a special type of object that can be instantiated exactly once for each process. The first client that uses CoCreateInstance to instantiate a singleton causes the singleton object's class factory to create the object. When subsequent clients in the same process cocreate the singleton object, they receive a pointer to the existing instance of the singleton—no additional copies of the object are instantiated. Therefore, if a DSP plug-in were a singleton, for example, a UI plug-in might simply cocreate it to get a pointer to IUnknown, and then call QueryInterface to connect to the DSP plug-in using some custom interface.
In fact, creating Windows Media Player plug-ins as singletons is problematic because there are situations where the Player requires that multiple instances of a particular plug-in exist. For example, consider the case where multiple instances of the Windows Media Player ActiveX control exist in the same process. One way this could happen is if the user opens a Web page that embeds multiple instances of the control. Normally, each instance of the control creates and uses its own DSP plug-in instance. If the DSP plug-in were a singleton, multiple instances of the Player control would try to use the same plug-in object. It is not likely that this would yield the desired result. There are other scenarios in Windows Media Player where this problem presents itself as well.
Rather than create plug-ins as singletons, you should instead create a singleton object that behaves as a registrar agent for your plug-ins. This article will refer to such an object as "the plug-in agent", or simply "the agent". For each process, each plug-in can cocreate the agent to get an interface pointer and then call a method to register itself with the agent object. The agent maintains a list of plug-ins active in the process. Individual plug-ins can query the agent to determine whether a partner plug-in exists and retrieve information about how to connect to the partner.
Once a plug-in has verified that its partner exists in the same process, it needs a pointer to a COM interface that it can use to call methods that the partner implements. Because Windows Media Player plug-ins may be instantiated in individual apartments, this pointer must be marshaled across apartment boundaries. COM provides a facility, called the Global Interface Table (GIT), which enables you to register COM interfaces so that objects in the same process can retrieve a marshaled interface pointer. When you register an interface using the GIT, you get a cookie that uniquely identifies the registered interface. By passing the cookie to another object in the process (an action that doesn't require marshaling), the other object can use the cookie to retrieve the marshaled pointer to the registered interface.
Because multiple instances of Windows Media Player can exist in the same process, there needs to be a way to associate a particular plug-in object with a particular instance of the Player. To accommodate this, the Player provides a value that identifies the individual playback engine instance to plug-ins that implement the IWMPPlugin::Init method. This value is the playback context identifier.
The remainder of this article shows you how to create a plug-in agent object that you can use and customize to fit your needs. The agent object maintains a linked list of data structures that identify a particular plug-in using its class ID GUID, its playback context identifier, and the cookie the plug-in retrieves when it registers its interface in the GIT. Once you have created the plug-in agent, this article shows you how to use it to create connections between plug-ins generated by the Windows Media Player Plug-in Wizard.
About the Plug-in Agent
The sample plug-in agent is designed to work with any combination of Windows Media Player plug-ins. The agent is simply a registrar—a directory of sorts—that exposes three methods on its public interface. Two of the methods handle registering and un-registering plug-ins with the agent; the third method enables a plug-in to locate its partner by returning the partner's GIT cookie.
The plug-in agent maintains its data in the form of a singly linked list. When a plug-in registers with the agent, the agent simply appends the data to the end of the linked list. When a plug-in attempts to find a partner, the agent searches the list sequentially, beginning with the first list element, in order to locate the partner plug-in. Un-registering a plug-in works in a similar fashion, except the plug-in is removed from the linked list once it is located. This implementation assumes that there will never be more than a few plug-ins registered with the agent at any one time (which is a reasonable assumption given that a particular agent will only be used by plug-ins created by the agent's author). Even with a few hundred plug-ins registered, this implementation should provide adequate performance in most cases. If you find that you require better performance, you can modify the sample code to use more advanced searching and sorting algorithms.
The sample plug-in agent project is an ATL dynamic-link library (DLL) project created in Visual Studio .NET. It was created without the Attributed check box selected in the ATL Project Wizard dialog box. The sample project is actually a Visual Studio .NET solution that contains two projects. The first project, named WMPlayerPluginAgent, contains the main DLL project files. The second project is named WMPlayerPluginAgentPS; it contains the code for a proxy/stub DLL that provides the marshaling code for the interfaces defined in the main project. This is important because, in addition to the default interface required for the agent, the .idl file also defines two interfaces that the sample plug-ins can use to connect to each other and to transfer data. The agent itself does not implement these interfaces or call methods on these interfaces. The plug-in agent project is simply a convenient place to declare these common interfaces for plug-in communication and to create the required proxy/stub code. Without the proxy/stub DLL, the GIT cannot marshal the registered interface pointers across apartment boundaries.
More information about the plug-in communication interfaces is provided in a later section.
When inspecting WMPPluginAgent.h, you should note that the object class inherits from CComObjectRootEx using CComMultiThreadModel. Also, note that the object registers the threading model as 'Free' in WMPPluginAgent.rgs. Finally, you should notice that the header file declares the object as a singleton in using the ATL macro as follows:
DECLARE_CLASSFACTORY_SINGLETON( CWMPPluginAgent )
Together, these code sections fulfill the requirement for creating the agent as a free-threaded singleton object.
About IWMPPluginAgent
The default interface for the agent object defines the following three methods:
HRESULT RegPlugin([in]GUID guidPlugin,
[in]DWORD dwPBContext,
[in]DWORD dwGITCookie);
HRESULT UnRegPlugin([in]DWORD dwGITCookie);
HRESULT FindPartner([in]GUID guidPluigin,
[in]DWORD dwPBContext,
[out]DWORD *pdwGITCookie);
Plug-ins call the RegPlugin method to register with the agent. Through the parameters, a plug-in provides the following three values:
- The plug-in class ID (guidPlugin). Partner plug-ins search for this unique identifier.
- The playback context identifier (dwPBContext).
- The GIT cookie (dwGITCookie). Plug-ins retrieve this value when registering with the GIT.
Plug-ins that have previously registered with the agent call UnRegPlugin to un-register, thus becoming unavailable for connection with partner plug-ins. The value provided using the dwGITCookie parameter must match the cookie that was provided when RegPlugin was called.
Plug-ins locate partner plug-ins by calling FindPartner. When a plug-in needs to find its partner, it provides the following two values through the parameters:
- The class ID of the partner to which the plug-in can connect (guidPlugin)
- The playback context identifier for the plug-in (dwPBContext)
If the FindPartner method locates the requested partner plug-in, it returns a pointer to the GIT cookie for the partner through the out parameter (pdwGITCookie).
The plug-in agent stores the data provided by a call to RegPlugin using a linked list. The data structure that comprises the linked list is defined in WMPPluginAgent.h as follows:
// Linked list element.
typedef struct PLUGIN_LIST_ELEMENT
{
GUID guidPlugin; // The GUID of the plug-in that the element represents.
DWORD dwGITCookie; // The GIT cookie provided by the plug-in.
DWORD dwPBContext; // The playback context ID provided by the plug-in.
struct PLUGIN_LIST_ELEMENT *pNext; // Pointer to the next list element.
}lstElement;
You can see that each of the parameters in RegPlugin corresponds exactly to a member of the structure. The additional member, pNext, is the linked list pointer that binds the list together. The header file also declares two member variables used to store pointers to the head and the tail of the linked list. Storing these two pointers facilitates traversing the list by starting at the head or appending items to the list tail. The following declarations are from the sample header file:
lstElement *m_pLLFirst; // Pointer to the first list element.
lstElement *m_pLLLast; // Pointer to the last list element.
Registering with the Agent
Before a plug-in registers with the agent, it must register with the GIT and retrieve the GIT cookie. This process is described in a later section. For now, you should understand that the GIT cookie is simply a DWORD value that uniquely identifies an interface registered in the GIT. After the plug-in retrieves the GIT cookie, it calls the RegPlugin method to register with the agent. The following code shows the implementation of RegPlugin in the sample agent:
STDMETHODIMP CWMPPluginAgent::RegPlugin( GUID guidPlugin,
DWORD dwPBContext,
DWORD dwGITCookie )
{
HRESULT hr = S_FALSE;
DWORD dwTempCookie = 0;
Lock();
// Test whether the plug-in is already registered.
if( m_pLLFirst )
{
// Test whether the plug-in is already registered.
hr = FindPartner( guidPlugin, dwPBContext, &dwTempCookie );
if( hr != S_FALSE )
{
if( dwTempCookie == dwGITCookie )
{
Unlock();
// The plug-in is already there and it matches.
return S_OK;
}
else
{
Unlock();
// The plug-in does not match the one registered.
// The plug-in must call UnRegPlugin first
// to remove the old entry.
return E_ACCESSDENIED;
}
}
}
// Register the new plug-in.
lstElement *pPlugin = new lstElement; // New element instance for the linked list.
if( NULL == pPlugin )
{
Unlock();
return E_OUTOFMEMORY;
}
// Last hr should have been S_FALSE. Reset it.
hr = S_OK;
// Set up the new list element.
pPlugin->dwGITCookie = dwGITCookie;
pPlugin->dwPBContext = dwPBContext;
pPlugin->guidPlugin = guidPlugin;
pPlugin->pNext = NULL;
// Test whether there are no elements in the list.
if( !m_pLLFirst )
{
// Start the list.
m_pLLFirst = pPlugin;
m_pLLLast = pPlugin;
}
// Add the plug-in to the end of the list
else
{
// Add the plug-in to the end of the list.
m_pLLLast->pNext = pPlugin;
m_pLLLast = pPlugin;
}
Unlock();
return hr;
}
Because the agent object is free-threaded, the code performs much of the work within critical sections in order to protect global data. You can see that this happens by calling the methods CComObjectRootEx::Lock and CComObjectRootEx::Unlock.
Before proceeding to register the plug-in, the code first tests whether the plug-in is currently registered by calling the FindPartner method. (The implementation of the FindPartner method will be described later in this document.) If a partner's class ID is found with a matching playback context identifier, the code compares the GIT cookie supplied by the dwGITCookie parameter to the GIT cookie associated with the registered plug-in. If the cookies match, the plug-in is already registered and the method simply returns S_OK; if the cookies do not match, the method returns E_ACCESSDENIED, which ensures that only one plug-in of a given type can register with the agent for a particular playback context identifier.
If the plug-in is not currently registered, the code allocates a new list element on the heap and then starts a new linked list, if one does not exist, or appends the new element to the tail of the existing list.
Un-Registering with the Agent
To un-register, a plug-in calls UnRegPlugin, supplying the original cookie with which it registered itself. The following code shows the implementation of UnRegPlugin from the sample:
STDMETHODIMP CWMPPluginAgent::UnRegPlugin ( DWORD dwGITCookie )
{
HRESULT hr = S_OK;
lstElement *pPosition = NULL; // Pointer to the element before the one to be removed.
lstElement *pRemove = NULL; // Pointer to the element to be removed.
Lock();
// Retrieve a pointer to the list element before the one having the cookie.
hr = FindPosition( dwGITCookie, &pPosition );
if ( SUCCEEDED ( hr ) )
{
if ( hr == S_FALSE )
{
Unlock();
// The cookie was not found.
return E_INVALIDARG;
}
// The element is the first in the list.
else if ( NULL == pPosition )
{
pRemove = m_pLLFirst;
// Test whether there is only one element.
if( m_pLLFirst == m_pLLLast )
{
// Just initialize the member variables.
m_pLLFirst = NULL;
m_pLLLast = NULL;
}
else
{
// Remove the first element from the list.
m_pLLFirst = m_pLLFirst->pNext;
}
}
// The element is elsewhere.
else
{
pRemove = pPosition->pNext;
// Remove the element from the list.
pPosition->pNext = pRemove->pNext;
// Test whether there is a new last element.
if( NULL == pPosition->pNext )
{
m_pLLLast = pPosition;
}
}
// Free the memory.
delete pRemove;
pRemove = NULL;
}
Unlock();
return hr;
}
The preceding code first calls the private method named FindPosition. This method traverses the linked list from beginning to end attempting to locate the provided cookie. If the method locates the cookie, FindPosition returns a pointer to the list element immediately preceding the element having the requested cookie; this pointer may equal NULL if the matching element is at the head of the list. You can see the implementation of FindPosition in the sample file named WMPPluginAgent.cpp.
Once FindPosition returns, the code performs a series of tests to determine how to remove the list element from the linked list. You can refer to the comments to follow the logic for this process. Finally, the code frees the memory for the removed list element structure.
Finding a Partner Plug-in
A plug-in calls the FindPartner method to locate a partner plug-in that has the correct playback context identifier and exists in the same process. The following code is from the sample implementation of FindPartner:
STDMETHODIMP CWMPPluginAgent::FindPartner ( GUID guidPlugin,
DWORD dwPBContext,
/*out*/ DWORD *pdwGITCookie )
{
HRESULT hr = S_FALSE;
if( NULL == pdwGITCookie )
{
return E_POINTER;
}
Lock();
if( m_pLLFirst )
{
lstElement *pTemp = m_pLLFirst;
do
{
// dwPBContext == 0 means UI plug-in.
if( ( ( pTemp->dwPBContext == dwPBContext) || ( 0 == dwPBContext ) )
&&
( pTemp->guidPlugin == guidPlugin ) )
{
// Return the cookie.
*pdwGITCookie = pTemp->dwGITCookie;
hr = S_OK;
break;
}
pTemp = pTemp->pNext;
}while( pTemp != NULL );
}
Unlock();
return hr;
}
This code is straightforward. If there is at least one list element in the linked list, the code enters a loop. The code inside the loop compares the class ID and playback context identifier values to the corresponding members of each list element until it finds a match or reaches the tail of the list. If the method finds a match, it returns a pointer to the corresponding GIT cookie; if no match is found, it returns S_FALSE, which indicates that the method returned successfully but the method did not find a partner.
Notice that when the provided playback context identifier equals zero the code does not attempt to match the playback context identifiers. This is because UI plug-ins do not implement IWMPPlugin::Init and therefore never receive a playback context identifier from the Player. You will examine how UI plug-ins connect to partner plug-ins shortly.
About the Plug-in Communication Interfaces
The .idl file for the WMPlayerPluginAgent solution defines two sample interfaces that the sample plug-ins implement to enable interoperability. The interfaces are defined as follows:
interface IWMPPluginData : IUnknown
{
HRESULT get_scale([out] double *pVal);
HRESULT put_scale([in] double newVal);
};
interface IWMPPluginConnect : IWMPPluginData
{
HRESULT BeginConnection([in] GUID guidPlugin, [in] DWORD dwGITCookie);
HRESULT EndConnection([in] DWORD dwGITCookie);
};
The IWMPPluginData interface is used in the UI2DSP and DSP2UI samples. It provides two accessor methods for the scale factor value used by the sample audio and video DSP plug-ins created by the Windows Media Player Plug-in Wizard. This is a type double value between zero and one. The sample audio DSP plug-in uses this value as a multiplier to scale audio volume. This interface acts as the data bridge between the sample plug-ins.
The IWMPPluginConnect interface is used in the Render2DSP and DSP2Render samples. The methods of this interface enable the rendering and DSP plug-ins to connect and disconnect in an orderly fashion. Note that the interface derives from IWMPPluginData. This is because the sample video DSP plug-in uses the scale factor value as a multiplier to scale the color saturation level.
The sections that follow examine how these interfaces are used.
Connecting a UI Plug-in to a DSP Plug-in
The goal when connecting a UI plug-in to a DSP plug-in is to create a user interface that provides controls the user can manipulate to change settings of the DSP plug-in. This is a one-way conversation—the UI plug-in transmits some data to the DSP plug-in. Remember that at any given moment either plug-in can be instantiated alone. This means that the DSP plug-in should continue to work if the UI plug-in is disabled and the UI plug-in should indicate to the user when no DSP plug-in is available.
The sample UI plug-in in the UI2DSP sample was created using the plug-in wizard and then modified to create a slider control in the Windows Media Player settings area. The slider has a range from 0 to 100. This range is mapped to the scale factor range for the partner audio DSP plug-in such that dividing the current slider value by 100 yields the desired scale factor. When the user moves the slider, the DSP plug-in scale factor changes, which causes the audio volume to change. (This does not make a very convincing volume control. Audio DSP plug-ins buffer data, so there can be a perceptible delay between the time the user moves the slider to a new position and the moment when the volume change occurs. However, this does make for a workable sample. If you wish to provide a volume control as part of a UI plug-in, you should use the Player Settings.Volume property.)
The sample audio DSP plug-in (DSP2UI) was created using the plug-in wizard and then modified to enable connection with the UI plug-in. This means that it registers with the GIT, registers with the plug-in agent, and exposes the IWMPPluginData interface. Because the DSP plug-in cannot send data to the UI plug-in, the text control in the sample property page is disabled to prevent user input.
Preparing the DSP Plug-in to Connect
When the Player instantiates the sample DSP plug-in, the plug-in performs the following steps to enable a connection:
- Queries the registry for a default value. This is the standard behavior from the sample plug-in created by the wizard.
- Registers in the GIT and retrieve a cookie. The following code from CDSP2UI::FinalConstruct demonstrates this process:
// Retrieve a pointer to the GIT.
hr = m_spGIT.CoCreateInstance ( CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER );
// Add this plug-in to the GIT.
if( SUCCEEDED( hr ) )
{
hr = m_spGIT->RegisterInterfaceInGlobal (
reinterpret_cast<IUnknown*>(this),
__uuidof( IWMPPluginData ), &m_dwGITCookie );
}
In this code, m_spGIT is a smart pointer to IGlobalInterfaceTable, which is the GIT interface. You can see that the plug-in must first cocreate the GIT to retrieve a pointer. The code then calls IGlobalInterfaceTable::RegisterInterfaceInGlobal to add the IWMPPluginData pointer to the GIT and retrieve a cookie. The cookie is stored in the member variable m_dwGITCookie.
- Cocreates the plug-in agent to retrieve a pointer to the agent. The following code is also from CDSP2UI::FinalConstruct:
hr = m_spAgent.CoCreateInstance ( CLSID_WMPPluginAgent, NULL,
CLSCTX_INPROC_SERVER );
Note that the constant for the plug-in agent class ID is defined in the header file named DSP2UI.h.
- Registers the plug-in with the agent. The following code from CDSP2UI::Init demonstrates this:
hr = m_spAgent->RegPlugin( CLSID_DSP2UI, dwPlaybackContext,
m_dwGITCookie );
Establishing the Connection
Once the DSP plug-in is registered with the plug-in agent, the UI plug-in can take steps to locate it and transmit data to it. However, this presents an interesting problem—the Player creates and destroys DSP plug-ins as needed and there is no mechanism for the DSP plug-in to notify the UI plug-in that the Player has released it. In the rendering and DSP plug-in scenario, the methods of the IWMPPluginConnect interface enable the plug-ins to advise one another about the start or end of a connection. This technique does not work well for the UI and DSP scenario because it can create a blocked thread issue in the UI plug-in when disconnecting the plug-ins. Instead, the sample UI plug-in determines whether to connect to a DSP plug-in when the UI plug-in is created and when the Player.PlayStateChange event signals that the Player is playing.
When the Player instantiates the sample UI plug-in, the plug-in performs the following steps to enable a connection:
- Retrieves a pointer to the plug-in agent and a pointer to the GIT. This code is identical to the code that the DSP plug-in uses; you can see it in CUI2DSP::FinalConstruct. Note that the UI plug-in does not register in the GIT or with the agent; it simply uses these pointers to retrieve information.
- Tries to find the partner DSP plug-in. You can see this call to CUI2DSP::ConnectToPartner in CUI2DSP::Create. You will examine the code in ConnectToPartner in the next section.
- If connected, queries the partner plug-in for the current setting. If not connected, queries the same registry key that the partner uses to persist its setting. You can see the following code in CreateSlider:
double fTemp = 1.0F; // The partner's scale factor setting.
// Test whether a partner DSP plug-in is connected.
if( m_spPartnerData )
{
// Get the partner's current setting.
m_spPartnerData->get_scale( &fTemp );
}
else
{
CRegKey key;
LONG lResult;
// Get the setting from the registry.
lResult = key.Open(HKEY_CURRENT_USER, kszPrefsRegKey, KEY_READ);
if (ERROR_SUCCESS == lResult)
{
DWORD dwValue = 0;
lResult = key.QueryDWORDValue(kszPrefsScaleFactor, dwValue);
if (ERROR_SUCCESS == lResult)
{
fTemp = dwValue / 65536.0;
}
}
}
- Creates the slider. Sets the handle position to represent the current scale factor value. You can see this code in CreateSlider in the file named UI2DSP.cpp.
About CUI2DSP::ConnectToPartner
Before attempting to connect to a partner plug-in, the code in ConnectToPartner terminates any existing connection as follows:
// If there's a partner, release it.
if( m_spPartnerData )
{
m_spPartnerData.Release();
m_dwPartnerCookie = 0;
}
Next, the code queries the agent to determine if a partner exists in the same process. Notice that the playback context ID provided is zero to signal that this plug-in is a UI plug-in and therefore playback context is irrelevant.
// Attempt to find the partner plug-in in the agent.
// dwPBContext == 0 means this is a UI plug-in.
hr = m_spAgent->FindPartner( CLSID_DSP2UI, 0, &dwPartnerCookie );
If a partner plug-in is located, the code caches the supplied cookie and then uses it to retrieve the marshaled interface pointer from the GIT.
// Copy the partner's cookie to the member variable.
m_dwPartnerCookie = dwPartnerCookie;
// Get a pointer to the partner.
hr = m_spGIT->GetInterfaceFromGlobal( m_dwPartnerCookie,
__uuidof( IWMPPluginData ),
(void**)&m_spPartnerData );
If the pointer is retrieved, the code determines the current slider position and performs the necessary calculation to determine the value to pass to the DSP plug-in. Then, the code uses the marshaled interface pointer to call the method named put_scale to set the new value.
LRESULT lResult = 0;
double newVal = 0.0f;
// Get the current slider position.
lResult = ::SendMessage( m_hwndSlider, TBM_GETPOS, 0, 0 );
// Convert to double.
newVal = (double) lResult / 100;
// Set the DSP value to the current slider position.
m_spPartnerData->put_scale( newVal );
The code then enables the slider to signal to the user that a DSP plug-in is connected and active.
// Enable the slider.
SendMessage( m_hwndSlider, WM_ENABLE, (WPARAM)true, 0 );
If no connection was established, the code simply disables the slider to signal to the user that no DSP plug-in is connected.
if( m_hwndSlider )
{
// Disable the slider.
SendMessage( m_hwndSlider, WM_ENABLE, (WPARAM)false, 0 );
}
Updating the DSP Settings
When the user changes the slider position, the plug-in window receives the WM_HSCROLL message, which it maps to a function named OnSliderNotification. You can see the implementation of this function in the sample file named CPluginWindow.h. This function uses the slider position to calculate a new value to transmit to the DSP plug-in and then calls IWMPPluginData::put_scale to set the new value. The DSP plug-in then uses this value to update its settings in exactly the same manner as when the property page was the user interface.
Managing the Connection
In addition to establishing a connection when first created, the UI plug-in must also deal with the fact that Windows Media Player destroys and creates the partner DSP plug-in as needed. Because there is no reciprocal connection and because the Player does not fire a specific event to handle this situation, the UI plug-in must periodically determine whether to attempt to find and connect to a partner. There are two situations where this should happen. First, any time a new digital media file starts playing it is possible that the Player will create a new instance of the DSP plug-in, perhaps to accommodate a different format. Second, it is possible that the user could choose to disable or enable the DSP plug-in at any time. In both instances, the Player.PlayStateChange event occurs. Therefore, the UI2DSP sample simply calls ConnectToPartner from the event handler for PlayStateChange whenever the new state equals wmppsPlaying. This has the effect of releasing the DSP plug-in to which the UI plug-in is connected and then connecting to the DSP plug-in currently registered with the agent, if one exists. You can see this call in the implementation of CUI2DSP::PlayStateChange in the sample file named UI2DSPevents.cpp.
Of course, this means that it is important that the partner DSP plug-in un-registers with the agent whenever the Player calls IWMPPlugin::Shutdown. If the DSP plug-in were to wait for the destructor to be called before un-registering with the agent, this would never happen because the UI plug-in keeps a reference count on the partner. You can see the following code in CDSP2UI::Shutdown in the sample file named DSP2UI.cpp:
if( m_spAgent )
{
// Unregister the plug-in from the agent.
m_spAgent->UnRegPlugin( m_dwGITCookie );
}
Shutting Down
The process of shutting down the connected plug-ins is straightforward. Because only the Player keeps a reference count on the UI plug-in, the UI plug-in implementation of FinalRelease is always called before the DSP plug-in implementation. In FinalRelease, the UI plug-in simply calls release through the pointers it maintains to the GIT, the agent, and the partner. After both the UI plug-in and the Player release the partner, the partner's implementation of FinalRelease is called. Here, the DSP plug-in un-registers itself from the GIT by calling RevokeInterfaceFromGlobal, and then releases the GIT and the agent. You can see the implementation of CDSPUI::FinalRelease in the sample file named DSP2UI.cpp.
Using the Plug-ins
To use the plug-ins, you must build and register the DLL file for each plug-in, as well as the DLL files for the plug-in agent and the agent proxy/stub code. When you distribute your own projects, you must install and register each of these files on the user's computer. To see the list of registered plug-ins in Windows Media Player, on the Tools menu, point to Plug-ins. When you play a digital media file and switch to Now Playing, you should see the UI2DSP plug-in slider in the settings area just below the video display region. If you do not see the slider, check to make sure that the UI2DSP plug-in is selected. If the slider is disabled, check to make sure that the DSP2UI plug-in is selected. Changing the position of the slider will change the playback level.
Connecting a Rendering Plug-in to a DSP Plug-in
The goal when connecting a rendering plug-in to a DSP plug-in is to use the rendering plug-in to retrieve data from an arbitrary data stream and then transmit that data to the DSP plug-in. This can enable some interesting scenarios. For example, the arbitrary data stream could contain coordinates for hot spot regions in a video stream. The DSP plug-in could use the data to alter the video in some way to indicate to the user where he or she can click, while the rendering plug-in could use this data to respond when the user clicks the hot spot regions.
The sample rendering plug-in in the Render2DSP sample was created using the plug-in wizard and then modified. First, the code that creates a window and renders a bitmap was removed, as well as the code for the property page. Then, the code was added to enable connection to the DSP plug-in and to forward data retrieved from the arbitrary data stream.
The sample DSP plug-in (DSP2Render) in the Render2DSP sample was created using the plug-in wizard and then modified to enable connection to the rendering plug-in. This means that it registers with the GIT, registers with the plug-in agent, and exposes the IWMPPluginConnect interface.
Code Removed from the Rendering Plug-in
For this sample, the rendering plug-in does not need to create a window and does not perform any rendering functions. Therefore, you can remove much of the code generated by the plug-in wizard. The following code was removed from the sample rendering plug-in:
- The definition of the IRender2DSP interface. This interface was also removed from the inheritance list for the CRender2DSP class and the COM map. The method declarations and implementations for this interface were removed (get_color and put_color).
- The color constants and the variable that stores the text color.
- The registry location constants and the code that queries the registry in FinalConstruct.
- The message map.
- The method declarations and implementations for the methods related to rendering in a window: OnEraseBackground, OnPaint, DoRendering, CreateRenderWin, DestroyRenderWin, Repaint, OnPluginWindowMessage, and MakeBitmapFromData.
- The variable that stores the memory device context handle.
- The smart pointers for IWMPServices, IWMPNodeRealEstateHost, IWMPNodeWindowedHost, and IWMPNodeWindowlessHost.
- The variables that store the rendering RECT structures, the Boolean variable that stores the hosted state, and the code that initializes their values.
- From ProcessInput, the code that shows the rendering window and the code that initiates a repaint.
- From GetPages, all the code. GetPages was changed to simply return E_NOTIMPL.
- From Init, the code that initiates the creation of the rendering window.
- From Shutdown, the code that initiates the destruction of the rendering window.
- From AdviseWMPServices, all the code. The implementation was changed to simply return S_OK. The same is true for the implementation of UnAdviseWMPServices.
- From GetDesiredSize, the code that requests a 300 x 300 pixel rendering region. This code was changed to request a 0 x 0 pixel rendering region.
- From SetRects, all the code. The implementation was changed to simply return S_OK.
- From GetRects, all the code. The implementation was changed to return pointers to empty RECT structures.
- From SetOwnerWindow, all the code. The implementation was changed to simply return S_OK.
- From OnDraw, all the code. The implementation was changed to simply return S_OK.
- The files related to the property page and the include statements that reference them were removed: Render2DSPPropPage.h, Render2DSPPropPage.cpp, and Render2DSPPropPage.rgs.
Preparing to Connect
The connection between the plug-ins is reciprocal. This means each plug-in registers in the GIT and retrieves a pointer to the agent when created. You can see this code in the implementations of FinalConstruct in the sample files named Render2DSP.cpp and DSP2Render.cpp. Each plug-in also registers with the agent when the Player calls Init. This code is similar to the code used by the DSP plug-in in the UI-to-DSP plug-in connection.
Each plug-in implements the sample interface IWMPPluginConnect, which derives from IWMPPluginData. Recall that, in addition to the methods exposed by IWMPPluginData, IWMPPluginConnect exposes two methods: BeginConnection and EndConnection. These are the methods that the plug-ins use to establish and terminate connections.
Establishing the Connection
When the Player calls Init, each plug-in calls a function to attempt to connect to a partner plug-in. The implementation of this function is virtually identical for both the rendering and the DSP plug-in. The following code is from CDSP2Render::ConnectToPartner:
STDMETHODIMP CDSP2Render::ConnectToPartner()
{
HRESULT hr = S_FALSE;
DWORD dwPartnerCookie;
if( !m_spGIT ||
!m_spAgent )
{
return E_POINTER;
}
// If there is a partner, disconnect and release it.
if( m_spPartner )
{
m_spPartner->EndConnection( NULL );
m_spPartner.Release();
m_dwPartnerCookie = 0;
}
// Attempt to find the partner plug-in in the agent.
hr = m_spAgent->FindPartner( CLSID_Render2DSP,
m_dwPlaybackContext,
&dwPartnerCookie );
if( SUCCEEDED( hr ) &&
hr != S_FALSE )
{
// Copy the partner's cookie to the member variable.
m_dwPartnerCookie = dwPartnerCookie;
// Get the interface pointer from the GIT.
hr = m_spGIT->GetInterfaceFromGlobal( m_dwPartnerCookie,
__uuidof( IWMPPluginConnect ),
(void**)&m_spPartner );
// Try to connect to the partner.
if( SUCCEEDED( hr ) && m_spPartner )
{
hr = m_spPartner->BeginConnection( CLSID_DSP2Render, m_dwGITCookie );
}
if( FAILED( hr ) )
{
m_spPartner.Release();
}
}
return hr;
}
The preceding example code first terminates any existing connection. Then, the code calls FindPartner to attempt to locate a partner plug-in registered with the agent. If the partner is found, the code retrieves a marshaled pointer to the partner's IWMPPluginConnect interface and then calls BeginConnection. The plug-in passes as arguments its own class ID, to identify itself as the caller, and the GIT cookie that the partner can use to retrieve a marshaled interface pointer to the caller's IWMPPluginConnect interface.
The implementation of BeginConnection is also similar in each plug-in. Here is the code from CRender2DSP::BeginConnection:
STDMETHODIMP CRender2DSP::BeginConnection ( GUID guidPlugin, DWORD dwGITCookie )
{
HRESULT hr = E_ACCESSDENIED;
// Test whether a valid partner is attempting to connect.
if( !m_spPartner &&
guidPlugin == CLSID_DSP2Render &&
m_spGIT )
{
m_dwPartnerCookie = dwGITCookie;
// Get a pointer to the partner from the GIT.
hr = m_spGIT->GetInterfaceFromGlobal ( m_dwPartnerCookie,
__uuidof( IWMPPluginConnect ),
reinterpret_cast<void**>( &m_spPartner ) );
}
return hr;
}
The preceding sample code first inspects the caller's GUID to determine whether the caller should be permitted to connect. Note that if a connection already exists, the plug-in rejects any additional connections. If the connection is allowed, the plug-in caches the supplied GIT cookie and uses it to retrieve a marshaled interface pointer to the caller's IWMPPluginConnect interface. Once this process is complete, each plug-in holds a reference count on the other and the connection is therefore established. Keep in mind that it makes no difference which plug-in initiates the connection; the process is the same regardless of which plug-in the Player instantiates first.
Updating the DSP Settings
The values for the DSP plug-in scale factor setting are embedded in an arbitrary data stream in the Windows Media file. The rendering plug-in registers as a handler for the media type that represents the data in the arbitrary data stream. When Windows Media Player opens a file that contains arbitrary data, it locates and loads the plug-in and then delivers the data to the plug-in as the data becomes available from the stream by calling IMediaObject::ProcessInput. In the sample plug-in, the code in ProcessInput was modified so that it calls a custom function named SendDataToPartner. The following sample code is from Render2DSP.cpp:
STDMETHODIMP CRender2DSP::SendDataToPartner()
{
HRESULT hr = S_OK;
BYTE *pbInputData = NULL; // Pointer to data in input buffer.
DWORD cbInputLength = 0; // Byte count of input buffer length.
int iData = 0;
double fData = 0.0f;
// Get the data pointer and the buffer length.
if( m_spInputBuffer )
{
hr = m_spInputBuffer->GetBufferAndLength( &pbInputData, &cbInputLength );
}
if( SUCCEEDED( hr ) && m_spPartner )
{
// Get the data and convert to double.
iData = (int)*pbInputData;
fData = (double)iData / 100;
// Query interface for a pointer
// to the partner's IWMPPluginData interface.
// Note that because the marshaled interface derives from
// IWMPPluginData there is no need to marshal this pointer.
CComPtr<IWMPPluginData> spPluginData( m_spPartner );
// Give the data to the DSP plug-in.
hr = spPluginData->put_scale( fData );
}
else
{
hr = E_POINTER;
}
return hr;
}
The preceding sample code retrieves the data in the buffer referenced by m_spInputBuffer and converts it to a double value that represents a new scale factor for the DSP plug-in. Then, the code gets a pointer to the IWMPPluginData interface from the DSP plug-in and uses that pointer to call the put_scale method. The implementation of put_scale in the DSP plug-in works the same as when the property page calls the method. The result is that the video saturation level changes during playback each time a new value is delivered from the arbitrary data stream.
Ending the Connection
At any time, either plug-in can terminate the connection. This happens when either plug-in calls its partner's implementation of EndConnection. In the sample plug-ins, this happens when the Player calls Shutdown. When you inspect this code, notice that the plug-in un-registers with the agent prior to ending any active connection to a partner. This prevents a plug-in from attempting to reconnect with a partner that is in the process of shutting down. The following sample code is from CRender2DSP::EndConnection:
STDMETHODIMP CRender2DSP::EndConnection ( DWORD dwGITCookie )
{
HRESULT hr = E_ACCESSDENIED;
// Test whether this is the right partner.
if( m_spPartner &&
dwGITCookie == m_dwPartnerCookie )
{
// Disconnect.
m_spPartner.Release();
m_dwPartnerCookie = 0;
hr = S_OK;
}
return hr;
}
The preceding sample code simply releases the caller, which allows it to complete its shutdown process, and initializes the variable that stores the partner's GIT cookie.
Using the Plug-ins
To use the plug-ins, you must build and register the DLL file for each plug-in, as well as the DLL files for the plug-in agent and the agent proxy/stub code. When distributing your own projects, you must install and register each of these files on the user's computer. When you run Windows Media Player, the DSP2Render plug-in is available from the Tools menu when you point to Plug-ins. The Render2DSP plug-in is listed in the Options dialog box when view the Plug-ins tab and select Renderer in the Category list box.
When you play the sample file, Render2DSP.asf, Windows Media Player automatically loads the rendering plug-in. The sample file has scale factor values embedded at 1 second, 6 seconds, and 11 seconds from the start of the file. At 1 second, the DSP2Render plug-in changes the video to grayscale; at 6 seconds, it changes the video to 50 percent color saturation; at 11 seconds, it restores full color saturation. Note that when you first open the file, the video may be grayscale from the start because this is the default setting for the DSP plug-in.
For More Information
- To learn more about Windows Media Player, see Windows Media Player Help. You can download Windows Media Player 10 from the Windows Media Download Center (http://www.microsoft.com/windows/windowsmedia/download/).
- To learn more about Windows Media Player plug-ins, see the Windows Media Player 10 SDK. You can download the Windows Media Player 10 SDK from the Windows Media Downloads Web page. (http://msdn.microsoft.com/library/default.asp?url=/downloads/list/winmedia.asp)
- To view the latest Windows Media Player 10 SDK documentation, see the MSDN Web site (http://msdn.microsoft.com/library/en-us/wmplay10/mmp_sdk/windowsmediaplayer10sdk.asp).