65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 高级视图模型风格视频播放器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (32投票s)

2010 年 4 月 19 日

Ms-PL

12分钟阅读

viewsIcon

86010

downloadIcon

4024

功能齐全的 Silverlight 视图模型风格视频播放器。

imgFwdRwd.jpg

一个高级视频播放器

实时示例: http://silverlight.adefwebserver.com/

这是本系列文章 Silverlight 4 视频播放器 的第二部分。那篇文章侧重于视图模型样式 (模型-视图-视图模型) 模式,以及它如何支持设计师/开发者的协作和工作流程。但是,当其他人尝试将该项目用于实际网站时,他们发现需要视频播放器通常提供的全部控件。我决定利用这个机会深入研究视图模型样式,并演示使用Microsoft Expression Blend 4 (或更高版本) 时它的真正简单性。这一次我们将涵盖“棘手的部分”。然而,实现这一点将令人惊讶地容易。

本项目创建一个完全“可设计”的 Silverlight 视频播放器。这不应与“可换肤”视频播放器混淆。可换肤视频播放器允许您更改视频播放器现有控件的外观和感觉。可设计播放器允许设计师使用任何控件集来实现视频播放器。例如,设计师可以将所有按钮替换为用户旋转到不同位置的单个旋钮,或者他们可以更改用户用于从可用视频列表中选择的方法。这可以在 Expression Blend 中完成,而无需编写任何代码。

2010 年 5 月 16 日更新: Victor Gaudioso 提供了一个示例代码,演示了如何快进和快退视频。他的代码已改编用于此项目。他的原始文章可以在这里找到: http://victorgaudioso.wordpress.com/2010/05/15/new-silverlight-video-tutorial-how-to-create-fast-forward-for-the-mediaelement/

其他下载

您还需要下载并安装Silverlight Toolkit: http://silverlight.codeplex.com/

视图模型样式 (概述)

视图模型样式模式允许程序员创建一个绝对没有 UI 的应用程序。程序员只创建一个视图模型和一个模型。没有任何编程能力的设计师,然后可以从一个空白页面开始,完全在Microsoft Expression Blend 4 (或更高版本) 中创建视图(UI)。

是的,您也可以将代码放在代码隐藏中 (例如,您将一个按钮放在页面上,然后双击它,它就会在代码隐藏中连接一个事件处理程序),但那样设计师只能使用一个按钮来完成该任务。他们不能将元素更改为旋钮。最重要的是,设计师需要了解一点编码,因为他们将直接与代码交互。使用视图模型样式 (或在 Expression Blend 模板中现在称为数据驱动应用程序),基本上

  • 程序员创建由属性集合命令组成的代码。
  • 设计师使用 Expression Blend,无需编写任何代码即可创建完整的 UI。

模型是数据所在的位置,通常是 Web 服务。视图是 UI,或“前端”。视图模型是“魔术”发生的地方。它由

  • 属性 – 一种东西。这可以是一个字符串或一个对象。实现了INotifyPropertyChanged,因此任何绑定到它的元素在它发生更改时都会自动收到通知。
  • 集合 – 一种东西的集合。这是ObservableCollection类型,因此任何绑定到它的元素在它发生更改时都会自动收到通知。
  • 命令 – 可以引发的事件。另外,可以传递一个 Object 类型的参数。这实现了ICommand

如果您是视图模型样式的新手,建议您阅读 Silverlight 视图模型样式:一个(过于)简化的解释 以获得介绍。

入门解决方案

我们将从 **MVVMVideoPlayer_Starter.zip** 文件中包含的解决方案开始。运行项目时,Web 项目中的 Web 服务将检测“Video”文件夹中有哪些视频,并将它们传递给 Silverlight 应用程序。

  • 视频列表将在组合框下拉列表中显示。
  • 进度条将显示当前进度。
  • 视频的确切位置将显示在视频上方。
  • 视频的当前时间和总时间也将显示。
  • 播放按钮将播放视频
  • 停止按钮将停止视频。

这就是 Silverlight 4 视频播放器 文章所涵盖的内容。要制作功能齐全的视频播放器,我们需要添加以下功能

  • 音量控制 - 用户需要能够更改视频的音量。
  • 暂停按钮 - 用户需要能够暂停视频,然后通过再次单击暂停按钮或单击播放按钮来恢复播放。
  • “搜寻”控件 - 用户应该能够在视频播放时轻松地向前和向后跳动。
  • “视频缓冲”通知 - 当视频加载时,需要出现一个通知,并指示视频开始播放还需要多长时间。
  • 全屏视频 - 观看视频时,最好有一个选项,单击按钮即可全屏观看视频。您需要能够无缝地来回切换。另外,只有视频应该是全屏的,而不是整个 Silverlight 应用程序。

