The Media Control
It took a while, but the EasyPlayer media control is finally working as I'd like (even if it looks a little bad!):

Methods
One of the first stumbling blocks was how to interact with the MediaElement from a Caliburn.Micro ViewModel. Typically, you just databind the properties, however, the MediaElement has a few key methods: SetSource, Play, Pause, Stop. It wasn't immediately obvious how to handle this.
I finally arrived at using some attached properties:
<UserControl x:Class="EasyPlayer.MediaControl.NowPlayingView"
xmlns:local="clr-namespace:EasyPlayer.MediaControl"
...>
<!-- snip -->
<MediaElement x:Name="m_MediaPlayer" Stretch="UniformToFill"
local:Media.MediaStream="{Binding MediaStream}"
local:Media.PlayerState="{Binding MediaPlayerState}"/>
The MediaStream property calls the SetSource method of the MediaElement; the PlayerState is an enum representing the Play/Pause/Stop state and the methods are called accordingly on the value changed event. Adding an attached property seems to take quite a chunk of code, so I'll just post the MediaStream implementation here:
public static class Media
{
public static readonly DependencyProperty MediaStreamProperty =
DependencyProperty.RegisterAttached(
"MediaStream",
typeof(Stream),
typeof(Media),
new PropertyMetadata(OnMediaStreamChanged)
);
public static void SetMediaStream(DependencyObject d, Stream stream)
{
d.SetValue(MediaStreamProperty, stream);
}
public static Stream GetMediaStream(DependencyObject d)
{
return d.GetValue(MediaStreamProperty) as Stream;
}
private static void OnMediaStreamChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == e.OldValue)
return;
var mediaEl = d as MediaElement;
if (mediaEl == null) return;
mediaEl.SetSource(e.NewValue as Stream);
mediaEl.Play();
}
}
The Slider
Major stumbling block #2 was the progress bar. I opted to use a slider control. The MSDN documentation recommends to not data bind to the Position property of the MediaElement control as this will cause performance problems. So that means again accessing the control from within the VIewModel. Additionally, to display the current and end time along the slider, I need to access a few more properties from the Media Element (e.g. NaturalDuration).
One option in Caliburn.Micro to access the view is to override the OnViewLoaded method:
protected override void OnViewLoaded(object view)
{
base.OnViewLoaded(view);
var viewEl = view as FrameworkElement;
mediaElement = viewEl.FindName("m_MediaPlayer") as MediaElement;
}
I store the reference in an instance variable then create a timer to periodically update the slider:
var updateProgressTimer = new DispatcherTimer();
updateProgressTimer.Interval = TimeSpan.FromMilliseconds(300);
updateProgressTimer.Tick += (o, s) => UpdateProgress();
public void UpdateProgress()
{
SliderPosition = mediaElement.Position.TotalSeconds;
}
And the SliderPosition set:
sliderPosition = value;
var duration = mediaElement.NaturalDuration.TimeSpan;
var current = TimeSpan.FromSeconds(sliderPosition);
MediaPositionText = string.Format("{0:00}:{1:00}:{2:00} / {3:00}:{4:00}:{5:00}",
current.Hours, current.Minutes, current.Seconds, duration.Hours, duration.Minutes, duration.Seconds);
NotifyOfPropertyChange(() => SliderPosition);
The slider values are initialised when the media is loaded:
var ts = mediaElement.NaturalDuration.TimeSpan;
MediaPositionMax = ts.TotalSeconds;
MediaPositionLargeChange = Math.Min(10, ts.Seconds / 10);
MediaPositionText = string.Format("00:00:00 / {0:00}:{1:00}:{2:00}",
ts.Hours, ts.Minutes, ts.Seconds);
Dragging the Slider
The real problem comes when trying to drag the media position. As the timer is still firing, the slider keeps jumping back to the 'correct' position when attempting to move it. The simple solution is to suspend updating the slider position while dragging.
This turned out to be harder than first thought. Apparently, as the slider is already capturing the mouse down event, it doesn't bubble up. There is however a way - but again, I need direct access to the control. So in the OnViewLoaded method, I add the following:
//need to do some funky stuff to capture mouse down/up events on the slider
var slider = viewEl.FindName("MediaPosition") as UIElement;
if (slider != null)
{
slider.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler((s, e) => SliderMouseDown()), true);
slider.AddHandler(FrameworkElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler((s, e) => SliderMouseUp()), true);
}
Finally, in the slider mouse down / up methods, set a 'dragging' flag and if set, the UpdateProgress method no longer updates the slider position.
References to the Control
I'm pretty happy with the behaviour now, but the code itself feels like a hack. As soon as you have a reference to the control, it can be a slippery slope and turn the ViewModel into a regular code behind class. This is definitely an area I want to re-visit to see if there's a better / cleaner way. If nothing else, I'd like to separate out the hack from the rest of the ViewModel.
The Code
The code is on GitHub:
