An Advanced Video player
Live example: http://silverlight.adefwebserver.com/
This is part 2 to the article Silverlight 4 Video Player. That article focused on the View Model Style (Model-View-ViewModel) pattern, and how it supports Designer / Developer collaboration and workflow. However, when others tried to use that project for a real website, they found they needed the full set of controls that a video player normally provides. I decided to use this as an opportunity to dig deeper into View Model Style and demonstrate how easy it truly is, when using Microsoft Expression Blend 4 (or higher). This time we will cover the "hard stuff". However, implementing this will be surprisingly easy.
This project creates a fully "Designable" Silverlight Video Player. This is not to be confused with a "Skinable" Video Player. A Skinable Video Player allows you to change the look and feel of the existing controls of the Video Player. A Designable player, allows a Designer to use ANY set of controls to implement the Video Player. For example, a Designer could replace all the buttons with a single dial that a user rotated into different positions, or they could change the method a user uses to select from the list of available videos. This can be done in Expression Blend without writing any code.
5/16/2010 Update: Victor Gaudioso has provided example code that demonstrates how to fast forward and rewind video. His code has been adapted for use in this project. His original article is found here: http://victorgaudioso.wordpress.com/2010/05/15/new-silverlight-video-tutorial-how-to-create-fast-forward-for-the-mediaelement/
Additional Downloads
You will also need to download and install the Silverlight Toolkit: http://silverlight.codeplex.com/
View Model Style (An overview)
The View Model Style pattern allows a programmer to create an application that has absolutely no UI. The programmer only creates a ViewModel and a Model. A designer with no programming ability at all, is then able to start with a blank page and completely create the View (UI) in Microsoft Expression Blend 4 (or higher).
Yes, you could just put your code in the code behind (for example, you place a button on a page and then double-click on it, and it wires up an event handler in the code behind), but then a Designer could only use a button for that task. They could not change the element to a Dial. Most importantly, the Designer would need to know a bit about coding because they would be directly interacting with the code. With View Model Style (or Data Driven Application as it is now referred to in the Expression Blend templates), basically:
- The Programmer creates code that consist of Properties, Collections, and Commands.
- The Designer uses Expression Blend, and creates the complete UI, without writing any code.
The Model is where the data goes, usually this is a web service. The View is the UI, or the "front end". The ViewModel is where the "magic" happens. It consists of:
- Properties – One of something. This could be a String or an Object. Implements INotifyPropertyChanged, so that any element bound to it, is automatically notified whenever it changes.
- Collections – A collection of something. This is of type ObservableCollection, so that any element bound to it, is automatically notified whenever it changes.
- Commands – An event that can be raised. Also, one parameter of type Object can be passed. This implements ICommand.
If you are new to View Model Style it is suggested that you read Silverlight View Model Style : An (Overly) Simplified Explanation for an introduction.
The Starter Solution
We will start with the solution contained in the MVVMVideoPlayer_Starter.zip file. When you run the project, the web service in the web project, will detect what videos are in the "Video" folder, and pass them to the Silverlight application.
- The list of videos will show in the combo box dropdown.
- A progress bar will show the current progress.
- The exact location of the video will display above the video.
- The current and total time of the video will also display.
- The Play button will play the video
- The Stop button will stop the video.
This is what the Silverlight 4 Video Player article covered. To make a full featured video player, we need to add the following features:
- Volume control - A user needs the ability to change the volume of the video.
- Pause button - A user needs to be able to pause the video and resume play by clicking the pause button again, or by clicking the play button.
- "Seek" control - A user should be able to easily skip forward and back through a video even while the video is playing.
- "Video Buffering" notification - When the video is loading, a notification needs to appear, and indicate how much longer it will take for the video to start playing.
- Full Screen Video - When viewing a video, it is nice to have the option to click a button and see the video full screen. You need to be able to switch back and forth seamlessly. Also, only the video should be full screen, not the entire Silverlight application.
We will implement the code for all of this by only adding code to the ViewModel. the Model is not changed at all. The code is already in the MVVMVideoPlayer_Starter.zip file (it will be covered in detail at the end of this article). For now, we will start with the steps the Designer would use to implement this additional functionality. To start, the Designer opens the project up in Expression Blend...
The Volume Control
This control actually does two things:
- Indicates what the current volume is
- Allows the volume to be changed
In Expression Blend, in Assets, get a Slider control.
Drop it in the [StackPanel] in the Objects and Timeline window.
Click the "V" key on the keyboard to switch to the Selection tool. Then on the design surface, use the mouse to widen the slider a bit.
In the Properties for the Slider, set the left Margin to 5
Set the Maximum level to 1 and Minimum to 0 (the volume on a Media Element actually goes only from 0 to 1. For example it starts at 0.5.)
Click the Advanced options box next to Value (in Common Properties).
Select Data Binding...
- Select the Element Property tab
- Select mediaElement in the Scene elements section
- Select Volume in the Properties section
- Select TwoWay for Binding direction
- Click the OK button
Hit F5 to build and run the project. The web browser will open and you will now be able to control the volume.
The Pause Button
Again, there is more than meets the eye:
- When you click Pause the video should stop
- If the video is paused and you click Pause again, it should continue from where it left off
- If the video is paused and you click Play, it should still just continue from where it left off
In Assets, get a Button control.
Drop it between the PlayButton and the StopButton, in the Objects and Timeline window. Right-click on it and rename it to "PauseButton".
In the Properties, set the Content to "Pause" and set the left Margin to 5.
In the Assets, get an InvokeCommand behavior.
Drop it under the PauseButton (remember you renamed it in the earlier step), in the Objects and Timeline window.
In the Properties for the behavior, select Data bind next to Command.
Select PauseVideoCommand in the Data Context window, and click the OK button.
Hit F5 to build and run the project. The web browser will open and you will now be able to Pause the video.
The Seek Control
We want to provide the ability to skip forward and backward while the video is playing. This functionality can be implemented using any control. For example, you could use a Slider control or a custom dial control. In this example, we will just use the ProgressBar control. The ProgressBar control will still display the progress, but when you click on the control, it will navigate to the section of the video.
In the Assets, get an InvokeCommandAction behavior.
Drop it under the mediaElement in the Objects and Timeline window. Right-click on it and rename it to "SetSeekControl".
In the Properties for the behavior:
- Set the EventName to MediaOpened (this event is fired automatically when a video is set as a source for the MediaElement and it has found the video and is ready to play it).
- Click the Data bind button next to Command.
Bind it to SetSeekControlCommand.
Click the Advanced options box next to CommandParameter.
- Select the Element Property tab
- Select progressBar in the Scene elements section
- Select ProgressBar in the Properties section
- Click the OK button
Hit F5 to build and run the project. The web browser will open and you will now be able to skip to parts of the video by clicking on the ProgressBar control.
Video Buffering Notification
When the MediaElement is playing a video, it loads a bit of the video "ahead" of the part that is playing. If it is not able to load the video fast enough, it will stop and "buffer" the video and then resume playing after buffering is complete (when buffering has reached 100%). We want the buffering notification to perform the following functions:
- Display when the video is being buffered
- Disappear when the video is not being buffered
- Display the percentage that it has buffered, in real time, as it is being buffered
In Assets, search for the BusyIndicator.
Drag it to the design surface and place it over the MediaElement.
In the Properties for the BusyIndicator:
- Clear BusyContent
- Clear Content
Bind BusyContent to MediaBufferingTimeProperty (this displays the buffering progress).
Bind the IsBusy property to MediaBufferingProperty (this controls when the buffering box displays).
You will know you have bound the properties because they will have a gold box around them.
Full Screen Video
For the full screen video functionality we want the following things:
- When in full screen mode, we want to only see the video, not the other controls
- When we switch back and forth, we want the video to continue playing seamlessly
- We want the video to go to full screen, if we click on the video, or the Full Screen button
Switching a Silverlight application to full screen mode is very simple. Switching only one element of a Silverlight application, involves a few extra steps. We need to create a Grid, then instruct the ViewModel to use this Grid when going into full screen mode. The ViewModel will automatically create a VideoBrush and set it's source to the MediaElement so that the video stays in sync in both modes.
Ion the Panel section on the Tools bar, select Grid.
Double-click on Grid to insert it onto the design surface.
in the Properties for the Grid:
- Set the Width and Height to Auto
- Set the RowSpan to 2
- Set HorizontalAlignment and VerticalAlignment to Stretch
Drag and drop an InvokeCommand behavior under the MediaElement and rename it to "FullScreen".
In the Properties for the behavior, bind the Command to the SetFullScreenCommand.
Bind the CommandParameter to the [Grid].
This will enable full screen mode when you click on the video. Now to trigger a return to normal mode...
Drag and drop an InvokeCommand behavior under LayoutRoot in the Objects and Timeline window.
In the Properties for the behavior:
- Set the EventName to SizeChanged
- Bind the Command to ExitFullScreenCommand
- Bind the Grid to CommandParameter
Hit F5 to build and run the project. When you click on the video it will switch to full screen mode.
Return to Expression Blend and perform the following functions to create a Full Screen button:
- Place a button next to the Stop button and set it's content to "Full Screen"
- Set the left Margin to 5
- Drag and drop an InvokeCommand behavior under it
- In the Properties for the behavior, bind the Command to the SetFullScreenCommand
- Bind the CommandParameter to the [Grid]
The Code
- Designers can skip to the end, there is nothing to see here :) -
Let's look at the relevant code used to implement each section...
Volume
There is no code! The Slider control is bound directly to the Volume property of the MediaElement. I know... Wow!
Pause
In the constructor of the ViewModel, all the ICommands are set-up including the PauseVideoCommand:
public MainViewModel()
{
MediaOpenedCommand = new DelegateCommand(MediaOpened, CanMediaOpened);
PlayVideoCommand = new DelegateCommand(PlayVideo, CanPlayVideo);
StopVideoCommand = new DelegateCommand(StopVideo, CanStopVideo);
PauseVideoCommand = new DelegateCommand(PauseVideo, CanPauseVideo);
SetSeekControlCommand = new DelegateCommand(SetSeekControl, CanSetSeekControl);
SetVideoCommand = new DelegateCommand(SetVideo, CanSetVideo);
SetFullScreenCommand = new DelegateCommand(SetFullScreen, CanSetFullScreen);
ExitFullScreenCommand = new DelegateCommand(ExitFullScreen, CanExitFullScreen);
GetListOfVideos();
}
The rest of the implementation is below. Basically it simply calls Pause() on the MediaElement. The rest of the code is handling if it's ok to pause, if it can pause, and if it is paused already, should it play instead.
#region PauseVideoCommand
public ICommand PauseVideoCommand { get; set; }
public void PauseVideo(object param)
{
if (MyMediaElement.CurrentState == MediaElementState.Playing)
{
if (MyMediaElement.CanPause)
{
MyMediaElement.Pause();
}
else
{
MyMediaElement.Stop();
}
if (progressTimer.IsEnabled)
{
progressTimer.Stop();
}
}
else
{
MyMediaElement.Play();
progressTimer.Start();
}
}
private bool CanPauseVideo(object param)
{
bool CanPause = false;
if (MyMediaElement != null)
{
if ((MyMediaElement.CurrentState == MediaElementState.Paused)
|| (MyMediaElement.CurrentState == MediaElementState.Playing))
{
CanPause = true;
}
}
return CanPause;
}
#endregion
Seek
This one is a bit interesting, Basically you first pass in a FrameworkElement that will be used to perform the Seek. Mouse events are attached to this Framework element, so that an event is raised if a person clicks on the element. When the event is raised, the width of the element is used to determine where they clicked on the element. That value is used to determine where to navigate in the video.
#region SetSeekControlCommand
public ICommand SetSeekControlCommand { get; set; }
public void SetSeekControl(object param)
{
SeekControl = (FrameworkElement)param;
SeekControl.MouseLeftButtonDown += new MouseButtonEventHandler(SeekControl_MouseLeftButtonDown);
}
private bool CanSetSeekControl(object param)
{
return true;
}
private void SeekControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
double position = e.GetPosition(SeekControl).X;
double percent = position / SeekControl.ActualWidth;
Seek(percent);
}
private void Seek(double percentComplete)
{
TimeSpan duration = MyMediaElement.NaturalDuration.TimeSpan;
int newPosition = (int)(duration.TotalSeconds * percentComplete);
MyMediaElement.Position = new TimeSpan(0, 0, newPosition);
SetCurrentPosition();
}
#endregion
Video Buffering
A DispatcherTimer is used to check the progress of the Video each second, and update properties, including the buffering properties:
#region Time display
private void ProgressTimer_Tick(object sender, EventArgs e)
{
SetCurrentPosition();
}
private void SetCurrentPosition()
{
if (CurrentPositionProperty > 0)
{
if (CurrentPositionProperty >= TotalDurationProperty)
{
var content = Application.Current.Host.Content;
if (content.IsFullScreen)
{
content.IsFullScreen = false;
}
CurrentPositionProperty = 0;
StopVideo(null);
}
}
CurrentProgressProperty = string.Format(
"{0}:{1} / {2}:{3}",
Math.Floor(MyMediaElement.Position.TotalMinutes).ToString("00"),
MyMediaElement.Position.Seconds.ToString("00"),
Math.Floor(MyMediaElement.NaturalDuration.TimeSpan.TotalMinutes).ToString("00"),
MyMediaElement.NaturalDuration.TimeSpan.Seconds.ToString("00"));
CurrentPositionProperty = MyMediaElement.Position.TotalSeconds;
TotalDurationProperty = MyMediaElement.NaturalDuration.TimeSpan.TotalSeconds;
MediaBufferingProperty = (MyMediaElement.CurrentState == MediaElementState.Buffering);
MediaBufferingTimeProperty = String.Format("Buffering {0} %", (MyMediaElement.BufferingProgress * 100).ToString("##"));
}
#endregion
Note: In hindsight, it may be possible to implement this entire example without using a DispatcherTimer. An InvokeAction behavior on the MediaElement may be all that is required to update the properties... oh well, perhaps next time.
Full Screen Video
The Designer must specify a Grid to be used for full screen mode. This Grid must be set to stretch and fill the entire screen, blocking out the rest of the application (so yes, the rest of the application is actually still behind the Grid in full screen mode).
The code dynamically places a VideoBrush in the Grid and sets it's source to the MediaElement so that the video will stay in sync. It is possible to put other elements in the Grid, so that you could, for example, show player controls when the mouse is moved.
#region SetFullScreenCommand
public ICommand SetFullScreenCommand { get; set; }
public void SetFullScreen(object param)
{
if (MyMediaElement != null)
{
var content = Application.Current.Host.Content;
content.IsFullScreen = true;
VideoBrush objVideoBrush = new VideoBrush();
objVideoBrush.SetSource(MyMediaElement);
objVideoBrush.Stretch = Stretch.UniformToFill;
Grid objGrid = (Grid)param;
objGrid.Visibility = Visibility.Visible;
objGrid.Background = objVideoBrush;
}
}
private bool CanSetFullScreen(object param)
{
var content = Application.Current.Host.Content;
return (!content.IsFullScreen);
}
#endregion
This is the code when you exit full screen:
#region ExitFullScreenCommand
public ICommand ExitFullScreenCommand { get; set; }
public void ExitFullScreen(object param)
{
Panel objPanel = (Panel)param;
objPanel.Visibility = Visibility.Collapsed;
}
private bool CanExitFullScreen(object param)
{
var content = Application.Current.Host.Content;
return (!content.IsFullScreen);
}
#endregion
"Pimp" This Video Player, Please!
Are you a Designer, or do you want to be? Download this project and come up with a cool design. The code is Open Source so you can redo the application and post a tutorial or blog showing what you create. To learn how to use Expression Blend, all you have to do is go to:
http://www.microsoft.com/design/toolbox/