Tuesday, April 10, 2007

Building Reactive User Interfaces in .NET: ISynchronizeInvoke on Idle Time

One of the most common things to do in a multi-threaded .NET Windows Forms application is to use the ISynchronizeInvoke interface on a Control to marshal things into the UI thread. The basic jist of things is: you can have the Windows Forms engine call your delegate on the thread that created the Control instance you're using. It does this using the window message pump. Typical uses of the ISynchronizeInvoke are: you want to read some data from a file, database, socket, or web service and you want your application to be responsive while you do it. When you're done you do a BeginInvoke/Invoke on the Control to update some UI elements. Well, sometimes using a Control's ISynchronizeInvoke leads to hung GUI's.

If you have a lot of asynchronous operations pending and they complete in bunches the affect on your UI can be the appearance of a hang or sluggishness as drawing is queued behind the delegates you registered with BeginInvoke on your control. During the login sequeunce in SoapBox Communicator there are potentially thousands of events that need to be processed on the UI thread. Yup, thousands. Your roster arrives from the server. You receive the current avialability from every online person on your roster. You got 10 messages while you were offline. Each of your contact's cached profiles (avatars, names, client capabilities, etc) are read from disk and compared with the user's current presence. This onslought of activity can be handled a number of ways. The basic principles I stumbled into after much trial and error is:

  1. Never, ever, ever, ever, ever do IO on a UI thread. Even something as simple as reading a 1KB image from disk can bring your UI to a halt under the right circumstances.
  2. Use timers and update common pieces of UI (like list views) in batches whose changes were caused changes by background operations. This will reduce flicker as you invalidate areas to redraw.
  3. Use Application.Idle to your advantage. (see my take on this below)
  4. Be careful how many window handles you create. Controls are useful, but sometimes you've just gotta draw your own.
  5. Profile your code where it seems sluggish.

I mentioned above that you should use Application.Idle to your advantage. The articles I've found on it all mention coding precise things you want to do in that event, like updating a single form. I wanted to do it more generically. So, I created an implementation of ISynchronizeInvoke that uses the Application.Idle event to process qeued items. I've created a (only slightly) contrived example that uses a Pi Calculator I found and an animated Gif to demonstrate a hanging GUI.


Both buttons calculate Pi to 50 digits on the UI thread using 100 separate ISynchronizeInvoke.BeginInvoke calls. The "Idle Invoke" button does so using my ApplicationIdleSynchronizer. The "Direct Invoke" button calls BeginInvoke on the Form directly. You can click the buttons any number of times and more and more Pi calculation runs will be queued. The completed label is incremented after every run. Here's the code:

    public partial class Form1 : Form
{
private const int WorkItems = 100;
private const int PiDigitsToCalc = 50;

private int _workItemsCompleted = 0;

private ApplicationIdleSynchronizer _idleSynchronizer = new ApplicationIdleSynchronizer();

public Form1()
{
InitializeComponent();
}

protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_idleSynchronizer.Dispose();
}

private void button1_Click(object sender, EventArgs e)
{
System.Threading.ThreadPool.QueueUserWorkItem(QueueOnBackgroundThread, _idleSynchronizer);
}

private void button2_Click(object sender, EventArgs e)
{
System.Threading.ThreadPool.QueueUserWorkItem(QueueOnBackgroundThread, this);
}

private void QueueOnBackgroundThread(object state)
{
for (int i = 0; i < WorkItems; i )
{
((ISynchronizeInvoke)state).BeginInvoke(new ThreadStart(MyWorkItem), null);
}
}

private void MyWorkItem()
{
PiCalculator.CalculatePi(PiDigitsToCalc);
_workItemsCompleted++;
label2.Text = _workItemsCompleted.ToString();
}
}

Click the buttons. Move the form around. Resize it. You might be suprised by the result (or not). Both very adequately use your CPU, but the Idle Invoke produces a much more reactive UI. You can download the full source code here: ApplicationIdleInvoker.zip. Note, this exact code isn't in use in production. I wrote it for this blog. YMMV. Let me know if it has any issues.

Thursday, April 5, 2007

YouTube Getter

Usually everything I post here is serious business. Well, not today! Today, we're going to look at how to get the raw video stream for YouTube videos. Why, you ask? Well, curiosity, mostly. :) Some people like sports, I like problem solving. There are a number of browser plug-ins and web sites that do this already, but hey, it was still fun.

YouTube offers a bit of an API for developers to mess with. It's mostly for grabbing sets of videos and preview images and such. They also let you embed their player in your pages. That's awfully nice of them, but what if I don't like their player? What if I want a sexier player? Well, if you've got something that can play/convert Flash Video (FLV's), here's the full C# code to grab the FLV URI (pieces dissected below):

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Web;
using System.Collections.Specialized;

namespace JDFun
{
public static class YouTubeGetter
{
public static Uri GetFlvUri(string viewUri)
{
return GetFlvUri(new Uri(viewUri));
}

public static Uri GetWatchUri(Uri flvUri)
{
NameValueCollection qry = HttpUtility.ParseQueryString(flvUri.Query);

string videoID = qry["video_id"];
string watchUri = string.Concat("http://www.youtube.com/watch?v=", HttpUtility.UrlEncode(videoID));

return new Uri(watchUri);
}

public static Uri GetImageUri(Uri flvUri)
{
NameValueCollection qry = HttpUtility.ParseQueryString(flvUri.Query);

string imageUri = qry["iurl"];

return new Uri(imageUri);
}

public static Uri GetFlvUri(Uri viewUri)
{
// so either i've got the embed link or the watch link

//watch link: http://www.youtube.com/watch?v=up-RX_YN7yA
//embed link: http://www.youtube.com/v/up-RX_YN7yA

string toQuery = null;

NameValueCollection queryString = HttpUtility.ParseQueryString(viewUri.Query);

string videoId = queryString["v"];

if (null != videoId)
{
toQuery = string.Concat("http://www.youtube.com/v/", videoId);
}
else
{
toQuery = viewUri.ToString();
}

if (null == toQuery)
throw new InvalidOperationException("Not a valid YouTube Uri.");

Uri queryUri = new Uri(toQuery);
//ok we have the uri to query, now go there and get redirected.
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(queryUri);
req.AllowAutoRedirect = true;

// make them think we're coming from a direct link
req.Referer = string.Empty;

// firefox rules!
req.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3";
using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
{
string absoluteRedirectLocation = resp.ResponseUri.AbsoluteUri;

//replace the swf with the get_video request
if (!absoluteRedirectLocation.Contains("/p.swf?"))
throw new InvalidOperationException("Unrecognized Uri. YouTube changed something.");

absoluteRedirectLocation = absoluteRedirectLocation.Replace("/p.swf?", "/get_video?");

//return the absolute URI for this request
return new Uri(absoluteRedirectLocation);
}
}
}
}

Ok, wow, that's a lot bigger than I remember. Here's the basic idea.

  1. Get the video ID from the given URI. The URI could be the "watch" URI, or it could be the URI from the embed code they give you.
  2. Load the video directly based on the video ID, at the embed URI.
  3. Make a HTTP Request and get redirected to another URI, (which happens to contain the Image URI, and a special token "t").
  4. Know the magic URI where they keep the video streams (I cheated) and use it.

Now wasn't that fun? :)

About the Author

Wow, you made it to the bottom! That means we're destined to be life long friends. Follow Me on Twitter.

I am an entrepreneur and hacker. I'm a Cofounder at RealCrowd. Most recently I was CTO at Hive7, a social gaming startup that sold to Playdom and then Disney. These are my stories.

You can find far too much information about me on linkedin: http://linkedin.com/in/jdconley. No, I'm not interested in an amazing Paradox DBA role in the Antarctic with an excellent culture!