Microsoft Expression Encoder Insider running on SharpSpace
Weblog
 

Tutorial: Building a Silverlight video player with commenting using Windows Azure, Part 1

This tutorial shows you how to create a Silverlight MediaPlayer that enables viewers to add comments at certain points in time as the video plays. This type of functionality has been made popular by services such as Viddler and I wanted to see how easy it would be to implement something similar in Silverlight using the Expression MediaPlayer control.  One of the design goals was to have the player be hostable anywhere and have the commenting still work.  This required a dedicated commenting web service and what better way of building such as service than using Windows Azure.

The material presented here is based on the second part of the talk I gave at MIX09 where I demonstrated the commenting capability in the context of the Silverlight 3 mediaplayers that will be shipping with Expression Encoder 3.  I have subsequently reworked the code to build on the Silverlight 2 MediaPlayer control that is shipping with Expression Encoder 2 SP1 since that is what is currently available.  I am expanding the content presented here to incorporate the Silverlight 3 code in the chapter on Silverlight MediaPlayers that will be part of  the Expression Encoder Unleashed book that I’m co-authoring.

Before we dive into the code, let’s first take a look at what we’re going to build.  The result will be a template for Expression Encoder.

image

When one or more videos are published using the template, we have a button to add comments and a timeline view that shows comments already made

image

If other uses are watching the video at the same time, new comment updates will be shown in real time by a flashing icon on the timeline as well as a comment bubble

image

If the second viewer wishes to respond to a comment, he can open the bubble and type the response.  At which point a notification is shown to other users who are online.

image

If the content author published a playlist of videos, the comments are tracked individually on a per video basis.

image

To build out this solution we need a number of pieces:

  1. A webservice in the cloud of keeping track of all of the comments which we will build using Windows Azure
  2. Some Silverlight controls for capturing and displaying comments on a timeline
  3. Client-side synchronization logic to keep all of the comments in sync between the mediaplayer running in Silverlight in the browser, the webservice and other player instances.
  4. A customized MediaPlayer control that wraps the previous elements into a coherent solution
  5. A template for Expression Encoder that makes it easy to repeatedly encode and publish content into the commenting-enabled player.

For the remainder of this post we will focus on the first requirement.  The series of blog posts that follow will cover each of the remaining areas in turn.

Walkthrough

Assuming you already have Visual Studio environment installed, the first step is to obtain and install the Windows Azure SDK and the Windows Azure tools for Visual Studio.  These are available here: Windows Azure SDK and here: Windows Azure Tools for Microsoft Visual Studio respectively.  I am making the assumption that if you have made it this far you will already have Visual Studio installed.

Once both SDKs are installed, we need to build the storage client sample which we’ll be using as a component in our project.  To do this, first of all expand the Azure samples which you will find here:

C:\Program Files\Windows Azure SDK\v1.0\samples.zip

then build the Storage Client sample by running this batch file:

"C:\Program Files\Windows Azure SDK\v1.0\samples\StorageClient\buildme.cmd"

Now we can fire up Visual Studio and create a new Web Cloud Service.. make sure you select Web Cloud Service from the Cloud Service group.

image 

This will result in a solution with two projects

image

The first contains the definition of our Azure project including both service definition and service configuration files.  The second project is our WebRole project where the web service will live.

To continue, we need to add our freshly compiled StorageClient.dll to the webrole project; to do this right-click on the WebRole project in the tree and select Add Reference then browse to StorageClient.dll in

"C:\Program Files\Windows Azure SDK\v1.0\samples\StorageClient\Lib\bin\Debug\StorageClient.dll"

image

The storageclient library includes functions for accessing Azure blob, table and queue storage.  We need it for the table storage.

While we are adding references, let’s grab the other things we’ll need.. 

  • System.Data.Services.Client
  • System.Runtime.Serialization

Azure Table Storage

In order to store comments in Azure table storage, we need to use the Entity First design approach in common with ADO.NET Data Services which the data access API for Azure table storage is based on.  This can seem a little odd if you are used to a more traditional database first model where you define your fields and tables followed by your data access classes.  With entity first, we use our data access classes as the model and Azure table storage then builds the underlying table structure for us.

At a high level, we need  to track unique videos and comments on those videos with a familiar master-detail relationship.  With Azure table storage classes that will look something like this:

image

Create a new source file in the WebRole project called Comments.cs.

