Laziness .NET
Duncan Mackenzie
Microsoft Developer Network
June 26, 2003
Summary: Duncan Mackenzie describes how to use remoting and a Pocket PC to create a simple remote control for Windows Media Player 9. (10 printed pages)
Applies to:
Microsoft® Visual Basic® .NET
Download the source code for this article.
I Am a Control Freak
Remote control, that is, although Microsoft® Windows® Forms controls probably are a close second; I like pressing a button from over "here" and having something happen over "there." Yep, I like that a lot. When I built my own music playing system, I had to have remote control support, so I used an IRMan hooked up to the serial port to give me IR support. But I have decided to move it up to the next level. One-way IR communication is no longer enough for me. I want a full-color remote that communicates with my system and gives me enough information to control the system when I can't see it. Turns out, Microsoft® .NET has the answer for me. Microsoft® .NET Remoting and the Microsoft® .NET Compact Framework will enable me to turn my Pocket PC into an extremely over-powered remote control replacement that works over a wireless network.
Figure 1. The main page of the Pocket PC Remote Control
Design of the Networked Music Control System (NMCS)
My end goal was to build a remote that works with my own Music system, but I thought it would be more interesting to you folks if I built a simple one that worked with a Microsoft® Visual Basic® .NET application (that has embedded the Microsoft® Windows Media® Player control onto a Form) running on your own machine.
The .NET Compact Framework application (running on the Pocket PC) will communicate with the host using remoting, retrieving information about your media library and sending a standard set of music commands back and forth. The set of commands will include:
Media information
- Get Current (Retrieve information about the currently playing song from the host.)
- Get Artists (Retrieve list of artists from the host's media library.)
- Get Albums (Retrieve list of albums from the host's media library.)*
- Get Albums By Artist
- Get Songs By Album*
- Get Songs By Artist
- Get Album Cover For Song (Retrieve the appropriate image for a specific song.)
*I implemented, but never used these methods in my samples.
Navigational controls
- Play Album | Artist | Song
- Stop, Pause/Play, Next/Previous Song
- Mute, Volume Up/Down
Of course, you could implement a more comprehensive command set, but that set of functions provides me with everything I need for now. If we take that set of functions as our baseline "specification," we can go ahead and build our complete host application (that will run the Windows Media Player) before we ever get to working on the client.
Creating the NMCS Host Application
For the host application, I am creating a Visual Basic .NET Windows application with a single form for hosting the Windows Media Player control. To properly embed the control, you need to follow the instructions in this article by Jim Travis. Make sure you follow all of the steps in that article, including registering the Primary Interop Assembly (PIA) that ships with the Windows Media Player SDK.
Figure 2. Embedding the Windows Media Player gives me access to its object model
With the control embedded in a form, you can access the key methods/properties we need. I am not going to delve deep into the workings of the object model of the Windows Media Player in this article, but I will be showing you a few representative pieces of my code. I am going to create two new classes that will implement all of the commands listed earlier, with one class covering the navigational controls and another handling the media information. The code for each of these commands is pretty simplistic, but each class needs access to the instance of the Windows Media Player embedded onto the Windows Form. As we will see a little bit later, our classes are easier to use for remoting if they do not require any initialization. So I am going to make the Windows Media Player instance available through the use of a Shared (Static) property on another class.
Imports AxMicrosoft.MediaPlayer.Interop
Public Class SharedMediaPlayer
Private Shared m_Player _
As AxWindowsMediaPlayer
Public Shared Property Player() _
As AxWindowsMediaPlayer
Get
Return m_Player
End Get
Set(ByVal Value _
As AxWindowsMediaPlayer)
m_Player = Value
End Set
End Property
End Class
When the application starts up and the main instance of Windows Media Player is created, I set the Shared property, making it available until the application exists.
Protected Overrides Sub OnLoad( _
ByVal e As System.EventArgs)
SharedMediaPlayer.Player = Me.wmp
SetUpRemoting()
End Sub
Once that shared property exists, I can move on to coding the three sets of commands:
- Navigational methods for controlling the state of the player;
- Informational methods for working with the media library;
- Methods for playing specific pieces of music.
I broke each of the sets of commands into its own class because the three sets of code are quite independent.
Programming the Navigational Controls
The navigation commands are extremely easy to write, as each of them corresponds perfectly to an already exposed method in the object model of Windows Media Player.
Imports NMCSHost.SharedMediaPlayer
Imports Microsoft.MediaPlayer.Interop
Public Class NavigationalControls
Inherits MarshalByRefObject
Public Sub VolumeUp()
If Player.settings.volume < 100 Then
SharedMediaPlayer.Player.settings.volume += 1
End If
End Sub
Public Sub VolumeDown()
If Player.settings.volume > 0 Then
SharedMediaPlayer.Player.settings.volume -= 1
End If
End Sub
Public Sub Mute()
'toggle mute
Player.settings.mute = Not Player.settings.mute
End Sub
Public Sub MoveNext()
Player.Ctlcontrols.next()
End Sub
Public Sub MovePrevious()
Player.Ctlcontrols.previous()
End Sub
Public Sub Play()
Player.Ctlcontrols.play()
End Sub
Public Sub Pause()
Player.Ctlcontrols.pause()
End Sub
Public Sub StopPlayer()
Player.Ctlcontrols.stop()
End Sub
End Class
The navigation methods are exposed through the Controls property of the Player object, but when you use the Windows Media Player OCX in .NET, the name of that property is automatically changed to Ctlcontrols due to a conflict with an existing property of the Microsoft® ActiveX® Control wrapper.
Digging into the Media Library
For the informational set of commands, data will have to be queried and retrieved from the Media Library. A key decision at this point is the data format in which information should be sent back to the remote application, since it has to be sent across the network. I decided to go with a combination of strongly typed arrays and custom objects. For commands that return a list of strings (GetAuthors is one such command), a string array will be used, but to return listings of Songs I use a custom Song class and pass back an array of that type.
<Serializable()> Public Class Song
Public Authors As String
Public Title As String
Public Duration As String
Public Album As String
Public ContentID As String
Public CollectionID As String
Public TrackNumber As String
End Class
To retrieve a listing of all the authors from my media library, the getAttributeStringCollection function works quite well, but (depending on the size of your own library) it can return a very large set of values. A custom string collection class is returned from the media library, but I copy those values into an ArrayList and then convert that into an array of string to return back to the client.
Public Function GetAuthors() As String()
Dim mc As IWMPMediaCollection
mc = Player.mediaCollection
Dim artists As IWMPStringCollection _
= mc.getAttributeStringCollection("Author", "AUDIO")
Dim artistList As New ArrayList
For i As Integer = 0 To artists.count - 1
artistList.Add(artists.Item(i))
Next
Dim saArtists(artistList.Count - 1) As String
Return artistList.ToArray(GetType(String))
End Function
Pulling back all of the albums for a particular author/artist is accomplished through a slightly more complicated process, grabbing all of the songs for that artist and scanning for unique album names.
Public Function GetAlbumsByArtist( _
ByVal ArtistName As String) As String()
Dim albums As New ArrayList
Dim songs As Song() = GetSongsByArtist(ArtistName)
For Each sng As Song In songs
If Not albums.Contains(sng.Album) Then
albums.Add(sng.Album)
End If
Next
Return albums.ToArray(GetType(String))
End Function
Public Function GetSongsByArtist( _
ByVal ArtistName As String) As Song()
Dim pl As IWMPPlaylist
pl = Player.mediaCollection.getByAuthor(ArtistName)
Return ConvertPlaylistToSongs(pl)
End Function
Private Function ConvertPlaylistToSongs( _
ByVal pl As IWMPPlaylist) As Song()
Dim songs As New ArrayList(pl.count - 1)
For i As Integer = 0 To pl.count - 1
songs.Add(GetItemAsSong(pl.Item(i)))
Next
Return songs.ToArray(GetType(Song))
End Function
Private Function GetItemAsSong( _
ByVal md As IWMPMedia) As Song
Try
Dim current As New Song
With current
If Not md.getItemInfo("WM/WMContentID") _
= "{00000000-0000-0000-0000-000000000000}" _
And Not md.getItemInfo("WM/WMContentID") _
= String.Empty Then
.Album = md.getItemInfo("WM/AlbumTitle")
.Authors = md.getItemInfo("Author")
.ContentID = md.getItemInfo("WM/WMContentID")
.Duration = md.durationString
.Title = md.getItemInfo("Title")
.TrackNumber = md.getItemInfo("WM/TrackNumber")
End If
End With
Return current
Catch ex As Exception
MsgBox(ex.Message)
Return Nothing
End Try
End Function
One of the more interesting functions in the MediaInformation class is GetImage, which accepts a ContentID as a parameter, then finds the appropriate media item in the library, and then tracks down the album image. If and when it finds the album image, it converts it into a byte array and then Base64 encodes it so that it can be returned as a string.
Public Function GetImage( _
ByVal ContentID As String) As String
Dim md As IWMPMedia
Dim ImagePath As String
md = Player.mediaCollection.getByAttribute( _
"WM/WMContentID", ContentID).Item(0)
ImagePath = IO.Path.Combine( _
IO.Path.GetDirectoryName(md.sourceURL), _
String.Format("AlbumArt_{0}_Large.jpg", _
md.getItemInfo("WM/WMCollectionID")))
If IO.File.Exists(ImagePath) Then
Dim ms As New IO.MemoryStream
Dim npImage As Image = New Bitmap(ImagePath)
npImage.Save(ms, ImageFormat.Bmp)
Dim imgBytes() As Byte = ms.ToArray()
Return Convert.ToBase64String(imgBytes)
Else
Return String.Empty
End If
End Function
This convoluted code was my way to make the graphic file easily passed across the wire when using SOAP and XML as my transport method. Later on in this article, I will show you the code that reconstructs the image in the client application.
Playing the Music
With the navigation and media library code written, all that is left to do is to provide some way to start up a specific album, artist, or song. In the PlayItems class, I've written three methods, PlayItem, PlayAlbum, and PlayArtist, to play a single song, an entire album, or all songs by a specific artist respectively.
Imports NMCSHost.SharedMediaPlayer
Imports Microsoft.MediaPlayer.Interop
Public Class PlayItems
Inherits MarshalByRefObject
Public Sub PlayItem(ByVal ContentID As String)
Dim md As IWMPMedia
Dim pl As IWMPPlaylist
Dim ImagePath As String
pl = Player.mediaCollection.getByAttribute( _
"WM/WMContentID", ContentID)
If Not pl Is Nothing Then
md = pl.Item(0)
If Not md Is Nothing Then
Player.currentMedia = _
Player.newMedia(md.sourceURL)
Player.Ctlcontrols.play()
End If
End If
End Sub
Public Sub PlayAlbum(ByVal AlbumName As String)
Dim pl As IWMPPlaylist
pl = Player.mediaCollection.getByAlbum(AlbumName)
Player.currentPlaylist = pl
Player.Ctlcontrols.play()
End Sub
Public Sub PlayArtist(ByVal ArtistName As String)
Dim pl As IWMPPlaylist
pl = Player.mediaCollection.getByAuthor(ArtistName)
Player.currentPlaylist = pl
Player.Ctlcontrols.play()
End Sub
End Class
Adding support for queuing up music would be nice, as that allows you to add more music into the current mix without replacing what is already playing, but I didn't worry about it for this sample.
Setting Up Remoting in the Host
To make my three classes available to the client application using SOAP over HTTP, I have to register them for remoting when I start up the host system.
Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
SharedMediaPlayer.Player = Me.wmp
SetUpRemoting()
End Sub
Private Sub SetUpRemoting()
RegisterWellKnownServiceType( _
GetType(NavigationalControls), _
"NMCSNavigation.soap", _
WellKnownObjectMode.SingleCall)
RegisterWellKnownServiceType( _
GetType(MediaInformation), _
"NMCSInformation.soap", _
WellKnownObjectMode.SingleCall)
RegisterWellKnownServiceType( _
GetType(PlayItems), _
"NMCSPlayItems.soap", _
WellKnownObjectMode.SingleCall)
'register the channel
Dim channel As New Channels.Http.HttpChannel(9000)
Channels.ChannelServices.RegisterChannel(channel)
End Sub
Yes, the port is hard coded; you might want to add a settings dialog, or configuration file to allow you to modify this value. With that addition to the main form, you can move onto the client. Make sure the host can run without any errors, and leave it running before firing up a new instance of Microsoft® Visual Studio® .NET to work on the client application.
Creating the Client Application
As you can see from Figure 1 at the top of this article, my client application is designed for the Pocket PC. With the host application running in its own instance of Visual Studio .NET, I opened a second instance of Visual Studio .NET and created a new Smart Device project in Visual Basic .NET. Some graphics would really improve the look of my interface, but I stuck with just creating a button for each of the navigational commands on one tab of a TabControl, and then a TreeView on the second tab to display the media library information.
Adding Web References to a Remoting Host
Although we are using remoting, not Web services, we are using HTTP as our transport protocol and SOAP as our format, so we can take advantage of the Web Reference concept in Visual Studio .NET. This makes our coding quite a bit easier, as our three classes end up available in our code just like any other reference. Before we can add the references, the host application has to be running, or the addresses we enter wouldn't be found. In my case I added references to http://duncanma:9000/NMCSNavigation.soap?wsdl, http://duncanma:9000/NMCSInformation.soap?wsdl, and http://duncanma:9000/NMCSPlayItems.soap?wsdl, but you will need to change the server name to fit your environment.
Note This method, using the Add Web Reference concept with a remoting server, will not always work if you are using complex types. Different code is used to create and parse SOAP messages for Web services than for remoting, and that difference could prevent some remoting servers from working through this method. In this example, I am only passing strings and arrays around, so it is working just fine.
Polling for "Now Playing" Information
Using a timer, with an interval of three seconds, I set the client application to poll the host for changes to the currently playing item. If the item appears to have changed, I pull down the associated image for that piece of content, decode it from Base64 back into an image, and display it in a PictureBox control.
Private Sub trmUpdate_Tick( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles trmUpdate.Tick
trmUpdate.Enabled = False
UpdateNowPlaying()
trmUpdate.Enabled = True
End Sub
Private Sub UpdateNowPlaying()
Dim currentSong As Information.Song
Dim objResult As Object
Dim info As Information.MediaInformationService
Try
info = New Information.MediaInformationService
objResult = info.CurrentItem
currentSong = CType(objResult, Information.Song)
Me.pbNetworkError.Visible = False
If Not currentSong Is Nothing Then
If Me.lblCurrentSong.Text <> currentSong.Title Then
Me.lblCurrentSong.Text = currentSong.Title
Me.lblCurrentArtist.Text = currentSong.Authors
Dim img As Bitmap
Dim pic As String = _
info.GetImage(currentSong.ContentID)
Dim picBuff() As Byte
picBuff = Convert.FromBase64String(pic)
img = New Bitmap(New IO.MemoryStream(picBuff))
Me.pbAlbum.Image = img
End If
End If
Catch ex As Exception
NetworkError()
End Try
End Sub
Throughout the client application I handle errors by calling NetworkError(), a little function that just displays a small error image on the form as an alternative to a very intrusive pop-up message box.
The Media Library View
On the second tab of my Form, I created a view of the host's media library, showing artists and albums in a tree view format. I don't bother with a song listing, but I suppose that could be added as an additional level of items in the tree.
Figure 3. The Media Library tab allows you to browse and select music.
Whenever the Media Library tab is selected, I check to see if any entries have been downloaded and, if not, I pull down the list of authors. This can be a very time consuming process, and it would be better to spin it off onto its own thread, but I have to leave some of the fancy stuff to future versions, don't I?
Private Sub remoteTab_SelectedIndexChanged( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles remoteTab.SelectedIndexChanged
If remoteTab.SelectedIndex = 1 Then
If Me.mediaLibraryTree.Nodes.Count = 0 Then
LoadMediaInfo()
End If
End If
End Sub
Private Sub LoadMediaInfo()
With Me.mediaLibraryTree
.Nodes.Clear()
Dim authors As String()
Dim albums As String()
authors = info.GetAuthors
For Each author As String In authors
'add to tree
Dim authorNode As TreeNode
authorNode = .Nodes.Add(author)
authorNode.SelectedImageIndex = 1
authorNode.ImageIndex = 1
Next
End With
End Sub
I wait to load in the albums for a particular artist until the artist is selected in the TreeView, and then I add them as nodes below the artist node.
Private Sub mediaLibraryTree_AfterSelect( _
ByVal sender As Object, _
ByVal e As TreeViewEventArgs) _
Handles mediaLibraryTree.AfterSelect
If e.Node.Parent Is Nothing Then
If e.Node.Nodes.Count = 0 Then
Dim albums As String()
albums = info.GetAlbumsByArtist(e.Node.Text)
For Each album As String In albums
If Not album Is Nothing _
AndAlso Not album = String.Empty Then
Dim albumNode As TreeNode
albumNode = e.Node.Nodes.Add(album)
albumNode.SelectedImageIndex = 0
albumNode.ImageIndex = 0
End If
Next
e.Node.Expand()
End If
End If
End Sub
Another nice-to-have feature would be a reload/refresh button for this TreeView, but I thought it was unlikely that the host's media library would be changing quickly enough to need it.
To play music, I added a context menu to the Media Library TreeView, with a single menu item, Play. If you have an artist selected when you bring up the context menu and click Play, the host will start playing all of the music by that artist, or if you have an album selected, it will play that album.
Finishing Touches
That is it for my quick and dirty remote control, but I'm sure you have a lot of ideas on how you could add your own features to make it into the ultimate system controller. If you decide to build on this sample, let me know. I would be very interested in what you end up creating. For myself, I will probably not add anything to this sample past this point, other than bug fixes if necessary, as my own remote needs to work with my personal music system. If you are interested in seeing what I build for myself, that code will end up in the MusicXP workspace on GotDotNet when it is finished. The current build of the MusicXP system is also available on GotDotNet as a User Sample.
Coding Challenge
At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create anything that uses remoting to accomplish something fun. Managed code is preferred (Visual Basic .NET, C#, J#, or managed C++ please), but an unmanaged component that exposes a COM interface would also be good. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).
Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!
Coding4Fun
Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.