Creating my own media player

I’ve been tasked at creating a new feature / porting an application from WinForms to WPF at work.  Since Channel9 deals heavily with video, you guessed it, I have to deal with video!

After attempting to see if there was a decent WPF player or example for the MediaElement, it really didn’t seem like there was.  Some of the examples did some crazy stuff when it wasn’t needed.

So here are my requirements for it as of now:

  • Play, Stop, Pause
  • Be able to see current position’s time code
  • Seek by holding down the slider and by clicking
  • Adjust Volume
  • Adjust Speed of playback
  • Adjust video position frame by frame
  • Seeing the current position

From a quick and dirty UI, this is what I came up with.  The 3 icons are from the Silk set by FamFamFam.
image

XAML:

<Grid Background="Black" >
	<Grid.RowDefinitions>
		<RowDefinition Height="*" />
		<RowDefinition Height="60" />
	</Grid.RowDefinitions>


	<MediaElement Grid.Row="0" Source="media\theoffice.wmv" Name="videoElement" 
				  LoadedBehavior="Manual" UnloadedBehavior="Stop" ScrubbingEnabled="true"
				  MediaOpened="Element_MediaOpened" MediaEnded="Element_MediaEnded" Loaded="videoElement_Loaded" />
	<StackPanel Grid.Row="1">
		<StackPanel Orientation="Horizontal">
			<TextBlock Foreground="White" Margin="5"  VerticalAlignment="Center">Seek To</TextBlock>
			<Slider Name="timelineSlider" Width="361" SmallChange="1" LargeChange="10000" 
					PreviewMouseUp="timelineSlider_MouseUp"  />
			<TextBlock Foreground="White" Margin="5"  VerticalAlignment="Center" Name="videoElementTime" >##:##:##:##</TextBlock>
		</StackPanel>
		<StackPanel HorizontalAlignment="Center" Width="450" Orientation="Horizontal">
			<Image Source="..\images\control_play.png" MouseDown="OnMouseDownPlayMedia" Margin="5" Name="test" />
			<Image Source="..\images\control_pause.png" MouseDown="OnMouseDownPauseMedia" Margin="5" />
			<Image Source="..\images\control_stop.png" MouseDown="OnMouseDownStopMedia" Margin="5" />
			<TextBlock Foreground="White" VerticalAlignment="Center" Margin="5">Volume</TextBlock>
			<Slider Name="volumeSlider" VerticalAlignment="Center" 
					ValueChanged="ChangeMediaVolume" Minimum="0" Maximum="1" Value="0.5" Width="70" IsSnapToTickEnabled="True" TickFrequency="0.1" />
			<TextBlock Foreground="White" Margin="5,5,30,0" VerticalAlignment="Center" Text="{Binding ElementName=volumeSlider, Path=Value}"/>

			<!-- Volume slider. This slider allows a Volume range between 0 and 10. -->
			<TextBlock Foreground="White" Margin="5"  VerticalAlignment="Center">Speed</TextBlock>
			<Slider Name="speedRatioSlider" VerticalAlignment="Center" 
					ValueChanged="ChangeMediaSpeedRatio" Value="1" Width="70" SmallChange=".5" IsSnapToTickEnabled="True" Maximum="4" LargeChange="0.5" TickFrequency="0.5" />
			<TextBlock Foreground="White" Margin="5" VerticalAlignment="Center" Text="{Binding ElementName=speedRatioSlider, Path=Value}"/>
		</StackPanel>
	</StackPanel>
</Grid>

Lets go through my requirements 1 by 1 for source code:

Play, Stop, Pause

This is very straight forward.

videoElement.Play();
videoElement.Stop();
videoElement.Pause();

Seek by holding down the slider and by clicking

Once again, pretty easy.  I’m using milliseconds for calculating everything.  I saw someone used

// in Element_MediaOpened
timelineSlider.Maximum = videoElement.NaturalDuration.TimeSpan.TotalMilliseconds;

// in timelineSlider_MouseUp
videoElement.Position = new TimeSpan(0, 0, 0, 0, (int)timelineSlider.Value);

Adjust Volume and speed of playback

Pretty easy as well

videoElement.Volume = volumeSlider.Value;
videoElement.SpeedRatio = speedRatioSlider.Value;

Adjust video position frame by frame

This is where stuff starts getting a bit interesting due to focus.  I want to use the arrow keys but if a slider has focus, the arrow keys will alter that element plus this.  This is the hack I came up with.

Since this is a user control, you need to get the window dynamically, by using Window.GetWindow(this), you can get it.  You attach an event to the PreviewKeyDown event.  From here now I can see the fact a key is getting used and adjust the focus back onto slider.  Since the slider has zero binding to video position, its position will be adjusted by UpdateUserInterfaceByTimer()!

private void videoElement_Loaded(object sender, RoutedEventArgs e)
{
	var window = Window.GetWindow(this);

	if (window != null)
		window.PreviewKeyDown += VideoElementPreviewKeyDown;
}

private readonly TimeSpan _timeSpanTick = new TimeSpan(0, 0, 0, 0, (int)((1.0 / 29.97) * 1000)); // time for 1 HD frame
private void VideoElementPreviewKeyDown(object sender, KeyEventArgs e)
{
	if (e.Key != Key.Right && e.Key != Key.Left)
		return;

	if (!timelineSlider.IsFocused)
		timelineSlider.Focus();

	PauseVideoElement();

	if (e.Key == Key.Left)
		videoElement.Position -= _timeSpanTick;
	else
		videoElement.Position += _timeSpanTick;

	UpdateUserInterfaceByTimer();
}

private void UpdateUserInterfaceByTimer()
{
	videoElementTime.Text = videoElement.Position.ToString();
	if (!timelineSlider.IsFocused)
		timelineSlider.Value = videoElement.Position.TotalMilliseconds;
}

Be able to see the current position’s time code

This may be possible but from what I read, I couldn’t see anyone doing a direct data bind here and everyone used a timer.  I used the UpdateUserInterfaceByTimer() function to then update stuff.  If the video wasn’t playing, I made sure to turn off the timer.  The 100 millisecond update timer was just something I figured seemed like a decent time code.  I doubt anyone will notice it isn’t updating at 60 frames a second.

private readonly DispatcherTimer _positionTimer;

public VideoElement()
{
	InitializeComponent();
	_positionTimer = new DispatcherTimer(new TimeSpan(0, 0, 0, 0, 100), DispatcherPriority.Normal, DispatcherTimerTick, Dispatcher);
}

private void DispatcherTimerTick(object sender, EventArgs e)
{
	UpdateUserInterfaceByTimer();
	CommandManager.InvalidateRequerySuggested();
}

void OnMouseDownPlayMedia(object sender, MouseButtonEventArgs args)
{
	_positionTimer.Start();
	videoElement.Play();
	InitializePropertyValues();
}

void OnMouseDownPauseMedia(object sender, MouseButtonEventArgs args)
{
	PauseVideoElement();
}

// Stop the media.
void OnMouseDownStopMedia(object sender, MouseButtonEventArgs args)
{
	PauseVideoElement();
	videoElement.Stop();
}

void PauseVideoElement()
{
	_positionTimer.Stop();
	videoElement.Pause();
}

And with that, you have a highly functional WPF MediaElement control.  Still can’t do everything that the Silverlight version can, but that is a different discussion…

No comments posted yet.

Post a Comment

Please add 8 and 7 and type the answer here: