使用WPF开发VLC媒体播放器

在利用LibVLCSharp.WPF开发WPF媒体播放器的过程中,出现了一些问题并进行了解决,这里做一下总结:

  1. 依据官方例子,需要引用2个程序包:

  1. 界面设计思考

具备播放/暂停/前一首/后一首/播放列表管理/音轨选择/全窗口拖放等功能,满足最便捷的界面,最简单的操作,无需复杂的参数设定,界面如下:

  1. 上部最右边一个关闭按钮,提供退出程序的功能
  2. 底部为操作面板,上面一排是播放进度条,下面一排的功能从左至右依次为:

音轨选择/当前正在播放的文件名称/前一首/播放暂停/后一首/声音及调节/打开单个或者多个文件/显示掩藏播放列表界面/窗口全屏

窗口采用无边框设计,属性设定如下:

Background="Black" 
WindowStyle="None" 
AllowsTransparency="True"
WindowStartupLocation="CenterScreen"

窗口具备全屏拖放/双击在最大化和正常化转换/窗口尺寸可调节,考虑到播放组件为Host模式,这个VideoView组件会完全遮挡窗口的各种事件响应,因此考虑在VideoView组件中内嵌一个容器,所有的事件针对容器进行:

如上述结构,VideoView组件就是播放控件,其外部套了一个Border,主要用于窗口边框显示,内嵌的Grid组件则是所有窗口事件的核心对象,窗口拖放操作/窗口双击事件/外部文件拖放至窗口事件/窗口鼠标移动事件都是针对它的,必须给这个Grid设置一个透明度为0的背景色,譬如ackground="#01000000" 否则Grid不会响应任何鼠标事件。

窗口的拖放必须单独定义,如果使用WindowChrome或者其他方式,都会被播放组件覆盖而失效,当然可以设置播放控件让开边框6-8个单位避免覆盖,但这样又会让视频不能完全覆盖整个窗口,周围有一圈不会显示,感觉不是很完美,为此可定义8个Thumb放在gridRoot里面,且一定要放在最顶层,即播放控件的上方,这样不会占用播放控件的空间且具备窗口尺寸调整功能,如下定义:

<!--#region 窗口尺寸调整元素-->
<Thumb x:Name="LeftThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeWE"
 HorizontalAlignment="Left" VerticalAlignment="Stretch" Margin="0 6" Width="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="RightThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeWE"
  HorizontalAlignment="Right" VerticalAlignment="Stretch" Margin="0 6" Width="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="TopThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNS"
  HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="6 0" Height="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="BottomThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNS"
  HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Margin="6 0" Height="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="LeftTopThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNWSE"
 HorizontalAlignment="Left" VerticalAlignment="Top" Width="6" Height="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="RightTopThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNESW"
  HorizontalAlignment="Right" VerticalAlignment="Top" Width="6" Height="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="LeftBottomThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNESW"
  HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="6" Height="6" DragDelta="Thumb_DragDelta"/>

<Thumb x:Name="RightBottomThumb" Style="{StaticResource TransportThumbStyle}" Cursor="SizeNWSE"
  HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="6" Height="6" DragDelta="Thumb_DragDelta"/>
<!--#endregion-->

Thumb需要设定为透明样式,否则在黑色背景下非常显眼:

<Style TargetType="{x:Type Thumb}" x:Key="TransportThumbStyle">
    <Setter Property="Background" Value="#01000000"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate  TargetType="{x:Type Thumb}">
                <Rectangle Fill="{TemplateBinding Background}"
                       Stroke="{TemplateBinding BorderBrush}"
                       StrokeThickness="{TemplateBinding BorderThickness}"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

窗口拖放具体的网上有很多例子,这里就不加说明了,需要说明的是,在最大窗口下需要隐藏这8个拖放块,代码如下:

//窗口尺寸调整
    private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
    {
        if (this.WindowState == WindowState.Maximized)
        {
            return;
        }
        var thumb = sender as Thumb;

        if (thumb != null)
        {
            double left = Left, top = Top, width = Width, height = Height;
            //x坐标调整
            if (thumb.Name.Contains("Left"))
            {
                left = Left + e.HorizontalChange;
                width = Width - e.HorizontalChange;
                if (width > 63)
                {
                    Left = left;
                    Width = width;
                }
            }
            if (thumb.Name.Contains("Right"))
            {
                left = Left;
                width = Width + e.HorizontalChange;
                if (width > 63)
                {
                    Left = left;
                    Width = width;
                }
            }

            if (thumb.Name.Contains("Top"))
            {

                top = Top + e.VerticalChange;
                height = Height - e.VerticalChange;
                if (height > 63)
                {
                    Top = top;
                    Height = height;
                }
            }
            if (thumb.Name.Contains("Bottom"))
            {
                top = Top;
                height = Height + e.VerticalChange;
                if (height > 63)
                {
                    Top = top;
                    Height = height;
                }
            }

        }
    }
  1. 操作面板的自动掩藏

当鼠标移动/窗口状态(WindowState属性)发生变化时,都需要显示操作面板,而在其他情况下延时并掩藏操作面板,在最大化窗口下还需要掩藏鼠标。

这里创建一个控制显示的绑定属性:

Visibility _PanelVisibility = Visibility.Visible;
/// <summary>
/// 面板可见性
/// <summary>
public Visibility PanelVisibility
{
    get { return _PanelVisibility; }
    set
    {
        if (_PanelVisibility != value)
        {
            if (value != Visibility.Visible && 
                (View.headerPanel.IsMouseOver || View.operatePanel.IsMouseOver
                || View.playListView.Visibility == Visibility.Visible))
            {
                return;
            }
            _PanelVisibility = value;
            OnPropertyChanged(() => PanelVisibility);
            View.PanelVisibilityChanged();
            if (View.WindowState == WindowState.Maximized && value != Visibility.Visible)
            {
                View.gridRoot.Cursor = Cursors.None;
            }
        }
    }
}
//控制定时器的启停如下:
public void PanelVisibilityChanged()
 {
     if (MainView.PanelVisibility != Visibility.Visible)
     {
         hidePanelTimer.Stop();
     }
     else
     {
         if (hidePanelTimer.IsEnabled)
         {
             hidePanelTimer.Stop();
         }
         hidePanelTimer.Start();
     }
}

3、有关数据绑定

使用WPF,就会考虑到MVVM模式,进行数据和界面的分离,本项目并不大,且会涉及到很多UI事件编程,这里创建一个视图模式类MainViewModel,主要是方便UI界面的绑定,并没有考虑它和窗口的代码分离,事实上会把窗口传递给它,视图可以操作窗口元素,窗口元素也可以操作视图,一般把事件触发放在窗口后台代码中,绑定属性和绑定命令放在视图中,这点如果WPF拥有UWP/WinUI3的x:Bind就可以简单绑定到窗口的属性,而不需要创建视图模式了。

4、播放器事件介绍

//初始化播放器
void InitPlayer()
{
    LibVLC = new LibVLC();
    Player = new MediaPlayer(LibVLC);
    // we need the VideoView to be fully loaded before setting a MediaPlayer on it.
    VideoView.Loaded += (sender, e) =>
    {
        VideoView.MediaPlayer = Player;
        Player.EnableHardwareDecoding = true;//容许硬件解码
        Player.PositionChanged += Player_PositionChanged;
        Player.EndReached += Player_EndReached;
        Player.LengthChanged += Player_LengthChanged;
        Player.Stopped += Player_Stopped;
        Player.Paused += Player_Paused;
        Player.Playing += Player_Playing;
        Player.Opening += Player_Opening;
    };
}

Vlc播放器没有状态发生改变事件,因此若需要检测播放器当前状态,需要配合三个事件的使用:

Player_Playing/ Player_Paused/ Player_Stopped,在视图中创建播放器的状态属性,然后通过这3个事件修改其值,通过绑定可更新UI

其他一些事件在下面的功能介绍中会用到。

