Live example: http://www.adefwebserver.com/silverlight/SilverlightCaptioning/SilverlightDynamicMediaMarkers/
This is another one of my “this blog post is not really about what this blog post is about”. Yes, I will deliver on what the title promises, but creating closed captaining with Silverlight is actually very easy. The thing that took me so much time to put together, was implementing a “MVVM like” pattern and have a code behind that has no application logic and looks like this:
using System.Windows.Controls;
namespace SilverlightDynamicMediaMarkers
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
}
}
From what I understand, it means that each layer is de-coupled and independently testable. When you change a caption and restart the video, it picks up the changes through bindings to dependency properties that are automatically updated.
But, first…
Creating Closed Captioning with Silverlight 3.0
The Silverlight 3 MediaElement allows you to create a collection of TimelineMarkers like this:
TimelineMarker objTimelineMarker1 = new TimelineMarker();
objTimelineMarker1.Text = Caption1;
objTimelineMarker1.Time = new TimeSpan(0, 0, 5);
objTimelineMarker1.Type = "Start";
TimelineMarker objTimelineMarker2 = new TimelineMarker();
objTimelineMarker2.Text = "";
objTimelineMarker2.Time = new TimeSpan(0, 0, 10);
objTimelineMarker2.Type = "Stop";
TimelineMarkerCollection colTimelineMarkerCollection =
new TimelineMarkerCollection();
colTimelineMarkerCollection.Add(objTimelineMarker1);
colTimelineMarkerCollection.Add(objTimelineMarker2);
And attach them to the media like this:
public void UpdateTimelineMarkers()
{
// Clear existing Markers
this.media.Markers.Clear();
if (MediaTimelineMarkerCollection != null)
{
// Add Markers
foreach (var Marker in MediaTimelineMarkerCollection)
{
TimelineMarker objTimelineMarker = new TimelineMarker();
objTimelineMarker.Text = Marker.Text;
objTimelineMarker.Time = Marker.Time;
objTimelineMarker.Type = Marker.Type;
this.media.Markers.Add(objTimelineMarker);
}
}
}
As the media is playing (this can be a video or even a Mp3 file) the MarkerReached Event will fire, passing an instance of the marker as an argument. It can then be used to display subtitles in a text box that is overlain on top of the video.
private void media_MarkerReached(object sender,
TimelineMarkerRoutedEventArgs e)
{
ClosedCaption.Visibility = (e.Marker.Type == "Start")
? Visibility.Visible :
Visibility.Collapsed;
ClosedCaption.Text = e.Marker.Text.ToString();
}
Note, that you could use the media markers for a lot of other things such as triggering a JavaScript method on the hosting .html page that causes other things on the page to change.
You can also create media markers and embed them in the media using a product such as Microsoft Expression Blend.
The Closed Captioning Program
Implementing closed captaining will take you about 5 minutes. However, my desire was to create a program that would allow you to enter captions on the fly and when you re-start the video, you would see them. Note, you have to stop, rewind a bit, and restart the video to get new captions to show up. You hover over the video to get the play control to show so you can stop the video.
I could have simply put a button on the page that would reload the video, but I wanted to use bindings to tie everything together in a “MVVM like” architecture
I say “MVVM like” because it is hard to get two people to agree on what MVVM is in Silverlight means because apparently Silverlight 3 does not allow “true MVVM” because it is missing things like commanding and triggers. There are frameworks available that get past these limitations.
The Captions
First, I created 3 TextBoxes to hold the Captions.
I set bindings on each of the controls:
<TextBox x:Name="txtCaption1" Grid.Column="2" Grid.Row="1"
Text="{Binding Caption1, Mode=TwoWay, UpdateSourceTrigger=Default}"
TextWrapping="Wrap"/>
The bindings work because I set a DataContext on the Parent object that they are contained in, to a class called ViewModel:
xmlns:local="clr-namespace:SilverlightDynamicMediaMarkers">
<Canvas x:Name="LayoutRoot">
<Canvas.DataContext>
<local:ViewModel />
</Canvas.DataContext>
In the ViewModel class I created a ObservableCollection of TimeLineMarkers property and set the TimelineMarkers property to rebuild each time one of the captions is changed. This happens because the binding is set to TwoWay and the ViewModel class implements the INotifyPropertyChanged interface and the Caption properties trigger NotifyPropertyChanged.
#region Caption1
public string Caption1
{
get
{
return this._Caption1;
}
set
{
if (value != this._Caption1)
{
this._Caption1 = value;
NotifyPropertyChanged("Caption1");
TimelineMarkers = BuildTimelineMarkers();
}
}
}
#endregion
The Video Player
The Blacklight Codeplex project has a great set of controls with full source code. I took the video player from that project and added a TextBlock over the video player to display the captions.
I now have a ViewModel class that is bound to the user interface. When you change a caption and move focus from the TextBox, the TimelineMarkers property will be rebuilt. You can set a break point in the code and see this for yourself.
Now, I need to bind the video player in the MediaPlayer control to the collection of TimelineMarkers in ViewModel. To do this, I created dependency property.
The UpdateTimelineMarkers() method in the MediaPlayer control, (listed above) gets the TimelineMarkers from the collection contained in the MediaTimelineMarkers property of the MediaPlayer control. Here is the code for that property:
public ObservableCollection<TimelineMarker> MediaTimelineMarkerCollection
{
get
{
return
(ObservableCollection<TimelineMarker>)GetValue(MediaTimelineMarkerProperty);
}
set { SetValue(MediaTimelineMarkerProperty, value); }
}
This property reads and sets the MediaTimelineMarkerProperty dependency property. here is the declaration for that property:
public static readonly DependencyProperty MediaTimelineMarkerProperty =
DependencyProperty.Register("MediaTimelineMarkerCollection",
typeof(ObservableCollection<TimelineMarker>), typeof(MediaPlayer),
new PropertyMetadata(new PropertyChangedCallback(MediaTimelineMarker_Changed)));
This declaration contains a pointer to MediaTimelineMarker_Changed that will fire whenever it is changed. Here is the code for that method:
private static void MediaTimelineMarker_Changed(DependencyObject
dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
MediaPlayer mediaPlayer = (MediaPlayer)dependencyObject;
mediaPlayer.MediaTimelineMarkerCollection =
(ObservableCollection<TimelineMarker>)eventArgs.NewValue;
}
This allows us to bind the TimelineMarkers collection in ViewModel, to the MediaTimelineMarkerCollection in the MediaPlayer control, by using simple binding code (in MainPage.xaml) like this:
<controls:MediaPlayer x:Name="myMediaPlayer"
MediaTimelineMarkerCollection="{Binding TimelineMarkers, Mode=TwoWay, UpdateSourceTrigger=Default}"
MediaSource="Media/niceday.wmv"
Canvas.Left="27" Canvas.Top="32" Height="248" Width="365" />
MVVM The good and the Bad
A lot of time was lost in creating this program when I had a typo in my Dependency property declaration. Also, when you make a mistake with a binding in XAML you wont always get a compile-time error, you will get a run-time error.
However, consuming the video player and binding to it’s properties using declarative binding and collections that automatically update does require less code.
Download the code here:
http://www.adefwebserver.com/silverlight/SilverlightCaptioning/SilverlightDynamicMediaMarkers.zip