A Cross-Domain Silverlight Channel 9 VideoRSS Player

Last week Kevin Ledley, keeper of the dev.live.com content, asked me for a bit of help to get a Silverlight video player working.  He wanted to "borrow" the cool scrolling video list and video player from our sister site, msdn2.microsoft.com and set it up on our dev.live.com homepage to show Windows Live related videos from Channel 9.  Copying the necessary JavaScript and XAML files for the Silverlight video player was easy enough, but even after fixing all the URL references, it still wasn't working.  So, I took a look.

Get the files

First, I needed to grab the files and set them up on my localhost IIS server.  The msdn2.microsoft.com page referenced the following JS files:

  • Silverlight.js - we know what that is
  • main.js
  • button.js
  • helpermethods.js
  • listbox.js
  • listboxitem.js
  • scrollbar.js
  • transportButtons.js
  • videoEntry.js
  • videoPlayer.js
  • videoService.js

main.js is what makes the call to create the Silverlight control, same as our CreateSilverlight.js file in helloworld earlier.  main.js has a lot of other code in it as well.  Scanning for ".xaml", we find there are actually three different xaml files used by main.js:

  • videoListHor.xaml
  • itemTemplate.xaml
  • videoPlayer.xaml

After copying these files locally, I scanned each for hard coded URL references and stripped them down to relative paths for easier tinkering on localhost. 

I made a small test page to host the video player, using the same variable and element names as the original msdn page:

vidtest.html

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Channel 9 Video on Silverlight</title>
        <script src="SilverLight.js" type="text/javascript"> </script>
        <script src="main.js" type="text/javascript"> </script>
        <script src="transportButtons.js" type="text/javascript"> </script>
        <script src="listbox.js" type="text/javascript"> </script>
        <script src="listboxitem.js" type="text/javascript"> </script>
        <script src="button.js" type="text/javascript"> </script>
        <script src="scrollbar.js" type="text/javascript"> </script>
        <script src="helperMethods.js" type="text/javascript"> </script>
        <script src="videoPlayer.js" type="text/javascript"> </script>
        <script src="videoEntry.js" type="text/javascript"> </script>
        <script src="videoService.js" type="text/javascript"> </script>
    </head>
    <body>
        <div style="position: relative;text-align:center;width:475px; z-index:10;">
            <div id="videoListHost">
                <script type="text/javascript">
                    var videoScrollerFeedUrl = 
"www.mscommunities.com/MixItUp/Search/default.aspx?q=msdn";
                    startVideoScroller();
                 </script>
            </div>
            <div id="videoPlayerHost" >
            </div>
        </div>
    </body>
</html>        

With this in place, I had all the bits in place on the localhost server to run the app.  Would it work?  Probably not, but no harm in trying!

Eliminate Framework Dependencies

Sure enough, vidtest.html threw a JavaScript exception on load.  The culprit:  Some of the code in the js files refers to a $get() function which appear to be a shortcut for document.getElementById.  A little bit of spelunking around in the msdn code with a JavaScript debugger confirmed this hypothesis. $get() is a helper function implemented in the JavaScript libraries that run the msdn web site.  I definitely did not want to start pulling on that thread.  $get() is innocuous enough, so we can placate the borrowed JS files on our localhost system by implementing a $get() function in vidtest.html

 function $get(id) {
    return document.getElementById(id);
}

Easy enough.  Will it work now? 

Bzzzt! The browser next complains about "Object required" or somesuch on the expression Sys.Application.notifyScriptLoaded() at the bottom of one of the .JS files.  This, too, is a bit of goo defined by the msdn web site infrastructure.  It notifies the application when the .JS file has finished loading, so that it's ok to construct the Silverlight control.

For vidtest.html, I have an equivalent, simpler approach:  construct the Silverlight control in the onLoad event of the body element.  The page's onLoad event won't fire until after all the JavaScript references in the head of the document have been loaded. 

Ok.  Will it work now?

Yes!  The Silverlight video scroller draws itself on the page! 

