go back to FFME. There is still a memory leak for cover arts.

This commit is contained in:
Paddy Xu
2017-10-16 00:40:01 +03:00
parent 95995eab0e
commit 61d4f7d2c2
19 changed files with 13277 additions and 4605 deletions

View File

@@ -22,6 +22,34 @@ using System.Windows.Data;
namespace QuickLook.Plugin.VideoViewer namespace QuickLook.Plugin.VideoViewer
{ {
public sealed class TimeSpanToSecondsConverter : DependencyObject, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TimeSpan span)
return span.TotalSeconds;
if (value is Duration duration)
return duration.HasTimeSpan ? duration.TimeSpan.TotalSeconds : 0d;
return 0d;
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var span = TimeSpan.Zero;
if (value != null)
span = TimeSpan.FromSeconds((double) value);
if (targetType == typeof(TimeSpan))
return span;
if (targetType == typeof(Duration))
return new Duration(span);
return Activator.CreateInstance(targetType);
}
}
public sealed class TimeSpanToShortStringConverter : DependencyObject, IValueConverter public sealed class TimeSpanToShortStringConverter : DependencyObject, IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
@@ -29,13 +57,17 @@ namespace QuickLook.Plugin.VideoViewer
if (value == null) if (value == null)
return "00:00"; return "00:00";
var v = (TimeSpan) value; var span = TimeSpan.Zero;
if (value is Duration duration)
span = duration.HasTimeSpan ? duration.TimeSpan : TimeSpan.Zero;
if (value is TimeSpan timespan)
span = timespan;
var s = string.Empty; var s = string.Empty;
if (v.Hours > 0) if (span.Hours > 0)
s += $"{v.Hours:D2}:"; s += $"{span.Hours:D2}:";
s += $"{v.Minutes:D2}:{v.Seconds:D2}"; s += $"{span.Minutes:D2}:{span.Seconds:D2}";
return s; return s;
} }
@@ -55,13 +87,9 @@ namespace QuickLook.Plugin.VideoViewer
if (value == null) if (value == null)
return Volumes[0]; return Volumes[0];
var v = (int) value; var v = (int) Math.Min(100, Math.Max((double) value * 100, 0));
if (v == 0)
return Volumes[0];
v = Math.Min(v, 100); return v == 0 ? Volumes[0] : Volumes[1 + v / 34];
return Volumes[1 + v / 34];
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Meta.Vlc; using System.Reflection;
using Meta.Vlc.Interop.Media; using System.Windows;
using Meta.Vlc.Wpf; using QuickLook.Plugin.VideoViewer.FFmpeg;
using Size = System.Windows.Size; using Unosquare.FFME;
using VideoTrack = Meta.Vlc.VideoTrack;
namespace QuickLook.Plugin.VideoViewer namespace QuickLook.Plugin.VideoViewer
{ {
@@ -38,21 +37,32 @@ namespace QuickLook.Plugin.VideoViewer
".flac", ".gsm", ".iklax", ".ivs", ".m4a", ".m4b", ".m4p", ".mmf", ".mp3", ".mpc", ".msv", ".ogg", ".flac", ".gsm", ".iklax", ".ivs", ".m4a", ".m4b", ".m4p", ".mmf", ".mp3", ".mpc", ".msv", ".ogg",
".oga", ".mogg", ".opus", ".ra", ".rm", ".raw", ".tta", ".vox", ".wav", ".wma", ".wv", ".webm" ".oga", ".mogg", ".opus", ".ra", ".rm", ".raw", ".tta", ".vox", ".wav", ".wma", ".wv", ".webm"
}; };
private ContextObject _context;
private ContextObject _context;
private ViewerPanel _vp; private ViewerPanel _vp;
private FFprobe probe;
public int Priority => 0 - 10; // make it lower than TextViewer public int Priority => 0 - 10; // make it lower than TextViewer
public bool AllowsTransparency => true; public bool AllowsTransparency => true;
public void Init() public void Init()
{ {
ApiManager.Initialize(VlcSettings.LibVlcPath, VlcSettings.VlcOptions); MediaElement.FFmpegDirectory = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "FFmpeg\\",
App.Is64Bit ? "x64\\" : "x86\\");
} }
public bool CanHandle(string path) public bool CanHandle(string path)
{ {
return !Directory.Exists(path) && Formats.Contains(Path.GetExtension(path).ToLower()); if (Directory.Exists(path))
return false;
return Formats.Contains(Path.GetExtension(path).ToLower());
//FFprobe is much slower than fixed extensions
//probe = new FFprobe(path);
//return probe.CanDecode() & (probe.HasAudio() | probe.HasVideo());
} }
public void Prepare(string path, ContextObject context) public void Prepare(string path, ContextObject context)
@@ -60,16 +70,9 @@ namespace QuickLook.Plugin.VideoViewer
_context = context; _context = context;
var def = new Size(500, 300); var def = new Size(500, 300);
probe = probe ?? new FFprobe(path);
var hasVideo = HasVideoStream(path, out Size mediaSize); if (!probe.HasVideo())
var windowSize = mediaSize == Size.Empty ? def : mediaSize;
windowSize.Width = Math.Max(def.Width, windowSize.Width);
windowSize.Height = Math.Max(def.Height, windowSize.Height);
context.SetPreferredSizeFit(windowSize, 0.6);
context.TitlebarOverlap = true;
if (!hasVideo)
{ {
context.CanResize = false; context.CanResize = false;
context.TitlebarAutoHide = false; context.TitlebarAutoHide = false;
@@ -80,52 +83,47 @@ namespace QuickLook.Plugin.VideoViewer
{ {
context.TitlebarAutoHide = true; context.TitlebarAutoHide = true;
context.UseDarkTheme = true; context.UseDarkTheme = true;
context.CanResize = true;
context.TitlebarAutoHide = true;
context.TitlebarBlurVisibility = true;
context.TitlebarColourVisibility = true;
} }
var windowSize = probe.GetViewSize() == Size.Empty ? def : probe.GetViewSize();
windowSize.Width = Math.Max(def.Width, windowSize.Width);
windowSize.Height = Math.Max(def.Height, windowSize.Height);
context.SetPreferredSizeFit(windowSize, 0.6);
context.TitlebarOverlap = true;
} }
public void View(string path, ContextObject context) public void View(string path, ContextObject context)
{ {
_vp = new ViewerPanel(context); _vp = new ViewerPanel(context, probe.HasVideo());
context.ViewerContent = _vp; context.ViewerContent = _vp;
_vp.mediaElement.VlcMediaPlayer.Opening += MarkReady; _vp.mediaElement.MediaOpened += MediaElement_MediaOpened;
_vp.LoadAndPlay(path); _vp.LoadAndPlay(path);
context.Title = $"{Path.GetFileName(path)}"; context.Title = $"{Path.GetFileName(path)}";
}
private void MediaElement_MediaOpened(object sender, RoutedEventArgs e)
{
_context.IsBusy = false;
} }
public void Cleanup() public void Cleanup()
{ {
if (_vp != null) if (_vp?.mediaElement != null)
_vp.mediaElement.VlcMediaPlayer.Opening -= MarkReady; _vp.mediaElement.MediaOpened -= MediaElement_MediaOpened;
_vp?.Dispose(); _vp?.Dispose();
_vp = null; _vp = null;
_context = null; _context = null;
} }
private void MarkReady(object sender, ObjectEventArgs<MediaState> e)
{
_context.IsBusy = false;
}
private bool HasVideoStream(string path, out Size size)
{
using (var vlc = new Vlc(VlcSettings.VlcOptions))
{
using (var media = vlc.CreateMediaFromPath(path))
{
media.Parse();
var tracks = media.GetTracks();
var video = tracks.FirstOrDefault(mt => mt.Type == TrackType.Video) as VideoTrack;
size = video == null ? Size.Empty : new Size(video.Width, video.Height);
return video != null;
}
}
}
} }
} }

