Over the past few weeks, I’ve been working on a little screensaver side project to run in an old radio cabinet that I’ve converted into a touchscreen jukebox. Details of that project will be coming, but for now, I thought I’d share some interesting software bits I’ve discovered.
The screensaver that I’ve working on is intended to:
1) Recognize a Media Player that’s running on the computer when the screensaver loads up.
2) Connect to and monitor that media player for any changes in what’s it playing. Particularly, I want to know Artist name, album name and track name.
3) Hit a few search engines for some images related to what’s playing and display them in a nice “Ken Burns Pan and Zoom” style animation.
Well, long story short, WPF made short work of the image animations, and connecting to two of my favorite media players, J River Media Center and Album Player, was pretty trivial.
But, within just a few days of posting the first version, someone asked if it worked, or could work, with Windows Media Player (and, as it turns out, Windows Media Center, which is just a nicer shell over WMP).
Why Not?
My first thought was, sure! All I have to do it be able to see what’s currently playing and when it changes. Shouldn’t be too tough, right?
Well, after quite of bit of digging, it turns out that Windows Media Player (WMP), is far more gracious about hosting “plugins” that it is about being remotely attached to. There are several plugins available for WMP that write out info on the current playing media to an XML file, or the registry, or a web service, or whatever. But that requires "my application’s user” to install some other application to make things work. Not cool. At least, not for me.
Plan two. Most MS Office apps register themselves with the Running Object Table (The ROT). Other programs can query the ROT and retrieve objects from it that they’re interested in connected to. You often see the VB GetObject() function used for this purpose.
But WMP doesn’t register itself with the ROT, so that’s not an option.
On to Plan C.
WMP Remoting
However, as luck would have it, MS did do something about this type of situation. They call it “Media Player Remoting”. However, it’s just about the least documented concept I’ve come across yet. There’s just very little info about exactly how to set up this “remoting” or what it’s capable of.
Eventually,though, I did come across mention of a demo project written by Jonathan Dibble, of Microsoft no less, that illustrates the technique in C#. There’s a thread here that contains links to the original code, though that page appears to be in Japanese.
Looking further, I found several variations of Dibble’s project, some vastly more involved and complex than others.
I grabbed the simpler version and started hacking!
The Conversion
Converting Mr. Dibble’s code was fairly straightforward. He did a pretty fair job in commenting it and breaking things down nicely. As usual, one of my favorite Web Resources, DeveloperFusion’s CodeConverter, got a workout, and did a fine job on most of the conversion gruntwork.
But when the dust cleared, it didn’t work.
After a lot of stepping with the debugger, it turns out that Jonathan’s handling of IOleClientSite.GetContainer isn’t quite right. His original code threw a “Not implemented” exception that would cause a crash for me every single time.
The function itself isn’t particularly useful for what I needed to do, and after reading up on the documentation, I felt certain that there’s really wasn’t anything that “needed” to be done in that function. But, it did get called by WMP, and something other than throwing an exception had to be done there.
Then, I realized that a number of other OLE-centric functions that Jonathan had implemented had a return value of HRESULT, and simply returned a E_NOTIMPL value.
So, I changed up the definition of GetContainer and had it returning an E_NOTIMPL, and presto! It works!
Since Jonathan’s demo project appears to be at least 4 years old, I’m not sure whether I may have indeed worked that way at one point, or was a bug, or, quite possibly, was something I didn’t get quite right in the conversion in the first place. Regardless, this version works, so bob’s your uncle.
How to Use It
For anyone not interested in the details, I’ll dive right in to how you actually use this technique.
First off, you’ll need to add a reference to WMP.DLL. This file should be in your Windows\system32 folder if a Windows Media Player is installed. Once added, you’ll have a WMPLib reference in your References tab:
Next, copy the WMPRemote.vb file into your own project.
Finally, though this isn’t strictly necessary, you may want to alter your project’s AssemblyInfo.vb file and set the CLSAttribute to false.
....
<Assembly: AssemblyCopyright("blah")>
<Assembly: AssemblyTrademark("blah")>
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'THE BELOW LINE YOU MIGHT ADD OR CHANGE TO "FALSE"
<Assembly: CLSCompliant(False)>
'The following GUID is for the ID of the typelib if this project is exposed to COM
<Assembly: Guid("12333333-33454-1233-1234-123451234512")>
.....
The main class to work with is WMPRemote. It has two static properties; IsWindowMediaPlayerLoaded, and ActivePlayer.
These are static properties, so you access them using the WMPRemote class, like so:
If WMPRemote.IsWindowsMediaPlayerLoaded Then
Dim Player = WMPRemote.ActivePlayer
End If
At that point, if WMP is loaded, you’ll have a reference to the full WMPlib.WindowsMediaPlayer object in the Player variable.
From there, you can do whatever you need to.
Query properties:
Debug.print Player.playState
Attach to events:
AddHandler Player.CurrentItemChange, AddressOf CurrentItemChange
AddHandler Player.PlayStateChange, AddressOf PlayStateChange
Or whatever else is necessary.
How It Works
WMPRemote
Since this class is the main access point, I’ll start here. There are only 2 static functions defined here:
IsWindowsMediaPlayerLoaded
This function simply uses the .net Processes object to query for any processes named WMPlayer. If there are any, it returns TRUE, if not, FALSE. Obviously, it could be wrong, but that’s not terribly likely.
ActivePlayer
If things have already been initialized, this function just returns whatever it’s already retrieved for the Active WindowsMediaPlayer object.
If not, it checks if WMP appears to be loaded and, if so, creates and shows an instance of the internal frmWMPRemote form. During the load of this form, it’s immediately hidden so the user will never see it.
The only purpose of frmWMPRemote, is to host the WindowsMediaPlayer ActiveX Control. This all happens during the form Load event:
Me.Opacity = 0
Me.ShowInTaskbar = False
_InternalPlayer = New WMPRemoteAx
_InternalPlayer.Dock = System.Windows.Forms.DockStyle.Fill
Me.Controls.Add(_InternalPlayer)
Me.Hide()
Note that it actually is creating an instance of the WMPRemoteAx control, and then siting it on the form.
WMPRemoteAx
This class is based on the AxHost control, and is what allows an ActiveX COM-based control to exist on a .net WinForms form.
Once created as actually sited on a control (or WinForms Form, in this case), the AttachInterfaces method is called by the .net runtime to connect this host up with whatever COM ActiveX Control it will be hosting. I’ve told this control to host the WindowsMediaPlayer ActiveX control by setting the GUID in the constructor:
MyBase.New("6bf52a52-394a-11d3-b153-00c04f79faa6")
AttachInterfaces connects up the ActiveX control with a clientsite as necessary, but more importantly, it exposes the internal OCX instance by casting it as an instance of WindowsMediaPlayer:
Dim oleObject As IOleObject = TryCast(Me.GetOcx(), IOleObject)
Dim a = TryCast(Me, IOleClientSite)
oleObject.SetClientSite(a)
_RemotePlayer = DirectCast(Me.GetOcx(), WMPLib.WindowsMediaPlayer)
At this point, COM calls several other interfaces that have been implemented by WMPRemoteAx, including:
IOleServiceProvider
IOleClientSite
Most of the methods on these interfaces need not actually do anything. They are required for more sophisticated integrations. However, IOleServiceProvider_QueryService does have a very specific purpose.
Remoting
Remember that this entire situation is made possible by something WMP calls “Remoting”. Turns out, this callback method is how our control communicates to WMP that we are, in fact, setting up a remoting situation.
If riid = New Guid("cbb92747-741f-44fe-ab5b-f1a48f3b2a59") Then
Dim iwmp As IWMPRemoteMediaServices = New RemoteHostInfo()
Return Marshal.GetComInterfaceForObject(iwmp, GetType(IWMPRemoteMediaServices))
End If
When WMP calls this function with the given riid as above, our control has to respond by returning an object of type IWMPRemoteMediaServices (in this case implemented by the RemoteHostInfo object in the project). That object has a few properties that WMP queries for some basic information, but really, the fact that our control (WMPRemoteAx) has responded by returning an IWMPRemoteMediaServices is the main thing that sets up the remoting connection.
The OLE Interfaces and Enums
The rest of WMPRemote.vb is made up of several COM Enums and interfaces that are necessary to bring all this together, but that aren’t readily defined in VB.net. None of that code should normally be altered in any way because it’s actually defining interfaces and values that have already been defined in COM, but that we need defined for use in a .net application. For instance:
<ComImport(), ComVisible(True), Guid("00000118-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IOleClientSite
This is the start of the definition of the IOleClientSite interface. That interface is a “well defined” interface with a specific GUID (the value of the GUID attribute), and a specific interface type. Changing our .net definition of that interface would likely break everything.
Where’s the code?
Right here. This particular version is in VS2010 with .net 4.0 framework, but I don’t imagine there’d be any problems converting back to VS2008, or possibly even VS2005. I suspect you will need at least .net framework 2.0 though, but I haven’t tested this.
Also, check out a few of my other projects in the Code Garage.
And finally, let me know what you think!