在利用LibVLCSharp.WPF开发WPF媒体播放器的过程中,出现了一些问题并进行了解决,这里做一下总结:
- 依据官方例子,需要引用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;
}
}
}
}
- 操作面板的自动掩藏
当鼠标移动/窗口状态(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播放器需要优化。