使用 MediaPlayer 播放音频和视频

本文介绍了如何在通用 Windows 应用中使用 MediaPlayer 类播放媒体。 在 Windows 10 版本 1607 中对媒体播放 API 进行了显著改进,包括简化了后台音频的单进程设计、自动与系统媒体传输控件 (SMTC) 集成、能够同步多个媒体播放器、能够将视频帧渲染到 Windows.UI.Composition 表面,并且提供用于创建和计划内容的媒体中断的简单界面。 若要充分利用这些改进功能,推荐用于播放媒体的最佳做法是将 MediaPlayer 类(而非 MediaElement)用于媒体播放。 引入了轻型 XAML 控件 MediaPlayerElement,该控件允许你在 XAML 页面中呈现媒体内容。 MediaElement 提供的许多播放控件和状态 API 现在都可通过新 MediaPlaybackSession 对象获取。 MediaElement 将继续运行以支持向后兼容,但不会向此类添加其他功能。

本文将向你介绍典型的媒体播放应用所使用的 MediaPlayer 功能。 请注意,MediaPlayerMediaSource 类用作所有媒体项目的容器。 此类允许你加载并播放来自许多不同源的媒体,这些来源包括本地文件、内存流和网络源等,但使用的都是同一界面。 此外,还有更高级的类能够与 MediaSource 一同使用,例如 MediaPlaybackItemMediaPlaybackList,这些类提供更加高级的功能,例如播放列表,以及通过音频、视频和元数据多轨道管理媒体源的功能。 有关 MediaSource 和相关 API 的详细信息,请参阅媒体项、播放列表和曲目

注意

Windows 10 N 和 Windows 10 KN 版本不包括使用 MediaPlayer 进行播放所需的媒体功能。 可以手动安装这些功能。 有关详细信息,请参阅 Windows 10 N 和 Windows 10 KN 版本的媒体功能包

使用 MediaPlayer 播放媒体文件

通过 MediaPlayer 进行基本媒体播放非常易于实现。 首先,创建 MediaPlayer 类的新实例。 应用可以同时使用多个 MediaPlayer 实例。 接下来,将播放器的 Source 属性设为实现 IMediaPlaybackSource 的对象,例如 MediaSourceMediaPlaybackItemMediaPlaybackList。 在本例中,MediaSource 创建于应用本地存储中的文件,MediaPlaybackItem 创建于源,随后分配到播放器的 Source 属性。

MediaElement 不同,默认情况下,MediaPlayer 不会自动开始播放。 可以通过调用 Play、将 AutoPlay 属性设为 True,或者等到用户使用内置媒体控件启动播放时开始播放。

mediaPlayer = new MediaPlayer();
mediaPlayer.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
mediaPlayer.Play();

当应用结束使用 MediaPlayer 后,应该调用 Close 方法(对应 C# 中的 Dispose)清理播放器使用的资源。

mediaPlayer.Dispose();

使用 MediaPlayerElement 在 XAML 中呈现视频

可以在 MediaPlayer 中播放媒体,无需在 XAML 中显示,但是许多媒体播放应用会希望在 XAML 页面中呈现媒体。 为此,请使用轻型 MediaPlayerElement 控件。 与 MediaElement 一样,MediaPlayerElement 支持指定是否应显示内置传输控件。

<MediaPlayerElement x:Name="_mediaPlayerElement" AreTransportControlsEnabled="False" HorizontalAlignment="Stretch"  Grid.Row="0"/>

可以通过调用 SetMediaPlayer,设置绑定元素的 MediaPlayer 实例。

_mediaPlayerElement.SetMediaPlayer(mediaPlayer);

还可以在 MediaPlayerElement 上设置播放源,元素将自动创建可以使用 MediaPlayer 属性访问的新 MediaPlayer 实例。

_mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
mediaPlayer = _mediaPlayerElement.MediaPlayer;
mediaPlayer.Play();

注意

如果你通过将 IsEnabled 设置为 false 禁用 MediaPlayerMediaPlaybackCommandManager,它将中断 MediaPlayerMediaPlayerElement 提供的 TransportControls 之间的链接,以致内置传输控件不再自动控制播放器的播放。 作为替代方法,你必须实现自己的控件才能控制 MediaPlayer

MediaPlayer 常见任务

本部分介绍如何使用 MediaPlayer 的一些功能。

设置音频类别

MediaPlayerAudioCategory 属性设为 MediaPlayerAudioCategory 枚举的其中一个值,让系统知道播放的媒体种类。 游戏应该将其音乐流归类为 GameMedia,以使游戏音乐在其他应用程序在后台播放音乐时自动静音。 音乐或视频应用程序应该将它们的流归类为 MediaMovie,以使它们能够优先于 GameMedia 流。

mediaPlayer.AudioCategory = MediaPlayerAudioCategory.Media;

输出到特定音频终结点

默认情况下,MediaPlayer 的音频输出路由到系统的默认音频终结点,但是可以指定 MediaPlayer 应该用于输出的特定音频终结点。 在以下示例中,MediaDevice.GetAudioRenderSelector 返回了唯一标识设备音频呈现类别的字符串。 接下来,调用 DeviceInformation 方法 FindAllAsync 获取所选类型的所有可用设备列表。 可以通过编程方式确定希望使用的设备,或者将返回的设备添加到 ComboBox 以允许用户选择设备。

string audioSelector = MediaDevice.GetAudioRenderSelector();
var outputDevices = await DeviceInformation.FindAllAsync(audioSelector);
foreach (var device in outputDevices)
{
    var deviceItem = new ComboBoxItem();
    deviceItem.Content = device.Name;
    deviceItem.Tag = device;
    _audioDeviceComboBox.Items.Add(deviceItem);
}

在用于设备组合框的 SelectionChanged 事件中,MediaPlayerAudioDevice 属性设置为所选设备,该属性存储在 ComboBoxItemTag 属性中。

private void _audioDeviceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    DeviceInformation selectedDevice = (DeviceInformation)((ComboBoxItem)_audioDeviceComboBox.SelectedItem).Tag;
    if (selectedDevice != null)
    {
        mediaPlayer.AudioDevice = selectedDevice;
    }
}

