Saturday, July 17, 2010

The SUESS Lifecycle: Stage 1 - Upload

Chris Domino, Director, Enterprise Architect

The SUESS Series

Introduction
Stage 1 - Upload
Stage 2 - Encode
Stage 3 - SmoothStreaming

Our in-depth media adventure begins, well, at the beginning of the SUESS "lifecycle" with Uploading. This stage contains two sub-components: the Uploader Silverlight control, and the WCF service on the Web Server that facilitates the file upload process. The only prerequisite required here is to have your data tier (Component 3) in place (which can simply be a folder on the Web Server).

Of course, there is nothing new about a file upload control; it's been in HTML longer than I have. Over the years, I've seen a lot of modifications to the familiar read only textbox and "Browse..." button to get the effects people are starting to expect in this whole Web 2.0 craze: progress bars, client-side file type filtering, large file support, etc.

To achieve these nice features, a lot of magic needs to happen client side. If you've read anything I've written before, you'll know that I don't consider AJAX to be magic; it's voodoo at best compared to the wizardry of Silverlight. The argument remains the Silverlight needs to be installed on the client's machine while AJAX comes down for free from the server along with the rest of the page. However, when it comes to programmatic benefits such as a first-class development experience (IntelliSense, compilation, UI designer, etc.), robustness of code, and extent of possibilities of what you can build, you cannot argue against Silverlight.

<RANT>  

Now don't get me wrong: AJAX is a really cute technology that has a lot of niche uses. The difference, as far as I'm concerned, falls in the mindset of the developer. Take animating progress bars for example. In AJAX/HTML, you'd need to arrange a few nested divs, set all their widths and background colors, and then when the upload mechanism reports progress, update the width of the inner-most div. To me, that's not really a true progress bar; it's HTML kludginess.

Now, in Silverlight, you see, you literally just animate a ProgressBar.

THAT'S the point.

 </RANT>

The example Uploader control I'll show you shortly lacks all the bells and whistles with which it could be adorned to truly make the life of a content manager much better. These features include drag and drop support, multi-file uploads, a slick UI, etc. However, the big ones, like progress bars and cancellation, will demonstrate how uploading large files (such as media) can be a pleasant experience for a user.

First of all, what does it look like? Well, not much, unfortunately. A pretty admin UI was not a requirement of the project through which I came up with SUESS, since this was all back end functionality. But here's what we have:

 

 

 

 

 

 

The title and description fields are watermarked textboxes with some gradient borders to implement the comps of the project. I really don't want to bore you with the details of the code that updates the visual state of the UI; it's hard to make hiding buttons and displaying error messages sexy. You'll be able to download SUESS in its entirety and comb through all the details yourself. Instead, let's take a quick trip through it, and then dive into the cool stuff.

 

 

 

