Fork me on GitHub

UWP-mediaPlayer

Week8

Request

  1. 利用自定义控件控制视频的播放,暂停,快进,快退
  2. 实现视频播放的全屏与退出全屏
  3. 自制 slider 实现视频的进度条
  4. 本地选择多媒体文件(视频,音乐)进行播放
  5. 添加 slider 版本音量控制
  6. 实现封面旋转
  7. 添加键盘按键响应

Accomplish

强烈推荐Microsoft Documents-MediaElement#multiple-audio-tracks)。这是第一次在官方文档找到如此棒的一个指引文档。
关于 MediaElement 的 xml 代码,可以仿照上面的文档进行设置,下面主要讲讲具体实现的基础功能按钮的代码。

  1. 利用自定义控件控制视频的播放,暂停,快进,快退
    函数中的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    ```csharp
    // Play media
    private void MdeiaPlay(object sender, RoutedEventArgs e) {
    // Reset the play rate to normal
    if (myMediaPlayer.DefaultPlaybackRate != 1) {
    myMediaPlayer.DefaultPlaybackRate = 1.0;
    }
    myMediaPlayer.Play();
    if (Cover.Visibility == Visibility.Visible) {
    RotateCover.Begin();
    }
    }

    // Pause media
    private void MediaPause(object sender, RoutedEventArgs e) {
    myMediaPlayer.Pause();
    if (Cover.Visibility == Visibility.Visible) {
    RotateCover.Pause();
    }
    }

    // Stop media
    private void MediaStop(object sender, RoutedEventArgs e) {
    myMediaPlayer.Stop();
    if (Cover.Visibility == Visibility.Visible) {
    RotateCover.Stop();
    }
    }

    // Set back playing rate
    private void MediaBack(object sender, RoutedEventArgs e) {
    myMediaPlayer.DefaultPlaybackRate = -2.0;
    myMediaPlayer.Play();
    }

    // Set forward playing rate
    private void MediaForward(object sender, RoutedEventArgs e) {
    myMediaPlayer.DefaultPlaybackRate = 2.0;
    myMediaPlayer.Play();
    }

  2. 实现视频播放的全屏与退出全屏
    这里的全屏为应用全屏。
    为了实现退出全屏,需要存储全屏之前的 size,所以需要一个 previousSize 来存储。
    全屏退出采取按键响应。对应在 keyUp 函数内调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // Set full screen or not
    private void FullScreenToggle() {
    this.IsFullScreen = !this.IsFullScreen;

    if (this.IsFullScreen) {
    TransportControlsPanel.Visibility = Visibility.Collapsed;
    previousSize.Width = videoContainer.ActualWidth;
    previousSize.Height = videoContainer.ActualHeight;

    videoContainer.Width = Window.Current.Bounds.Width;
    videoContainer.Height = Window.Current.Bounds.Height;
    myMediaPlayer.Width = Window.Current.Bounds.Width;
    myMediaPlayer.Height = Window.Current.Bounds.Height;
    } else {
    TransportControlsPanel.Visibility = Visibility.Visible;

    videoContainer.Width = previousSize.Width;
    videoContainer.Height = previousSize.Height;
    myMediaPlayer.Width = previousSize.Width;
    myMediaPlayer.Height = previousSize.Height;
    }
    }

    //Start full screen
    private void MediaFullScreen(object sender, RoutedEventArgs e) {
    FullScreenToggle();
    }

    // Listen for [ESC] to exit full screen
    if (IsFullScreen && e.Key == Windows.System.VirtualKey.Escape) {
    FullScreenToggle();
    }
  3. 自制 slider 实现视频的进度条
    在完整实现了官方文档之后,我很是怀疑,这个进度条,真的需要这么复杂吗?
    先来讲讲官方文档的实现逻辑:

    • 实现一个 slider 控件作为进度条
    • 实现 slider 控件的拖动分辨率
    • 实现一个 timer 跟随 mediaElement 一起播放计时和暂停
    • 实现一个 pointerPressed handler 将 mediaElement 的 position 值和 slider 值通过 timer 进行绑定
    • 每次新载入一个 media 文件,重置 timer
    • 初始化整个 APP 时,为 slider 注册 handler 事件
      其中 slider 拖动分辨率可以调整,这里我将其进一步精化了,为了适应 media 文件的总时长。
      代码太长了,而且逻辑层叠太过复杂,具体实现看代码文件,下面附上函数名。
      关于对这个冗杂的实现的改进,在文末提出解决方案。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      // Get the frequency of the steps on the slider's scale
      private double SliderFrequency(TimeSpan time) {}

      // Initialize the timer
      private void SetupTimer() {}

      // Keep timer in sync with media
      private void TimerTick(object sender, object e) {}

      // Start timer to count
      private void StartTimer() {}

      // Stop timer to count
      private void StopTimer() {}

      // slider pressed
      void SliderPointerEntered(object sender, PointerRoutedEventArgs e) {}

      // slider lost pressed
      void SliderPointerCaptureLost(object sender, PointerRoutedEventArgs e) {}

      void TimelineSliderValueChanged(object sender, RangeBaseValueChangedEventArgs e) {}

      // Reset timer for slider
      private void MyMediaOpened(object sender, RoutedEventArgs e) {}

      // Listen for the media state and timer
      private void MyMediaCurrentStateChanged(object sender, RoutedEventArgs e) {}
  4. 本地选择多媒体文件(视频,音乐)进行播放
    选择多媒体文件并不困难,使用 FileOpenPicker 就好了。
    需要注意的是,当读取的是音乐文件时,需要隐藏视频页面,显示 cover 页面(有点面包屑的意味,不需要跳转页面,省去开销)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // Select media to play
    private async void MediaSelect(object sender, RoutedEventArgs e) {
    FileOpenPicker picker = new FileOpenPicker();
    /// Initialize the picture file type to take
    picker.FileTypeFilter.Add(".mp4");
    picker.FileTypeFilter.Add(".wmv");
    picker.FileTypeFilter.Add(".wma");
    picker.FileTypeFilter.Add(".mp3");
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

    StorageFile file = await picker.PickSingleFileAsync();

    if (file != null) {
    /// Load the selected picture
    IRandomAccessStream ir = await file.OpenAsync(FileAccessMode.Read);
    myMediaPlayer.SetSource(ir, file.ContentType);
    myMediaPlayer.Play();
    if (file.ContentType == "audio/mpeg") {
    Cover.Visibility = Visibility.Visible;
    } else {
    Cover.Visibility = Visibility.Collapsed;
    }
    }
    }
  5. 添加 slider 版本音量控制
    理解数据绑定就很容易了,就是 slider 值和 mediaElement 的音量值绑定。
    Attention! 音量值为0 - 1,分割值为0.1,这是 mediaElement 官方类中给出的说明。所以,设置一下 slider 的值是必须的。(在这里被坑了很久,以为实现不了 slider 音量控制)

    1
    2
    3
    <Slider x:Name="volumeslider" Maximum="1" Minimum="0" StepFrequency="0.1"
    Value="{x:Bind myMediaPlayer.Volume, Mode=TwoWay}">
    </Slider>

    其中,UI 页面提供了音量按钮,所以,点击按钮直接静音,然后需要储存静音前的音量。逻辑与全屏类似,不赘述,实现代码见代码文件。

  6. 实现封面旋转

    • 播放视频时,隐藏封面页面(在读取文件中已实现)
    • 播放音乐时,封面开始旋转
    • 暂停音乐时,封面旋转暂停
    • 重置音乐或结束时,封面复位
      xml 中实现动画代码。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <Ellipse x:Name="Cover" RenderTransformOrigin="0.5,0.5">
      <Ellipse.RenderTransform>
      <CompositeTransform></CompositeTransform>
      </Ellipse.RenderTransform>
      <Ellipse.Resources>
      <Storyboard x:Name="RotateCover" RepeatBehavior="Forever">
      <DoubleAnimation Duration="0:0:20" To="360" Storyboard.TargetName="Cover"
      Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.Rotation)">
      </DoubleAnimation>
      </Storyboard>
      </Ellipse.Resources>
      <Ellipse.Fill>
      <ImageBrush x:Name="picture" ImageSource="Assets/cover.jpg"></ImageBrush>
      </Ellipse.Fill>
      </Ellipse>

      在需要动画开始,暂停,复位的地方,调用其对应函数即可。如,音乐暂停时,动画暂停:

      1
      2
      3
      4
      5
      6
      7
      // Pause media
      private void MediaPause(object sender, RoutedEventArgs e) {
      myMediaPlayer.Pause();
      if (Cover.Visibility == Visibility.Visible) {
      RotateCover.Pause();
      }
      }
  7. 添加键盘按键响应
    作为一个多媒体播放应用,空格键播放暂停,上下方向按键调节音量是基础功能,所以添加了这几个基础按键响应。
    mediaElement 提供了 keyUp 和 keyDown 两个接口,前者响应一次按键,后者响应持续按键。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // Keyboard listener for one press
    private void VideoContainerKeyUp(object sender, KeyRoutedEventArgs e) {
    // Listen for [Space] to stop or play media
    if (e.Key == Windows.System.VirtualKey.Space) {
    if (myMediaPlayer.CurrentState == MediaElementState.Playing) {
    myMediaPlayer.Pause();
    } else {
    myMediaPlayer.Play();
    }
    }

    // Listen for [Up] and [Down] to control volume
    if (e.Key == Windows.System.VirtualKey.Up) {
    if (myMediaPlayer.Volume < 1) {
    myMediaPlayer.Volume += 0.1;
    }
    } else if (e.Key == Windows.System.VirtualKey.Down) {
    if (myMediaPlayer.Volume > 0) {
    myMediaPlayer.Volume -= 0.1;
    }
    }

    e.Handled = true;
    }

    // Keyboard listener for holding press
    private void VideoContainerKeyDown(object sender, KeyRoutedEventArgs e) {
    // Listen for [Up] and [Down] to control volume
    if (e.Key == Windows.System.VirtualKey.Up) {
    if (myMediaPlayer.Volume < 1) {
    myMediaPlayer.Volume += 0.1;
    }
    } else if (e.Key == Windows.System.VirtualKey.Down) {
    if (myMediaPlayer.Volume > 0) {
    myMediaPlayer.Volume -= 0.1;
    }
    }
    }

成品图

video

video

music

music

后记

是的,像前面说的,官网文档提供的实现方法,太过复杂。有什么改进方法吗?
考虑值绑定。我们将 slider 的值和 mediaElement 的 position 值绑定在一起,就实现了这个进度条。
类似之前我们实现的 MyList 中 checkbox 和 line 的绑定,我们写一个 converter 将 position 这个 TimeSpan 类型的值 转换为 double 类型,就可以与 slider 值绑定了。