播放会话

如前文所述,许多由 MediaElement 类公开的函数都已移到 MediaPlaybackSession 类。 这包括有关播放器播放状态的信息,例如当前播放位置、播放器是已暂停还是正在播放,以及当前播放速度。 MediaPlaybackSession 还会提供几个事件,用于在状态更改时通知用户,这些更改包括所播放内容的当前缓冲和下载状态,以及当前播放视频内容的自然大小和纵横比。

以下示例演示了如何实现向前跳过 10 秒内容的按钮单击处理程序。 首先,使用 PlaybackSession 属性检索播放器的 MediaPlaybackSession 对象。 接下来,将 Position 属性设置为当前播放位置向后 10 秒的值。

private void _skipForwardButton_Click(object sender, RoutedEventArgs e)
{
    var session = mediaPlayer.PlaybackSession;
    session.Position = session.Position + TimeSpan.FromSeconds(10);
}

下一个示例介绍通过设置会话的 PlaybackRate 属性,使用切换按钮在正常播放速度和 2 倍播放速度之间切换。

private void _speedToggleButton_Checked(object sender, RoutedEventArgs e)
{
    mediaPlayer.PlaybackSession.PlaybackRate = 2.0;
}
private void _speedToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
    mediaPlayer.PlaybackSession.PlaybackRate = 1.0;
}

从 Windows 10 版本 1803 开始,可以对在 MediaPlayer 中显示的视频设置旋转(以 90 度为增量)。

mediaPlayer.PlaybackSession.PlaybackRotation = MediaRotation.Clockwise90Degrees;

检测预期和非预期缓冲

上一节中所述的 MediaPlaybackSession 对象提供了两个事件(BufferingStartedBufferingEnded)用于检测当前播放的媒体文件何时开始和结束缓冲。 这允许更新 UI 以向用户显示正在发生缓冲。 当媒体文件首次打开或用户切换到播放列表中的新项时,需要进行初始缓冲。 当网络速度降低或提供内容的内容管理系统遇到技术问题时,可能会发生非预期缓冲。 从 RS3 开始,可以使用 BufferingStarted 事件来确定缓冲事件是预期的还是非预期的并会中断播放。 此信息可以用作应用或媒体传送服务的遥测数据。

BufferingStartedBufferingEnded 事件注册处理程序以接收缓冲状态通知。

mediaPlayer.PlaybackSession.BufferingStarted += MediaPlaybackSession_BufferingStarted;
mediaPlayer.PlaybackSession.BufferingEnded += MediaPlaybackSession_BufferingEnded;

BufferingStarted 事件处理程序中,将传入事件的事件参数投放到 MediaPlaybackSessionBufferingStartedEventArgs 对象并检查 IsPlaybackInterruption 属性。 如果此值为 true,则触发事件的缓冲为非预期并中断播放。 否则为预期初始缓冲。

private void MediaPlaybackSession_BufferingStarted(MediaPlaybackSession sender, object args)
{
    MediaPlaybackSessionBufferingStartedEventArgs bufferingStartedEventArgs = args as MediaPlaybackSessionBufferingStartedEventArgs;
    if (bufferingStartedEventArgs != null && bufferingStartedEventArgs.IsPlaybackInterruption)
    {
        // update the playback quality telemetry report to indicate that
        // playback was interrupted
    }

    // update the UI to indicate that playback is buffering
}
private void MediaPlaybackSession_BufferingEnded(MediaPlaybackSession sender, object args)
{
    // update the UI to indicate that playback is no longer buffering
}

使用收缩手势缩放视频