The only input we actually need from the user is the file itself. Depending on your data tier, you could accept more metadata. (SUESS was actually born into SharePoint, so the sky was the limit for metadata.) But for now, we'll just have two optional textboxes for the name of the media and a description. If the name is blank, we'll use the file name. (As you'll see later, with the way Encoder outputs its SmoothStreaming formatted files, we don't have to worry about filename collisions; it's only a consideration during uploading.)

 

 

 

Next we have the "Encoding Quality" drop down. Once again, this is optional, and included only as an example to show another aspect of what you can do with SUESS in terms of explicitly controlling a myriad of Encoder options. (You'll see what I'm talking about in the Encoding post about SUESS.) These are hard-coded (barf, I know) values mimicking the Video Complexity (no documentation available) enumeration in the Encoder SDK. This is the best we can do to this extent, since we can't actually reference the DLL in our Silverlight project and reflect each enum member.

 

 

 

The purpose of including this in the example Uploader is to allow the user the ability to "throttle" the encoding process, which, as previously mentioned, could be very processor (and, of course, time) intensive. Unfortunately, I don't have a lot of metrics regarding just how much quality is sacrificed in terms of time taken to encode. However, I can say that media encoded with the highest quality settings indeed takes much longer than with the lowest.

 

 

 

Finally, we have the media itself. Silverlight gives us the OpenFileDialog control that is more or less identical to what you'd find in Windows Forms. The one immediate advantage it gives us over what is available in the HTML version is the ability to filter the file types (by extension) that the user is allowed to select in the dialog. This not only gives us a more elegant user experience (since we can "label" our filter with something like "Media Files,") but also makes the development much easier, since we don't have to go through the "hand slapping" input validation experience.

 

 

 

<RANT>

 

 

 

Back in the HTML/AJAX version of a file upload control, if you need to enforce a certain type (or types) of file(s), the best you can do is inspect the file the user selected, check its extension, and display an error message, forcing the user to have to go through the entire exercise again. Instead, with Silverlight, we can simply hide invalid file extensions in the folders they navigate within the dialog via the Filter property. This probably doesn't seem like a big deal, but I think it's huge: small UX improvements like these are what Web 2.0 is all about; with Silverlight, you are really using an application, not just a webpage.

 

 

 

</RANT>

 

 

 

Let's talk about a few of the more interesting code samples. The first one simply sets the OpenFileDialog to only allow the files that Encoder supports to be uploaded. It also has a hard coded value (which can easily be made to be configurable) to disallow files over 1GB (purely for sanity purposes).

 

 

 

       
       
 
  1. private void btnSelectFile_Click(object sender, RoutedEventArgs e)
  2. {
  3. //initialization
  4. OpenFileDialog ofd = new OpenFileDialog();
  5. //show open file dialog
  6. ofd.Multiselect = false;
  7. ofd.Filter = "Media Files|*.asf;*.avi;*.bmp;*.gif;*.jpg;*.jpeg;*.m2t;*.m2ts;*.mov;*.mp4;*.mpeg;*.mpg;*.png;*.tif;*.tiff;*.ts;*.vob;*.wmv;";
  8. bool? fileSelected = ofd.ShowDialog();
  9. //check for file
  10. if (fileSelected.HasValue && fileSelected.Value)
  11. {
  12. //check legth
  13. if (ofd.File.Length > this._maxFileSize)
  14. {
  15. //(omitted UI code)
  16. //file too large
  17. this.tbProgress.Text = "You cannot upload a file larger than 500 MB.";
  18. return;
  19. }
  20. //(omitted UI code)
  21. //start upload
  22. this.StartUpload(ofd.File);
  23. }
  24. }
   
       

 

 

 

Line #7 above sets the Filter property of the OpenFileDialog to the Encoder 3 supported file types. This is what ensures that Encoder will be able to handle whatever the user throws at it. Line #15 shows that "UI" code has been omitted. You'll see this in a lot the code samples throughout SUESS. Like I said, I don't want to bore you with the details of dealing with VisualStates in the application. Here are some more action shots:

 

 

 

 

 

 

 

 

 

 

 

 

Next let's look at the logic that implements the recursive "chunky" upload. If we send the entire file up to the server, we can't show progress bars, and really don't take advantage of client side functionality at all. Instead, the SUESS Uploader sends 1MB of the file up to the server at a time. After each call, the UI is updated with the current progress, and then the next "chunk" is uploaded.

 

 

 

Since all Silverlight service communication is done asynchronously, we need to daisy chain the "Completed" callback for each of these calls so that they are executed serially within it. Normally, if service calls can be done in parallel (such as downloading images or assembling the content of unselected tabs), you can kick off them all off at the same time; when they're done, they're done, and the corresponding part of the UI lights up.

 

 

 

Otherwise, if you need to call service A and then pass its result on to service B (or a subsequent call back to A, like the Uploader), then there will be some chaining going on. Here's a quick diagram that demonstrates these two paradigms.

 

 

 

 

 

 

In the top half, where we show "parallel" calls, all of the service references are explicit. We invoke a service, and when the associated completed event handler is fired, we do something back on the UI. What if we need to call the same service multiple (and in an unknowable amount of) times in a specific order? This is the case with the Uploader. Since we obviously can't know the size of the file, then we further don't know how many chunks we'll be dealing with. We need to make these calls serially.

 

 

 

The answer is recursion, as shown in the bottom half of the diagram. (Of course, this is only logical recursion, since the same physical method isn't actually calling itself.) I experimented with some crazy iterative algorithms, but they got real messy real quick, and were all ultimately besmirched by the asynchronousity of Silverlight. Instead, I created a method that uploads a chunk (array of bytes) of a file, and when it's done, increments a counter and progress bars. Finally, the same service is called again with the next chunk.

 

 

 

This algorithm, other than being sweet at uploading files, makes two additional features of the Uploader trivial: real time progress bars and upload cancellation. We'll discuss those next. First, however, let's look at this code. This piece is the StartUpload method that the above code block calls to kick off the recursion.

 

 

 

       
       
 
  1. private void StartUpload(FileInfo file)
  2. {
  3. //(omitted UI code that resets progress bars, clears errors, etc.)
  4. //start recursive upload
  5. this._index = 0;
  6. this._fileName = string.Format("{0}-{1}", Guid.NewGuid(), file.Name);
  7. this.UploadFile(file, true);
  8. }
   
       

 

 

 

The Boolean passed as the second parameter to UploadFile in Line #7 simply tells the algorithm this is the first chunk. UploadFile actually doesn't care; it just passes this along to the service so that it knows whether to create a new file on the data tier or open an existing one. This can probably be inferred on the server rather than made explicit, but I didn't want to have to burn an extra trip to the disk for each chunk if I didn't have to.

 

 

 

Here's the algorithm:

 

 

 

       
       
 
  1. private void UploadFile(FileInfo file, bool isFirstChunk)
  2. {
  3. //initialization
  4. byte[] buffer = new byte[this._bufferSize];
  5. MediaServiceSoapClient svc = Utilities.GetMediaClient();
  6. //get chunk
  7. using (Stream data = file.OpenRead())
  8. {
  9. //write to buffer
  10. data.Seek(this._index, SeekOrigin.Begin);
  11. data.Read(buffer, 0, this._bufferSize);
  12. }
  13. //upload chunk
  14. svc.UploadFileChunkAsync(this._fileName, buffer, isFirstChunk);
  15. svc.UploadFileChunkCompleted += (sender, args) =>
  16. {
  17. //determine result
  18. if (args.Cancelled || args.Error != null || !string.IsNullOrEmpty(args.Result))
  19. {
  20. //(error code very much truncated here; the above if statement should check each OR'ed condition separately and update the UI appropriately)
  21. }
  22. else if (this._index > -1)
  23. {
  24. //update text progress
  25. double progress = (Convert.ToDouble(this._index) / Convert.ToDouble(file.Length)) * 100;
  26. this.pbUpload.IsIndeterminate = false;
  27. this.tbProgress.Text = string.Format("{0} / {1} MB uploaded...", Convert.ToDouble(this._index) / Convert.ToDouble(this._bufferSize), this.GetTotalSize(file.Length));
  28. //animate value
  29. Storyboard sb = new Storyboard();
  30. DoubleAnimation da = new DoubleAnimation();
  31. Storyboard.SetTarget(da