5、播放进度条的实现

    这会用到播放器的两个属性,Length属性返回当前播放媒介的总长度(单位ms),Position为当前正在播放的位置(范围为0-1.0),应该代表着播放百分比。

   对应的xaml如下:

<Slider    Grid.Column="1" IsSelectionRangeEnabled="True" 
           IsMoveToPointEnabled="True"
           IsSnapToTickEnabled="False" 
           Focusable="False" Margin="10,0,10,0" VerticalAlignment="Center" 
           Value="{Binding MediaPosition, Mode=TwoWay}" 
           Minimum="0"
           Maximum="{Binding MediaLength}">
</Slider>

     正常来说媒体长度应该是在打开媒体后进行更新,但Vlc播放器没有打开完成事件,只有一个正在打开事件Player_Opening,在这个事件中无法获取到Length属性的值,因此只能添加一个媒体长度变更事件来进行更新:

 private void Player_LengthChanged(object sender, MediaPlayerLengthChangedEventArgs e)
 {
     MainView.MediaLength = Player.Length;//更新媒体长度
 }

  //创建一个位置绑定属性:
 public long MediaPosition
{
    get { return _MediaPosition; }
    set
    {
        if (_MediaPosition != value)
        {
            _MediaPosition = value;
            OnPropertyChanged(() => MediaPosition);
            //调整播放器当前位置
            if (MediaLength <= 0)
                View.Player.Position = 0;
            else
            {
                View.Player.Position = (float)(Convert.ToDouble(_MediaPosition) / Convert.ToDouble(MediaLength));

            }
        }
    }
}
//一个外部更新方法:
public void SetMediaPosition(long position)
{
    _MediaPosition = position;
    OnPropertyChanged(() => MediaPosition);
}

//添加播放器的位置变化事件:
private void Player_PositionChanged(object sender, MediaPlayerPositionChangedEventArgs e)
 {
     //设置媒体当前位置
     MainView.SetMediaPosition(Convert.ToInt64(e.Position* Player.Length));
 }

这样通过Player_PositionChanged事件实时更新进度条,同时在拖放进度条时,通过属性绑定立即修改播放器的当前位置。

6、声音大小的控制

通过播放器的Volume属性进行控制,其值为0-100,它和打开媒介无关,因此在播放器首次加载后执行一次属性值的获取,后面就可通过滑动条给播放器设定声音大小了

Xaml如下:

<Slider  Width="100" VerticalAlignment="Center"  Margin="0 0 20 0"
          Minimum="0" Maximum="100" 
          Value="{Binding MediaVolume,Mode=TwoWay}"/>

需要补充设定Slider属性IsMoveToPointEnabled="True",否则不会移动到鼠标点击位置。

绑定属性定义如下:

public int MediaVolume
{
    get { return _MediaVolume; }
    set
    {
        if (_MediaVolume != value)
        {
            _MediaVolume = value;
            OnPropertyChanged(() => MediaVolume);
            if (value < 0)
            {
                View.Player.Volume = 0;
            }
            else if (value > 100)
            {
                View.Player.Volume = 100;
            }
            else
            {
                View.Player.Volume = value;
            }
        }
    }
}

特别说明,不能使用VolumeChanged事件来更新UI的Volume显示绑定,实际测试时,每次打开新的媒体,播放器都把Volume设定为-1了,不知是否为一个Bug.

7、Mute禁音的控制

Xaml如下:

<telerik:RadPathButton  Command="{Binding MuteCommand}">
    <telerik:RadPathButton.Style>
        <Style TargetType="telerik:RadPathButton" BasedOn="{StaticResource PathButtonStyleWithNoText}">
            <Setter Property="PathGeometry" Value="{StaticResource VolumePathData}"/>
            <Setter Property="Padding" Value="14"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsMute}" Value="true">
                    <Setter Property="PathGeometry" Value="{StaticResource MutePathData}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </telerik:RadPathButton.Style>
</telerik:RadPathButton>

注意,用Button控件如上类似的代码也可以实现。

添加绑定属性