MediaPlayer 支持在视频内容中指定应该呈现的源矩形,这可有效地支持视频放大。 所指定的矩形与规范化矩形 (0,0,1,1) 成比例,其中 0,0 是帧的左上角,1,1 指定帧的全宽和全高。 因此,如果要设置缩放矩形以呈现视频的右上角象限,需指定矩形 (.5,0,.5,.5)。 请务必检查这些值,确保源矩形位于 (0,0,1,1) 规范化矩形内。 尝试设置超出此范围的值将引发异常。

若要使用多点触控手势实现收缩以缩放,必须先指定要支持的手势。 在本例中,请求了缩放和平移手势。 发生一个订阅手势即会引发 ManipulationDelta 事件。 DoubleTapped 事件用于将缩放重置为完整的帧。

_mediaPlayerElement.ManipulationMode = ManipulationModes.Scale | ManipulationModes.TranslateX | ManipulationModes.TranslateY;
_mediaPlayerElement.ManipulationDelta += _mediaPlayerElement_ManipulationDelta;
_mediaPlayerElement.DoubleTapped += _mediaPlayerElement_DoubleTapped;

接下来,声明存储当前缩放源矩形的 Rect 对象。

Rect _sourceRect = new Rect(0, 0, 1, 1);

ManipulationDelta 处理程序调整缩放矩形的缩放或平移。 如果增量缩放值不为 1,则意味着用户执行了收缩手势。 如果值大于 1,源矩形应变小以放大内容。 如果值小于 1,则应使源矩形变大以缩小。在设置新的缩放值之前,将检查生成的矩形以确保其完全位于 (0,0,1,1) 限制内。

如果缩放值为 1,则处理平移手势。 矩形仅根据手势的像素数除以控件的宽度和高度所得结果平移。 同样,检查生成的矩形,确保它位于 (0,0,1,1) 边界内。

最后,将 MediaPlaybackSessionNormalizedSourceRect 设置为新调整的矩形,从而指定视频帧中应该呈现的区域。

private void _mediaPlayerElement_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{

    if (e.Delta.Scale != 1)
    {
        var halfWidth = _sourceRect.Width / 2;
        var halfHeight = _sourceRect.Height / 2;

        var centerX = _sourceRect.X + halfWidth;
        var centerY = _sourceRect.Y + halfHeight;

        var scale = e.Delta.Scale;
        var newHalfWidth = (_sourceRect.Width * e.Delta.Scale) / 2;
        var newHalfHeight = (_sourceRect.Height * e.Delta.Scale) / 2;

        if (centerX - newHalfWidth > 0 && centerX + newHalfWidth <= 1.0 &&
            centerY - newHalfHeight > 0 && centerY + newHalfHeight <= 1.0)
        {
            _sourceRect.X = centerX - newHalfWidth;
            _sourceRect.Y = centerY - newHalfHeight;
            _sourceRect.Width *= e.Delta.Scale;
            _sourceRect.Height *= e.Delta.Scale;
        }
    }
    else
    {
        var translateX = -1 * e.Delta.Translation.X / _mediaPlayerElement.ActualWidth;
        var translateY = -1 * e.Delta.Translation.Y / _mediaPlayerElement.ActualHeight;

        if (_sourceRect.X + translateX >= 0 && _sourceRect.X + _sourceRect.Width + translateX <= 1.0 &&
            _sourceRect.Y + translateY >= 0 && _sourceRect.Y + _sourceRect.Height + translateY <= 1.0)
        {
            _sourceRect.X += translateX;
            _sourceRect.Y += translateY;
        }
    }

    mediaPlayer.PlaybackSession.NormalizedSourceRect = _sourceRect;
}

DoubleTapped 事件处理程序中,源矩形重新设为 (0,0,1,1),以呈现整个视频帧。

private void _mediaPlayerElement_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
    _sourceRect = new Rect(0, 0, 1, 1);
    mediaPlayer.PlaybackSession.NormalizedSourceRect = _sourceRect;
}

注意 本部分介绍触摸输入。 触控板发送指针事件,不会发送操作事件。

处理基于策略的播放降级

在某些情况下,系统可能会根据策略而不是性能问题对媒体项的播放进行降级,例如降低分辨率(压缩)。 例如,如果使用未签名的视频驱动程序播放视频,则系统可能会对视频进行降级。 可以调用 MediaPlaybackSession.GetOutputDegradationPolicyState 来确定是否以及为什么会发生这种基于策略的降级,并提醒用户或记录原因用于遥测。

下面的示例显示了 MediaPlayer.MediaOpened 事件的处理程序的实现,该事件是在播放机打开新媒体项时引发的。 在传递给处理程序的 MediaPlayer 上调用 GetOutputDegradationPolicyStateVideoConstrictionReason 的值指示视频被压缩的策略原因。 如果值不是 None,则此示例记录降级原因用于遥测。 此示例还显示将当前播放的 AdaptiveMediaSource 的比特率设置为最低带宽以节省数据使用量,因为视频被压缩,并且不会以高分辨率显示。 有关使用 AdaptiveMediaSource 的详细信息,请参阅自适应流式处理

