I've recently bought a digital camera, which can also make videos. Thing is, if you turn it 90 degrees when you film the video, the video is flipped 90 degrees when you play it. No surprise there, I guess, but still very annoying.
Strangely, Windows Media Player doesn't seem to have any flipping capabilities, nor does any other (free) media player that I could find. You can actually permanently transform the video with Windows Movie Maker, but for some reason my videos turned out with crappy sound.
And so I figured, 'Hey, let's write something in WPF. That shouldn't be so hard'. And indeed, flipping a video in WPF is as easy as giving a MediaElement control a RotateTransform. Here is the result of the work. The bar on the right side allows you to flip the video in several directions.
As you can tell, an upside down parrot. You can download the binaries and code right here. Still, it took me a few good hours to write this tool, and not 10 minutes as I anticipated, as I had to battle several issues.
The first issue was getting the video to play automatically after you open a file. I am using a MediaElement which has a MediaTimeline assigned to it. The problem is, that it seems the Loaded event of the MediaElement (when it's working with a MediaTimeline and not in independent mode) fires instantly when the window is created, and not when the video is actually loaded. So I had to create my own RoutedEvent for this. The XAML code looks something like this.
<Window.Triggers>
<EventTrigger RoutedEvent="local:Main.MediaLoaded" SourceName="MainWindow">
<EventTrigger.Actions>
<BeginStoryboard Name="beginStoryboard">
<Storyboard>
<MediaTimeline x:Name="videoTimeline" Storyboard.TargetName="mediaElement" CurrentTimeInvalidated="videoTimeline_CurrentTimeInvalidated" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Trigger>
...
...
<MediaElement x:Name="mediaElement" MediaOpened="mediaElement_MediaOpened" />
And here is the declaration of the event.
public static readonly RoutedEvent MediaLoadedEvent;
static Main()
{
MediaLoadedEvent = EventManager.RegisterRoutedEvent("MediaLoaded", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Main));
}
protected virtual void OnMediaLoaded()
{
RaiseEvent(new RoutedEventArgs(MediaLoadedEvent, this));
}
private void Open_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = VideoFilesFilter;
bool? result = dialog.ShowDialog();
if (result == true)
{
videoTimeline.Source = new Uri(dialog.FileName);
mediaControlsPanel.Visibility = System.Windows.Visibility.Visible;
}
OnMediaLoaded();
}
As you can see the event is fired right after the user selects a video file in the OpenFileDialog that is displayed to him. This triggers the EventTrigger that fires the StoryBoard, which in turn plays the video.
The second issue was getting the video to display in it's natural size, but allowing the user to expand it by resizing the video. Playing with the window's SizeToContent property, and the MediaElement's Stretch property did not give me the satisfactory results. Either the video appeared full screen (when the MediaElement.Stretch is set to 'Uniform'), or it appeared correctly, but would not change its size when window is maximized (when MediaElement.Stretch is set to 'None'). I ended up setting leaving Stretch at 'Uniform' (the default) but giving the window a height and width (and not use SizeToContent). When a video is loaded, I manually change the window's size.
private void mediaElement_MediaOpened(object sender, RoutedEventArgs e)
{
Height = mediaElement.NaturalVideoHeight + openButton.ActualHeight;
Width = mediaElement.NaturalVideoWidth;
}
The final issue was with the Slider, i.e. the track-bar that allows you to drag the video to a certain location. The example here showed me how to let the Slider track the video, but I couldn't figure out how to catch the event of the user dragging the Slider. And indeed, the Slider does not expose these events, you have to get the Slider's Thumb control and register to the event's on that thingy. Bah.
private bool _enableSliderAutoUpdate = true;
private void Main_Loaded(object sender, RoutedEventArgs e)
{
Track sliderTrack = timelineSlider.Template.FindName("PART_Track", timelineSlider) as Track;
sliderThumb = sliderTrack.Thumb;
sliderThumb.DragCompleted += sliderThumb_DragCompleted;
sliderThumb.PreviewMouseLeftButtonDown += sliderThumb_PreviewMouseLeftButtonDown;
}
private void videoTimeline_CurrentTimeInvalidated(object sender, EventArgs e)
{
if (_enableSliderAutoUpdate)
timelineSlider.Value = mediaElement.Position.Ticks;
}
private void sliderThumb_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_enableSliderAutoUpdate = false;
}
private void sliderThumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
beginStoryboard.Storyboard.Seek(this, new TimeSpan((long)timelineSlider.Value), TimeSeekOrigin.BeginTime);
_enableSliderAutoUpdate = true;
}
private void mediaElement_MediaOpened(object sender, RoutedEventArgs e)
{...
timelineSlider.Maximum = mediaElement.NaturalDuration.TimeSpan.Ticks;
}
Notice that we register to the events on the Loaded event in the Window, and not in the constructor. If you do it in the constructor expect a NullReferenceException as the Slider's Thumb is not yet there. Also note the usage of a boolean - _enableSliderAutoUpdate - which makes sure that while the user is dragging the Slider's thumb its position is not updated, even though the video keeps playing.
And that's about it. Other than that the code is rather straight-forward and again, can be downloaded here.