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.
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:
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:
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:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://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 = "http://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!
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!
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);}
Microsoft.Mtps.Rendering.Behaviors.VideoService.GetVideoEntryList( videoScrollerFeedUrl, loadVideos);
What's all that about?
videoScrollerFeedUrl is a string with a value of http://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.
I called Kevin up with the bad news. Our options were few and ugly:
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.
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:
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!
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.
Here's the vidtest main page again, with the Google Feed API spliced in and bootstrapped:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://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=http://www.google.com/jsapi?key=xxxGetYerOwnAPIKeyxxx" type="text/javascript"></script> <script type="text/javascript"> google.load("feeds", "1"); var videoScrollerFeedUrl = "http://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:
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, "http://search.yahoo.com/mrss", "content"); if (!test || !test.length) continue; vidEntry.VideoUrl = test[0].getAttribute("url"); test = google.feeds.getElementsByTagNameNS( entry.xmlNode, "http://search.yahoo.com/mrss", "thumbnail"); if (!test || !test.length) continue; vidEntry.VideoThumbnailUrl = test[0].getAttribute("url"); test = google.feeds.getElementsByTagNameNS( entry.xmlNode, "http://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:
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!