MULTIMEDIA

MediaElement in Silverlight

7/25/2010 5:08:11 PM
sec0703.html
Regardless of whether you want to use video or audio data, you need to know about only one element: . It takes care of playing the media content, as long as the format is supported.

1. Embedding Multimedia

supports a number of attributes. Some of them are general, and some of them are media-specific. For example, the NaturalVideoWidth and NaturalVideoHeight properties/attributes determine the size of the video. Obviously, this does not make sense for audio content because these two properties have the value 0.

The most important attribute is Source. It provides the URL of the audio or video data to play. Since audio data is just a special case of video data (no visual output), we will focus on video in this chapter. Example 1 shows the simplest Silverlight video player imaginable. Figure 1 shows the output in the browser.

Example 1. A simple video player (Page.xaml, project Player)

<UserControl x:Class="Player.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="640"
Height="480"
>
<Grid x:Name="LayoutRoot" Background="White">
<Canvas Canvas.Left="25" Canvas.Top="25">
<MediaElement Source="video.wmv"/>
</Canvas>
</Grid>
</UserControl>

Figure 1. The (spartan) video player


What's missing in Figure 1? The UI for using the player. This is not shipped as part of Silverlight; however, Silverlight provides a powerful C# API for controlling multimedia content.

NOTE

We will now successively add features to the player. The filenames remain the same, but the number of features grows. In the downloadable code for this book, you will see the final version (see http://www.oreilly.com/catalog/9780596519988).

The Silverlight SDK contains a complete list of attributes. A number of them will be introduced throughout the remainder of this chapter, but some of them are of general interest and will be discussed here:


AutoPlay

By default, referenced media files are played as soon as the application has been fully loaded. You can disable this behavior by setting the attribute/property to False.

Balance

This provides the audio volume ratio between the left and right speakers. -1 means that all audio is played in the left speaker only, and 1 means that all audio is played in the right speaker only. The default value of 0 means that the audio is evenly shared between the left and right speakers. You can choose any float values in between.

IsMuted

This is a Boolean value that depends on whether the audio output by the is muted (True) or not (False).

NaturalDuration

This is the duration of the media file; it can only be read, not set.

NaturalVideoHeight

This is the height of the video. It is 0 for audio files, and it can only be read, not set.

NaturalVideoWidth

This is the width of the video. It is 0 for audio files, and it can only be read, not set.

Stretch

This stretches the media if the display area is greater than the video size. The following options are available:


None

The video remains at its original size.


Fill

The video fills up the available area, losing its aspect ratio.


Uniform

The video size is increased, maintaining the aspect ratio, until the video has the width or the height of the display area.


UniformToFill

The video size is increased, maintaining the aspect ratio, until the video width and height are both greater than or equal to the width and height of the display area. If necessary, parts of the video are cropped.

Volume

The audio volume has a value between 0 (muted) and 1 (the maximum volume made available by the operating system). The default value is 0.5.

When (programmatically) assigning a value to Volume, make sure you always provide a string value. JavaScript can usually automatically convert numbers into strings, but the Silverlight API will complain.


2. Controlling Multimedia

plays a media file. If it is audio, the sound is played. If it is video, the video is shown (and if there is sound within the video, the sound is played too). Apart from that, does not have any kind of output or UI. Instead, you need to create your own UI. Figure 2 shows a simple UI for our Silverlight media player. It was initially created in Expression Design, exported as XAML, imported into Expression Blend 2, and then tweaked and tuned. To keep the code size maintainable and printable, very basic structures were used to create the UI.

Figure 2. The Silverlight player UI (loaded in Expression Blend 2)


The UI consists of several buttons and other elements:

  • A play and pause button (depending on the state of the media content)

  • A button to jump to the previous marker

  • A button to jump to the next marker

  • Buttons to increase and decrease the volume

  • Text containing the current volume level

  • A timeline that serves as a progress bar for the media playback, including a pointer

  • A text field which will later show the name of any marker (not visible at the moment)

The XAML file is structured so that every button (including <Path> elements on the button) is grouped in a <Canvas> element. We use attributes to assign event handlers. Example 2 shows the complete XAML markup for the UI. Don't worry about whether the meaning of all the event handlers is obvious at the moment. Here is a list of all of the assigned handler functions:

PlayOrPause()

Called when the play/pause button is pressed; needs to play or pause the media content

GotoPreviousMarker()

Called when the previous marker button is pressed; needs to seek to the position of the previous marker, if available

GotoNextMarker()

Called when the next marker button is pressed; needs to seek to the position of the next marker, if available

VolumeUp()

Called when the upper volume button is pressed; needs to increase the volume by 0.1, if possible

VolumeDown()

Called when the lower volume button is pressed; needs to decrease the volume by 0.1, if possible

ShowMarker()

Called when a marker is reached; needs to display the name of that marker

InitVideo()

Called when the video header has been loaded; needs to initialize the application and also start updating the progress bar pointer's position

We will discuss these event handlers in more detail when we write the associated code. You will also learn the significance of the two storyboards at the end of the XAML file.

Example 2. The Silverlight media player, the XAML file (Page.xaml, project Player)

<UserControl x:Class="Player.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="1000"
Height="600">
<Grid x:Name="LayoutRoot" Background="White">
<Canvas Canvas.Left="25" Canvas.Top="25">
<Canvas x:Name="PlayerControls">
<Canvas x:Name="PlayPause" MouseLeftButtonUp="playOrPause">
<Rectangle Width="122.014" Height="91.0105"
Canvas.Left="-0.50001" Canvas.Top="-0.5"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Path x:Name="Path" Width="53.5274" Height="58.6534"
Canvas.Left="13.9888" Canvas.Top="17.1706"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"
Data="F1 M 67.0161,47.9973L 14.4888,17.6706L 14.4888,75.324L
67.0161,47.9973 Z"/>
<Rectangle Width="12.9994" Height="46.9976"
Canvas.Left="73.5164" Canvas.Top="24.5136"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"/>
<Rectangle Width="12.9994" Height="46.9976"
Canvas.Left="91.5155" Canvas.Top="24.5138"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"/>
</Canvas>
<Canvas x:Name="Marker">
<Canvas x:Name="PrevMarker" MouseLeftButtonDown="gotoPreviousMarker">
<Rectangle Width="122.014" Height="91.0105"
Canvas.Left="149.003" Canvas.Top="-0.5"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Rectangle Width="12.9994" Height="46.9976"
Canvas.Left="170.511" Canvas.Top="24.5139"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"/>
<Path Width="53.5274" Height="58.6534"
Canvas.Left="191.245" Canvas.Top="17.6856"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"
Data="F1 M 191.745,45.5123L 244.273,75.839L 244.273,18.1856L
191.745,45.5123 Z "/>
</Canvas>
<Canvas x:Name="NextMarker" MouseLeftButtonDown="gotoNextMarker">
<Rectangle Width="122.014" Height="91.0105"
Canvas.Left="301.486" Canvas.Top="-0.5"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Path Width="53.5273" Height="58.6534"
Canvas.Left="327.24" Canvas.Top="17.6856"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"
Data="F1 M 380.267,48.5123L 327.74,18.1856L 327.74,75.8391L
380.267,48.5123 Z "/>
<Rectangle Width="12.9994" Height="46.9976"
Canvas.Left="387.502" Canvas.Top="24.5138"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"/>
</Canvas>
<TextBlock x:Name="MarkerName" FontFamily="Segoe UI" FontSize="24"
Foreground="#FF000000" Canvas.Left="50" Canvas.Top="150" />
</Canvas>
<Canvas x:Name="Volume">
<Canvas x:Name="Up" MouseLeftButtonDown="volumeUp">
<Rectangle Width="122.014" Height="43.0129"
Canvas.Left="452.987" Canvas.Top="-0.5"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Path Width="88.9956" Height="24.0292"
Canvas.Left="468.496" Canvas.Top="8.87177"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"
Data="F1 M 512.994,9.37177L 468.996,32.401L 556.992,32.401L
512.994,9.37177 Z "/>
</Canvas>
<Canvas x:Name="Down" MouseLeftButtonDown="volumeDown">
<Rectangle Width="122.014" Height="43.0128"
Canvas.Left="452.987" Canvas.Top="46.5052"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Path Width="88.9955" Height="24.0292"
Canvas.Left="467.497" Canvas.Top="55.9971"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFFFFFFF"
Data="F1 M 511.995,79.5264L 555.992,56.4971L 467.997,56.4971L
511.995,79.5264 Z "/>
</Canvas>
<TextBlock x:Name="VolumeText" FontFamily="Segoe UI" FontSize="48"
Text="0.5" Foreground="#FF000000"
Canvas.Left="590" Canvas.Top="10" />
</Canvas>
<Canvas x:Name="Position">
<Rectangle x:Name="Timeline" Width="669.987" Height="12.9994"
Canvas.Left="-0.5" Canvas.Top="119.509"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FF000000"/>
<Rectangle x:Name="Pointer" Width="32.0184" Height="36.9982"
Canvas.Left="-0.5" Canvas.Top="105.51"
Stretch="Fill" StrokeLineJoin="Round"
Stroke="#FF000000" Fill="#FFA01F1F"/>
</Canvas>
</Canvas>
<Canvas x:Name="Player" Canvas.Top="200">
<MediaElement x:Name="Video" Source="video.wmv" AutoPlay="False"
MarkerReached="showMarker" MediaOpened="initVideo" />
</Canvas>
</Canvas>
<Canvas>
<Canvas.Resources>
<Storyboard x:Name="intervalStoryboard"
Duration="0:0:1" Completed="intervalStoryboardCompleted" />
<Storyboard x:Name="timeoutStoryboard"
Duration="0:0:2" Completed="timeoutStoryboardCompleted" />
</Canvas.Resources>
</Canvas>
</Grid>
</UserControl>


2.1. Play and pause

The application needs permanent access to the embedded video file. Since Silverlight 2, you can directly access any XAML element that has its x:Name attribute set. However, in this example, we will use the FindName() approach. This is a good opportunity to see this alternative technique in action; it will also facilitate re-creating the same effect with JavaScript code.

To have permanent access to the embedded media we save a reference in a global property whenever the first event handler fires. This will be the handler for the MediaOpened event (if a user clicks a button before the video has been loaded, the user's request cannot be fulfilled at that time). In the handler function, the global video property will be filled:

var video = null;

private void initVideo(object sender, RoutedEventArgs e)
{
if (video == null)
{
video = (sender as MediaElement);
}
// ...
}

Silverlight supports several methods for controlling whether given media content is played:

Pause()

Pauses the media content

Play()

Plays the media content if it is stopped, or resumes it if it is paused

Stop()

Stops the media content

NOTE

Conveniently, you will not get any C# errors if you try an invalid operation, such as playing media content that is already playing, or pausing stopped media content. So, you do not need any error handling in that respect.

You can determine the current state of the movie by accessing its CurrentState property. The following values are possible:

MediaElementState.Buffering

The video is loading, but not enough data has been streamed yet, so the control is buffering the data.

MediaElementState.Closed

The video has been closed.

MediaElementState.Error

Anerror has occurred while loading (or playing) the video.

MediaElementState.Opening

The video is currently being opened.

MediaElementState.Paused

The video is paused.

MediaElementState.Playing

The video is being played.

MediaElementState.Stopped

The video has been stopped.

If the video is paused or stopped, the code tries to play it; otherwise, JavaScript tries to pause it:

private void playOrPause(object sender, MouseButtonEventArgs e)
{
if (video.CurrentState == MediaElementState.Paused ||
video.CurrentState == MediaElementState.Stopped)
{
video.Play();
} else {
video.Pause();
}
}

And indeed, if you include this code in your XAML code-behind C# file (and also provide empty shells for the other event handler methods), a click on the movie will play it (note that it does not start automatically, thanks to AutoPlay="False"), a second click will pause it, and a third click will resume it.

2.2. Setting the volume

You can set the volume of a media file using the aforementioned Volume property (remember that C# properties are identical to XAML attributes). So, when a user clicks on the upper volume button, the volume increases by 0.1; clicking on the lower button decreases the volume by 0.1. In theory, that's easy, but you do have to take some extra precautions. First, you need to make sure the new volume is within the valid interval (from 0 through 1). Silverlight is not very forgiving when you supply invalid values here. Therefore, make sure the value is correct. For instance, when increasing the volume, determine the old volume, add 0.1, and then check whether this is greater than 1.0, like this:

double newVolume = Math.Min(1.0, video.Volume + 0.1); 

Now we can set the new volume:

video.volume = newVolume;

Finally, we display the new volume in the text field. To get rid of any extraneous decimal digits, we cut off everything after the first decimal digit:

TextBlock volumeText = video.FindName("VolumeText") as TextBlock;
volumeText.Text = String.Format("{0:0.0}", newVolume);

Here is the complete code for both the volumeUp() and the volumeDown() methods:

private void volumeUp(object sender, MouseButtonEventArgs e)
{
double newVolume = Math.Min(1.0, video.Volume + 0.1);
video.Volume = newVolume;
TextBlock volumeText = video.FindName("VolumeText") as TextBlock;
volumeText.Text = String.Format("{0:0.0}", newVolume);
}

private void volumeDown(object sender, MouseButtonEventArgs e)
{
double newVolume = Math.Max(0.0, video.Volume - 0.1);
video.Volume = newVolume;
TextBlock volumeText = video.FindName("VolumeText") as TextBlock;
volumeText.Text = String.Format("{0:0.0}", newVolume);
}

When you run the code in the browser, you will actually hear that your code works when clicking on the volume buttons, and you will also see the new volume values, as Figure 2 shows.

Figure 2. The Silverlight media player can now control the volume


2.3. Determining the media position

Although, playing and pausing a video is relatively trivial, determining its current position requires an extra step. Fortunately, a property is available that exposes this information: position. However, this property is of type TimeSpan, so you need to be careful when calculating with these values. A subproperty of any TimeSpan value is TotalSeconds, which converts the TimeSpan value into a float value, or the number of seconds (not necessarily integral). You can read out this value and set it.

But before that, we will tackle the progress bar. We need to look up the current position of the media with video.Position.TotalSeconds. Then we need to determine the length of the media with Video.NaturalDuration.TimeSpan.TotalSeconds.

The NaturalDuration.Timespan property also returns a TimeSpan value, so accessing the TotalSeconds subproperty yields the desired result.

By dividing the current position by the length of the media, we have a percentage of how much has already been played—for example, 30%. Consequently, we need to put the progress bar pointer at 30% of the progress bar length! Or, to be exact, the center of the progress bar pointer needs to be at 30%. If the pointer has a width of 30 pixels, we need to place it at 30% of the progress bar and then move it 15 pixels (half the length) to the left. Generally speaking, this code calculates the correct position of the pointer, relative to the progress bar:

double currentPosition = video.Position.TotalSeconds;
double length = video.NaturalDuration.TimeSpan.TotalSeconds;
Rectangle progressBar = video.FindName("Timeline") as Rectangle;
Rectangle progressPointer = video.FindName("Pointer") as Rectangle;
double relativePosition = (currentPosition / length) * progressBar.Width -
(progressPointer.Width / 2);

The relative position needs to be converted into an absolute position within the surrounding <Canvas> element. The easiest way to achieve this is to determine the position of the progress bar and then just add the relativePosition value:

Canvas.SetLeft(progressPointer, Canvas.GetLeft(progressBar) + relativePosition);				  

We need to make sure the position is updated again (and again and again). As of this writing, Silverlight does not have a working timer component. However, there is a neat workaround. Remember the Storyboard elements in the XAML files? We will use them to execute code at a given interval. The first storyboard has a duration of one second. When the storyboard is completed, the event handler is executed. In this event handler, the position of the progress pointer is updated (with the preceding code), and then the storyboard is restarted. That way, the pointer position is updated again after one second, and again and again. Here is the complete code for this method:

private void intervalStoryboardCompleted(object sender, EventArgs e)
{
double currentPosition = video.Position.TotalSeconds;
double length = video.NaturalDuration.TimeSpan.TotalSeconds;
Rectangle progressBar = video.FindName("Timeline") as Rectangle;
Rectangle progressPointer = video.FindName("Pointer") as Rectangle;
double relativePosition = (currentPosition / length) * progressBar.Width -
(progressPointer.Width / 2);
Canvas.SetLeft(progressPointer, Canvas.GetLeft(progressBar) + relativePosition);
(video.FindName("intervalStoryboard") as Storyboard).Begin();
}

In the initVideo() method, intervalStoryboardCompleted() is then called for the first time by starting intervalStoryboard.

NOTE

You may want to shorten the interval in which the pointer position is updated so that the pointer moves smoothly over the progress bar.

Figure 3 shows the result. The progress pointer is moving as the movie is playing along.

Figure 3. The movie's progress is displayed


2.4. Working with markers

A marker defines a certain spot within a media file. There are at least two UI approaches for markers. One is to notify the user when a marker has been reached, and the other is to offer to let the user jump between markers. Let's start with the first one. The MediaElement element supports the MarkerReached event. When, a marker is reached while a media file is playing, this event is fired. As always, the event handling function passes two arguments: the sending object (the MediaElement) and event information. The latter argument is of great interest here: its marker property provides three fields from the marker:

Text

The name of the marker

Time

The time of the marker (again as a TimeSpan value)

Type

The type of the marker (depending on which marker types are supported by the format used)

The XAML file already contains a text field (named MarkerName) for the name of the marker. Whenever a marker is reached, its name is written into that field:

private void showMarker(object sender, TimelineMarkerRoutedEventArgs e)
{
TextBlock markerName = video.FindName("MarkerName") as TextBlock;
...
}

However, it would be a bad idea in terms of usability to let this information stay where it is. A few seconds later, the name of the marker may no longer be suitable. Therefore, we remove this text after two seconds by using a storyboard timeout (timeoutStoryboard):

(video.FindName("timeoutStoryboard") as Storyboard).Begin();


...

private void timeoutStoryboardCompleted(object sender, EventArgs e)
{
TextBlock markerName = video.FindName("MarkerName") as TextBlock;
markerName.Text = ""
}

What happens if there are two markers within a span of two seconds? Imagine that there is one marker at 00:00:02, and one at 00:00:03. The appearance of the first marker sets a timeout that will be fired at 00:00:04; the second marker sets a timeout that will be fired at 00:00:05 (after two seconds each). However, the 00:00:04 timeout will empty the text field, which has just been populated by the second marker at 00:00:03! Therefore, we need to make sure there are no pending timeouts. To do so, we just stop timeoutStoryboard at the beginning of the showMarker() method:

(video.FindName("timeoutStoryboard") as Storyboard).Stop(); 

Here is the complete code for the showMarker() method:

private void showMarker(object sender, TimelineMarkerRoutedEventArgs e)
{
(video.FindName("timeoutStoryboard") as Storyboard).Stop();
TextBlock markerName = video.FindName("MarkerName") as TextBlock;
markerName.Text = e.Marker.Text;
(video.FindName("timeoutStoryboard") as Storyboard).Begin();
}

As Figure 4 shows, the marker names are displayed once a marked position in the movie has been reached.

Figure 4. A marker has been reached


The final item on our to-do list is also related to markers: remember that we still have functionless buttons that allow users to jump to the next or previous marker. To implement this functionality, we first have to determine all the markers. We could also look up all the markers once one of the buttons is pressed, but then we would be repeating our efforts. Therefore, the markers are retrieved once the movie has started load. Once the MediaOpened event has been fired, we have access to all markers. The embedded media's markers property provides us with a collection of all markers. Every collection item is a marker with the aforementioned properties of Text, Time, and Type. We are interested only in the marker time, and we save this information in a global C# list. At the end, we numerically sort the array, just to make sure the marker times are in the correct position:

private List markerTimes = new List();

private void loadMarkers()
{
for (int i = 0; i < video.Markers.Count; i++)
{
markerTimes.Add(video.Markers[i].Time);
}
markerTimes.Sort(delegate(TimeSpan a, TimeSpan b)
{
return (a.TotalSeconds == b.TotalSeconds) ? 0 :
(a.TotalSeconds > b.TotalSeconds) ? 1 : -1;
});
}

The loadMarkers() method is called in the MediaOpened event handler.

First we tackle the gotoNextMarker() method. We determine the current position of the clip and then go through all the markers. If we find a marker that is later than the current media position (assuming that all the markers are sorted by their time), we use this marker and immediately leave the loop. Here is the code:

private void gotoNextMarker(object sender, MouseButtonEventArgs e)
{
double currentTime = video.Position.TotalSeconds;
for (int i = 0; i < markerTimes.Count; i++)
{
if (markerTimes[i].TotalSeconds > currentTime)
{
video.Position = markerTimes[i];
break;
}
}
}

The gotoPreviousMarker() method is implemented in an analogous fashion. Make sure you start searching from the end. We go through the markers starting with the last one, until we find one that is earlier than the current media position:

private void gotoPreviousMarker(object sender, MouseButtonEventArgs e)
{
double currentTime = video.Position.TotalSeconds;
for (int i = markerTimes.Count - 1; i >= 0; i--)
{
if (markerTimes[i].TotalSeconds < currentTime)
{
video.Position = markerTimes[i];
break;
}
}
}

If you run this example in the browser, you will notice that the player jumps to the appropriate position in the clip (see Figure 5), but only if the media format supports jumping to markers. If not, you conveniently do not get an error message.

Figure 5. Clicking on the button jumps to the next marker


Previous Silverlight versions had a (possible) bug that prevented you from setting video.Position.Seconds (setting the value had no effect). This is fixed now, but in case you are running into this in legacy applications, our special example shows how we can avoid this issue: we save the TimeSpan marker values in an array and can then assign the TimeSpan values back to video.Position when jumping to a marker.


Every feature implemented in this chapter should not have been extraordinarily difficult to understand, and if you combine them, you will get a very functional application. Example 3 contains the complete JavaScript code for the Silverlight media player.

Example 3. The Silverlight media player, the XAML C# file (Page.xaml.cs, project Player)

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;

namespace Player
{
public partial class Page : UserControl
{
private MediaElement video = null;
private List markerTimes = new List();

public Page()
{
// Required to initialize variables
InitializeComponent();
}

private void showMarker(object sender, TimelineMarkerRoutedEventArgs e)
{
(video.FindName("timeoutStoryboard") as Storyboard).Stop();
TextBlock markerName = video.FindName("MarkerName") as TextBlock;
markerName.Text = e.Marker.Text;
(video.FindName("timeoutStoryboard") as Storyboard).Begin();
}

private void initVideo(object sender, RoutedEventArgs e)
{
if (video == null)
{
video = (sender as MediaElement);
}
loadMarkers();
(video.FindName("intervalStoryboard") as Storyboard).Begin();
}

private void loadMarkers()
{
for (int i = 0; i < video.Markers.Count; i++)
{
markerTimes.Add(video.Markers[i].Time);
}
markerTimes.Sort(delegate(TimeSpan a, TimeSpan b)
{
return (a.TotalSeconds == b.TotalSeconds) ? 0 :
(a.TotalSeconds > b.TotalSeconds) ? 1 : -1;
});
}

private void playOrPause(object sender, MouseButtonEventArgs e)
{
if (video.CurrentState == MediaElementState.Paused ||
video.CurrentState == MediaElementState.Stopped)
{
video.Play();
} else {
video.Pause();
}
}

private void gotoPreviousMarker(object sender, MouseButtonEventArgs e)
{
double currentTime = video.Position.TotalSeconds;
for (int i = markerTimes.Count - 1; i >= 0; i--)
{
if (markerTimes[i].TotalSeconds < currentTime)
{
video.Position = markerTimes[i];
break;
}
}
}

private void gotoNextMarker(object sender, MouseButtonEventArgs e)
{
double currentTime = video.Position.TotalSeconds;
for (int i = 0; i < markerTimes.Count; i++)
{
if (markerTimes[i].TotalSeconds > currentTime)
{
video.Position = markerTimes[i];
break;
}
}
}

private void volumeUp(object sender, MouseButtonEventArgs e)
{
double newVolume = Math.Min(1.0, video.Volume + 0.1);
video.Volume = newVolume;
TextBlock volumeText = video.FindName("VolumeText") as TextBlock;
volumeText.Text = String.Format("{0:0.0}", newVolume);
}

private void volumeDown(object sender, MouseButtonEventArgs e)
{
double newVolume = Math.Max(0.0, video.Volume - 0.1);
video.Volume = newVolume;
TextBlock volumeText = video.FindName("VolumeText") as TextBlock;
volumeText.Text = String.Format("{0:0.0}", newVolume);
}

private void intervalStoryboardCompleted(object sender, EventArgs e)
{
double currentPosition = video.Position.TotalSeconds;
double length = video.NaturalDuration.TimeSpan.TotalSeconds;
Rectangle progressBar = video.FindName("Timeline") as Rectangle;
Rectangle progressPointer = video.FindName("Pointer") as Rectangle;
double relativePosition = (currentPosition / length) * progressBar.Width
-(progressPointer.Width / 2);
Canvas.SetLeft(progressPointer, Canvas.GetLeft(progressBar) +
relativePosition);
(video.FindName("intervalStoryboard") as Storyboard).Begin();
}

private void timeoutStoryboardCompleted(object sender, EventArgs e)
{
TextBlock markerName = video.FindName("MarkerName") as TextBlock;
markerName.Text = "";
}

}
}


Storing Markers

Usually markers are stored within a media file. However, you can also temporarily create markers.  You can also use C# by dynamically adding elements to the element (you cannot do this via regular markup). The trick is to dynamically create a TimelineMarker object, and then add this to the Markers collection of the media content. This is how the code could look:

TimelineMarker t = new TimelineMarker();
t.Text = "Olympic Stadium";
t.Time = new TimeSpan(12, 34, 56);
video.Markers.Add(t);


Using audio and video from Silverlight is quite convenient. Just convert your content into a supported file format and use markup. With a little bit of C# you can turn your UI into a full-fledged media player; but obviously, if it came out of the box that way it would be even more convenient.

Other  
 
Most View
Windows 7 : Troubleshooting Network Printers (part 1) - Using the Printer Troubleshooter, Monitoring Printer Events
Exchange Server 2010 : Outlook Integration (part 7) - Document Library Integration
Windows Server 2003 : Securing Network Communication - Planning an IPSec Implementation
Sony Xperia Z Review (Part 1)
Nook HD+ - A 9-Inch Tablet E-Reader/Media Player
Fujifilm FinePix F800EXR Super-Zoom Compact With WiFi Connection
Totem Wind Speaker Review
The Cowon EM1 Earphones - Musically Yours
Windows Small Business Server 2011 : Installing the Second Server (part 5) - Customizing the Server
iPhone 5 Greets The World (Part 2)
Top 10
Windows 8 : Using Hyper-V - Configuring virtual machine networking and storage (part 3)
Windows 8 : Using Hyper-V - Configuring virtual machine networking and storage (part 2) - Hyper-V virtual switch
Windows 8 : Using Hyper-V - Configuring virtual machine networking and storage (part 1) - Introducing storage and networking for Hyper-V
The Aston Martin V8 Vantage N430 – Dynamics And Athleticism (Part 2)
The Aston Martin V8 Vantage N430 – Dynamics And Athleticism (Part 1)
The Aston Martin Vantage N430 – Time For Retirement, 007?
The Audi RS6 Avant – Significantly Faster
The Ferrari F50 – Mostly Created To Be Fast
Porsche Cayenne Turbo Versus Mercedes ML63 AMG – Battle Of The Behemoths (Part 2)
Porsche Cayenne Turbo Versus Mercedes ML63 AMG – Battle Of The Behemoths (Part 1)