我们将仅通过向视图模型添加代码来实现所有这些功能。模型根本没有改变。代码已包含在 **MVVMVideoPlayer_Starter.zip** 文件中 (将在本文末尾详细介绍)。现在,我们将从设计器用来实现此附加功能的步骤开始。首先,设计器在 Expression Blend 中打开项目...

音量控制

此控件实际上执行两项操作

  • 指示当前音量
  • 允许更改音量

Expression Blend中,在Assets中,获取一个Slider控件。

将其拖放到Objects and Timeline窗口中的[StackPanel]中。

按键盘上的“V”键切换到Selection工具。然后,在设计表面上,使用鼠标稍微加宽滑块。

SliderProperties中,将左侧Margin设置为5

Maximum级别设置为1,将Minimum设置为0 (Media Element上的音量实际上仅从 0 到 1。例如,它从 0.5 开始。)

单击Value旁边的Advanced options框 (在Common Properties中)。

选择数据绑定...

  • 选择Element Property选项卡
  • Scene elements部分选择mediaElement
  • Properties部分选择Volume
  • Binding direction选择TwoWay
  • 单击 **确定** 按钮

F5构建并运行项目。 Web 浏览器将打开,您现在可以控制音量了。

暂停按钮

同样,眼见不一定为实

  • 单击Pause时,视频应停止
  • 如果视频已暂停,再次单击Pause,它应从中断处继续
  • 如果视频已暂停,单击Play,它仍应从中断处继续

Assets中,获取一个Button控件。

将其拖放到Objects and Timeline窗口中PlayButtonStopButton之间。右键单击它并将其重命名为“PauseButton”。

Properties中,将Content设置为“Pause”,并将左侧Margin设置为5

Assets中,获取一个InvokeCommand行为。

将其拖放到Objects and Timeline窗口中**PauseButton**下方 (请记住您在之前的步骤中已将其重命名)。

在行为的Properties中,选择Data bind旁边的Command

Data Context窗口中选择PauseVideoCommand,然后单击OK按钮。

F5构建并运行项目。 Web 浏览器将打开,您现在可以Pause视频了。

搜寻控件

我们希望提供在视频播放时向前和向后跳动到特定位置的功能。此功能可以使用任何控件来实现。例如,您可以使用Slider控件或自定义旋钮控件。在本例中,我们将仅使用ProgressBar控件。ProgressBar控件仍将显示进度,但当您单击控件时,它将导航到视频中的该部分。

Assets中,获取一个InvokeCommandAction行为。

将其拖放到Objects and Timeline窗口中**mediaElement**下方。右键单击它并将其重命名为“SetSeekControl”。

在行为的Properties

  • EventName设置为MediaOpened (当视频设置为MediaElement的源,并且它找到了视频并准备好播放它时,此事件会自动触发)。
  • 单击Command旁边的Data bind按钮。

将其绑定到SetSeekControlCommand

单击CommandParameter旁边的Advanced options框。

  • 选择Element Property选项卡
  • Scene elements部分选择progressBar
  • Properties部分选择ProgressBar
  • 单击 **确定** 按钮

F5构建并运行项目。 Web 浏览器将打开,您现在可以通过单击ProgressBar控件来跳到视频的各个部分了。

视频缓冲通知

MediaElement播放视频时,它会加载视频“提前”播放的部分。如果它无法足够快地加载视频,它将停止并“缓冲”视频,然后在缓冲完成后 (缓冲达到 100% 时) 恢复播放。我们希望缓冲通知执行以下功能

  • 视频正在缓冲时显示
  • 视频未缓冲时消失
  • 实时显示缓冲百分比,在缓冲过程中

在 Assets 中,搜索BusyIndicator

将其拖到设计表面,并将其放置在MediaElement上。

BusyIndicatorProperties

  • 清除BusyContent
  • 清除Content

BusyContent绑定到MediaBufferingTimeProperty (这将显示缓冲进度)。

IsBusy属性绑定到MediaBufferingProperty (这将控制缓冲框何时显示)。

您可以通过它们周围的金色框知道您已绑定属性。

全屏视频

对于全屏视频功能,我们希望实现以下目标

  • 处于全屏模式时,我们只想看到视频,而不是其他控件
  • 来回切换时,我们希望视频无缝地继续播放
  • 如果我们单击视频或全屏按钮,我们希望视频进入全屏模式

将 Silverlight 应用程序切换到全屏模式非常简单。仅切换 Silverlight 应用程序的一个元素涉及一些额外的步骤。我们需要创建一个Grid,然后指示ViewModel在进入全屏模式时使用此GridViewModel将自动创建一个VideoBrush并将其源设置为MediaElement,以便视频在两种模式下保持同步。

Tools栏的Panel部分,选择Grid

双击Grid将其插入到设计表面。

GridProperties

  • WidthHeight设置为Auto
  • RowSpan设置为2
  • HorizontalAlignmentVerticalAlignment设置为Stretch

将一个InvokeCommand行为从Objects and Timeline窗口拖放到MediaElement下方,并将其重命名为“FullScreen”。

在行为的Properties中,将Command绑定到SetFullScreenCommand

CommandParameter绑定到[Grid]

这将启用单击视频时进入全屏模式。现在要触发返回正常模式...

将一个InvokeCommand行为从Objects and Timeline窗口中的LayoutRoot拖放到下方。

在行为的Properties

  • EventName设置为SizeChanged
  • Command绑定到ExitFullScreenCommand
  • Grid绑定到CommandParameter

F5构建并运行项目。当您单击视频时,它将切换到全屏模式。

返回Expression Blend并执行以下操作来创建Full Screen按钮

  • 将一个按钮放在Stop按钮旁边,并将其内容设置为“Full Screen
  • 将左侧Margin设置为5
  • 将一个InvokeCommand行为从它下方拖放。
  • 在行为的Properties中,将Command绑定到SetFullScreenCommand
  • CommandParameter绑定到[Grid]

代码

- 设计师可以跳到结尾,这里没什么可看的 :) -

让我们看看用于实现每个部分的相应代码...

没有代码!Slider控件直接绑定到MediaElementVolume属性。我知道……哇!

Pause

ViewModel的构造函数中,所有ICommands都已设置好,包括PauseVideoCommand

    public MainViewModel()
    {
        // Set the command property
        MediaOpenedCommand = new DelegateCommand(MediaOpened, CanMediaOpened);
        PlayVideoCommand = new DelegateCommand(PlayVideo, CanPlayVideo);
        StopVideoCommand = new DelegateCommand(StopVideo, CanStopVideo);
        PauseVideoCommand = new DelegateCommand(PauseVideo, CanPauseVideo);
        SetSeekControlCommand = new DelegateCommand(SetSeekControl, CanSetSeekControl);
        SetVideoCommand = new DelegateCommand(SetVideo, CanSetVideo);
        SetFullScreenCommand = new DelegateCommand(SetFullScreen, CanSetFullScreen);
        ExitFullScreenCommand = new DelegateCommand(ExitFullScreen, CanExitFullScreen);   
    
        // Call the Model to get the collection of Videos
        GetListOfVideos();
    }

其余的实现如下。基本上,它只是调用MediaElement上的Pause()。其余代码处理是否可以暂停,是否已暂停,以及如果已暂停,是否应改为播放。

       #region PauseVideoCommand
        public ICommand PauseVideoCommand { get; set; }
        public void PauseVideo(object param)
        {
            // We only want to Pause if the media is Playing
            if (MyMediaElement.CurrentState == MediaElementState.Playing)
            {
                // If we can Pause the Video, Pause it
                if (MyMediaElement.CanPause)
                {
                    // Pause Video
                    MyMediaElement.Pause();
                }
                else
                {
                    // We can't pause the Video so Stop it
                    MyMediaElement.Stop();
                }

                if (progressTimer.IsEnabled)
                {
                    progressTimer.Stop();
                }
            }
            else
            {
                // The Media is not Playing so we are Paused
                // Play Video
                MyMediaElement.Play();
                progressTimer.Start();
            }

        }

        private bool CanPauseVideo(object param)
        {
            bool CanPause = false;

            if (MyMediaElement != null)
            {
                // Only allow this Command if paused or Playing
                if ((MyMediaElement.CurrentState == MediaElementState.Paused)
                    || (MyMediaElement.CurrentState == MediaElementState.Playing))
                {
                    CanPause = true;
                }
            }

            return CanPause;
        }
        #endregion

Seek

这个有点意思,基本上,你首先传入一个FrameworkElement,它将用于执行 Seek。鼠标事件会附加到这个 FrameworkElement 上,这样当有人单击该元素时就会引发一个事件。当事件被引发时,元素的宽度将用于确定用户在元素上的哪个位置单击了。该值用于确定在视频中导航到哪个位置。

        #region SetSeekControlCommand
        public ICommand SetSeekControlCommand { get; set; }
        public void SetSeekControl(object param)
        {
            // Hook Events into the Seek Control
            SeekControl = (FrameworkElement)param;
            SeekControl.MouseLeftButtonDown += new MouseButtonEventHandler(SeekControl_MouseLeftButtonDown);
        }

        private bool CanSetSeekControl(object param)
        {
            return true;
        }

        private void SeekControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            double position = e.GetPosition(SeekControl).X;
            double percent = position / SeekControl.ActualWidth;
            Seek(percent);
        }

        private void Seek(double percentComplete)
        {
            TimeSpan duration = MyMediaElement.NaturalDuration.TimeSpan;
            int newPosition = (int)(duration.TotalSeconds * percentComplete);
            MyMediaElement.Position = new TimeSpan(0, 0, newPosition);
            SetCurrentPosition();
        }
        #endregion

视频缓冲

使用DispatcherTimer每秒检查一次视频进度,并更新属性,包括缓冲属性。

        #region Time display
        private void ProgressTimer_Tick(object sender, EventArgs e)
        {
            SetCurrentPosition();
        }

        private void SetCurrentPosition()
        {
            // If the Media play is complete stop the media
            if (CurrentPositionProperty > 0)
            {
                if (CurrentPositionProperty >= TotalDurationProperty)
                {
                    // If in full screen mode - exit full screen mode
                    var content = Application.Current.Host.Content;
                    if (content.IsFullScreen)
                    {
                        content.IsFullScreen = false;
                    }

                    CurrentPositionProperty = 0;
                    StopVideo(null);
                }
            }

            // Update the time text e.g. 01:50 / 03:30
            CurrentProgressProperty = string.Format(
                "{0}:{1} / {2}:{3}",
                Math.Floor(MyMediaElement.Position.TotalMinutes).ToString("00"),
                MyMediaElement.Position.Seconds.ToString("00"),
                Math.Floor(MyMediaElement.NaturalDuration.TimeSpan.TotalMinutes).ToString("00"),
                MyMediaElement.NaturalDuration.TimeSpan.Seconds.ToString("00"));

            CurrentPositionProperty = MyMediaElement.Position.TotalSeconds;
            TotalDurationProperty = MyMediaElement.NaturalDuration.TimeSpan.TotalSeconds;
            MediaBufferingProperty = (MyMediaElement.CurrentState == MediaElementState.Buffering);
            MediaBufferingTimeProperty = String.Format("Buffering {0} %", (MyMediaElement.BufferingProgress * 100).ToString("##"));
        }
        #endregion

注意: 回过头来看,完全有可能在不使用DispatcherTimer的情况下实现整个示例。MediaElement上的InvokeAction行为可能就是更新属性所需的一切……好吧,也许下次吧。

全屏视频

设计师必须指定一个Grid用于全屏模式。此Grid必须设置为拉伸并填充整个屏幕,遮挡应用程序的其余部分 (所以是的,在全屏模式下,应用程序的其余部分实际上仍然在Grid后面)。

代码动态地将一个VideoBrush放入Grid中,并将其源设置为MediaElement,以便视频保持同步。可以在Grid中放置其他元素,例如,您可以在鼠标移动时显示播放器控件。

        #region SetFullScreenCommand
        public ICommand SetFullScreenCommand { get; set; }
        public void SetFullScreen(object param)
        {
            if (MyMediaElement != null)
            {
                // Put application in full screen mode
                var content = Application.Current.Host.Content;
                content.IsFullScreen = true;

                // Set the Video Brush to the content of the MediaElement
                VideoBrush objVideoBrush = new VideoBrush();
                objVideoBrush.SetSource(MyMediaElement);
                objVideoBrush.Stretch = Stretch.UniformToFill;

                // A Grid to show in full screen needs to be passed as the parameter
                // Set the background content of that panel to the VideoBrush

                // Note: Other elements and controls can be placed on this Grid
                // It does not have to be blank
                Grid objGrid = (Grid)param;
                objGrid.Visibility = Visibility.Visible;
                objGrid.Background = objVideoBrush;
            }
        }

        private bool CanSetFullScreen(object param)
        {
            // Only allow full screen if not in full screen
            var content = Application.Current.Host.Content;
            return (!content.IsFullScreen);
        }
        #endregion

这是退出全屏时的代码

        #region ExitFullScreenCommand
        public ICommand ExitFullScreenCommand { get; set; }
        public void ExitFullScreen(object param)
        {
            // A Panel to show in full screen needs to be passed as the parameter
            // Set this element to invisible
            Panel objPanel = (Panel)param;
            objPanel.Visibility = Visibility.Collapsed;
        }

        private bool CanExitFullScreen(object param)
        {
            // Only allow exit full screen if not in full screen
            // This may seem odd, but this command is being called by a SizeChanged event
            // If we ARE in full screen then we were actually just recently NOT in full screen
            // We only want to fire the ExitFullScreen if we were actually just In full screen
            // but NOT in full screen any more
            var content = Application.Current.Host.Content;
            return (!content.IsFullScreen);
        }
        #endregion

请“粉饰”此视频播放器!

您是设计师,还是想成为设计师?下载此项目,设计一个很酷的设计。代码是开源的,因此您可以重做应用程序,并发布教程或博客来展示您创建的内容。要了解如何使用 Expression Blend,您只需转到

http://www.microsoft.com/design/toolbox/

© . All rights reserved.