On my internal application for Channel 9, I have to create thumbnails at certain time codes in a movie. After a giant headache attempting to track down the proper way of doing this, I figured out how to do this even if for a few hours I thought 127 seconds was the same at 1 minute and 27 seconds. That caused some testing headaches as my test clip is 1:40 long.
In this example I’ll write out the code for a threaded video thumbnail creating tool in WPF (XAML) and c#.
To get access to the tester app, head over to codeplex and source code can be found there too!
First, here is my XAML for my control. Basic but shows the point.
<StackPanel>
<Button Click="Button_Click">
Capture
</Button>
<Border BorderThickness="2" BorderBrush="Cyan">
<MediaElement Name="video"
Source="media\theOffice.wmv"
LoadedBehavior="Pause"
ScrubbingEnabled="True"
Visibility="Collapsed"/>
</Border>
<StackPanel Orientation="Horizontal">
<Border BorderThickness="2" BorderBrush="Red">
<Image Name="image0" />
</Border>
<Border BorderThickness="2" BorderBrush="Green">
<Image Name="image1" />
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Border BorderThickness="2" BorderBrush="Blue">
<Image Name="image2" />
</Border>
<Border BorderThickness="2" BorderBrush="Yellow">
<Image Name="image3" />
</Border>
</StackPanel>
</StackPanel>
The Click event on the button will be where we get the width, height and source of the video. I’m creating 4 images to show it off the fact it is threaded.
private void Button_Click(object sender, RoutedEventArgs e)
{
var source = video.Source;
setScreenCapture(image0.Name, TimeSpan.FromSeconds(10), source);
setScreenCapture(image1.Name, TimeSpan.FromSeconds(43) + TimeSpan.FromMilliseconds(760), source);
setScreenCapture(image2.Name, TimeSpan.FromSeconds(60), source);
setScreenCapture(image3.Name, TimeSpan.FromSeconds(80), source);
}
We’ll be using a ThreadPool to accomplish the processing due to a need for a Thread.Sleep in the function following this one. This will make things a bit more complicated but not locking the UI is worth the extra pain.
private void setScreenCapture(string controlName,
TimeSpan timeSpan, Uri source)
{
ThreadPool.QueueUserWorkItem(delegate
{
setScreenCaptureWorker(controlName,
timeSpan, source);
});
}
Now that we have our queuing function done, on to the worker.
private void setScreenCaptureWorker(string controlName,
TimeSpan timeSpan, Uri source)
{ /* code */ }
We’ll be using a MediaPlayer, RenderTargetBitmap, DrawingVisual, and DrawingContext to accomplish the thumbnail creation.
var player = new MediaPlayer {Volume = 0, ScrubbingEnabled = true};
player.Open(source);
player.Pause();
player.Position = timeSpan;
Thread.Sleep(1000);
var width = player.NaturalVideoWidth;
var height = player.NaturalVideoHeight;
var rtb = new RenderTargetBitmap(
width, height, 96, 96, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using(DrawingContext dc = dv.RenderOpen())
dc.DrawVideo(player, new Rect(0, 0, width, height));
rtb.Render(dv);
var frame = BitmapFrame.Create(rtb).GetCurrentValueAsFrozen();
// you now have a thumbnail frame!
The MediaPlayer needs a moment to get stuff going. Adding in the Thread.Sleep allows this to get synced up. The key thing to remember here is we are no longer on the UI thread. The user interface won’t lock now. But due to that, we have elements on non-UI threads. The RenderTargetBitmap and even the BitmapFrame created are both tied to the current thread. Passing these back to the UI will cause thread context issues. Using a Freezable object solves the context issue.
In a perfect world, I’d have a different function do this and pass it in as a delegate but this was example code. Due to the sample codeness of it, I’m creating the image in the function and then calling the dispatcher. You’ll want to replace this code with whatever your goal is, however this code does both tasks I could see someone using this for.
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(frame as BitmapFrame);
string filename = controlName + ".jpg";
using (var fs = new FileStream(filename, FileMode.Create))
encoder.Save(fs);
Dispatcher.Invoke(
new setImageDelegate(setImage), controlName, frame);
And now to update the UI elements. Since we used Dispatcher.Invoke to call the delegate, we can do this since that forces us back on the User Interface Thread!
private delegate void setImageDelegate(string controlName, BitmapFrame frame);
private void setImage(string controlName, BitmapFrame frame)
{
var control = FindName(controlName);
if (control != null)
((Image)control).Source = frame;
}
Enjoy!