In order to define our data storage layer we first need a class that provides the the equivalent of our row-level field definitions for our video comments by inheriting from TableStorageEntity.

   1: public class Comment : TableStorageEntity
   2:     {
   3:         public Guid CommentId { get; set; }
   4:         public string CommentText { get; set; }
   5:         public Double VideoTime { get; set; }
   6:         public DateTime CommentTimeStamp { get; set; }
   7:         public Guid VideoId { get; set; }
   8:  
   9:         public Comment()
  10:         {
  11:  
  12:         }
  13:  
  14:         public Comment(string comment, Double videoTime, Guid videoId) 
  15:             : base("",string.Format("{0:d10}",DateTime.UtcNow.Ticks))
  16:         {
  17:             CommentTimeStamp = DateTime.Now;
  18:             CommentText = comment;
  19:             VideoTime = videoTime;
  20:             VideoId = videoId;
  21:             CommentId = Guid.NewGuid();
  22:         }

Notice that we are calling a base constructor which takes two strings; one for a partition key and one for a row key. In this sample, we are using an empty partition key and are passing in Utc Ticks to represent the row key.  In a real world implementation, this wouldn’t give great scalability for huge data sets since the partition key is used to partition the data across multiple servers.  For the relatively modest needs of a commenting system this should be just fine.  For a deeper discussion on how to choose partition and row keys see this MSDN article.

Once we have the equivalent of row level data defined, we need an equivalent of a table to store this data. In Azure table storage this comes in the form of a type that inherits from TableStorageDataServiceContext which is a base type provided by ADO.NET Data Services that takes care of storing our entities and exposing them to us as an IQueriable LINQ compatible datasource.

The derived type definition is pretty straight forward in terms of the implementation we need to provide.. we are defining a DataServiceQuery property that represents our IQueryable end point and passing in some account configuration to the base constructor.

   1: public class CommentsTable : TableStorageDataServiceContext
   2:    {
   3:        public CommentsTable():
   4:            base(StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration())
   5:        {
   6:        }
   7:  
   8:        public DataServiceQuery<Comment> CommentTable
   9:        {
  10:            get { return CreateQuery<Comment>("CommentTable"); }
  11:        }
  12:    }

We are using some boilerplate code from the Storage Client sample to read the Storage account information from the ServiceConfiguration file.  In order for this code to run, we need to provide the configuration settings so let’s take a moment to do this now.  The settings we are going to provide will work on the local development fabric which will allow us to test the service locally before we deploy it to the cloud.

image

In the configuration project, open the ServiceDefinition.csdef file and enter the following lines of XML:

   1: <ConfigurationSettings>
   2:       <Setting name="AccountName"/>
   3:       <Setting name="AccountSharedKey"/>
   4:       <Setting name="TableStorageEndpoint" />
   5: </ConfigurationSettings>

as shown in context:

image

Now do the same for the ServiceConfiguration.cscfg file:

   1: <ConfigurationSettings>
   2:   <Setting name="AccountName" value="devstoreaccount1"/>
   3:   <Setting name="AccountSharedKey" value="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="/>
   4:   <Setting name="TableStorageEndpoint" value="http://127.0.0.1:10002"/>
   5: </ConfigurationSettings>

as shown:

image

Having defined storage classes for Comments, we will now add the equivalent storage classes for tracking videos.  Add a file to the WebRole project called Videos.cs and add the following code:

   1: public class VideoTableEntry : TableStorageEntity
   2:     {
   3:         public Guid VideoId { get; set; }
   4:  
   5:         public VideoTableEntry()
   6:         {}
   7:  
   8:         public VideoTableEntry(Guid videoId)
   9:             : base("", string.Format("{0:d10}", DateTime.UtcNow.Ticks))
  10:         {
  11:             VideoId = videoId;
  12:         }
  13:     }
  14:  
  15:     public class VideosTable : TableStorageDataServiceContext
  16:     {
  17:         public VideosTable()
  18:             : base(StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration())
  19:         {
  20:         }
  21:  
  22:         public DataServiceQuery<VideoTableEntry> VideoTable
  23:         {
  24:             get { return CreateQuery<VideoTableEntry>("VideoTable"); }
  25:         }
  26:     }

The videos table is very simple.. we are only tracking a single field for each video.. it’s unique ID. The observant reader may wonder why we need this “table” at all.  Surely it would be possible to derive unique videos from the comments table by using the Distinct predicate in LINQ something like

var uniqeVideos = (from comment in ct.CommentTable select comment.VideoId).Distinct();

The answer is that currently, the Azure LINQ provider does not support the Distinct clause.

As far as the rest of the class definition goes, the pattern is pretty much exactly the same for comments so doesn’t merit further discussion at this stage.

WCF Web Service

In this section, we will define a web-callable interface that will allow our Silverlight client to create comment records using the storage layer already defined in the previous section. For this purpose, we will use a Windows Communication Foundation service; let’s add one to our project, in this case a Silverlight compatible one:

image 

We now need to add methods to the service for saving comments, retrieving comments as well as a data class to pass comments over the wire.  Let us start with that one and work backwards.

In the code behind file for the commenting service, add the following class definition:

   1: [DataContract]
   2: public class WireComment
   3: {
   4:     [DataMember]
   5:     public string CommentText { get; set; }
   6:     [DataMember]
   7:     public Double VideoTime { get; set; }
   8:     [DataMember]
   9:     public DateTime CommentTimeStamp { get; set; }
  10:     [DataMember]
  11:     public Guid VideoId { get; set; }
  12:     [DataMember]
  13:     public Guid CommentId { get; set; }
  14:  
  15:     public WireComment()
  16:     {}
  17:  
  18:     public WireComment(Comment Comment)
  19:     {
  20:         this.CommentText = Comment.CommentText;
  21:         this.CommentTimeStamp = Comment.CommentTimeStamp;
  22:         this.VideoId = Comment.VideoId;
  23:         this.VideoTime = Comment.VideoTime;
  24:         this.CommentId = Comment.CommentId;
  25:     }
  26: }

This definition looks very similar to the Comment class we created earlier the main difference being that we are not inheriting from a base type and we also have attributed both the class and properties with DataContract and DataMember respectively.  These attributes are used to control how WCF serializes the class.

We need a type definition for moving comments between client and service primarily to afford loose coupling between the service and storage layers so we could potentially change the storage implementation (eg to use the new SQL Data Services backend) without having to alter the client code.

Now let’s add our data access methods; first a web method to add comments:

   1: [ServiceContract(Namespace = "")]
   2: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   3: public class CommentService
   4: {
   5:     [OperationContract]
   6:     public void StoreWithId(WireComment comment)
   7:     {
   8:         var tables = TableStorage.Create(StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration());
   9:         tables.TryCreateTable("CommentTable");
  10:  
  11:         CommentsTable ct = new CommentsTable();
  12:         ct.AddObject("CommentTable", new Comment(comment.CommentText, 
  13:             comment.VideoTime, comment.VideoId, 
  14:             comment.CommentTimeStamp, comment.CommentId));
  15:         ct.SaveChanges();
  16:  
  17:         VideosTable vt = new VideosTable();
  18:         var outp = vt.VideoTable.Where(v => v.VideoId == comment.VideoId);
  19:         if (outp.Count() == 0)
  20:         {
  21:             vt.AddObject("VideoTable", new VideoTableEntry(comment.VideoId));
  22:             vt.SaveChanges();
  23:         }
  24:     }
  25: }

This method is attributed as OperationContract that will cause WCF to expose it as a public method and it takes a WireComment as an argument.  It then uses the storage classes we defined earlier to persist the comment to Azure Table Storage.  The call to TryCreateTable takes the entity first data definitions and ensures that Azure has created the necessary backing storage to work with these.  It is then simply a matter of adding creating Comment objects based on the WireComment passed in and passing these to the CommentsTable object for persistence.

We then query the VideosTable to see if the videoid referred to in the WireComment exists; if not, we create one.

Finally, we will add a method for retrieving comments:

   1: [OperationContract]
   2:       public WireComment[] RetrieveForVideoId(Guid vidId)
   3:       {
   4:           var tables = TableStorage.Create(StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration());
   5:           tables.TryCreateTable("CommentTable");
   6:  
   7:           CommentsTable ct = new CommentsTable();
   8:  
   9:           if (vidId != Guid.Empty)
  10:           {
  11:               return ConvertToWire(ct.CommentTable.Where(comment => comment.VideoId == vidId));
  12:           }
  13:           else
  14:           {
  15:               return ConvertToWire(ct.CommentTable);
  16:           }
  17:       }
  18:  
  19:       public WireComment[] ConvertToWire(IEnumerable<Comment> Comments)
  20:       {
  21:           List<WireComment> returnComments = new List<WireComment>();
  22:  
  23:           foreach (Comment c in Comments)
  24:           {
  25:               returnComments.Add(new WireComment(c));
  26:           }
  27:           return returnComments.ToArray();
  28:       }

This function retrieves and array of WireComments.  If a videoid is passed in as a valid Guid, the query returns comments just for that video by using a LINQ where clause.  If the Guid is Empty, the function returns all comments.  A helper function is used to convert the Comment objects from the data layer to WireComments for return.

We now need to clean up the WCF bindings since the defaults won’t work:

At the bottom of Web.Config,

  1. edit the endpoint statement to remove the bindingconfiguration attribute.
  2. Change the Binding attribute to be basicHttpBinding
  3. And finally comment out the binding definition containing the customBinding declaration in the section above.

You should end up with a WCF configuration looking something like this:

   1: <system.serviceModel>
   2:       <behaviors>
   3:           <serviceBehaviors>
   4:               <behavior name="CloudCommentsWalkthrough_WebRole.CommentServiceBehavior">
   5:                   <serviceMetadata httpGetEnabled="true" />
   6:                   <serviceDebug includeExceptionDetailInFaults="false" />
   7:               </behavior>
   8:           </serviceBehaviors>
   9:       </behaviors>
  10:       <!--<bindings>
  11:           <customBinding>