This is a screenshot of a Silverlight app. Had this been an actual Silverlight app, it would look much cooler.

But it's empty.  Why is it empty?  Let's go look at that videoScrollerFeedURL variable and where it's used in the code.

A quick grep of the JS files brings us to videoService.js, in a function called getVideoEntryList:

 function getVideoEntryList() {
    Microsoft.Mtps.Rendering.Behaviors.VideoService.GetVideoEntryList(
        videoScrollerFeedUrl, loadVideos);}

What's all that about?

Bubble Burster

videoScrollerFeedUrl is a string with a value of <www.mscommunities.com/MixItUp/Search/default.aspx?q=msdn>.  If you follow that link, you'll see that it's an RSS feed.  But you should also notice that the URL is in a domain that is not msdn.  How is the msdn web code able to use the RSS data from mscommunities.com?

Microsoft.Mtps.Rendering.Behaviors.VideoService.GetVideoEntryList must be a cross-domain proxy of some kind responsible for fetching RSS entries from mscommunities.com and returning them to the JavaScript running on msdn.microsoft.com.  A little friendly debugger inspecting confirms that this function call receives back an array of JavaScript objects - a JSON result - describing the video description, a thumbnail image URL and a video stream URL.

This presents a not so small problem for our objective.  dev.live.com is not msdn.microsoft.com.  We can't call that GetVideoEntryList function or use the web service behind it to find out what videos are available on Channel 9.  Full stop.

The Options: Few and Ugly

I called Kevin up with the bad news.  Our options were few and ugly:

  1. Get the server proxy code from msdn and install it on dev.live.com.  Great plan, but even if we could get our hands on the code, chances are high that some sort of server configuration difference (installed version of ASP.NET, installed version of .NET framework, etc) between how msdn is set up from how dev.live.com is set up could still make this a no-go. 
  2. Write our own server side proxy to perform the same relay of RSS content.  Trivially simple, perhaps, but the depth of security and code reviews and testing that would be required to roll out live code on a production server would turn our cute little video thingie into a weeks-long fire-breathing hydra from hell.  No thanks.  I already do that for my day job - why take on all that for a side project?
  3. Hardcode the URLs of the videos we want shown in this video scroller into the JS code.  Kevin: "We don't have that many videos."  Danny:  "Yet."  Doesn't scale, labor intensive.
  4. Construct an RSS feed on the dev.live.com domain to feed video URLs into the video scroller.  Much more maintainable over time than #3, but still requires constant maintenance to update the local feed when something relevant shows up on Channel 9.  The whole reason for RSS is to have the machine take care of discovery and updating.

Kevin said he'd work on #1 and #4.  I opted for #5: Stew on it some more.  Take the dog on a long walk. Check the mailbox.  Return.

Strange Bedfellows

A funny thing happened on the way back from the mailbox - it dawned on me that I was trying to solve the wrong problem, or at least, more problem than we needed to solve.  We needed a way to read the video RSS feed in the browser across domain boundaries, but we didn't need an all-purpose cross domain data conduit (like we use in the Windows Live Contacts web control). A conduit that could move just RSS data and only RSS data would suffice for this project.

That's when I recalled hearing about Google's recent launch of an RSS Feed API.  Mark Lucovsky, who I had a chance to meet (and almost work for) in my Google days now 400 days ago, announced about a month and a half ago a new API built from a subset of the Google Search API that returns only RSS data. 

In concept, the Google Feed API acts as a cross-domain proxy service between your web app and the RSS content you want to use.  In reality, it's far simpler:  it returns the RSS data cached in Google's search index.  This approach creates a multitude of curious artifacts with positive and negative spin: 

  • Response latency to your RSS requests is determined solely by Google's network infrastructure, not the server originating the RSS data.  The server of origin might not even be up but you can still get the RSS data from the Feed API.
  • This use of RSS data is entirely in the spirit of syndication. It's a bummer that the server of origin can't find out how many secondary and tertiary consumers of their data are out there, but hey, that's the nature of syndication. 
  • The RSS data you get from the Feed API could be stale or out of sync with the server of origin by several hours or more depending on how quickly Google's web crawlers return to the server of origin to index new content.  That's not a major concern for our video viewer, but could be a show stopper for, say, live broadcasting.
  • It's possible the RSS feed you want is not indexed by Google.  Hey, it does happen - the RSS feed could be too new to be in the index, or too isolated (no inbound links) to be considered indexible, or the server of origin could have a robots.txt file that tells search engine spiders to piss off. It seems a bit contradictory to have an RSS feed that does not want to be found, but stranger things have happened.