private void MediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
    MediaPlaybackSessionOutputDegradationPolicyState info = sender.PlaybackSession.GetOutputDegradationPolicyState();

    if (info.VideoConstrictionReason != MediaPlaybackSessionVideoConstrictionReason.None)
    {
        // Switch to lowest bitrate to save bandwidth
        adaptiveMediaSource.DesiredMaxBitrate = adaptiveMediaSource.AvailableBitrates[0];

        // Log the degradation reason or show a message to the user
        System.Diagnostics.Debug.WriteLine("Logging constriction reason: " + info.VideoConstrictionReason);
    }
}

使用 MediaPlayerSurface 将视频呈现到 Windows.UI.Composition 界面

从 Windows 10 版本 1607 开始,可以使用 MediaPlayer 将视频呈现到 ICompositionSurface,这可以支持播放器与 Windows.UI.Composition 命名空间中的 API 进行互操作。 合成框架允许你在 XAML 与低级别 DirectX 图形 API 之间的可视化层处理图形。 这可以使任意 XAML 控件都能够执行呈现视频等方案。 有关使用合成 API 的详细信息,请参阅可视化层

以下示例介绍了如何将视频播放器内容呈现到 Canvas 控件。 本例中特定于媒体播放器的调用为 SetSurfaceSizeGetSurfaceSetSurfaceSize 告知系统为呈现内容应该分配的缓冲区大小。 GetSurfaceCompositor 视作参数,并且检索 MediaPlayerSurface 类的实例。 使用此类可以访问用于通过 CompositionSurface 属性创建表面和公开表面自身的 MediaPlayerCompositor

本例中的其余代码创建视频要呈现到的 SpriteVisual,还将大小设为显示视觉效果的画布元素大小。 接下来,从 MediaPlayerSurface 创建 CompositionBrush,并将其分配到视觉效果的 Brush 属性。 然后,创建 ContainerVisual,在它的可视树顶部插入 SpriteVisual。 最后,调用 SetElementChildVisual,将容器视觉效果分配到 Canvas

mediaPlayer.SetSurfaceSize(new Size(_compositionCanvas.ActualWidth, _compositionCanvas.ActualHeight));

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
MediaPlayerSurface surface = mediaPlayer.GetSurface(compositor);

SpriteVisual spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Size =
    new System.Numerics.Vector2((float)_compositionCanvas.ActualWidth, (float)_compositionCanvas.ActualHeight);

CompositionBrush brush = compositor.CreateSurfaceBrush(surface.CompositionSurface);
spriteVisual.Brush = brush;

ContainerVisual container = compositor.CreateContainerVisual();
container.Children.InsertAtTop(spriteVisual);

ElementCompositionPreview.SetElementChildVisual(_compositionCanvas, container);

使用 MediaTimelineController 跨多台播放器同步内容。

如前文所述,应用可以同时使用多个 MediaPlayer 对象。 默认情况下,所创建的每个 MediaPlayer 都独立操作。 在某些情况下(例如同步视频的解说音轨),你可能希望同步多台播放器的状态、播放位置和播放速度。 从 Windows 10 版本 1607 开始,可以使用 MediaTimelineController 类实现这种行为。

实现播放控件

以下示例展示了如何使用 MediaTimelineController 控制 MediaPlayer 的两个实例。 首先,MediaPlayer 的每个实例均进行实例化,并且 Source 设置为媒体文件。 接下来,创建新 MediaTimelineController。 对于每个 MediaPlayer,将 IsEnabled 属性设为 False 禁用与每台播放器关联的 MediaPlaybackCommandManager。 然后,将 TimelineController 属性设置为时间线控制器对象。

MediaTimelineController _mediaTimelineController;
mediaPlayer = new MediaPlayer();
mediaPlayer.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
_mediaPlayerElement.SetMediaPlayer(mediaPlayer);


_mediaPlayer2 = new MediaPlayer();
_mediaPlayer2.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video_2.mkv"));
_mediaPlayerElement2.SetMediaPlayer(_mediaPlayer2);

_mediaTimelineController = new MediaTimelineController();

mediaPlayer.CommandManager.IsEnabled = false;
mediaPlayer.TimelineController = _mediaTimelineController;

_mediaPlayer2.CommandManager.IsEnabled = false;
_mediaPlayer2.TimelineController = _mediaTimelineController;

警告MediaPlaybackCommandManagerMediaPlayer 和系统媒体传输控件 (SMTC) 之间提供自动集成,但是这种自动集成无法和使用 MediaTimelineController 控制的媒体播放器一起使用。 因此,必须先禁用媒体播放器的命令管理器才能设置播放器的时间线控制器。 否则将导致引发异常并显示以下消息:“由于对象的当前状态,附加媒体时间线控制器的操作被阻止”。有关媒体播放器与 SMTC 集成的更多信息,请参阅与系统媒体传输控件集成。 使用 MediaTimelineController 时仍然可以手动控制 SMTC。 有关详细信息,请参阅手动控制系统媒体传输控件

MediaTimelineController 附加到一个或多个媒体播放器后,可以通过使用控制器公开的方法控制播放状态。 以下示例调用 Start 使所有关联的媒体播放器从媒体开头处开始播放。

private void PlayButton_Click(object sender, RoutedEventArgs e)
{
    _mediaTimelineController.Start();
}

本例介绍暂停并恢复所有附加的媒体播放器。

private void PauseButton_Click(object sender, RoutedEventArgs e)
{
    if(_mediaTimelineController.State == MediaTimelineControllerState.Running)
    {
        _mediaTimelineController.Pause();
        _pauseButton.Content = "Resume";
    }
    else
    {
        _mediaTimelineController.Resume();
        _pauseButton.Content = "Pause";
    }
}

为了使所有连接的媒体播放器快进,请将播放速度设置为大于 1 的值。

private void FastForwardButton_Click(object sender, RoutedEventArgs e)
{
    _mediaTimelineController.ClockRate = 2.0;
}

下一个示例介绍如何使用 Slider 控件显示时间线控制器的当前播放位置,该位置与某一个连接的媒体播放器播放的内容持续时间有关。 首先,创建新 MediaSource,并且为媒体源的 OpenOperationCompleted 注册处理程序。

var mediaSource = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
mediaSource.OpenOperationCompleted += MediaSource_OpenOperationCompleted;
mediaPlayer.Source = mediaSource;
_mediaPlayerElement.SetMediaPlayer(mediaPlayer);

使用 OpenOperationCompleted 处理程序有机会发现媒体源内容的持续时间。 确定持续时间后,Slider 控件的最大值设置为媒体项的总秒数。 此值在 RunAsync 调用中设置,以确保它在 UI 线程上运行。

TimeSpan _duration;
private async void MediaSource_OpenOperationCompleted(MediaSource sender, MediaSourceOpenOperationCompletedEventArgs args)
{
    _duration = sender.Duration.GetValueOrDefault();

    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        _positionSlider.Minimum = 0;
        _positionSlider.Maximum = _duration.TotalSeconds;
        _positionSlider.StepFrequency = 1;
    }); 
}

接下来,为时间线控制器的 PositionChanged 事件注册处理程序。 系统将定期调用它,频率大约为每秒 4 次。

_mediaTimelineController.PositionChanged += _mediaTimelineController_PositionChanged;

PositionChanged 的处理程序中,更新滑块值以反映时间线控制器的当前位置。

private async void _mediaTimelineController_PositionChanged(MediaTimelineController sender, object args)
{
    if (_duration != TimeSpan.Zero)
    {
        await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
        {
            _positionSlider.Value = sender.Position.TotalSeconds / (float)_duration.TotalSeconds;
        });
    }
}

使播放位置偏离时间线位置

在某些情况下,与时间线控制器关联的一个或多个媒体播放器的播放位置可能需要偏离其他播放器。 为此,可以设置要偏离的 MediaPlayer 对象的 TimelineControllerPositionOffset 属性。 以下示例使用两个媒体播放器的内容持续时间设置两个滑块控件的最小值和最大值,以增加或减少项目的时间长度。

_timelineOffsetSlider1.Minimum = -1 * _duration.TotalSeconds;
_timelineOffsetSlider1.Maximum = _duration.TotalSeconds;
_timelineOffsetSlider1.StepFrequency = 1;

_timelineOffsetSlider2.Minimum = -1 * _duration2.TotalSeconds;
_timelineOffsetSlider2.Maximum = _duration2.TotalSeconds;
_timelineOffsetSlider2.StepFrequency = 1;

在每个滑块的 ValueChanged 事件中,将每个播放器的 TimelineControllerPositionOffset 设置为相应值。

private void _timelineOffsetSlider1_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    mediaPlayer.TimelineControllerPositionOffset = TimeSpan.FromSeconds(_timelineOffsetSlider1.Value);
}

private void _timelineOffsetSlider2_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    _mediaPlayer2.TimelineControllerPositionOffset = TimeSpan.FromSeconds(_timelineOffsetSlider2.Value);
}

请注意,如果播放器的偏离值映射到反向播放位置,剪辑将保持暂停状态,直到偏离值达到零后才开始播放。 同样地,如果偏离值映射到的播放位置大于媒体项的持续时间,将显示最后一帧,就像单个媒体播放器播放完内容一样。

使用 MediaPlayer 播放球面视频

从 Windows 10 版本 1703 开始,MediaPlayer 支持用于球面视频播放的 equirectangular 投影。 由于只要视频编码受支持,MediaPlayer 便会呈现视频,因此球面视频内容与常规的平面视频没什么不同。 对于包含的元数据标记指定视频使用 equirectangular 投影的球面视频,MediaPlayer 可以使用指定视野和视图方向呈现视频。 这样可实现使用头盔显示屏的虚拟现实视频播放或是仅仅允许用户使用鼠标或键盘输入在球面视频中四处平移这类方案。

若要播放球面视频,请使用本文前面介绍的用于播放视频内容的步骤。 还有一个步骤是为 MediaPlayer.MediaOpened 事件注册处理程序。 此事件使你可以启用并控制球面视频播放参数。

mediaPlayer = new MediaPlayer();
mediaPlayer.MediaOpened += _mediaPlayer_MediaOpened;
mediaPlayer.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video_spherical.mp4"));
_mediaPlayerElement.SetMediaPlayer(mediaPlayer);
mediaPlayer.Play();

MediaOpened 处理程序中,首先通过检查 PlaybackSession.SphericalVideoProjection.FrameFormat 属性来检查新打开的媒体项的帧格式。 如果此值为 SphericaVideoFrameFormat.Equirectangular,则系统可以自动投影视频内容。 首先,将 PlaybackSession.SphericalVideoProjection.IsEnabled 属性设置为 true。 还可以调整媒体播放器会用于投影视频内容的属性(如视图方向和视野)。 在此示例中,通过设置 HorizontalFieldOfViewInDegrees 属性,将视野设置为较宽值 120 度。

如果视频内容是球面的,但采用 equirectangular 以外的格式,则可以通过使用媒体播放器的帧服务器模式接收和处理各个帧来实现自己的投影算法。

private void _mediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
    if (sender.PlaybackSession.SphericalVideoProjection.FrameFormat == SphericalVideoFrameFormat.Equirectangular)
    {
        sender.PlaybackSession.SphericalVideoProjection.IsEnabled = true;
        sender.PlaybackSession.SphericalVideoProjection.HorizontalFieldOfViewInDegrees = 120;

    }
    else if (sender.PlaybackSession.SphericalVideoProjection.FrameFormat == SphericalVideoFrameFormat.Unsupported)
    {
        // If the spherical format is unsupported, you can use frame server mode to implement a custom projection
    }
}

以下示例代码演示如何使用向左和向右箭头键来调整球面视频视图方向。

protected override void OnKeyDown(KeyRoutedEventArgs e)
{
    if (mediaPlayer.PlaybackSession.SphericalVideoProjection.FrameFormat != SphericalVideoFrameFormat.Equirectangular)
    {
        return;
    }

    switch (e.Key)
    {
        case Windows.System.VirtualKey.Right:
            mediaPlayer.PlaybackSession.SphericalVideoProjection.ViewOrientation *= Quaternion.CreateFromYawPitchRoll(.1f, 0, 0);
            break;
        case Windows.System.VirtualKey.Left:
            mediaPlayer.PlaybackSession.SphericalVideoProjection.ViewOrientation *= Quaternion.CreateFromYawPitchRoll(-.1f, 0, 0);
            break;
    }
}

如果应用支持视频的播放列表,则可能要在 UI 中标识包含球面视频的播放项。 媒体播放列表在文章媒体项、播放列表和轨中进行了详细讨论。 以下示例演示如何创建新播放列表、添加项以及为 MediaPlaybackItem.VideoTracksChanged 事件(解析媒体项的视频轨时发生)注册处理程序。

var playbackList = new MediaPlaybackList();
var item = new MediaPlaybackItem(MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/RIFTCOASTER HD_injected.mp4")));
item.VideoTracksChanged += Item_VideoTracksChanged;
playbackList.Items.Add(item);
mediaPlayer.Source = playbackList;

VideoTracksChanged 事件处理程序中,通过调用 VideoTrack.GetEncodingProperties 来获取任何已添加视频轨的编码属性。 如果编码属性的 SphericalVideoFrameFormat 属性是 SphericaVideoFrameFormat.None 之外的值,则视频轨包含球面视频,可以相应地更新 UI(如果选择)。

private void Item_VideoTracksChanged(MediaPlaybackItem sender, IVectorChangedEventArgs args)
{
    if (args.CollectionChange != CollectionChange.ItemInserted)
    {
        return;
    }
    foreach (var videoTrack in sender.VideoTracks)
    {
        if (videoTrack.GetEncodingProperties().SphericalVideoFrameFormat != SphericalVideoFrameFormat.None)
        {
            // Optionally indicate in the UI that this item contains spherical video
        }
    }
}

在帧服务器模式中使用 MediaPlayer

从 Windows 10 版本 1703 开始,可以在帧服务器模式中使用 MediaPlayer。 在此模式中,MediaPlayer 不会自动将帧呈现到关联的 MediaPlayerElement。 相反,应用会将当前帧从 MediaPlayer 复制到实现 IDirect3DSurface 的对象。 此功能实现的主要方案是使用像素着色器处理 MediaPlayer 提供的视频帧。 应用负责在处理之后显示每个帧,如通过在 XAML Image 控件中显示帧。

在以下示例中,会初始化新 MediaPlayer 并加载视频内容。 接下来,会注册 VideoFrameAvailable 的处理程序。 通过将 MediaPlayer 对象的 IsVideoFrameServerEnabled 属性设置为 true 来启用帧服务器模式。 最后,使用 Play 调用开始媒体播放。

mediaPlayer = new MediaPlayer();
mediaPlayer.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
mediaPlayer.VideoFrameAvailable += mediaPlayer_VideoFrameAvailable;
mediaPlayer.IsVideoFrameServerEnabled = true;
mediaPlayer.Play();

下一个示例演示 VideoFrameAvailable 的处理程序,它使用 Win2D 将简单模糊效果添加到视频的每个帧,然后在 XAML Image 控件中显示经过处理的帧。

每当调用 VideoFrameAvailable 处理程序时,都会使用 CopyFrameToVideoSurface 方法将帧的内容复制到 IDirect3DSurface。 还可以使用 CopyFrameToStereoscopicVideoSurfaces 将 3D 内容复制到两个图面,以便单独处理左眼和右眼内容。 为了获取实现 IDirect3DSurface 的对象,此示例创建一个 SoftwareBitmap,然后使用该对象创建实现所需接口的 Win2D CanvasBitmap。 CanvasImageSource 是 Win2D 对象,可以用作 Image 控件的源,因此会新建一个并设置为将在其中显示内容的 Image 的源。 接下来创建一个 CanvasDrawingSession。 这由 Win2D 用于呈现模糊效果。

实例化了所有所需对象之后,会调用 CopyFrameToVideoSurface,它将当前帧从 MediaPlayer 复制到 CanvasBitmap 中。 接下来,创建一个 Win2D GaussianBlurEffect,其 CanvasBitmap 设置为操作的源。 最后,调用 CanvasDrawingSession.DrawImage 以将应用了模糊效果的源图像绘制到与 Image 控件关联的 CanvasImageSource 中,从而在 UI 中绘制它。

private async void mediaPlayer_VideoFrameAvailable(MediaPlayer sender, object args)
{
    CanvasDevice canvasDevice = CanvasDevice.GetSharedDevice();

    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        if(frameServerDest == null)
        {
            // FrameServerImage in this example is a XAML image control
            frameServerDest = new SoftwareBitmap(BitmapPixelFormat.Rgba8, (int)FrameServerImage.Width, (int)FrameServerImage.Height, BitmapAlphaMode.Ignore);
        }
        if(canvasImageSource == null)
        {
            canvasImageSource = new CanvasImageSource(canvasDevice, (int)FrameServerImage.Width, (int)FrameServerImage.Height, DisplayInformation.GetForCurrentView().LogicalDpi);//96); 
            FrameServerImage.Source = canvasImageSource;
        }

        using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromSoftwareBitmap(canvasDevice, frameServerDest))
        using (CanvasDrawingSession ds = canvasImageSource.CreateDrawingSession(Windows.UI.Colors.Black))
        {

            mediaPlayer.CopyFrameToVideoSurface(inputBitmap);

            var gaussianBlurEffect = new GaussianBlurEffect
            {
                Source = inputBitmap,
                BlurAmount = 5f,
                Optimization = EffectOptimization.Speed
            };

            ds.DrawImage(gaussianBlurEffect);

        }
    });
}

private void FrameServerSubtitlesButton_Click(object sender, RoutedEventArgs e)
{

    mediaPlayer = new MediaPlayer();
    var source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/example_video.mkv"));
    var item = new MediaPlaybackItem(source);

    item.TimedMetadataTracksChanged += Item_TimedMetadataTracksChanged;


    mediaPlayer.Source = item;
    mediaPlayer.VideoFrameAvailable += mediaPlayer_VideoFrameAvailable_Subtitle;
    mediaPlayer.IsVideoFrameServerEnabled = true;
    mediaPlayer.Play();

    mediaPlayer.IsMuted = true;

}

private void Item_TimedMetadataTracksChanged(MediaPlaybackItem sender, IVectorChangedEventArgs args)
{
    if(sender.TimedMetadataTracks.Count > 0)
    {
        sender.TimedMetadataTracks.SetPresentationMode(0, TimedMetadataTrackPresentationMode.PlatformPresented);
    }
}

private async void mediaPlayer_VideoFrameAvailable_Subtitle(MediaPlayer sender, object args)
{
    CanvasDevice canvasDevice = CanvasDevice.GetSharedDevice();

    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        if (frameServerDest == null)
        {
            // FrameServerImage in this example is a XAML image control
            frameServerDest = new SoftwareBitmap(BitmapPixelFormat.Rgba8, (int)FrameServerImage.Width, (int)FrameServerImage.Height, BitmapAlphaMode.Ignore);
        }
        if (canvasImageSource == null)
        {
            canvasImageSource = new CanvasImageSource(canvasDevice, (int)FrameServerImage.Width, (int)FrameServerImage.Height, DisplayInformation.GetForCurrentView().LogicalDpi);//96); 
            FrameServerImage.Source = canvasImageSource;
        }

        using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromSoftwareBitmap(canvasDevice, frameServerDest))
        {
            using (CanvasDrawingSession ds = canvasImageSource.CreateDrawingSession(Windows.UI.Colors.Black))
            {

                mediaPlayer.CopyFrameToVideoSurface(inputBitmap);

                //Rect subtitleTargetRect = new Rect(0, 0, inputBitmap.Bounds.Width, inputBitmap.Bounds.Bottom * .1);
                Rect subtitleTargetRect = new Rect(0, 0, 100, 100);

                mediaPlayer.RenderSubtitlesToSurface(inputBitmap);//, subtitleTargetRect);

                //var gaussianBlurEffect = new GaussianBlurEffect
                //{
                //    Source = inputBitmap,
                //    BlurAmount = 5f,
                //    Optimization = EffectOptimization.Speed
                //};

                //ds.DrawImage(gaussianBlurEffect);

                ds.DrawImage(inputBitmap);
            }
        }
    });
}

有关 Win2D 的详细信息,请参阅 Win2D GitHub 存储库。 若要尝试上面显示的示例代码,需要按照以下说明将 Win2D NuGet 程序包添加到项目。

将 Win2D NuGet 程序包添加到你的效果项目

  1. 在“解决方案资源管理器”中,右键单击项目并选择“管理 NuGet 包”。
  2. 在窗口顶部,选择“浏览”选项卡。
  3. 在搜索框中,输入 Win2D
  4. 选择“Win2D.uwp”,然后选择右侧窗格中的“安装”。
  5. “查看更改”对话框将向你显示要安装的程序包。 单击 “确定”
  6. 接受程序包许可证。

检测系统的音频级别更改并做出响应

从 Windows 10 版本 1803 开始,应用可检测到系统何时降低或静音当前播放的 MediaPlayer 的音频级别。 例如,当警报响起时,系统可能降低(或者“闪避”)音频播放级别。 如果应用没有在应用清单中声明 backgroundMediaPlayback 功能,系统将在应用进入后台时将其静音。 AudioStateMonitor 类可用于注册以接收系统修改音频流的音量时出现的事件。 访问 MediaPlayerAudioStateMonitor 属性并为 SoundLevelChanged事件注册处理程序,以在系统更改该 MediaPlayer 的音频级别时收到通知。

mediaPlayer.AudioStateMonitor.SoundLevelChanged += AudioStateMonitor_SoundLevelChanged;

在处理 SoundLevelChanged 事件时,可以根据正在播放的内容的类型采取不同的操作。 如果当前正在播放音乐,可能希望在音量闪避时继续播放音乐。 但是,如果正在播放播客,那么可能希望在音频闪避时暂停播放,这样用户就不会错过任何内容。

此示例声明一个变量,该变量跟踪当前播放的内容是否为播客,假设在为 MediaPlayer 选择内容时将其设置为适当的值。 我们还创建类变量来跟踪在音频级别发生变化时何时以编程方式暂停播放。

bool isPodcast;
bool isPausedDueToAudioStateMonitor;

SoundLevelChanged 事件处理程序中,检查 AudioStateMonitor 发件人的 SoundLevel 属性,以确定新的音量。 此示例检查新的音量是否为完整音量,即系统已停止静音或闪避音量,或者音量是否已经降低,但正在播放非播客内容。 如果发生其中任何一种情况,并且内容以前以编程方式暂停,则恢复播放。 如果新的音量为静音,或者如果当前内容是播客,并且音量较低,则暂停播放,并将变量设置为跟踪,暂停以编程方式启动。

private void AudioStateMonitor_SoundLevelChanged(Windows.Media.Audio.AudioStateMonitor sender, object args)
{
    if ((sender.SoundLevel == SoundLevel.Full) || (sender.SoundLevel == SoundLevel.Low && !isPodcast))
    {
        if (isPausedDueToAudioStateMonitor)
        {
            mediaPlayer.Play();
            isPausedDueToAudioStateMonitor = false;
        }
    }
    else if ((sender.SoundLevel == SoundLevel.Muted) ||
         (sender.SoundLevel == SoundLevel.Low && isPodcast))
    {
        if (mediaPlayer.PlaybackSession.PlaybackState == MediaPlaybackState.Playing)
        {
            mediaPlayer.Pause();
            isPausedDueToAudioStateMonitor = true;
        }
    }

}

用户可以决定想要暂停还是继续播放,即使系统闪避音频。 此示例显示播放和暂停按钮的事件处理程序。 在暂停按钮中单击处理程序暂停,如果播放已经以编程方式暂停,则更新变量以指示用户已暂停内容。 在播放按钮中单击处理程序,继续播放并清除跟踪变量。

private void PauseButton_User_Click(object sender, RoutedEventArgs e)
{
    if (isPausedDueToAudioStateMonitor)
    {
        isPausedDueToAudioStateMonitor = false;
    }
    else
    {
        mediaPlayer.Pause();
    }
}

public void PlayButton_User_Click()
{
    isPausedDueToAudioStateMonitor = false;
    mediaPlayer.Play();
}