I am attempting to create a Windows Media Player (WMP) Visualization plugin in C#. I am quite new to exposing C# to COM and may have missed something basic. I have persisted with this for 3 days (about 20 hours) and not got past the single issue I will describe below.
For those who don't know, WMP visualizations are the pretty images that show in media player while listening to music.
In a nutshell: WMP will call certain methods on my C# COM interface, but not others.
I have WMP 11 installed
I downloaded the Latest Windows SDK which contains a C++ plugin wizard to compile a working visualization sample. This sample registers and works without issues in WMP.
The dev kit contains a C++ header file named effects.h that contains 2 interfaces that must be implemented to get the plugin to work with WMP. It doesn't appear to much more complicated than that.
Here they are
MIDL_INTERFACE("D3984C13-C3CB-48e2-8BE5-5168340B4F35")
IWMPEffects : public IUnknown
{
public:
virtual /* [helpstring][local] */ HRESULT STDMETHODCALLTYPE Render(
/* [in] */ TimedLevel *pLevels,
/* [in] */ HDC hdc,
/* [in] */ RECT *prc) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE MediaInfo(
/* [in] */ LONG lChannelCount,
/* [in] */ LONG lSampleRate,
/* [in] */ BSTR bstrTitle) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetCapabilities(
/* [out] */ DWORD *pdwCapabilities) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetTitle(
/* [out] */ BSTR *bstrTitle) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetPresetTitle(
/* [in] */ LONG nPreset,
/* [out] */ BSTR *bstrPresetTitle) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetPresetCount(
/* [out] */ LONG *pnPresetCount) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE SetCurrentPreset(
/* [in] */ LONG nPreset) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetCurrentPreset(
/* [out] */ LONG *pnPreset) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE DisplayPropertyPage(
/* [in] */ HWND hwndOwner) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GoFullscreen(
/* [in] */ BOOL fFullScreen) = 0;
virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE RenderFullScreen(
/* [in] */ TimedLevel *pLevels) = 0;
};
MIDL_INTERFACE("695386EC-AA3C-4618-A5E1-DD9A8B987632")
IWMPEffects2 : public IWMPEffects
{
public:
virtual HRESULT STDMETHODCALLTYPE SetCore(
/* [in] */ IWMPCore *pPlayer) = 0;
virtual HRESULT STDMETHODCALLTYPE Create(
/* [in] */ HWND hwndParent) = 0;
virtual HRESULT STDMETHODCALLTYPE Destroy( void) = 0;
virtual HRESULT STDMETHODCALLTYPE NotifyNewMedia(
/* [in] */ IWMPMedia *pMedia) = 0;
virtual HRESULT STDMETHODCALLTYPE OnWindowMessage(
/* [in] */ UINT msg,
/* [in] */ WPARAM WParam,
/* [in] */ LPARAM LParam,
/* [in] */ LRESULT *plResultParam) = 0;
virtual HRESULT STDMETHODCALLTYPE RenderWindowed(
/* [in] */ TimedLevel *pData,
/* [in] */ BOOL fRequiredRender) = 0;
};
As I mentioned, my COM knowledge my not be the best. This is what I did to port it to C#.
I converted the interfaces to following
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace WmpTestPlugin
{
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("D3984C13-C3CB-48e2-8BE5-5168340B4F35")]
public interface IWmpEffects
{
int Render(ref TimedLevel pLevels, IntPtr Hdc, ref RECT pRC);
int MediaInfo(int lChannelCount, int lSampleRate, string bstrTitle);
int GetCapabilities(ref int pdwCapabilities);
int GetTitle(ref string bstrTitle);
int GetPresetTitle([In] int nPreset, [MarshalAs(UnmanagedType.BStr)] ref string bstrPresetTitle);
int GetPresetCount(ref int count);
int SetCurrentPreset(int currentpreset);
int GetCurrentPreset(ref int currentpreset);
int DisplayPropertyPage(IntPtr hwndOwner);
int GoFullScreen(bool fFullscreen);
int RenderFullScreen(ref TimedLevel pLevels);
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("695386EC-AA3C-4618-A5E1-DD9A8B987632")]
public interface IWmpEffects2 : IWmpEffects
{
int SetCore(IntPtr pPlayer);
int Create(IntPtr hwndParent);
int Destroy();
int NotifyNewMedia(IntPtr pMedia);
int OnWindowMessage(int Msg, int WParam, int LParam, ref int plResultParam);
int RenderWindowed(ref TimedLevel pData, bool fRequiredRender);
}
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential)]
public struct Data
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x400)]
public byte[] Data0;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x400)]
public byte[] Data1;
}
[ComVisible(true)]
public enum PlayerState
{
Stop_State,
Pause_State,
Play_State
}
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential)]
public struct TimedLevel
{
public Data Frequency;
public Data Waveform;
public PlayerState State;
public long TimeStamp;
}
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
}
The code for the class implementing the interfaces is as follows
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using WmpTestPlugin;
using System.IO;
using System.Windows.Forms;
[ComVisible(true)]
[Guid("C476FF24-5E5C-419d-9110-05EC2EED8511")]
//[ProgId("WmpTestPlugin.WmpTest")]
[ClassInterface(ClassInterfaceType.None)]
public class TestPlugin : IWmpEffects2
{
[DllImport("user32.dll", EntryPoint = "GetClientRect")]
private static extern bool getClientRect(IntPtr windowHandle, ref IntPtr rectangle);
private const int EFFECT_CANGOFULLSCREEN = 1;
private const int EFFECT_HASPROPERTYPAGE = 2;
private const int S_OK = 0;
private const int S_FALSE = 1;
private const int E_ABORT = unchecked((int)0x80004004);
private const int E_ACCESSDENIED = unchecked((int)0x80070005);
private const int E_FAIL = unchecked((int)0x80004005);
private const int E_HANDLE = unchecked((int)0x80070006);
private const int E_INVALIDARG = unchecked((int)0x80070057);
private const int E_NOINTERFACE = unchecked((int)0x80004002);
private const int E_NOTIMPL = unchecked((int)0x80004001);
private const int E_OUTOFMEMORY = unchecked((int)0x8007000E);
private const int E_POINTER = unchecked((int)0x80004003);
private const int E_UNEXPECTED = unchecked((int)0x8000FFFF);
public TestPlugin()
{
_parentHwnd = IntPtr.Zero;
_preset = 0;
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : Construct{1}", DateTime.Now.ToString(), Environment.NewLine));
}
~TestPlugin()
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : Deconstruct{1}", DateTime.Now.ToString(), Environment.NewLine));
}
#region IWmpEffects2 Members
/// <summary>
/// Set WMP core interface
/// </summary>
/// <param name="pPlayer"></param>
/// <returns></returns>
public int SetCore(IntPtr pPlayer)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : SetCore{1}", DateTime.Now.ToString(), Environment.NewLine));
// release any existing WMP core interfaces
//ReleaseCore();
if (pPlayer == IntPtr.Zero)
return S_OK;
_playerCore = pPlayer;
//connect up any events
return S_OK;
}
/// <summary>
/// Invoked when the visualization should be initialized.
///
/// If hwndParent != NULL, RenderWindowed() will be called and the visualization
/// should draw into the window specified by hwndParent. This will be the
/// behavior when the visualization is hosted in a window.
///
/// If hwndParent == NULL, Render() will be called and the visualization
/// should draw into the DC passed to Render(). This will be the behavior when
/// the visualization is hosted windowless (like in a skin for example).
/// </summary>
/// <param name="hwndParent"></param>
/// <returns></returns>
public int Create(IntPtr hwndParent)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : Create{1}", DateTime.Now.ToString(), Environment.NewLine));
_parentHwnd = hwndParent;
return S_OK;
}
/// <summary>
/// Invoked when the visualization should be released.
/// Any resources allocated for rendering should be released.
/// </summary>
/// <returns></returns>
public int Destroy()
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : Destroy{1}", DateTime.Now.ToString(), Environment.NewLine));
_parentHwnd = IntPtr.Zero;
return S_OK;
}
/// <summary>
/// Invoked when a new media stream begins playing.
/// The visualization can inspect this object for properties (like name or artist)
/// that might be interesting for visualization.
/// </summary>
/// <param name="pMedia"></param>
/// <returns></returns>
public int NotifyNewMedia(IntPtr pMedia)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : NotifyNewMedia{1}", DateTime.Now.ToString(), Environment.NewLine));
return S_OK;
}
/// <summary>
/// Window messages sent to the parent window.
/// </summary>
/// <param name="Msg"></param>
/// <param name="WParam"></param>
/// <param name="LParam"></param>
/// <param name="plResultParam"></param>
/// <returns></returns>
public int OnWindowMessage(int Msg, int WParam, int LParam, ref int plResultParam)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : OnWindowMessage{1}", DateTime.Now.ToString(), Environment.NewLine));
// return S_OK only if the plugin has handled the window message
// return S_FALSE to let the defWindowProc handle the message
//if (_parentHwnd == IntPtr.Zero)
//m_NonWindowedRenderer.OnWindowMessage(&m_RenderContext, msg, WParam, LParam, plResultParam);
//else
// m_WindowedRenderer.OnWindowMessage(&m_RenderContext, msg, WParam, LParam, plResultParam);
return S_FALSE;
}
/// <summary>
/// Called when an effect should render itself to the screen.
/// The fRequiredRender flag specifies if an update is required, otherwise the
/// update is optional. This allows visualizations that are fairly static (for example,
/// album art visualizations) to only render when the parent window requires it,
/// instead of n times a second for dynamic visualizations.
/// </summary>
/// <param name="pData"></param>
/// <param name="fRequiredRender"></param>
/// <returns></returns>
public int RenderWindowed(ref TimedLevel pData, bool fRequiredRender)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : RenderWindowed{1}", DateTime.Now.ToString(), Environment.NewLine));
//windowed
// NULL parent window should not happen
if (_parentHwnd == IntPtr.Zero)
return E_UNEXPECTED;
//RECT rect = new RECT();
//TestPlugin.getClientRect(_parentHwnd, ref rect);
//do render//
return S_OK;
}
#endregion
#region IWmpEffects Members
/// <summary>
/// Called when an effect should render itself to the screen.
/// </summary>
/// <param name="pLevels"></param>
/// <param name="Hdc"></param>
/// <param name="pRC"></param>
/// <returns></returns>
public int Render(ref TimedLevel pLevels, IntPtr Hdc, ref RECT pRC)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : Render{1}", DateTime.Now.ToString(), Environment.NewLine));
//not windowed
//do render
return S_OK;
}
/// <summary>
/// Everytime new media is loaded, this method is called to pass the
/// number of channels (mono/stereo), the sample rate of the media, and the
/// title of the media
/// </summary>
/// <param name="lChannelCount"></param>
/// <param name="lSampleRate"></param>
/// <param name="bstrTitle"></param>
/// <returns></returns>
public int MediaInfo(int lChannelCount, int lSampleRate, string bstrTitle)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : MediaInfo{1}", DateTime.Now.ToString(), Environment.NewLine));
return S_OK;
}
/// <summary>
/// Returns the capabilities of this effect. Flags that can be returned are:
/// EFFECT_CANGOFULLSCREEN -- effect supports full-screen rendering
/// EFFECT_HASPROPERTYPAGE -- effect supports a property page
/// </summary>
/// <param name="pdwCapabilities"></param>
/// <returns></returns>
public int GetCapabilities(ref int pdwCapabilities)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GetCapabilities{1}", DateTime.Now.ToString(), Environment.NewLine));
//no capabilities
pdwCapabilities = EFFECT_HASPROPERTYPAGE;
return S_OK;
}
/// <summary>
/// Invoked when a host wants to obtain the title of the effect
/// </summary>
/// <param name="bstrTitle"></param>
/// <returns></returns>
public int GetTitle(ref string bstrTitle)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GetTitle{1}", DateTime.Now.ToString(), Environment.NewLine));
bstrTitle = "Test Wmp C# Plugin";
return S_OK;
}
/// <summary>
/// Invoked when a host wants to obtain the title of the given preset
/// </summary>
/// <param name="nPreset"></param>
/// <param name="bstrPresetTitle"></param>
/// <returns></returns>
public int GetPresetTitle(int nPreset, ref string bstrPresetTitle)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GetPresetTitle{1}", DateTime.Now.ToString(), Environment.NewLine));
//bstrPresetTitle = "Default";
return S_OK;
}
/// <summary>
/// Invoked when a host wants to obtain the number of supported presets
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public int GetPresetCount(ref int count)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GetPresetCount{1}", DateTime.Now.ToString(), Environment.NewLine));
count = 1;
return S_OK;
}
/// <summary>
/// Invoked when a host wants to obtain the index of the current preset
/// </summary>
/// <param name="currentpreset"></param>
/// <returns></returns>
public int SetCurrentPreset(int currentpreset)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : SetCurrentPreset{1}", DateTime.Now.ToString(), Environment.NewLine));
_preset = currentpreset;
return S_OK;
}
/// <summary>
/// Invoked when a host wants to obtain the index of the current preset
/// </summary>
/// <param name="currentpreset"></param>
/// <returns></returns>
public int GetCurrentPreset(ref int currentpreset)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GetCurrentPreset{1}", DateTime.Now.ToString(), Environment.NewLine));
currentpreset = _preset;
return S_OK;
}
/// <summary>
/// Invoked when a host wants to display the property page for the effect
/// </summary>
/// <param name="hwndOwner"></param>
/// <returns></returns>
public int DisplayPropertyPage(IntPtr hwndOwner)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : DisplayPropertyPage, Owner={1}{2}", DateTime.Now.ToString(), hwndOwner.ToString(), Environment.NewLine));
MessageBox.Show("Hello Me!");
return S_OK;
}
public int GoFullScreen(bool fFullscreen)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : GoFullScreen{1}", DateTime.Now.ToString(), Environment.NewLine));
return S_OK;
}
public int RenderFullScreen(ref TimedLevel pLevels)
{
File.AppendAllText("C:\\wmp.txt", string.Format("{0} : RenderFullScreen{1}", DateTime.Now.ToString(), Environment.NewLine));
return S_OK;
}
#endregion
private IntPtr _parentHwnd;
private int _preset;
private IntPtr _playerCore;
}
As you can see my code is pretty empty, nothing more than stubs really. My debugging is simple but does the job.
Once this is compiled with a strong name it can be registered with:
regasm assemblyname.dll /tlb
then dropped in the gac.
Open regedit and add the the following info:
Under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MediaPlayer\Objects\Effects
New Key: WmpTestPlugin
Under the new key add a new string value: Name: classid Value: {C476FF24-5E5C-419d-9110-05EC2EED8511}
So we have built a plugin that conforms to the interfaces, registered it in the GAC and told media player it's there.
If you open media player and right-click on the visualization space a menu will appear. In that menu will be our new plugin. When you move your mouse down to the new item WMP will call GetPresetCount on the plugin (this will log to the file). Then WMP is supposed to call GetPresetTitle but it NEVER does for me.
If you open tools\options from the menu bar and select the Plugins tab you can select the new plugin. If you click Properties WMP will call GetCapabilities then DisplayPropertyPage and a message box from the plugin will appear. WMP then crashes. In the C++ version FinalConstruct() is called on a CComCoClass interface - I don't have this and don't know what it is. I think it may be lower level than I am using??
I have tried many many things to get this to work including changing the method declarations. Please can someone look at it and help. I've looked all over the web for a solution and found nothing.
Thanks for reading, Nanook
After looking at some Windows Shell interop code I discovered that I should have been using [ComImport] and NOT [ComVisible] for the interfaces. To make the signatures equal to that already registered I used [PreserveSig]. The interface inheritence also needed some help by redeclaring the base interface within the parent.
I hope this helps someone.
Here is the working class