Fading two Video Windows in WPF

Filed under WPF

imageI was discussing some future features of a commercial jukebox application with the author a few days ago and he lamented about what weak video support the library he had used had for video. In particular, he really wanted to be able to cleanly fade one playing video into another, very much along the lines of cross-fading one audio track into another.

I’d done a little bit of that kind of cross fade for a jukebox plugin I’d written a while back called the NowPlayingScreenSaver, so I thought I might have a go at it.

The result is a simple demo app that illustrates some of the basic WPF animation and media playback techniques quite nicely.

Setting Things Up

Fire up VS2010 and create a new WPF project.

For the mainwindow.xaml, define two mediaelement objects:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <MediaElement Height="210" HorizontalAlignment="Left" Margin="12,12,0,0" Name="media1" VerticalAlignment="Top" Width="316" LoadedBehavior="Manual" />
        <MediaElement Height="210" HorizontalAlignment="Left" Margin="12,12,0,0" Name="media2" VerticalAlignment="Top" Width="316" Opacity="0" LoadedBehavior="Manual" />
    </Grid>
</Window>

Pretty simple stuff. Note that media2 is completely transparent initially (Opacity=0).

For the codebehind in MainWindow.xaml.vb, first, add a few IMPORTS

Imports System.Windows.Media.Animation
Imports System.Windows.Threading

In the MainWindow class, you need to set up a few regional variables to hold the animation storyboards you’ll use, plus a few other bits

Class MainWindow
    Private _StoryBrd1 As Storyboard
    Private _StoryBrd2 As Storyboard

    Private v1up As Boolean = False

    Private WithEvents _tmr As DispatcherTimer

v1up is simply a flag that indicates which mediaelement is currently the “main", upfront” element.

The _tmr is a timer I’ll explain in a bit.

The Loaded Event

When this xaml windows loads, I want to start the media1 element playing, as well as configure a few things.

Private Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    '---- register media controls so we can animate them
    Me.RegisterName(media1.Name, media1)
    Me.RegisterName(media2.Name, media2)

    '---- position second mediaElement beneath the first
    media2.Source = New Uri("Video2.avi", UriKind.Relative)

    '---- kickstart
    '     video playing
    media1.Source = New Uri("Video1.avi", UriKind.Relative)
    v1up = True
    media1.Play()
End Sub

First, you have to use RegisterName on the MediaElements because you’ll be including them in an animation storyboard.

Next, set the Source properties for each MediaElement to point to a couple of video files. It doesn’t matter what you use, but I picked a couple that were around 20 seconds long for convenience sake. Whichever videos you pick, be sure to include them in the project and set them to COPY IF NEWER so that they will end up in your output folder, where the app can find them when it loads.

image

Set v1up = true to indicate media1 is the “current” video playing and start it up.

How Long’s That Video?

The whole point of this exercise is to fade one video into another near the end of the first video.

That means several things:

  1. You have to know how long the first video is.
  2. You have to know how long we want the fade to last.
  3. With that, you can determine at what point to start fading the current video out, but at the same time start playing and fading the other video in.

The duration you can get with the MediaElement.NaturalDuration.TimeSpan property. However, you can’t actually read that property till the video is loaded.

As a result, you can’t setup the animation until the MediaOpened event has fired:

Private Sub media_MediaOpened(sender As Object, e As System.Windows.RoutedEventArgs) Handles media1.MediaOpened, media2.MediaOpened
    '---- once the media has opened, setup storyboard
    SetupStoryboard(DirectCast(sender, MediaElement))
End Sub

Notice that I’m handling the MediaOpened event for BOTH of the MediaElements here.

Also, for the MediaOpened event to fire, the media must be closed when it’s complete, so we do that in the MediaEnded event:

Private Sub media_MediaEnded(sender As Object, e As System.Windows.RoutedEventArgs) Handles media1.MediaEnded, media2.MediaEnded
    With DirectCast(sender, MediaElement)
        .Close()
    End With
End Sub

Starting that Second Video

At some point, near the end of playing the first video, you need to begin playing the second video. There are a number of ways you could do this. For this demo, I chose a simple DispatcherTimer:

''' <summary>
''' When this timer fires, we're at the fade out of the visible video
''' so load the "other" media element and start it
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub _tmr_Tick(sender As Object, e As System.EventArgs) Handles _tmr.Tick
    _tmr.Stop()

    With If(v1up, media2, media1)
        v1up = Not v1up
        .Play()
    End With
End Sub

Notice that it just determines which mediaelement is the “current playing” element, and switches to the other mediaelement to begin playing it.

Don’t worry about setting up this timer now. You’ll do that in a bit.

The Fade Effect Animation

You’ll notice that the MediaOpened event contains a call to SetupStoryboard, so you need to define that function now.

A StoryBoard in WPF simply describes a sequence of animation steps that will be followed, given a timeline. The full possibilities and features of storyboards and WPF animation are far beyond what I can go into effectively here, but I’ll concentrate on what’s necessary for this effect.

First, the full procedure:

Private Sub SetupStoryboard(Media As MediaElement)
    '---- setup the storyboard object
    Dim StoryBrd = New Storyboard()

    Dim OpacityAnim = New DoubleAnimationUsingKeyFrames
    OpacityAnim.BeginTime = New TimeSpan(0)
    OpacityAnim.Duration = Media.NaturalDuration.TimeSpan
    Dim kf = New SplineDoubleKeyFrame               'keyframe to fade in
    kf.KeyTime = KeyTime.FromPercent(0)
    kf.Value = 0
    OpacityAnim.KeyFrames.Add(kf)
    kf = New SplineDoubleKeyFrame                   'Keyframe at max opacity
    kf.KeyTime = KeyTime.FromPercent(0.2)
    kf.Value = 1
    OpacityAnim.KeyFrames.Add(kf)
    kf = New SplineDoubleKeyFrame
    kf.KeyTime = KeyTime.FromPercent(0.8)           'keyframe to begin fade out
    kf.Value = 1
    OpacityAnim.KeyFrames.Add(kf)
    kf = New SplineDoubleKeyFrame
    kf.KeyTime = KeyTime.FromPercent(1)             'keyframe to be completely faded out
    kf.Value = 0
    OpacityAnim.KeyFrames.Add(kf)

    Storyboard.SetTargetName(OpacityAnim, Media.Name)
    Storyboard.SetTargetProperty(OpacityAnim, New PropertyPath(System.Windows.Controls.Image.OpacityProperty))

    '---- connect it to the storyboard as well
    StoryBrd.Children.Add(OpacityAnim)
    StoryBrd.Begin(Me)

    If v1up Then
        _StoryBrd1 = StoryBrd
    Else
        _StoryBrd2 = StoryBrd
    End If

    '---- set the trigger timer to 80% of duration from now
    _tmr = New DispatcherTimer()
    _tmr.Interval = New TimeSpan(0, 0, 0, 0, Media.NaturalDuration.TimeSpan.TotalMilliseconds * 0.8)
    _tmr.Start()
End Sub

First, create a new StoryBoard object.

Then, create a DoubleAnimationUsingKeyFrames. This is because:

You’ll be animating Opacity, which is a continuous value (as opposed to something like Visibility, which has discreet values). You want the Opacity value to vary smoothly from one point to some other point in the animation sequence. These points are called KeyFrames.

Since the animation will begin immediately, set the BeginTime to 0.

The overall duration of the animation will be the length of the video itself, so use the NaturalDuration property to retrieve that now.

The next, and probably most confusing, part is to define the KeyFrames. To visualize this, imagine that each KeyFrame object represents one distinct point in time. The animation, then, is responsible for changing whatever property you’ll be animating (in this case Opacity), smoothly from one value to another value, between one KeyFrame and the next.

So, define the first KeyFrame as:

Dim kf = New SplineDoubleKeyFrame               'keyframe to fade in
kf.KeyTime = KeyTime.FromPercent(0)
kf.Value = 0
OpacityAnim.KeyFrames.Add(kf)

and the second as:

kf = New SplineDoubleKeyFrame                   'Keyframe at max opacity
kf.KeyTime = KeyTime.FromPercent(0.2)
kf.Value = 1
OpacityAnim.KeyFrames.Add(kf)

And you can see how this means you’ll be changing the Opacity property from a Value of 0 (transparent), to a Value of 1 (opaque), during a timespan starting at the beginning of the video and lasting for 20% (the .2) of the video’s duration.

The other KeyFrames are defined in the same way.

Now, the animation is defined, but you haven’t told WPF what we’re animating, so do that now:

Storyboard.SetTargetName(OpacityAnim, Media.Name)
Storyboard.SetTargetProperty(OpacityAnim, New PropertyPath(System.Windows.Controls.Image.OpacityProperty))

These lines tell WPF that you’ll be applying this animation to a specific MediaElement, and that the animation itself will be effecting the Opacity Property.

StoryBoards can actually contain any number of animations. In our case, there’s only one though. So you need to add the animation to the StoryBoard and then start it running:

StoryBrd.Children.Add(OpacityAnim)
StoryBrd.Begin(Me)

Next, store the StoryBoard you just created in one or the other regional variables (this is because you need to keep the StoryBoard around long enough to allow it to run it’s course).

And finally, since you have the Video duration available at this point, you need to set up the timer to kick off the other video at the 80% mark in this video.

_tmr = New DispatcherTimer()
_tmr.Interval = New TimeSpan(0, 0, 0, 0, Media.NaturalDuration.TimeSpan.TotalMilliseconds * 0.8)
_tmr.Start()

Wrapup

With all that in place, you should be able to run and see one video smoothly fade in, play, then start to fade out as the second video begins to fade in.

Note that as with anything heavily graphical, you may need a fairly powerful PC and graphics card for the fades to be smooth. However, WPF is based on DirectX which is surprisingly capable even on moderate hardware.

What Next?

First, you may want to also animate the Volume property in a similar manner, to fade it in and out along with the video.

Since this is WPF, you can easily apply transforms to squish and fold the video, as well as all sorts of other forms of transitions (fade is just one example).

What can you come up with?

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*