public bool IsMute
 {
     get { return _IsMute; }
     set
     {
         _IsMute = value;
         OnPropertyChanged(() => IsMute);

     }
 }

添加绑定命令(关键代码):

void MuteCommandFun()
{
    if (View.Player.Mute)
    {
        View.Player.Mute = false;
        IsMute = false;
    }
    else
    {
        View.Player.Mute = true;
        IsMute = true;
    }
}

 测试过其他的Mute代码更新,由于给播放器发送Mute命令后,执行存在延迟,而播放器对应的事件没有MuteChanged事件,无法做到反馈式的更新UI,只能如上给播放器发送指令后,即会认为播放器肯定会执行,然后直接更新UI属性IsMute,实际测试时发现效果不错,能够保持同步。

8、自动播放下一个媒介

通用的打开媒介函数:

//打开媒体文件
public void OpenMedia(string filePath)
{
    if(View.Player.State!=VLCState.Ended)
        View.Player.Stop();
    var media = new Media(View.LibVLC, new Uri(filePath));
    bool succ= View.Player.Play(media);
    //更新标题
    MediaTitle = Path.GetFileNameWithoutExtension(filePath);
    _CurrentPlayedFilePath= filePath;
}

 检测当前媒介播放结束,需要用到Player_EndReached事件,在此事件中不能直接调用打开新媒介方法,否则线程会被锁死,必须异步调用UI线程,如下:

//视频终点位置到达
    private void Player_EndReached(object sender, EventArgs e)
    {   
        //异步调用UI线程
        Dispatcher.BeginInvoke(new Action(() =>
        {
            MainView.GotoPlayIndex(1);
        }));
    }

9、音轨选择

     通过Player的AudioTrackDescription属性可获取音轨集合,使用SetAudioTrack()方法设定音轨,实际通过尝试在打开媒体后无法立即获取到AudioTrackDescription属性,在Player_Opening事件中也无法获取,个人推测应该是在打开完成后才能获取有关媒体的各种信息,但Vlc播放器没有打开完成事件,这里通过Player_Opening和Player_Playing的事件组合实现:

     创建一个外部变量:

bool isOpening = false;

在正在打开事件中如下:

private void Player_Opening(object sender, EventArgs e)
{
    isOpening = true;
}

在正在播放事件中获取音轨数据如下:

private void Player_Playing(object sender, EventArgs e)
{
if (isOpening)
{
    isOpening = false;
    //更新音频轨道
    var trackList = new ObservableCollection<MediaAudioTrack>();

    foreach (var track in Player.AudioTrackDescription)
    {
        if (track.Id == -1) continue;
        var newTrack = new MediaAudioTrack(MainView)
        {
            Id = track.Id,
            Name = track.Name,
        };
        trackList.Add(newTrack);
    }
    MainView.MediaAudioTrack = trackList;
    trackList[0].IsChecked = true;
}

}

 

10、播放列表

这是通用的视图设计,只是每次显示播放列表时,需要自动滚动到选择项:

   

        很多播放器都有一个窗口自动适应视频尺寸的功能,个人很不喜欢这个功能,窗口经常忽大忽小,如上述设置,窗口由人工设定尺寸,打开视频后,视频会基于窗口尺寸自适应(不会产生画面拉伸),这才是需要的。

       另外播放器还需要具备保存播放记录的功能,下次打开直接点击播放时,既可以从上次中断位置继续播放,这点对于Vlc播放组件比较简单,在打开媒体后,立即设定其Positon属性即可实现。

        如上所述,Vlc缺少一些关键事件,譬如打开完成事件/播放状态改变事件/Mute状态改变事件等,因此都是通过变通的方式实现的。

      测试中发现,Vlc播放器在播放各种视频时,GPU1的占用一般在16-24%范围内,在各类基于WPF开发的播放器算是比较低的,不过发现,当最小化窗口时,不会释放视频资源,测试Vlc的官方播放器也是如下,其他一些播放器如Windows11的新媒体播放器在最小化时,资源占用直接降低到5%,这点Vlc播放器需要优化。

         

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值