Wait a second. Use a Google API in a Microsoft app? What blasphemy is this?!

Oh, grow up.  Use what works.  Windows Live Search has a perfectly good Search API, but it doesn't provide anything to bring RSS content from a specific RSS URL into our web app.

And why shouldn't a Microsoft app use a Google service when appropriate?  Google certainly uses plenty of Microsoft browsers and operating systems to reach end users!

API Keys

To use the Google Feed API (I don't know why they had to put "AJAX" in the middle of it), you have to agree to the usual mile long list of terms of use plus generate an API key associated your web site URL.  You use the API key when initializing the Google API subsystem.  Presumably, this is so they can get an idea of how much traffic your app is generating and be able to block your app's access if you're found to be abusing the system or violating the ToU.

As far as I can tell by experimentation, the API key only gives Google some idea of who is using their service; the key does not appear to automatically disable API access if the key is used from a domain different from the URL you specified when you generated the key.  I generated a key for use on localhost, but found that it works just as well when I run my app from 127.0.0.1.  (It doesn't matter that one resolves into the other - a DNS name and an IP address are considered distinct domain names by the browser.  Domain name matching in the browser is by string matching, not by what the strings mean)  Perhaps lockouts only occur after a significant volume of suspicious traffic goes by.  I dunno.

The Brass Tacks

Here's the vidtest main page again, with the Google Feed API spliced in and bootstrapped:

vidtest.html

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Channel 9 Video on Silverlight</title>
        <script src="SilverLight.js" type="text/javascript"> </script>
        <script src="main.js" type="text/javascript"> </script>
        <script src="transportButtons.js" type="text/javascript"> </script>
        <script src="listbox.js" type="text/javascript"> </script>
        <script src="listboxitem.js" type="text/javascript"> </script>
        <script src="button.js" type="text/javascript"> </script>
        <script src="scrollbar.js" type="text/javascript"> </script>
        <script src="helperMethods.js" type="text/javascript"> </script>
        <script src="videoPlayer.js" type="text/javascript"> </script>
        <script src="videoEntry.js" type="text/javascript"> </script>
        <script src="videoService.js" type="text/javascript"> </script>

        <script src=www.google.com/jsapi?key=xxxGetYerOwnAPIKeyxxx"
          type="text/javascript"></script>
        <script type="text/javascript">
            google.load("feeds", "1");
 

            var videoScrollerFeedUrl = 
"www.mscommunities.com/MixItUp/Search/default.aspx?q=msdn";
 
          function initialize() { 
              startVideoScroller();
          } 

 
          google.setOnLoadCallback(initialize);     
 
          function $get(id) {
              return document.getElementById(id);
          } 
    </script>
  </head>
  <body>
      <div style="position: relative;text-align:center;width:475px; z-index:10;">
          <div id="videoListHost">
          </div>
          <div id="videoPlayerHost" >
          </div>
      </div>
  </body>
</html>        

We've added a new script reference that pulls in jsapi from www.google.com, and includes our API key.  I replaced my API key with a dummy value.  You'll need to go generate your own key if you want to run this code yourself. 

google.load("feeds", "1") tells Google that we want to use the Feeds API.

google.setOnLoadCallback(initialize) tells Google to call our initialize() function when the requested API has been loaded. Dynamic loading of JavaScript might not be finished before the body onLoad event fires, so we have to take this route to wait for the Feed API to load.  Body onLoad event deleted.

Now, let's go load that RSS content and build our video list, in videoService.js:

videoService.js

 function getVideoEntryList() {
    var feed = new google.feeds.Feed(videoScrollerFeedUrl); 
    feed.setNumEntries(100);     
    feed.setResultFormat(google.feeds.Feed.MIXED_FORMAT);
    feed.load(function(result) {      
        if (!result.error) {       
            var videoEntries = [];  
            for (var i = 0; i < result.feed.entries.length; i++) {            
                var entry = result.feed.entries[i];    
                var vidEntry = new Object;
                vidEntry.VideoTitle = entry.title;
 
                var test = google.feeds.getElementsByTagNameNS(
                        entry.xmlNode, 
                        "search.yahoo.com/mrss", 
                        "content");
                if (!test || !test.length) continue;
                vidEntry.VideoUrl = test[0].getAttribute("url");       
 
                test = google.feeds.getElementsByTagNameNS(
                        entry.xmlNode, 
                        "search.yahoo.com/mrss", 
                        "thumbnail");
                if (!test || !test.length) continue;
                vidEntry.VideoThumbnailUrl = test[0].getAttribute("url");
 
                test = google.feeds.getElementsByTagNameNS(
                        entry.xmlNode, 
                        "purl.org/dc/elements/1.1/", 
                        "creator")
                if (test && test.length) {
                    vidEntry.Author = test[0].text;
                }
 
                videoEntries.push(vidEntry);
            } 
            loadVideos(videoEntries);     
        }      
    });    
}

The msdn2.microsoft.com video scroller expects an array of vidEntry objects as input to build its playlist.  The msdn server side proxy takes care of digesting the RSS XML content down to a simple list of vidEntry objects. 

For our dev.live.com version of the video scroller, we need to build the vidEntry objects ourselves. The Google Feed API can return JSON, XML, or both.  The title of the RSS entry is standard fare, so that's easy to grab from the JSON field entry.title. The other fields we need - thumbnail URL, video URL, and author - are not normal RSS fields.  These fields are defined in the Media RSS (MRSS) RSS extension module and are generated by the Channel 9 video RSS feed as such.

We make short work of finding the exact XML elements we're looking for by using a utility function provided by the google.feeds library: a cross-browser implementation of getElementsByTagNameNS(). 

We push each vidEntry object into the array and loop until we run out of RSS items.

Finally, we hand off the array of objects to loadVideos and return to the original code path of the video scroller implementation.

The result looks something like this:

Screenshot of Silverlight video player in action

Why stop there?

So now we have what we set out to build:  a Silverlight video player that can show videos from the Channel 9 libraries, using the video RSS feeds returned by the mscommunities.com query service. Note that this is built on the Silverlight 1.0 beta control using browser JavaScript, not the Silverlight 1.1 Alpha.

The fact that we ended up using a generic RSS provider to get the specific RSS feed we needed opens some interesting possibilities:  Aren't we just a hair's breadth from showing any video from any video RSS feed on the Internet?  All we'd need to change is the videoScrollerFeedURL to point to a different RSS feed, right?  Perhaps this could be wired directly into Yahoo's Video Search service, which returns MRSS results for keyword searches of videos across the web?

The answer is: yes and no.  Yes, the app actually is just a small step away from using any video RSS feed, or even a video search service that returns MRSS results.  The catch is: there are a bezillion different video formats and codecs out there in the wild.  It would be unreasonable to expect Silverlight 1.0 beta to be able to play all of them.  Silverlight's media player isn't designed to play all video formats - it's designed to play media content encoded for Silverlight playback, using tools such as Expression Media.  Apparently, the Channel 9 video library is already encoded in a format that Silverlight can use.  Most video in the wild is not.

That's no ding against Silverlight - all video sharing sites with built-in players play only one video format - their own.  When you upload a video to the sharing site, it is almost always converted (transcoded) into the site's native encoding format so that the site's player can play it.  You can build that kind of video sharing site with Silverlight.

With a few tricks from this article, you can show Channel 9 videos on your web site or blog pages with Silverlight!