View File

@@ -59,11 +59,11 @@
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Meta.Vlc"> <Reference Include="ffme">
<HintPath>.\Meta.Vlc.dll</HintPath> <HintPath>References\ffme.dll</HintPath>
</Reference> </Reference>
<Reference Include="Meta.Vlc.Wpf"> <Reference Include="policy.2.0.taglib-sharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=db62eba44689b5b0, processorArchitecture=MSIL">
<HintPath>.\Meta.Vlc.Wpf.dll</HintPath> <HintPath>..\..\packages\taglib.2.1.0.0\lib\policy.2.0.taglib-sharp.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@@ -71,12 +71,16 @@
<Reference Include="System.Xaml"> <Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework> <RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference> </Reference>
<Reference Include="System.Xml" />
<Reference Include="taglib-sharp, Version=2.1.0.0, Culture=neutral, PublicKeyToken=db62eba44689b5b0, processorArchitecture=MSIL">
<HintPath>..\..\packages\taglib.2.1.0.0\lib\taglib-sharp.dll</HintPath>
</Reference>
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
<Reference Include="PresentationCore" /> <Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" /> <Reference Include="PresentationFramework" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="VlcSettings.cs" /> <Compile Include="ffmpeg\FFprobe.cs" />
<Page Include="Styles.xaml"> <Page Include="Styles.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
@@ -100,6 +104,7 @@
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<None Include="key.snk" /> <None Include="key.snk" />
<None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\QuickLook\QuickLook.csproj"> <ProjectReference Include="..\..\QuickLook\QuickLook.csproj">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vlc="clr-namespace:Meta.Vlc.Wpf;assembly=Meta.Vlc.Wpf" xmlns:ffme="clr-namespace:Unosquare.FFME;assembly=ffme"
xmlns:local="clr-namespace:QuickLook.Plugin.VideoViewer" xmlns:local="clr-namespace:QuickLook.Plugin.VideoViewer"
xmlns:glassLayer="clr-namespace:QuickLook.Controls.GlassLayer;assembly=QuickLook" xmlns:glassLayer="clr-namespace:QuickLook.Controls.GlassLayer;assembly=QuickLook"
mc:Ignorable="d" mc:Ignorable="d"
@@ -13,6 +13,7 @@
<ResourceDictionary> <ResourceDictionary>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<local:TimeSpanToShortStringConverter x:Key="TimeSpanToShortStringConverter" /> <local:TimeSpanToShortStringConverter x:Key="TimeSpanToShortStringConverter" />
<local:TimeSpanToSecondsConverter x:Key="TimeSpanToSecondsConverter" />
<local:VolumeToIconConverter x:Key="VolumeToIconConverter" /> <local:VolumeToIconConverter x:Key="VolumeToIconConverter" />
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles.xaml" /> <ResourceDictionary Source="Styles.xaml" />
@@ -20,14 +21,13 @@
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<vlc:VlcPlayer x:Name="mediaElement" LibVlcPath="{Binding LibVlcPath, ElementName=viewerPanel}" <ffme:MediaElement x:Name="mediaElement" />
VlcOption="{Binding VlcOption, ElementName=viewerPanel}" />
<Grid x:Name="coverArtPersenter" ClipToBounds="True" Background="{StaticResource MainWindowBackground}"> <Grid x:Name="coverArtPersenter" ClipToBounds="True" Background="{StaticResource MainWindowBackground}">
<Grid.Style> <Grid.Style>
<Style TargetType="Grid"> <Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" /> <Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding HasVideo, ElementName=viewerPanel}" Value="False"> <DataTrigger Binding="{Binding ShowVideo, ElementName=viewerPanel}" Value="False">
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
@@ -91,7 +91,7 @@
</TextBlock> </TextBlock>
<TextBlock x:Name="metaLength" Grid.Row="5" FontSize="14" Padding="3" TextTrimming="CharacterEllipsis" <TextBlock x:Name="metaLength" Grid.Row="5" FontSize="14" Padding="3" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource WindowTextForeground}" Foreground="{DynamicResource WindowTextForeground}"
Text="{Binding ElementName=mediaElement, Path=Length, Converter={StaticResource TimeSpanToShortStringConverter}}" /> Text="{Binding ElementName=mediaElement, Path=NaturalDuration, Converter={StaticResource TimeSpanToShortStringConverter}}" />
</Grid> </Grid>
</Grid> </Grid>
@@ -115,13 +115,13 @@
</Storyboard> </Storyboard>
</Grid.Resources> </Grid.Resources>
<glassLayer:GlassLayer OverlayColor="{DynamicResource CaptionBackground}" <glassLayer:GlassLayer OverlayColor="{DynamicResource CaptionBackground}"
GlassVisibility="{Binding ElementName=viewerPanel, Path=HasVideo,Converter={StaticResource BooleanToVisibilityConverter}}" GlassVisibility="{Binding ElementName=viewerPanel, Path=ShowVideo,Converter={StaticResource BooleanToVisibilityConverter}}"
ColorOverlayVisibility="{Binding ElementName=viewerPanel, Path=HasVideo,Converter={StaticResource BooleanToVisibilityConverter}}"> ColorOverlayVisibility="{Binding ElementName=viewerPanel, Path=ShowVideo,Converter={StaticResource BooleanToVisibilityConverter}}">
<glassLayer:GlassLayer.Style> <glassLayer:GlassLayer.Style>
<Style TargetType="glassLayer:GlassLayer"> <Style TargetType="glassLayer:GlassLayer">
<Setter Property="BlurredElement" Value="{Binding ElementName=mediaElement}" /> <Setter Property="BlurredElement" Value="{Binding ElementName=mediaElement}" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding ElementName=viewerPanel,Path=HasVideo}" Value="True"> <DataTrigger Binding="{Binding ElementName=viewerPanel,Path=ShowVideo}" Value="True">
<Setter Property="BlurredElement" Value="{Binding ElementName=mediaElement}" /> <Setter Property="BlurredElement" Value="{Binding ElementName=mediaElement}" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
@@ -134,7 +134,7 @@
<Style TargetType="Button" BasedOn="{StaticResource ControlButtonStyle}"> <Style TargetType="Button" BasedOn="{StaticResource ControlButtonStyle}">
<Setter Property="Content" Value="&#xE769;" /> <Setter Property="Content" Value="&#xE769;" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsPlaying, ElementName=viewerPanel}" <DataTrigger Binding="{Binding IsPlaying, ElementName=mediaElement}"
Value="False"> Value="False">
<Setter Property="Content" Value="&#xE768;" /> <Setter Property="Content" Value="&#xE768;" />
</DataTrigger> </DataTrigger>
@@ -148,7 +148,7 @@
<Setter Property="Content" <Setter Property="Content"
Value="{Binding ElementName=mediaElement, Path=Volume, Converter={StaticResource VolumeToIconConverter}}" /> Value="{Binding ElementName=mediaElement, Path=Volume, Converter={StaticResource VolumeToIconConverter}}" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsMuted, ElementName=viewerPanel}" <DataTrigger Binding="{Binding IsMuted, ElementName=mediaElement}"
Value="True"> Value="True">
<Setter Property="Content" Value="&#xE74F;" /> <Setter Property="Content" Value="&#xE74F;" />
</DataTrigger> </DataTrigger>
@@ -166,11 +166,11 @@
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding ElementName=buttonTime, Path=Tag}" Value="Time"> <DataTrigger Binding="{Binding ElementName=buttonTime, Path=Tag}" Value="Time">
<Setter Property="Text" <Setter Property="Text"
Value="{Binding ElementName=mediaElement, Path=Time, Converter={StaticResource TimeSpanToShortStringConverter}}" /> Value="{Binding ElementName=mediaElement, Path=Position, Converter={StaticResource TimeSpanToShortStringConverter}}" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding ElementName=buttonTime, Path=Tag}" Value="Length"> <DataTrigger Binding="{Binding ElementName=buttonTime, Path=Tag}" Value="Length">
<Setter Property="Text" <Setter Property="Text"
Value="{Binding ElementName=mediaElement, Path=Length, Converter={StaticResource TimeSpanToShortStringConverter}}" /> Value="{Binding ElementName=mediaElement, Path=NaturalDuration, Converter={StaticResource TimeSpanToShortStringConverter}}" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
@@ -178,14 +178,16 @@
</TextBlock> </TextBlock>
</Button> </Button>
<Slider x:Name="sliderProgress" Style="{StaticResource PositionSliderStyle}" <Slider x:Name="sliderProgress" Style="{StaticResource PositionSliderStyle}"
SmallChange="0.00001" LargeChange="0.01" Maximum="1" SmallChange="{Binding FrameStepDuration, ElementName=mediaElement, Converter={StaticResource TimeSpanToSecondsConverter}}"
Value="{Binding Position, ElementName=mediaElement}" /> LargeChange="{Binding FrameStepDuration, ElementName=mediaElement, Converter={StaticResource TimeSpanToSecondsConverter}}"
Maximum="{Binding NaturalDuration, ElementName=mediaElement, Converter={StaticResource TimeSpanToSecondsConverter}}"
Value="{Binding Position, ElementName=mediaElement, Converter={StaticResource TimeSpanToSecondsConverter}}" />
</DockPanel> </DockPanel>
</Grid> </Grid>
<Grid x:Name="volumeSliderLayer" Background="Transparent" Visibility="Collapsed"> <Grid x:Name="volumeSliderLayer" Background="Transparent" Visibility="Collapsed">
<Grid Height="32" Width="130" HorizontalAlignment="Right" VerticalAlignment="Bottom" <Grid Height="32" Width="130" HorizontalAlignment="Right" VerticalAlignment="Bottom"
Background="{DynamicResource CaptionButtonHoverBackground}" Margin="0,0,0,32"> Background="{DynamicResource CaptionButtonHoverBackground}" Margin="0,0,0,32">
<Slider Style="{StaticResource CustomSliderStyle}" Width="110" Maximum="100" <Slider Style="{StaticResource CustomSliderStyle}" Width="110" Maximum="1"
Value="{Binding ElementName=mediaElement, Path=Volume}" /> Value="{Binding ElementName=mediaElement, Path=Volume}" />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -18,18 +18,18 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.IO;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using Meta.Vlc; using System.Windows.Media.Imaging;
using Meta.Vlc.Interop.Media;
using QuickLook.Annotations; using QuickLook.Annotations;
using QuickLook.ExtensionMethods; using QuickLook.ExtensionMethods;
using MediaState = Meta.Vlc.Interop.Media.MediaState; using TagLib;
using File = TagLib.File;
namespace QuickLook.Plugin.VideoViewer namespace QuickLook.Plugin.VideoViewer
{ {
@@ -40,16 +40,13 @@ namespace QuickLook.Plugin.VideoViewer
{ {
private readonly ContextObject _context; private readonly ContextObject _context;
private Uri _coverArt; private BitmapSource _coverArt;
private bool _hasAudio;
private bool _hasEnded;
private bool _hasVideo;
private bool _isMuted;
private bool _isPlaying;
private bool _wasPlaying; private bool _wasPlaying;
public ViewerPanel(ContextObject context) public ViewerPanel(ContextObject context, bool hasVideo)
{ {
ShowVideo = hasVideo;
InitializeComponent(); InitializeComponent();
// apply global theme // apply global theme
@@ -58,8 +55,8 @@ namespace QuickLook.Plugin.VideoViewer
ShowViedoControlContainer(null, null); ShowViedoControlContainer(null, null);
viewerPanel.PreviewMouseMove += ShowViedoControlContainer; viewerPanel.PreviewMouseMove += ShowViedoControlContainer;
mediaElement.PropertyChanged += PlayerPropertyChanged; //mediaElement.PropertyChanged += PlayerPropertyChanged;
mediaElement.StateChanged += PlayerStateChanged; //mediaElement.StateChanged += PlayerStateChanged;
_context = context; _context = context;
@@ -70,15 +67,14 @@ namespace QuickLook.Plugin.VideoViewer
sliderProgress.PreviewMouseDown += (sender, e) => sliderProgress.PreviewMouseDown += (sender, e) =>
{ {
_wasPlaying = mediaElement.VlcMediaPlayer.IsPlaying; _wasPlaying = mediaElement.IsPlaying;
mediaElement.Pause(); mediaElement.Pause();
}; };
sliderProgress.PreviewMouseUp += (sender, e) => sliderProgress.PreviewMouseUp += (sender, e) =>
{ {
if (_wasPlaying) mediaElement.Play(); if (_wasPlaying) mediaElement.Play();
}; };
mediaElement.MediaFailed += ShowErrorNotification;
mediaElement.VlcMediaPlayer.EncounteredError += ShowErrorNotification;
/*mediaElement.MediaEnded += (s, e) => /*mediaElement.MediaEnded += (s, e) =>
{ {
if (mediaElement.IsOpen) if (mediaElement.IsOpen)
@@ -89,90 +85,32 @@ namespace QuickLook.Plugin.VideoViewer
} }
};*/ };*/
PreviewMouseWheel += (sender, e) => ChangeVolume((double) e.Delta / 120 * 4); PreviewMouseWheel += (sender, e) => ChangeVolume((double) e.Delta / 120 * 0.05);
} }
public bool IsMuted public bool ShowVideo { get; private set; }
{
get => _isMuted;
set
{
if (value == _isMuted) return;
_isMuted = value;
mediaElement.IsMute = value;
OnPropertyChanged();
}
}
public bool HasEnded public BitmapSource CoverArt
{
get => _hasEnded;
private set
{
if (value == _hasEnded) return;
_hasEnded = value;
OnPropertyChanged();
}
}
public bool HasAudio
{
get => _hasAudio;
private set
{
if (value == _hasAudio) return;
_hasAudio = value;
OnPropertyChanged();
}
}
public bool HasVideo
{
get => _hasVideo;
private set
{
if (value == _hasVideo) return;
_hasVideo = value;
OnPropertyChanged();
}
}
public bool IsPlaying
{
get => _isPlaying;
private set
{
if (value == _isPlaying) return;
_isPlaying = value;
OnPropertyChanged();
}
}
public Uri CoverArt
{ {
get => _coverArt; get => _coverArt;
private set private set
{ {
if (value == _coverArt) return; if (Equals(value, _coverArt)) return;
if (value == null) return; if (value == null) return;
_coverArt = value; _coverArt = value;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public string LibVlcPath { get; } = VlcSettings.LibVlcPath;
public string[] VlcOption { get; } = VlcSettings.VlcOptions;
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this);
try try
{ {
Task.Run(() => CoverArt = null;
{ mediaElement?.Dispose();
mediaElement?.Dispose(); mediaElement = null;
mediaElement = null;
});
} }
catch (Exception e) catch (Exception e)
{ {
@@ -191,7 +129,7 @@ namespace QuickLook.Plugin.VideoViewer
private void AutoHideViedoControlContainer(object sender, EventArgs e) private void AutoHideViedoControlContainer(object sender, EventArgs e)
{ {
if (!HasVideo) if (ShowVideo)
return; return;
if (videoControlContainer.IsMouseOver) if (videoControlContainer.IsMouseOver)
@@ -202,69 +140,26 @@ namespace QuickLook.Plugin.VideoViewer
hide.Begin(); hide.Begin();
} }
private void PlayerStop(object sender, MouseButtonEventArgs e) private void UpdateMeta(string path)
{ {
HasEnded = false; if (ShowVideo)
IsPlaying = false;
mediaElement.Position = 0;
mediaElement.Stop();
}
private void PlayerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var prop = e.PropertyName;
switch (prop)
{
case nameof(mediaElement.IsMute):
IsMuted = mediaElement.IsMute;
break;
}
}
private void PlayerStateChanged(object sender, ObjectEventArgs<MediaState> e)
{
var state = e.Value;
switch (state)
{
case MediaState.Opening:
HasVideo = mediaElement.VlcMediaPlayer.VideoTrackCount > 0;
HasAudio = mediaElement.VlcMediaPlayer.AudioTrackCount > 0;
break;
case MediaState.Playing:
UpdateMeta();
DetermineTheme();
HasVideo = mediaElement.VlcMediaPlayer.VideoTrackCount > 0;
HasAudio = mediaElement.VlcMediaPlayer.AudioTrackCount > 0;
IsPlaying = true;
break;
case MediaState.Paused:
IsPlaying = false;
break;
case MediaState.Ended:
IsPlaying = false;
HasEnded = true;
break;
case MediaState.Error:
ShowErrorNotification(sender, e);
break;
}
}
private void UpdateMeta()
{
if (HasVideo)
return; return;
var path = mediaElement.VlcMediaPlayer.Media.GetMeta(MetaDataType.ArtworkUrl); using (var h = File.Create(path))
if (!string.IsNullOrEmpty(path)) {
CoverArt = new Uri(path); metaTitle.Text = h.Tag.Title;
metaArtists.Text = h.Tag.FirstPerformer;
metaTitle.Text = mediaElement.VlcMediaPlayer.Media.GetMeta(MetaDataType.Title); metaAlbum.Text = h.Tag.Album;
metaArtists.Text = mediaElement.VlcMediaPlayer.Media.GetMeta(MetaDataType.Artist);
metaAlbum.Text = mediaElement.VlcMediaPlayer.Media.GetMeta(MetaDataType.Album);
//var cs = h.Tag.Pictures.FirstOrDefault(p => p.Type == TagLib.PictureType.FrontCover);
var cs = h.Tag.Pictures.FirstOrDefault();
if (cs != default(IPicture))
using (var ms = new MemoryStream(cs.Data.Data))
{
CoverArt = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
DetermineTheme();
}
}
metaArtists.Visibility = string.IsNullOrEmpty(metaArtists.Text) metaArtists.Visibility = string.IsNullOrEmpty(metaArtists.Text)
? Visibility.Collapsed ? Visibility.Collapsed
: Visibility.Visible; : Visibility.Visible;
@@ -275,16 +170,16 @@ namespace QuickLook.Plugin.VideoViewer
private void DetermineTheme() private void DetermineTheme()
{ {
if (HasVideo) if (ShowVideo)
return; return;
if (CoverArt == null) if (CoverArt == null)
return; return;
var dark = false; bool dark;
using (var bitmap = new Bitmap(CoverArt.LocalPath)) using (var b = CoverArt.ToBitmap())
{ {
dark = bitmap.IsDarkImage(); dark = b.IsDarkImage();
} }
_context.UseDarkTheme = dark; _context.UseDarkTheme = dark;
@@ -292,34 +187,20 @@ namespace QuickLook.Plugin.VideoViewer
private void ChangeVolume(double delta) private void ChangeVolume(double delta)
{ {
IsMuted = false; mediaElement.IsMuted = false;
var newVol = mediaElement.Volume + (int) delta; mediaElement.Volume += delta;
newVol = Math.Max(newVol, 0);
newVol = Math.Min(newVol, 100);
mediaElement.Volume = newVol;
}
private void Seek(TimeSpan delta)
{
_wasPlaying = mediaElement.VlcMediaPlayer.IsPlaying;
mediaElement.Pause();
mediaElement.Time += delta;
if (_wasPlaying) mediaElement.Play();
} }
private void TogglePlayPause(object sender, EventArgs e) private void TogglePlayPause(object sender, EventArgs e)
{ {
if (mediaElement.VlcMediaPlayer.IsPlaying) if (mediaElement.IsPlaying)
{ {
mediaElement.Pause(); mediaElement.Pause();
} }
else else
{ {
if (HasEnded) if (mediaElement.HasMediaEnded)
mediaElement.Stop(); mediaElement.Stop();
mediaElement.Play(); mediaElement.Play();
} }
@@ -339,15 +220,16 @@ namespace QuickLook.Plugin.VideoViewer
public void LoadAndPlay(string path) public void LoadAndPlay(string path)
{ {
mediaElement.LoadMedia(path); UpdateMeta(path);
mediaElement.Volume = 50;
mediaElement.Source = new Uri(path);
mediaElement.Volume = 0.5;
mediaElement.Play(); mediaElement.Play();
} }
~ViewerPanel() ~ViewerPanel()
{ {
GC.SuppressFinalize(this);
Dispose(); Dispose();
} }

View File

@@ -1,34 +0,0 @@
// Copyright © 2017 Paddy Xu
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System.IO;
using System.Reflection;
namespace QuickLook.Plugin.VideoViewer
{
public static class VlcSettings
{
public static string LibVlcPath = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
App.Is64Bit ? @"LibVLC\x64" : @"LibVLC\x86");
public static string[] VlcOptions =
{
"-I", "--dummy-quiet", "--ignore-config", "--no-video-title", "--no-sub-autodetect-file"
};
}
}

View File

@@ -0,0 +1,131 @@
// Copyright © 2017 Paddy Xu
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Xml.XPath;
namespace QuickLook.Plugin.VideoViewer.FFmpeg
{
internal class FFprobe
{
private static readonly string _probePath =
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "FFmpeg\\",
App.Is64Bit ? "x64\\" : "x86\\", "ffprobe.exe");
private XPathNavigator _infoNavigator;
public FFprobe(string media)
{
Run(media);
}
private bool Run(string media)
{
string result;
using (var p = new Process())
{
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = _probePath;
p.StartInfo.Arguments = $"-v quiet -print_format xml -show_streams -show_format \"{media}\"";
p.StartInfo.StandardOutputEncoding = Encoding.UTF8;
p.Start();
p.WaitForExit();
result = p.StandardOutput.ReadToEnd();
}
if (string.IsNullOrWhiteSpace(result))
return false;
ParseResult(result);
return true;
}
private void ParseResult(string result)
{
_infoNavigator = new XPathDocument(new StringReader(result)).CreateNavigator();
}
public bool CanDecode()
{
var info = _infoNavigator.SelectSingleNode("/ffprobe/format[@probe_score>25]");
return info != null;
}
public string GetFormatName()
{
var format = _infoNavigator.SelectSingleNode("/ffprobe/format/@format_name")?.Value;
return format ?? string.Empty;
}
public string GetFormatLongName()
{
var format = _infoNavigator.SelectSingleNode("/ffprobe/format/@format_long_name")?.Value;
return format ?? string.Empty;
}
public bool HasAudio()
{
var duration = _infoNavigator.SelectSingleNode("/ffprobe/streams/stream[@codec_type='audio'][1]/@duration")
?.Value;
if (duration == null)
return false;
double.TryParse(duration, out var d);
return Math.Abs(d) > 0.01;
}
public bool HasVideo()
{
var fps = _infoNavigator.SelectSingleNode("/ffprobe/streams/stream[@codec_type='video'][1]/@avg_frame_rate")
?.Value;
if (fps == null)
return false;
return fps != "0/0";
}
public Size GetViewSize()
{
if (!HasVideo())
return Size.Empty;
var width = _infoNavigator.SelectSingleNode("/ffprobe/streams/stream[@codec_type='video'][1]/@coded_width")
?.Value;
var height = _infoNavigator.SelectSingleNode("/ffprobe/streams/stream[@codec_type='video'][1]/@coded_height")
?.Value;
if (width == null || height == null)
return Size.Empty;
return new Size(double.Parse(width), double.Parse(height));
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="taglib" version="2.1.0.0" targetFramework="net462" />
</packages>

View File

@@ -18,6 +18,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
@@ -66,6 +67,19 @@ namespace QuickLook.ExtensionMethods
return bs; return bs;
} }
public static Bitmap ToBitmap(this BitmapSource source)
{
using (var outStream = new MemoryStream())
{
BitmapEncoder enc = new BmpBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(source));
enc.Save(outStream);
var bitmap = new Bitmap(outStream);
return new Bitmap(bitmap);
}
}
private static PixelFormat ConvertPixelFormat( private static PixelFormat ConvertPixelFormat(
System.Drawing.Imaging.PixelFormat sourceFormat) System.Drawing.Imaging.PixelFormat sourceFormat)
{ {
@@ -89,7 +103,7 @@ namespace QuickLook.ExtensionMethods
image = image.Clone(new Rectangle(0, 0, image.Width, image.Height), image = image.Clone(new Rectangle(0, 0, image.Width, image.Height),
System.Drawing.Imaging.PixelFormat.Format24bppRgb); System.Drawing.Imaging.PixelFormat.Format24bppRgb);
var sampleCount = (int) (0.2 * image.Width * image.Height); var sampleCount = (int) (0.2 * 400 * 400);
const int pixelSize = 24 / 8; const int pixelSize = 24 / 8;
var data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), var data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height),
ImageLockMode.ReadWrite, image.PixelFormat); ImageLockMode.ReadWrite, image.PixelFormat);