[UWP]MediaElementの再生位置を取得する

### 追記
Windows10 Anniversary Update 以後では [MediaPlayer 及び MediaPlayerElement を利用する](https://docs.microsoft.com/ja-jp/windows/uwp/audio-video-camera/play-audio-and-video-with-mediaplayer)ことで、メディアトランスポートやバックグラウンド再生が簡単にサポートできるようになりました。

動画ストリームをMediaSourceとして扱う必要がありますが、MediaElementで動画を扱うよりもコントロールが容易になります。

後方互換性が要件が必須な場合を除いては、MediaPlayer/MediaPlayerElementの利用をオススメします。



## はじめに
ここで扱うのは Windows.UI.Xaml.Controls.MediaElement です。
System.Windows.Controls.MediaElement ではないのでご注意ください。

## MediaElement.Positionプロパティについて
MediaElementの再生位置はPositionプロパティを通じて行いますが、MediaElement上で再生を開始してもPositionプロパティへの操作は内部からは行われません。そのためPositionプロパティのプロパティ変更通知もトリガーされません。

Positionプロパティについて
https://msdn.microsoft.com/ja-jp/library/system.windows.controls.mediaelement.position(v=vs.110).aspx

このページによると、Clockプロパティが設定されてない時にPositionが設定されたとき再生位置を指定できる。とありますが、読み取りについては記述がありません。

また、Positionプロパティの変更を伝えるイベントも存在しないようです。

そうなると再生に合わせてPositionプロパティの値を取得するには、Timer等でPositionの内容をモニターして定期的に取り出す処理が必要になります。

## MediaElementの再生状況を知る方法
基本的にはMediaElement.CurrentStateプロパティまたは、CurrentStateChangedイベントを通じて現在の再生状況を把握することが出来ます。

TimerでPositionプロパティをモニターする際、メディアのクローズ後やポーズ中にモニター処理が走らないように工夫できると良さそうですね。


## Positionプロパティを定期読み取りするビヘイビア

```csharp:MediaElementPositionExtractBehavior.cs
using System;
using System.Threading;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;

namespace MyApp.Views.Behaviors
{
	public class MediaElementExtractPositionBehavior : Behavior<MediaElement>
	{

		#region Position Property

		public static readonly DependencyProperty PositionProperty =
			DependencyProperty.Register("Position"
					, typeof(TimeSpan)
					, typeof(MediaElementExtractPositionBehavior)
					, new PropertyMetadata(default(TimeSpan))
				);

		public TimeSpan Position
		{
			get { return (TimeSpan)GetValue(PositionProperty); }
			set { SetValue(PositionProperty, value); }
		}

		#endregion


		#region Interval Property

		public static readonly DependencyProperty IntervalProperty =
			DependencyProperty.Register("Interval"
					, typeof(TimeSpan)
					, typeof(MediaElementExtractPositionBehavior)
					, new PropertyMetadata(default(TimeSpan))
				);

		public TimeSpan Interval
		{
			get { return (TimeSpan)GetValue(IntervalProperty); }
			set { SetValue(IntervalProperty, value); }
		}

		#endregion


		#region MediaElement Event Handling

		protected override void OnAttached()
		{
			base.OnAttached();

			this.AssociatedObject.Loaded += AssociatedObject_Loaded;
			
		}


		private void AssociatedObject_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
		{
			this.AssociatedObject.CurrentStateChanged += AssociatedObject_CurrentStateChanged;
			this.AssociatedObject.SeekCompleted += AssociatedObject_SeekCompleted;


			this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
			this.AssociatedObject.Unloaded += AssociatedObject_Unloaded;
		}

		private void AssociatedObject_SeekCompleted(object sender, RoutedEventArgs e)
		{
			this.ExtractPosition();
		}

		private void AssociatedObject_CurrentStateChanged(object sender, RoutedEventArgs e)
		{
			var mediaElem = (MediaElement)sender;

			switch (mediaElem.CurrentState)
			{
				case Windows.UI.Xaml.Media.MediaElementState.Closed:
					TimerExit();
					break;
				case Windows.UI.Xaml.Media.MediaElementState.Opening:
					RefreshTimerSetting();
					break;
				case Windows.UI.Xaml.Media.MediaElementState.Buffering:
					break;
				case Windows.UI.Xaml.Media.MediaElementState.Playing:
					RefreshTimerSetting();
					break;
				case Windows.UI.Xaml.Media.MediaElementState.Paused:
					TimerExit();
					break;
				case Windows.UI.Xaml.Media.MediaElementState.Stopped:
					TimerExit();
					break;
				default:
					break;
			}
		}

		private void AssociatedObject_Unloaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
		{
			TimerExit();
		}


		protected override void OnDetaching()
		{
			base.OnDetaching();

			TimerExit();
		}


		#endregion


		#region Management Timer


		private Timer _ExtractTimingTimer;


		private void RefreshTimerSetting()
		{
			TimerExit();

			_ExtractTimingTimer = new Timer((x) =>
			{
				var me = (MediaElementExtractPositionBehavior)x;

				me.ExtractPosition();

			}, this, 0, (int)Interval.TotalMilliseconds);
		}

		private void TimerExit()
		{
			_ExtractTimingTimer?.Dispose();
			_ExtractTimingTimer = null;
		}


		private async void ExtractPosition()
		{
			var mediaElem = this.AssociatedObject as MediaElement;
			
			await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Low,
					() => this.Position = mediaElem.Position);
		}


		#endregion
	}
}
```

気軽にTimerスレッドを作ったり消したりしていますが、気になる場合にはCurrentStateのOpeningとClosedでだけ作成と削除を行えば問題ないと思います。

SeekCompletedイベントにも反応するようにしていますが、シーク後にもPlayingステートへの変更が呼ばれると思う（動作未確認）ので不要かもしれません。

Xamlで利用する方法は以下の通り

```xml:MediaElementWithGetPosition
<Page
    x:Class="MyApp.Views.VideoPlayerPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    xmlns:mybehaviors="MyApp.Views.Behaviors"
    mc:Ignorable="d">

    <Grid>
        <MediaElement Source="smile.mp4">
            <i:Interaction.Behaviors>
                <mybehaviors:MediaElementExtractPositionBehavior 
                    Position="{Binding CurrentVideoPosition, Mode=TwoWay}"
                    Interval="00:00:00.016" 
                    />
            </i:Interaction.Behaviors>
        </MediaElement>
    </Grid>
</Page>
```


PositionにBindingしているCurrentVideoPositionは、VideoPlayerPageのDataContextに設定するVideoPlayerPageViewModel上で定義します。

IntervalにはTimeSpan型の固定値を渡しています。この例では0.016秒(約1/60秒)ごとにPositionプロパティを読み込むことになります。

他によくわからないところはコメントもらえれば補足します。


## 参考
MediaElement
https://msdn.microsoft.com/ja-jp/library/windows/apps/windows.ui.xaml.controls.mediaelement