使 Windows Media Player 插件协同工作
Jim Travis
Microsoft Corporation
适用于:
Microsoft® Windows Media® Player 9 Series SDK
Microsoft Windows Media Player 10 SDK
**摘要:**描述使 Microsoft Windows Media Player 插件可以交互操作的使用技巧。本文包括两个常用的方案:将 UI 插件连接到 DSP 插件和将呈现插件连接到 DSP 插件。
本页内容
简介
关于示例
修改示例项目
连接框架概述
关于插件代理
关于插件通讯接口
将 UI 插件连接到 DSP 插件
将呈现插件连接到 DSP 插件
更多信息
简介
Microsoft® Windows Media® Player 9 Series 软件开发工具包 (SDK) 引入了一种新的插件体系结构。这种新的体系结构支持下列新插件类型:
数字信号处理 (DSP) 插件,能使您在播放器呈现音频和视频之前处理它们
用户界面 (UI) 插件,能使您扩展播放器 UI
呈现插件,能使您使用并呈现来自 Windows Media Format SDK 任意数据流的数据
三种插件类型的每种类型都提供自定义 Windows Media Player 的有效方法。
通过组合不同类型的插件,可为用户提供额外的功能。例如,DSP 插件能够提供可以配置的属性页,这样用户就可以更改插件的设置。然而,您可能想要创建一种可以替代在 Windows Media Player 中显示 UI 元素的设计。用户通过更改插件设置可以操作这些元素,很像播放器提供的 Graphic Equalizer。通过组合 UI 插件和 DSP 插件,可创建这样的设计。在该设计中,设置区域 UI 插件提供控件并连接到处理 DSP 任务的 DSP 插件。从用户的角度来看,两个插件好像无缝地交互操作。
播放器 SDK 不提供在插件间建立这种通讯的方法。因为 Windows Media Player 插件是 Microsoft 组件对象模型 (COM) 对象,所以可通过使用 COM 技术创建自己的连接插件机制。
本文为您展示如何在两个不同的配置内连接播放器插件:
UI 插件连接到音频 DSP 插件。这是前面讲过的方案。
呈现插件连接到视频 DSP 插件。在这种方案中,使用呈现插件接收来自 .asf 文件任意数据流的数据,然后将数据转发到 DSP 插件。DSP 插件使用该数据来更改其设置。
了解这两种方案将为您提供所需的信息,从而可以基于本文的示例创建自己的插件连接体系结构。
本文假定您有 Microsoft Visual C++® 和 COM 方面的应用知识,并且您对 Windows Media Player SDK 插件体系结构和插件向导有些了解。本文的示例是使用 Microsoft Visual Studio® .NET 2002 创建的,但该版本的 Visual Studio 不是执行所描述任务的最低要求。
注 本文中所讲述的方法可在它们所支持的操作系统中的 Windows Media Player 9 Series 和 Windows Media Player 10 中使用。它们在后续版本中可能不可用。
本文档包括以下主题:
关于示例
本文包含下面五个示例项目:
WMPlayerPluginAgent。该项目创建帮助插件定位并彼此连接的 COM 对象。
UI2DSP。该项目创建 UI 插件,其设计是为了连接到音频 DSP 插件伙伴。
DSP2UI。该项目创建一个连接到 UI2DSP 插件的伙伴音频 DSP 插件。
Render2DSP。该项目创建一个连接到视频 DSP 插件伙伴的呈现插件。
DSP2Render。该项目创建一个连接到 Render2DSP 插件的伙伴视频 DSP 插件。
要下载这些示例,单击下列链接:示例。
这些项目包含工作码,但它们不在 Visual Studio 中编译,因为占位符替换了全局唯一标识符 (GUID)。例如,在 .reg 文件中需要类标识符 (ID) 的地方就使用标记“### CLSID”。为了完成这些项目,必须生成新的 GUID 并用生成的 GUID 替换这些标记。
当创建自己的项目时,应该从使用 Windows Media Player 插件向导开始。插件向导在创建项目时自动为您生成新的 GUID。您还应该定义自己的接口,而不应利用项目中的示例接口。如果想修改示例项目以编译它们,下面的部分将为您提供指导。否则,可向前跳到连接框架概述。
注 编译示例项目需要 Visual Studio .NET 2002 或更高版本。
修改示例项目
示例项目根文件夹包含一个命令行实用程序,名称为 ReplaceGUIDs.exe,该程序为您生成新的 GUID,并把它们放到示例代码中。要运行该实用程序,只需要浏览名称为 Connect Plugins Samples 的文件夹,然后双击可执行文件。该实用程序需要 Microsoft .NET Framework 1.0 或更高版本。必须从 Connect Plugins Samples 文件夹运行该实用程序。
注 一定要将开发环境指向 WMPlayerPluginAgent 文件夹路径,将它作为所包含文件的目录,因为每个插件项目都与 WMPlayerPluginAgent.h 相关。编译 WMPlayerPluginAgent.idl 时将创建 WMPlayerPluginAgent.h。
要手动修改示例项目,遵循下列指示:
创建 GUID
要创建 GUID,必须生成一系列 GUID 并对它们进行标记。然后,必须将生成的 GUID 复制到代码中正确的位置。按照下面的步骤创建 GUID:
打开 Create GUID 实用程序 (guidgen.exe)。在 Visual Studio .NET 2002 中,该实用程序可以从 Tools 菜单运行。
选择 DEFINE GUID。该格式以 DEFINE GUID 格式创建 GUID 的文本。它还在注释行中以注册表格式提供同样的 GUID,该注释行就在 DEFINE GUID 格式的上面。需要根据它们在代码中的用途使用这两种格式。
打开文本编辑器,例如记事本。
将下列标签复制并粘贴到记事本中:
IID1
IID2
IID3
LIBID
CLSID Agent
APPID Agent
CLSID UI2DSP
CLSID DSP2UI
IDSP2UI
CLSID DSP2UIPropPage
CLSID Render2DSP
CLSID DSP2Render
IDSP2Render
CLSID DSP2RenderPropPage
在每个标签下,复制并粘贴一个唯一的 GUID。记住每次都要单击 New GUID。完成后,您就有了 14 个不同的 GUID,每个 GUID 都有一个唯一的标签。
修改 WMPlayerPluginAgent
打开 WMPlayerPluginAgent.idl。用标记为 IID1、IID2 和 IID3 的三个 GUID 分别替换三个“### IID”标记。使用注册表格式,但删掉大括号。
使用注册表格式(没有大括号),用标记为 LIBID 的 GUID 替换 WMPlayerPluginAgent.idl 中的“### LIBID”标记。使用注册表格式,还是用该 GUID 替换 WMPluginAgent.rgs 中的“### LIBID”标记。(示例 .rgs 文件中有带大括号的标记。不要使用两组大括号。)
用标记为 CLSID Agent 的 GUID 替换 WMPlayerPluginAgent.idl 和 WMPluginAgent.rgs 中的“### CLSID”标记。使用注册表格式,但删掉大括号。
在 Solution Explorer 中,右键单击 WMPlayerPluginAgent.idl 并选择 Compile 以编译文件。这就生成了 proxy/stub .dll 的中间文件。
打开 WMPlayerPluginAgent.cpp。用标记为 APPID Agent 的 GUID 替换“### APPID”标记。
构建解决方案。
修改 UI2DSP
打开 UI2DSP.h。使用 DEFINE GUID 格式,用标记为 CLSID UI2DSP 的 GUID 替换 CLSID UI2DSP 定义中的“### CLSID1”标记。使用注册表格式,还是用该 GUID 替换 UI2DSP.rgs 中的“### CLSID”标记。
用标记为 CLSID Agent 的 GUID 替换 CLSID WMPPluginAgent 定义中的“### CLSID2”标记。使用 DEFINE GUID 格式。
用标记为 CLSID DSP2UI 的 GUID 替换 CLSID DSP2UI 定义中的“### CLSID3”标记。使用 DEFINE GUID 格式。
构建解决方案。示例中不包括 .sln 文件,所以出现提示时您必须保存为您创建的那个 .sln 文件。
修改 DSP2UI
打开 DSP2UI.h。使用 DEFINE GUID 格式,用标记为 CLSID DSP2UI 的 GUID 替换 CLSID DSP2UI 定义中的“### CLSID1”标记。使用注册表格式,还是用该 GUID 替换 DSP2UI.rgs 中的“### CLSID”标记。
用标记为 CLSID UI2DSP 的 GUID 替换 CLSID UI2DSP 定义中的“### CLSID2”标记。使用 DEFINE GUID 格式。
用标记为 CLSID Agent 的 GUID 替换 CLSID WMPPluginAgent 定义中的“### CLSID3”标记。使用 DEFINE GUID 格式。
用标记为 IDSP2UI 的 GUID 替换“### IID”标记。使用注册表格式。
打开 DSP2UIPropPage.h。使用 DEFINE GUID 格式,用标记为 CLSID DSP2UIPropPage 的 GUID 替换 CLSID DSP2UIPropPage 定义中的“### CLSID”标记。使用注册表格式,还是用该 GUID 替换 DSP2UIPropPage.rgs 中的“### CLSID”标记。
构建解决方案。该示例中不包括 .sln 文件,所以出现提示时您必须保存为您创建的那个 .sln 文件。
修改 Render2DSP
打开 Render2DSP.h。使用 DEFINE GUID 格式,用标记为 CLSID Render2DSP 的 GUID 替换 CLSID Render2DSP 定义中的“### CLSID1”标记。使用注册表格式,还是用该 GUID 替换 Render2DSP.rgs 中的“### CLSID”标记。
使用 DEFINE GUID 格式,用标记为 CLSID DSP2Render 的 GUID 替换 CLSID DSP2Render 定义中的“### CLSID2”标记。
用标记为 CLSID Agent 的 GUID 替换 CLSID WMPPluginAgent 定义中的“### CLSID3”标记。使用 DEFINE GUID 格式。
构建解决方案。该示例中不包括 .sln 文件,所以出现提示时您必须保存为您创建的那个 .sln 文件。
修改 DSP2Render
打开 DSP2Render.h。使用 DEFINE GUID 格式,用标记为 CLSID DSP2Render 的 GUID 替换 CLSID DSP2Render 定义中的“### CLSID1”标记。使用注册表格式,还是用该 GUID 替换 DSP2Render.rgs 中的“### CLSID”标记。
用标记为 CLSID Render2DSP 的 GUID 替换 CLSID Render2DSP 定义中的“### CLSID2”标记。使用 DEFINE GUID 格式。
用标记为 CLSID Agent 的 GUID 替换 CLSID WMPPluginAgent 定义中的“### CLSID3”标记。使用 DEFINE GUID 格式。
使用注册表格式,用标记为 IDSP2Render 的 GUID 替换“### IID”标记。
打开 DSP2RenderPropPage.h。使用 DEFINE GUID 格式,用标记为 CLSID DSP2RenderPropPage 的 GUID 替换 CLSID DSP2Render 定义中的“### CLSID”标记。使用注册表格式,还是用该 GUID 替换 DSP2RenderPropPage.rgs 中的“### CLSID”标记。
构建解决方案。该示例中不包括 .sln 文件,所以出现提示时您必须保存为您创建的那个 .sln 文件。
注 DSP 插件要求安装并引用 Microsoft DirectX® SDK。有关更多信息,请参见 Windows Media Player 10 SDK。
示例数字媒体文件
下载还包括名称为 Render2DSP.asf 的示例数字媒体文件,它与 Render2DSP 示例呈现插件一同工作。该文件播放视频时伴有音频曲目。注意,这是个真正的视频,不是 Windows Media Player 显象。
连接框架概述
Windows Media Player 实例化用户选择安装和使用的插件。在任何指定时刻,都可以在播放器内激活所安装插件的任何组合。播放器在播放各种类型的数字媒体内容时创建并破坏 DSP 插件。呈现插件只在播放包含插件支持的媒体类型流的文件时才创建。用户在任何时候都可禁用 UI 插件,而某些 UI 插件只在 Windows Media Player Now Playing 功能可见时才显示。这在我们想要使两个插件进行交互操作时就产生了问题:如何找到特定插件并连接到伙伴插件?
因为您熟悉 COM,所以可能对这样的建议感兴趣:一个或两个插件可以创建成为 COM singleton 对象。您可能已经回忆起来,singleton 是个特殊的对象类型,每个过程只能实例化一次。使用 CoCreateInstance 的实例化 singleton 的第一个客户端导致 singleton 对象的类工厂创建该对象。当同一进程中的后续客户端共同创建 singleton 对象时,它们接收指向现有 singleton 实例的指针—没有额外的对象副本被实例化。因此,例如,如果某个 DSP 插件是 singleton,UI 插件仅通过共同创建它就可获得指向 IUnknown 的指针,然后利用某个自定义接口调用 QueryInterface 以连接到 DSP 插件。
事实上,将 Windows Media Player 插件创建为 singletons 是有问题的,因为在有些情况下播放器要求特定插件存在多个实例。例如,考虑 Windows Media Player ActiveX 控件在同一进程中存在多个实例的情况。仅当用户打开嵌入多个控件实例的 Web 页时,才可能发生这种情况。通常,每个控件实例都创建并使用其自己的 DSP 插件实例。如果 DSP 插件是 singleton,则多个播放器控件实例将试图使用同一插件对象。这不太可能产生想要的结果。Windows Media Player 中的其他方案中也会出现这样的问题。
不应当将插件创建为 singleton,而应创建一个 singleton 对象来作为插件的注册代理。本文将这样的对象称为“插件代理”或只称为“代理”。对每个进程来说,每个插件都能共同创建代理来获得接口指针,然后调用一个方法以用代理对象对自身进行注册。该代理维护在进程中处于激活状态的一系列插件。单个插件可以查询代理以确定是否存在伙伴插件,并检索有关如何连接到伙伴的信息。
一旦插件验证在同一进程中存在它的伙伴,则它需要一个指向 COM 接口的指针,用该指针来调用该伙伴实现的方法。因为 Windows Media Player 插件可以在单独的单元中实例化,所以该指针必须跨单元边界进行封送处理。COM 提供一个称为全局接口表 (GIT) 的功能,它使您能够注册 COM 接口,以便同一进程中的对象就能检索封送的接口指针。当利用 GIT 注册接口时,您就获得能够唯一标识已注册接口的 cookie。通过将 cookie 传递给该进程中的另一个对象(该行为不需封送处理),另一个对象可以使用 cookie 来检索经封送处理的、指向已注册接口的指针。
因为在同一进程中可存在多个 Windows Media Player 实例,所以需要一种将特定的插件对象与特定的播放器实例关联起来的方法。为适应这一点,播放器为实现 IWMPPlugin::Init 方法的插件提供了一个标识单个播放引擎实例的值。该值就是播放上下文标识符。
本文的其余部分说明如何创建可以使用并可以自定义以满足您的需要的插件代理对象。代理对象维护一个数据结构链接列表,该列表用其类 ID GUID、播放上下文标识符,以及它在 GIT 中注册其接口时插件检索的 cookie 来标识某个特定插件。创建了插件代理之后,本文将为您讲述如何利用它来创建由 Windows Media Player 插件向导生成的插件之间的连接。
关于插件代理
设计的示例插件代理旨在与任何 Windows Media Player 插件组合一同工作。该代理只是一个注册器 — 一个分类目录 — 它在其公共接口上公开了三种方法。其中两个方法用代理处理注册和注销插件;第三个方法通过返回伙伴的 GIT cookie 使得插件能够找到它的伙伴。
插件代理以一个链接列表的形式来维护其数据。当用代理注册插件时,代理只是将数据附加到链接列表的结尾处。当插件试图寻找伙伴时,代理从第一个列表元素开始,按顺序搜索列表,以找到伙伴插件。除了一旦找到插件就将其从链接列表中删除之外,注销插件以类似的方式工作。这种实现假定任何时候用代理注册的插件都很少(假如由代理的创作者创建的插件只使用某个特定的代理,这样的假设是合理的)。即使注册了几百个插件,在大多数情况下该实现也应提供足够的性能。如果发现需要更好的性能,可修改示例代码以使用更高级的搜索和排序算法。
该插件代理示例项目是一个在 Visual Studio .NET 中创建的 ATL 动态链接库 (DLL) 项目。它是在没选中“ATL Project Wizard”对话框中的 Attributed 复选框时创建的。该示例项目实际上是个包括两个项目的 Visual Studio .NET 解决方案。第一个项目名称为 WMPlayerPluginAgent,包含主 DLL 项目文件。第二个项目名称为 WMPlayerPluginAgentPS;它包含 proxy/stub DLL 代码,可为在主项目中定义的接口提供封送处理代码。这很重要,因为除了代理需要的默认接口外,.idl 文件还定义了示例插件可用来彼此连接并传送数据的两个接口。代理本身并不实现这些接口或者调用这些接口上的方法。插件代理项目只是一个声明这些插件通讯公共接口以及创建所需的 proxy/stub 代码的合适位置。没有 proxy/stub DLL,GIT 就不能跨单元边界对已注册的接口指针进行封送处理。
有关插件通讯接口的更多信息,将在后面部分给出。
当检查 WMPPluginAgent.h 时,应注意该对象类利用CComMultiThreadModel 继承了 CComObjectRootEx。还要注意,该对象在 WMPPluginAgent.rgs 中将线程模型注册为‘Free’。最后,应注意到,在利用 ATL 宏时,头文件将该对象声明为 singleton,如下所示:
DECLARE_CLASSFACTORY_SINGLETON( CWMPPluginAgent )
这几部分代码一起可以满足将代理创建为与线程无关的 singleton 对象的要求。
关于 IWMPPluginAgent
代理对象的默认接口定义了以下三种方法:
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);
插件调用 RegPlugin 方法来用代理注册。通过参数,插件提供以下三个值:
插件类 ID (guidPlugin)。伙伴插件搜索该唯一的标识符。
播放上下文标识符 (dwPBContext)。
GIT cookie (dwGITCookie)。插件在用 GIT 注册时检索该值。
之前已经用代理注册的插件调用 UnRegPlugin 进行注销,因此就不能与伙伴插件连接。利用 dwGITCookie 参数提供的值必须与调用 RegPlugin 时提供的 cookie 匹配。
通过调用 FindPartner,插件可以找到伙伴插件。当插件需要查找其伙伴时,它通过这些参数,提供了下面的两个值:
插件可连接到的伙伴的类 ID (guidPlugin)
插件的播放上下文标识符 (dwPBContext)
如果 FindPartner 方法找到了所需的伙伴插件,它通过输出参数 (pdwGITCookie) 返回指向伙伴的 GIT cookie 的指针。
插件代理通过利用链接列表调用 RegPlugin 来存储提供的数据。组成链接列表的数据结构在 WMPPluginAgent.h 中定义,如下所示:
// 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;
可以看到,RegPlugin 中的每个参数都正好对应该结构的一个成员。附加成员 pNext,是与列表绑定在一起的链接列表指针。头文件还声明了用来将指针存储到链接列表头和尾的两个成员变量。存储这两个指针方便了遍历列表:从头开始或将项目附加到列表尾部。下列声明来自示例头文件:
lstElement *m_pLLFirst; // Pointer to the first list element. lstElement *m_pLLLast; // Pointer to the last list element.
用代理注册
在插件用代理注册之前,必须先用 GIT 注册并检索 GIT cookie。该过程将在后面部分说明。目前,您应理解 GIT cookie 只是一个 DWORD 值,它唯一标识在 GIT 中注册的接口。在插件检索 GIT cookie 后,它调用 RegPlugin 方法以用代理注册。下列代码显示了在该示例代理中 RegPlugin 的实现:
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; }
因为代理对象是与线程无关的,所以该代码执行重要区域中的大量工作以保护全局数据。通过调用方法 CComObjectRootEx::Lock 和 CComObjectRootEx::Unlock,可以看到这种情况的发生。
在继续注册该插件之前,该代码首先通过调用 FindPartner 方法测试插件是否在最近注册过。(FindPartner 方法的实现将在本文档的后面部分讲述。)如果找到某个伙伴的类 ID,并且播放上下文标识符匹配,则代码将把由 dwGITCookie 参数提供的 GIT cookie 与和注册插件有关的 GIT cookie 进行比较。如果这些 cookies 匹配,则该插件已经注册,该方法只返回 S_OK;如果 cookies 不匹配,则该方法返回 E_ACCESSDENIED,这保证了对于一个特定的播放上下文标识符只能用代理注册一个特定类型的插件。
如果插件目前没有注册,代码将在堆中分配一个新的列表元素,之后如果不存在链接列表,则启动一个新的链接列表,或者将新元素附加到现有列表的尾部。
用代理注销
为了注销,插件调用 UnRegPlugin,提供插件用其对自身进行注册的原始 cookie。下列代码显示了该示例中 UnRegPlugin 的实现:
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; }
上述代码首先调用名称为 FindPosition 的私有方法。该方法从头到尾遍历链接列表,试图找到提供的 cookie。如果该方法找到了 cookie,则 FindPosition 返回一个指针,它指向后面紧跟具有所需 cookie 的元素的列表元素;如果匹配元素在列表的头部,则该指针可能相当于 NULL。在名称为 WMPPluginAgent.cpp 的示例文件中,可看到 FindPosition 的实现。
一旦 FindPosition 返回,代码将执行一系列测试以确定如何删除链接列表中的列表元素。可以参考注释以遵循这个过程的逻辑。最后,代码释放已删除列表元素结构的内存。
查找伙伴插件
插件调用 FindPartner 方法以查找具有正确的播放上下文标识符并存在于同一进程中的伙伴插件。下列代码来自于 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; }
该代码很简单。如果链接列表中至少有一个列表元素,则代码进入一个循环。循环内的代码将类 ID 和播放上下文标识符值与每个列表元素的对应成员相比较,直到找到一个匹配元素或到达列表尾部。如果方法找到一个匹配元素,则返回指向对应的 GIT cookie 的指针;如果没找到匹配元素,则返回 S_FALSE,这表明该方法成功返回但没找到伙伴。
注意,当提供的播放上下文标识符等于 0 时,代码并不试图与播放上下文标识符匹配。这是因为 UI 插件不实现 IWMPPlugin::Init,因而从不从播放器接收播放上下文标识符。您将检查 UI 插件是如何迅速连接到伙伴插件的。
关于插件通讯接口
用于 WMPlayerPluginAgent 解决方案的 .idl 文件定义了该示例插件实现的两个示例接口以能够进行交互操作。接口定义如下:
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); };
UI2DSP 和 DSP2UI 示例中使用了 IWMPPluginData 接口。它为比例因子值提供了两种访问器方法,该值由 Windows Media Player 插件向导创建的示例音频和视频 DSP 插件使用。这是一个 0 和 1 之间的 double 类型的值。示例音频 DSP 插件用该值作为音频音量的比例因子。该接口充当示例插件间的数据桥。
IWMPPluginConnect 接口在 Render2DSP 和 DSP2Render 示例中使用。该接口的方法使得呈现和 DSP 插件可以有序地连接和断开。注意,该接口派生于 IWMPPluginData。这是因为示例视频 DSP 插件用该比例因子值作为色彩饱和度的比例因子。
接下来的部分检查这些接口是如何使用的。
将 UI 插件连接到 DSP 插件
将 UI 插件连接到 DSP 插件的目的是,创建为用户提供能操作控件的用户界面以更改 DSP 插件的设置。这是一种单向的会话—UI 插件将一些数据传送到 DSP 插件。记住,在任意给定时刻任一插件都可单独实例化。这意味着,如果禁用 UI 插件,则 DSP 插件应该继续工作,而如果 DSP 插件不可用,则 UI 插件应向用户指明。
UI2DSP 示例中的示例 UI 插件是利用插件向导创建的,然后进行更改以在 Windows Media Player 设置区域创建一个滑块控件。该滑块的范围是从 0 到 100。该范围映射到伙伴音频 DSP 插件的比例因子范围,这样将当前的滑块值除以 100 就得到期望的比例因子。当用户移动滑块时,DSP 插件比例因子发生变化,从而导致音频音量发生变化。(这并没产生十分合理的音量控制。音频 DSP 插件缓冲数据,所以,在用户将滑块移动到新位置与音量发生变化的那一刻间将存在一个明显的延迟。然而,这确实创建了一个可以工作的示例。如果想提供一个音量控件作为 UI 插件的一部分,则应使用播放器的 Settings.Volume 属性。)
示例音频 DSP 插件 (DSP2UI) 是利用插件向导创建的,然后进行修改以能够与 UI 插件连接。这意味着,它用 GIT 注册,用插件代理注册,并且公开 IWMPPluginData 接口。因为 DSP 插件不能向 UI 插件发送数据,所以禁用示例属性页中的文本控件以防止用户输入。
准备连接 DSP 插件
当播放器实例化示例 DSP 插件时,插件执行下列步骤来启用连接:
查询注册表中的默认值。这是向导创建的示例插件的标准行为。
在 GIT 中注册,并检索 cookie。下列来自 CDSP2UI::FinalConstruct 的代码表明了这个过程:
// 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 ); }
在该代码中,m\_spGIT 是指向 IGlobalInterfaceTable 的智能指针,IGlobalInterfaceTable 是 GIT 接口。可以看到插件必须首先共同创建 GIT 以检索指针。代码随后调用 **IGlobalInterfaceTable::RegisterInterfaceInGlobal**,将IWMPPluginData 指针添加到 GIT 并检索 cookie。该 cookie 存储在成员变量 m dwGITCookie 中。
共同创建插件代理以检索指向该代理的指针。下列代码也来自 CDSP2UI::FinalConstruct:
hr = m_spAgent.CoCreateInstance ( CLSID_WMPPluginAgent, NULL, CLSCTX_INPROC_SERVER );
注意,插件代理类 ID 常量是在名称为 DSP2UI.h 的头文件中定义的。
用代理注册插件。下面来自 CDSP2UI::Init 的代码证明了这一点:
hr = m_spAgent->RegPlugin( CLSID_DSP2UI, dwPlaybackContext, m_dwGITCookie );
建立连接
用插件代理注册 DSP 插件后,UI 插件可以设法找到它并将数据传送给它。但这就带来了一个有趣的问题—播放器根据需要创建并破坏 DSP 插件,而没有 DSP 插件用来通知 UI 插件播放器已释放了它的机制。在呈现插件和 DSP 插件方案中,IWMPPluginConnect 接口的方法使得插件能够相互通知连接的开始和结束。对于 UI 和 DSP 方案,该方法不是很有效,因为当断开插件时可以在 UI 插件中造成阻塞线程的问题。相反,在创建 UI 插件后并且 Player.PlayStateChange 事件发出播放器正在播放的信号时,示例 UI 插件才确定是否要连接到 DSP 插件。
当播放器实例化示例 UI 插件时,插件执行下列步骤来启用连接:
检索指向插件代理的指针和指向 GIT 的指针。该代码与 DSP 插件使用的代码是相同的;可在 CUI2DSP::FinalConstruct 中看到。注意,UI 插件没有在 GIT 中或用代理注册;它只是使用这些指针来检索信息。
试着找到伙伴 DSP 插件。可以在 CUI2DSP::Create 中看到对 CUI2DSP::ConnectToPartner 的这个调用。可在下一部分的 ConnectToPartner 中检查该代码。
如果已连接,则查询伙伴插件的当前设置。如果没有连接,则查询伙伴用于保留其设置的同一注册表项。可在 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;
}
}
}
- 创建滑块。设置滑块柄的位置来表示当前的比例因子值。可以在名称为 UI2DSP.cpp 的文件中的 CreateSlider 中看到该代码。
关于 CUI2DSP::ConnectToPartner
在试图连接到伙伴插件之前,ConnectToPartner 中的代码终止任何现有的连接,如下所示:
// If there's a partner, release it. if( m_spPartnerData ) { m_spPartnerData.Release(); m_dwPartnerCookie = 0; }
接下来,代码查询代理以确定伙伴是否在同一进程中存在。注意,提供的播放上下文 ID 为 0 表示该插件是一个 UI 插件,因此播放上下文不相关。
// 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 );
如果找到了伙伴插件,该代码缓存所提供的 cookie,然后用它来检索来自 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 );
如果检索到指针,该代码确定当前滑块的位置并执行必要的计算来确定传送到 DSP 插件的值。然后,代码使用进行封送处理的接口指针调用名称为 put scale 的方法,设置新值。
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 );
然后,该代码启用滑块,以发信号通知用户 DSP 插件已经连接并且激活。
// Enable the slider. SendMessage( m_hwndSlider, WM_ENABLE, (WPARAM)true, 0 );
如果未建立连接,则代码只是禁用滑块,以发信号通知用户未连接任何 DSP 插件。
if( m_hwndSlider ) { // Disable the slider. SendMessage( m_hwndSlider, WM_ENABLE, (WPARAM)false, 0 ); }
更新 DSP 设置
当用户改变滑块位置时,插件窗口将接收到 WM HSCROLL 消息,该消息映射到名称为 OnSliderNotification 的函数。可在名称为 CPluginWindow.h 的示例文件中看到该函数的实现。该函数用滑块位置来计算传送给 DSP 插件的新值,然后调用 IWMPPluginData::put scale 来设置该新值。DSP 插件随后用该值更新其设置,其方式与属性页为用户界面时使用的方式是完全相同的。
管理连接
除了在最初创建时建立连接外,UI 插件还必须处理这样的事实:Windows Media Player 按照需要破坏并创建伙伴 DSP 插件。因为没有互惠的连接,而且由于播放器没有激发特定的事件来处理这种情况,所以 UI 插件必须定期确定是否尝试找到并连接到伙伴。这在两种情况下会发生。第一种情况,无论何时开始播放一个新的数字媒体文件时,播放器都将创建一个新的 DSP 插件实例,可能提供一种不同的格式。第二种情况,用户在任何时候都有可能选择禁用或启用 DSP 插件。在这两种情况中,将发生 Player.PlayStateChange 事件。因此,只要新状态等于 wmppsPlaying,UI2DSP 示例就从 PlayStateChange 的事件处理程序中调用 ConnectToPartner。这样的效果就是释放与 UI 插件连接的 DSP 插件,然后连接到目前用代理注册过的 DSP 插件,如果这样的插件存在的话。可以在名称为 UI2DSPevents.cpp 的示例文件中的 CUI2DSP::PlayStateChange 实现中看到这个调用。
当然,这意味着无论何时播放器调用 IWMPPlugin::Shutdown,用代理注销伙伴 DSP 插件都是很重要的。如果 DSP 插件在用代理注销之前等待析构函数被调用,这将永远不会发生,因为 UI 插件保留了对伙伴的引用计数。可以在名称为 DSP2UI.cpp 的示例文件中看到 CDSP2UI::Shutdown 中的下列代码:
if( m_spAgent ) { // Unregister the plug-in from the agent. m_spAgent->UnRegPlugin( m_dwGITCookie ); }
关闭
关闭连接插件的过程很简单。因为只有播放器保留对 UI 插件的引用计数,所以 FinalRelease 的 UI 插件实现在 DSP 插件实现之前总是被调用。在 FinalRelease 中,UI 插件只是请求释放它维护的指向 GIT、代理和伙伴的指针。在 UI 插件和播放器都释放了伙伴之后,才调用伙伴的 FinalRelease 实现。这里,通过调用 RevokeInterfaceFromGlobal,DSP 插件从 GIT 注销自己,随后释放 GIT 和代理。可在名称为 DSP2UI.cpp 的示例文件中看到 CDSPUI::FinalRelease 中的实现。
使用插件
要使用插件,必须为每个插件、插件代理构建并注册 DLL 文件,还要构建并注册代理 proxy/stub 代码。当您发布自己的项目时,必须在用户的计算机上安装并注册所有这些文件。要在 Windows Media Player 中查看注册插件的列表,在 Tools 菜单上,指向 Plug-ins。当播放数字媒体文件并切换到 Now Playing 时,应该能够在视频播放区域下方的设置区域中看到 UI2DSP 插件滑块。如果没看到滑块,检查并确保选择了 UI2DSP 插件。如果滑块被禁用,检查并确保选择了 DSP2UI 插件。更改滑块的位置将改变播放级别。
将呈现插件连接到 DSP 插件
将呈现插件连接到 DSP 插件的目的是为了使用呈现插件来检索来自任意数据流的数据,然后将该数据传送到 DSP 插件。这可以启用一些有趣的方案。例如,任意数据流可能包括视频流中的热点区域的坐标。DSP 插件可以按某些方式使用数据改变视频,向用户表明他或她可以单击哪里,而在用户单击热点区域时,呈现插件可使用该数据进行响应。
Render2DSP 示例中的示例呈现插件是利用插件向导创建的,并且随后进行了修改。首先,删除创建窗口并呈现位图的代码以及属性页的代码。然后,添加代码,以便能够连接到 DSP 插件,并转发从任意数据流检索到的数据。
Render2DSP 示例中的示例 DSP 插件 (DSP2Render) 是利用插件向导创建的,随后进行修改以便能够连接到呈现插件。这意味着它用 GIT 注册,用插件代理注册,并公开 IWMPPluginConnect 接口。
从呈现插件删除的代码
对于本示例,呈现插件不需创建窗口也不执行任何呈现功能。因此,可以删除大量由插件向导生成的代码。下列代码是从示例呈现插件删除的:
IRender2DSP 接口的定义。该接口还可以从 CRender2DSP 类的继承列表和 COM 映射中删除。该接口的方法声明和实现被删除(get_color 和 put_color)。
颜色常量和存储文本颜色的变量。
注册表位置常量和查询 FinalConstruct 中注册表的代码。
消息映射。
方法的声明以及与在窗口中呈现有关的方法的实现:OnEraseBackground、OnPaint、DoRendering、CreateRenderWin、DestroyRenderWin、Repaint、OnPluginWindowMessage 和 MakeBitmapFromData。
存储内存设备上下文句柄的变量。
用于 IWMPServices、IWMPNodeRealEstateHost、IWMPNodeWindowedHost 和 IWMPNodeWindowlessHost 的智能指针。
存储呈现 RECT 结构的变量,存储宿主状态的 Boolean 变量以及初始化它们的值的代码。
ProcessInput 中,显示呈现窗口的代码和启动重新绘制的代码。
GetPages 中,所有代码。更改 GetPages 仅返回 E_NOTIMPL。
Init 中,启动创建呈现窗口的代码。
Shutdown 中,启动呈现窗口析构函数的代码。
AdviseWMPServices 中,所有代码。更改实现仅返回 S_OK。对UnAdviseWMPServices 的实现做同样的处理。
GetDesiredSize 中,要求 300 x 300 像素呈现区域的代码。更改代码以要求 0 x 0 像素的呈现区域。
SetRects 中,所有代码。更改该实现仅返回 S_OK。
GetRects 中,所有代码。更改该实现返回指向空 RECT 结构的指针。
SetOwnerWindow 中,所有代码。更改该实现仅返回 S_OK。
OnDraw 中,所有代码。更改该实现仅返回 S_OK。
与属性页有关的文件以及引用它们的包括语句被删除:Render2DSPPropPage.h、Render2DSPPropPage.cpp 和 Render2DSPPropPage.rgs。
准备连接
插件间的连接是互惠的。这意味着每个插件在 GIT 中注册,并在创建时检索指向代理的指针。您可以在名称为 Render2DSP.cpp 和 DSP2Render.cpp 的示例文件中的 FinalConstruct 实现中看到该代码。当播放器调用 Init 时,每个插件还用代理注册。该代码与 UI 到 DSP 插件连接中 DSP 插件使用的代码相似。
每个插件都实现示例接口 IWMPPluginConnect,该接口派生于 IWMPPluginData。回想一下,除了 IWMPPluginData 公开的方法外,IWMPPluginConnect 公开了两个方法:BeginConnection 和 EndConnection。这些就是插件用来建立和终止连接的方法。
建立连接
当播放器调用 Init 时,每个插件都调用一个函数以试图连接到伙伴插件。该函数的实现实际上对于呈现插件和 DSP 插件都是相同的。下列代码来自 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; }
上述示例代码首先终止任何现有的连接。然后,代码调用 FindPartner 以试图找到用代理注册的伙伴插件。如果找到了伙伴,则代码检索指向伙伴的 IWMPPluginConnect 接口的进行封送处理的指针,然后调用 BeginConnection。插件作为变量传送自己的类 ID,将自己标识为调用方,并传送伙伴可用来检索指向调用方 IWMPPluginConnect 接口的进行封送处理的接口指针的 GIT cookie。
BeginConnection 的实现在每个插件中也都是相似的。下面是来自 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; }
上述示例代码首先检查调用方的 GUID 以确定是否应该允许调用方进行连接。注意,如果连接已经存在,则插件将拒绝任何其他连接。如果允许连接,则插件缓存所提供的 GIT cookie 并用它检索指向调用方 IWMPPluginConnect 接口的进行封送处理的接口指针。一旦完成该过程,每个插件都保留对另一个的引用计数,因此连接已建立。记住,哪一个插件启动连接都无关紧要;不管播放器首先实例化哪个插件,该过程都是相同的。
更新 DSP 设置
DSP 插件比例因子设置的值被嵌入到 Windows 媒体文件的任意数据流中。呈现插件将媒体类型注册为处理程序,它代表任意数据流中的数据。当 Windows Media Player 打开一个包含任意数据的文件时,它定位并加载插件,然后当流中的数据可用时通过调用 IMediaObject::ProcessInput 将数据传送到插件。在示例插件中,修改 ProcessInput 中的代码,以便它调用一个名称为 SendDataToPartner 的自定义函数。下列示例代码来自 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; }
上述示例代码检索缓冲区中 m spInputBuffer 引用的数据,将它转换成表示 DSP 插件的新比例因子的 double 值。然后代码从 DSP 插件获得指向 IWMPPluginData 接口的指针,并使用该指针调用 put_scale 方法。DSP 插件中 put_scale 的实现与属性页调用该方法的工作方式相同。结果是,播放时每次从任意数据流传送来新值,视频饱和度就改变。
结束连接
在任何时候,任一插件都可以终止连接。这在任一插件调用其伙伴的 EndConnection 实现时会发生。在示例插件中,这在播放器调用 Shutdown 时会发生。当您检查该代码时会注意到,用代理注销插件是在结束任何到伙伴的激活连接之前进行的。这可以防止插件试图与正在关闭的伙伴重新连接。下列代码来自: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; }
上述示例代码只释放了调用方,这允许它完成关闭过程,并初始化存储伙伴的 GIT cookie 的变量。
使用插件
要使用插件,必须为每个插件、插件代理构建并注册 DLL 文件,还要构建并注册代理 proxy/stub 代码。当发布您自己的项目时,必须在用户的计算机上安装并注册这些文件。如果运行 Windows Media Player,当指向 Plug-ins 时,DSP2Render 插件可在 Tools 菜单中找到。当查看 Plug-ins 选项卡,并选中 Category 列表框中的 Renderer 时,Render2DSP 插件列在 Options 对话框中。
当播放示例文件 Render2DSP.asf 时,Windows Media Player 会自动加载呈现插件。该示例文件在启动后 1 秒、6 秒和 11 秒时嵌入比例因子值。在 1 秒时,DSP2Render 插件将视频改为灰度;在 6 秒时,它将视频改为 50% 的色彩饱和度;在 11 秒时,它恢复为完全色彩饱和度。注意,当首次打开该文件时,视频开始可能是灰度,因为这是 DSP 插件的默认设置。
更多信息
要了解有关 Windows Media Player 的更多信息,请参阅 Windows Media Player 帮助。可以从 Windows Media Download Center (https://www.Microsoft.com/Windows/Windowsmedia/download/) 下载 Windows Media Player 10。
要了解有关 Windows Media Player 插件的更多信息,请参阅 Windows Media Player 10 SDK。可从 Windows Media 下载网页 (https://msdn.Microsoft.com/library/default.asp?url=/downloads/list/winmedia.asp) 下载 Windows Media Player 10 SDK。
要查看最新的 Windows Media Player 10 SDK 文档,请访问 MSDN 网站 (https://msdn.Microsoft.com/library/en-us/wmplay10/mmp sdk/Windowsmediaplayer10sdk.asp)。