mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-11 09:49:07 +00:00
413 lines
12 KiB
C#
413 lines
12 KiB
C#
// Copyright © 2017-2025 QL-Win Contributors
|
|
//
|
|
// 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/>.
|
|
|
|
extern alias MediaInfoWrapper;
|
|
|
|
using MediaInfoWrapper::MediaInfo;
|
|
using QuickLook.Common.Annotations;
|
|
using QuickLook.Common.Helpers;
|
|
using QuickLook.Common.Plugin;
|
|
using QuickLook.Plugin.VideoViewer.AudioTrack;
|
|
using QuickLook.Plugin.VideoViewer.Extensions;
|
|
using QuickLook.Plugin.VideoViewer.LyricTrack;
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Threading;
|
|
using UtfUnknown;
|
|
using WPFMediaKit.DirectShow.Controls;
|
|
using WPFMediaKit.DirectShow.MediaPlayers;
|
|
|
|
namespace QuickLook.Plugin.VideoViewer;
|
|
|
|
public partial class ViewerPanel : UserControl, IDisposable, INotifyPropertyChanged
|
|
{
|
|
private readonly ContextObject _context;
|
|
private BitmapSource _coverArt;
|
|
private DispatcherTimer _lyricTimer;
|
|
private LrcLine[] _lyricLines;
|
|
private MidiPlayer _midiPlayer;
|
|
|
|
private bool _hasVideo;
|
|
private bool _isPlaying;
|
|
private bool _wasPlaying;
|
|
private bool _shouldLoop;
|
|
|
|
public ViewerPanel(ContextObject context)
|
|
{
|
|
InitializeComponent();
|
|
LoadAndInsertGlassLayer();
|
|
|
|
// apply global theme
|
|
Resources.MergedDictionaries[0].MergedDictionaries.Clear();
|
|
|
|
_context = context;
|
|
|
|
mediaElement.MediaUriPlayer.LAVFilterDirectory =
|
|
IntPtr.Size == 8 ? "LAVFilters-x64\\" : "LAVFilters-x86\\";
|
|
|
|
//ShowViedoControlContainer(null, null);
|
|
viewerPanel.PreviewMouseMove += ShowViedoControlContainer;
|
|
|
|
mediaElement.MediaUriPlayer.PlayerStateChanged += PlayerStateChanged;
|
|
mediaElement.MediaOpened += MediaOpened;
|
|
mediaElement.MediaEnded += MediaEnded;
|
|
mediaElement.MediaFailed += MediaFailed;
|
|
|
|
ShouldLoop = SettingHelper.Get("ShouldLoop", false, "QuickLook.Plugin.VideoViewer");
|
|
|
|
buttonPlayPause.Click += TogglePlayPause;
|
|
buttonLoop.Click += ToggleShouldLoop;
|
|
buttonTime.Click += (sender, e) => buttonTime.Tag = (string)buttonTime.Tag == "Time" ? "Length" : "Time";
|
|
buttonMute.Click += (sender, e) => volumeSliderLayer.Visibility = Visibility.Visible;
|
|
volumeSliderLayer.MouseDown += (sender, e) => volumeSliderLayer.Visibility = Visibility.Collapsed;
|
|
|
|
sliderProgress.PreviewMouseDown += (sender, e) =>
|
|
{
|
|
_wasPlaying = mediaElement.IsPlaying;
|
|
mediaElement.Pause();
|
|
};
|
|
sliderProgress.PreviewMouseUp += (sender, e) =>
|
|
{
|
|
if (_wasPlaying) mediaElement.Play();
|
|
};
|
|
|
|
PreviewMouseWheel += (sender, e) => ChangeVolume((double)e.Delta / 120 * 0.04);
|
|
}
|
|
|
|
private partial void LoadAndInsertGlassLayer();
|
|
|
|
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 bool ShouldLoop
|
|
{
|
|
get => _shouldLoop;
|
|
private set
|
|
{
|
|
if (value == _shouldLoop) return;
|
|
_shouldLoop = value;
|
|
OnPropertyChanged();
|
|
if (!IsPlaying)
|
|
{
|
|
IsPlaying = true;
|
|
|
|
mediaElement.Play();
|
|
}
|
|
}
|
|
}
|
|
|
|
public BitmapSource CoverArt
|
|
{
|
|
get => _coverArt;
|
|
private set
|
|
{
|
|
if (ReferenceEquals(value, _coverArt)) return;
|
|
if (value == null) return;
|
|
_coverArt = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// old plugin use an int-typed "Volume" config key ranged from 0 to 100. Let's use a new one here.
|
|
SettingHelper.Set("VolumeDouble", LinearVolume, "QuickLook.Plugin.VideoViewer");
|
|
SettingHelper.Set("ShouldLoop", ShouldLoop, "QuickLook.Plugin.VideoViewer");
|
|
|
|
try
|
|
{
|
|
mediaElement?.Close();
|
|
|
|
Task.Run(() =>
|
|
{
|
|
mediaElement?.MediaUriPlayer.Dispose();
|
|
mediaElement = null;
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.WriteLine(e);
|
|
}
|
|
|
|
_lyricTimer?.Stop();
|
|
_lyricTimer = null;
|
|
_lyricLines = null;
|
|
_midiPlayer?.Dispose();
|
|
_midiPlayer = null;
|
|
}
|
|
|
|
private void Panel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.LeftButton == MouseButtonState.Pressed)
|
|
{
|
|
Window.GetWindow(this)?.DragMove();
|
|
}
|
|
}
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
private void MediaOpened(object o, RoutedEventArgs args)
|
|
{
|
|
if (mediaElement == null)
|
|
return;
|
|
|
|
HasVideo = mediaElement.HasVideo;
|
|
|
|
_context.IsBusy = false;
|
|
}
|
|
|
|
private void MediaFailed(object sender, MediaFailedEventArgs e)
|
|
{
|
|
((MediaUriElement)sender).Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
_context.ViewerContent =
|
|
new Label { Content = e.Exception, VerticalAlignment = VerticalAlignment.Center };
|
|
_context.IsBusy = false;
|
|
}));
|
|
}
|
|
|
|
private void MediaEnded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (mediaElement == null)
|
|
return;
|
|
|
|
mediaElement.MediaPosition = 0;
|
|
if (ShouldLoop)
|
|
{
|
|
IsPlaying = true;
|
|
|
|
mediaElement.Play();
|
|
}
|
|
else
|
|
{
|
|
IsPlaying = false;
|
|
|
|
mediaElement.Pause();
|
|
}
|
|
}
|
|
|
|
private void ShowViedoControlContainer(object sender, MouseEventArgs e)
|
|
{
|
|
var show = (Storyboard)videoControlContainer.FindResource("ShowControlStoryboard");
|
|
if (videoControlContainer.Opacity == 0 || videoControlContainer.Opacity == 1)
|
|
show.Begin();
|
|
}
|
|
|
|
private void AutoHideViedoControlContainer(object sender, EventArgs e)
|
|
{
|
|
if (!HasVideo)
|
|
return;
|
|
|
|
if (videoControlContainer.IsMouseOver)
|
|
return;
|
|
|
|
var hide = (Storyboard)videoControlContainer.FindResource("HideControlStoryboard");
|
|
|
|
hide.Begin();
|
|
}
|
|
|
|
private void PlayerStateChanged(PlayerState oldState, PlayerState newState)
|
|
{
|
|
switch (newState)
|
|
{
|
|
case PlayerState.Playing:
|
|
IsPlaying = true;
|
|
break;
|
|
|
|
case PlayerState.Paused:
|
|
case PlayerState.Stopped:
|
|
case PlayerState.Closed:
|
|
IsPlaying = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UpdateMeta(string path, MediaInfoLib info)
|
|
{
|
|
if (HasVideo)
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (info == null)
|
|
throw new NullReferenceException();
|
|
|
|
var title = info.Get(StreamKind.General, 0, "Title");
|
|
var artist = info.Get(StreamKind.General, 0, "Performer");
|
|
var album = info.Get(StreamKind.General, 0, "Album");
|
|
|
|
metaTitle.Text = !string.IsNullOrWhiteSpace(title) ? title : Path.GetFileName(path);
|
|
metaArtists.Text = artist;
|
|
metaAlbum.Text = album;
|
|
|
|
var coverData = info.Get(StreamKind.General, 0, "Cover_Data");
|
|
if (!string.IsNullOrEmpty(coverData))
|
|
{
|
|
var coverBytes = Convert.FromBase64String
|
|
(
|
|
coverData.Contains(' ') // MediaInfo may will return multiple covers
|
|
? coverData.Split(" / ")[0] // Get the first cover only
|
|
: coverData
|
|
);
|
|
using var ms = new MemoryStream(coverBytes);
|
|
|
|
CoverArt = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
metaTitle.Text = Path.GetFileName(path);
|
|
metaArtists.Text = metaAlbum.Text = string.Empty;
|
|
}
|
|
|
|
metaArtists.Visibility = string.IsNullOrEmpty(metaArtists.Text)
|
|
? Visibility.Collapsed
|
|
: Visibility.Visible;
|
|
metaAlbum.Visibility = string.IsNullOrEmpty(metaAlbum.Text)
|
|
? Visibility.Collapsed
|
|
: Visibility.Visible;
|
|
|
|
var lyricPath = Path.ChangeExtension(path, ".lrc");
|
|
|
|
if (File.Exists(lyricPath))
|
|
{
|
|
var buffer = File.ReadAllBytes(lyricPath);
|
|
var encoding = CharsetDetector.DetectFromBytes(buffer).Detected?.Encoding ?? Encoding.Default;
|
|
|
|
_lyricLines = LrcHelper.ParseText(encoding.GetString(buffer)).ToArray();
|
|
_lyricTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
|
_lyricTimer.Tick += (sender, e) =>
|
|
{
|
|
if (_lyricLines != null && _lyricLines.Length != 0)
|
|
{
|
|
var lyric = LrcHelper.GetNearestLrc(_lyricLines, new TimeSpan(mediaElement.MediaPosition));
|
|
metaLyric.Text = lyric?.LrcText?.Trim();
|
|
}
|
|
else
|
|
{
|
|
metaLyric.Text = null;
|
|
metaLyric.Visibility = Visibility.Collapsed;
|
|
}
|
|
};
|
|
_lyricTimer.Start();
|
|
|
|
metaLyric.Visibility = Visibility.Visible;
|
|
}
|
|
else
|
|
{
|
|
metaLyric.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
public double LinearVolume
|
|
{
|
|
get => mediaElement.Volume;
|
|
set
|
|
{
|
|
mediaElement.Volume = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
private void ChangeVolume(double delta)
|
|
{
|
|
LinearVolume += delta;
|
|
}
|
|
|
|
private void TogglePlayPause(object sender, EventArgs e)
|
|
{
|
|
if (mediaElement.IsPlaying)
|
|
mediaElement.Pause();
|
|
else
|
|
mediaElement.Play();
|
|
}
|
|
|
|
private void ToggleShouldLoop(object sender, EventArgs e)
|
|
{
|
|
ShouldLoop = !ShouldLoop;
|
|
}
|
|
|
|
public void LoadAndPlay(string path, MediaInfoLib info)
|
|
{
|
|
// Detect whether it is other playback formats
|
|
if (!HasVideo)
|
|
{
|
|
string audioCodec = info?.Get(StreamKind.Audio, 0, "Format");
|
|
|
|
if (audioCodec?.Equals("MIDI", StringComparison.OrdinalIgnoreCase) ?? false)
|
|
{
|
|
_midiPlayer = new MidiPlayer(this, _context);
|
|
_midiPlayer.LoadAndPlay(path);
|
|
return; // Midi player will handle the playback at all
|
|
}
|
|
}
|
|
|
|
UpdateMeta(path, info);
|
|
|
|
// detect rotation
|
|
double.TryParse(info?.Get(StreamKind.Video, 0, "Rotation"), out var rotation);
|
|
// Correct rotation: on some machine the value "90" becomes "90000" by some reason
|
|
if (rotation > 360)
|
|
rotation /= 1e3;
|
|
if (Math.Abs(rotation) > 0.1)
|
|
mediaElement.LayoutTransform = new RotateTransform(rotation, 0.5, 0.5);
|
|
|
|
mediaElement.Source = new Uri(path);
|
|
// old plugin use an int-typed "Volume" config key ranged from 0 to 100. Let's use a new one here.
|
|
LinearVolume = SettingHelper.Get("VolumeDouble", 1d, "QuickLook.Plugin.VideoViewer");
|
|
|
|
mediaElement.Play();
|
|
}
|
|
|
|
[NotifyPropertyChangedInvocator]
|
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
}
|