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.

  • 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.