From a0b9af039d7505f510e2496607e8b62bd6ecc6c6 Mon Sep 17 00:00:00 2001 From: Jojo Zhou <39350350+Joanna-Zhou@users.noreply.github.com> Date: Mon, 15 May 2023 14:06:08 -0700 Subject: [PATCH] Yizzho/peek/videos (#25983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add basics of VideoPreviewer to build on * WIP * Minimal working code, todo next:dimension + MTC * Nits * Change back to GetImageSize as it indeed doesn't work with videos * Add win32 helper methods to get video size; Refactor get size operation; * Remove unused code * Set VideoTask; Add message error for HR result; * Add open read only for filestream * Remove unused code * Update expect.txt * Remove comment * Cleanup code * Force pause videopreview on previewer change --------- Co-authored-by: Jojo Zhou Co-authored-by: Yawen Hou Co-authored-by: Clint Rutkas Co-authored-by: Yawen Hou Co-authored-by: Samuel Chapleau 🌈 --- .github/actions/spell-check/expect.txt | 11 +- .../Extensions/DispatcherExtensions.cs | 24 ++++ .../Extensions/IFileSystemItemExtensions.cs | 16 +-- .../Helpers/PropertyStoreHelper.cs | 37 ++++-- .../Peek.Common/Models/IFileSystemItem.cs | 5 - .../Peek.Common/Models/Win32/PropertyKey.cs | 2 + .../peek/Peek.FilePreviewer/FilePreview.xaml | 16 +++ .../Peek.FilePreviewer/FilePreview.xaml.cs | 6 + .../Previewers/Interfaces/IVideoPreviewer.cs | 13 ++ .../Helpers/NativeMethods.cs | 0 .../Helpers/ThumbnailHelper.cs | 0 .../Helpers/WICHelper.cs | 0 .../ImagePreviewer.cs | 0 .../MediaPreviewer/VideoPreviewer.cs | 120 ++++++++++++++++++ .../Previewers/PreviewerFactory.cs | 4 + 15 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IVideoPreviewer.cs rename src/modules/peek/Peek.FilePreviewer/Previewers/{ImagePreviewer => MediaPreviewer}/Helpers/NativeMethods.cs (100%) rename src/modules/peek/Peek.FilePreviewer/Previewers/{ImagePreviewer => MediaPreviewer}/Helpers/ThumbnailHelper.cs (100%) rename src/modules/peek/Peek.FilePreviewer/Previewers/{ImagePreviewer => MediaPreviewer}/Helpers/WICHelper.cs (100%) rename src/modules/peek/Peek.FilePreviewer/Previewers/{ImagePreviewer => MediaPreviewer}/ImagePreviewer.cs (100%) create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index b369066821..e5c4b5ef87 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -113,6 +113,7 @@ arsinh artanh arw asdf +asf AShortcut ASingle ASSOCCHANGED @@ -225,8 +226,8 @@ bytearray CABD CALG callbackptr -cameligo calpwstr +cameligo Cangjie CANRENAME CAPTUREBLT @@ -771,6 +772,7 @@ google gpedit gpo GPOCA +gpp GPT gpu graphql @@ -1121,13 +1123,13 @@ LPCTSTR LPCWSTR lpdw lpfn -lpmi LPINPUT +lpmi LPMINMAXINFO LPMONITORINFO LPOSVERSIONINFOEXW -lprc LPPOINT +lprc LPRECT LPSAFEARRAY LPSTR @@ -1230,6 +1232,7 @@ Miracast mjpg mkd mkdn +mkv mlcfg mmc mmcexe @@ -1511,7 +1514,6 @@ pft pgp pgsql pguid -pkey PHANDLE phbm phbmp @@ -1525,6 +1527,7 @@ pinfo pinvoke pipename PKBDLLHOOKSTRUCT +pkey PKEY plib PLK diff --git a/src/modules/peek/Peek.Common/Extensions/DispatcherExtensions.cs b/src/modules/peek/Peek.Common/Extensions/DispatcherExtensions.cs index d6793d5316..c90dd90115 100644 --- a/src/modules/peek/Peek.Common/Extensions/DispatcherExtensions.cs +++ b/src/modules/peek/Peek.Common/Extensions/DispatcherExtensions.cs @@ -33,5 +33,29 @@ namespace Peek.Common.Extensions return tcs.Task; } + + /// + /// Run work on UI thread safely. + /// + /// True if the work was run successfully, False otherwise. + public static Task RunOnUiThread(this DispatcherQueue dispatcher, Action work) + { + var tcs = new TaskCompletionSource(); + dispatcher.TryEnqueue(() => + { + try + { + work(); + + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + return tcs.Task; + } } } diff --git a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs index 50cc7f9c93..0d976fd288 100644 --- a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs +++ b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs @@ -8,6 +8,7 @@ using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; +using Peek.Common.Helpers; using Peek.Common.Models; using Scripting; using Windows.Foundation; @@ -19,17 +20,12 @@ namespace Peek.Common.Extensions { public static Size? GetImageSize(this IFileSystemItem item) { - Size? size = null; + return PropertyStoreHelper.TryGetUintSizeProperty(item.Path, PropertyKey.ImageHorizontalSize, PropertyKey.ImageVerticalSize); + } - var width = item.Width; - var height = item.Height; - - if (width != null && height != null) - { - size = new Size((int)width, (int)height); - } - - return size; + public static Size? GetVideoSize(this IFileSystemItem item) + { + return PropertyStoreHelper.TryGetUintSizeProperty(item.Path, PropertyKey.FrameWidth, PropertyKey.FrameHeight); } public static Size? GetSvgSize(this IFileSystemItem item) diff --git a/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs b/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs index 2ca58fe7f2..392e347ad1 100644 --- a/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs +++ b/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs @@ -7,24 +7,13 @@ using System.Globalization; using System.Runtime.InteropServices; using Peek.Common.Extensions; using Peek.Common.Models; +using Windows.Foundation; using Windows.Win32.UI.Shell.PropertiesSystem; namespace Peek.Common.Helpers { public static partial class PropertyStoreHelper { - /// - /// Gets a uint type value from PropertyStore from the given item. - /// - /// The file/folder path - /// The property key - /// a nullable uint - public static uint? TryGetUintProperty(string path, PropertyKey key) - { - using DisposablePropertyStore propertyStore = GetPropertyStoreFromPath(path); - return propertyStore.TryGetUInt(key); - } - /// /// Gets a ulong type value from PropertyStore from the given item. /// @@ -49,6 +38,28 @@ namespace Peek.Common.Helpers return propertyStore.TryGetString(key); } + /// + /// Gets Size composed of weight (uint) and height (uint) from PropertyStore from the given item. + /// + /// The file/folder path + /// The property key for width + /// The property key for height + /// a nullable string + public static Size? TryGetUintSizeProperty(string path, PropertyKey widthKey, PropertyKey heightKey) + { + Size? size = null; + using DisposablePropertyStore propertyStore = GetPropertyStoreFromPath(path); + uint? width = propertyStore.TryGetUInt(widthKey); + uint? height = propertyStore.TryGetUInt(heightKey); + + if (width != null && height != null) + { + size = new Size((float)width, (float)height); + } + + return size; + } + /// /// Gets a IPropertyStore interface (wrapped in DisposablePropertyStore) from the given path. /// @@ -73,7 +84,7 @@ namespace Peek.Common.Helpers if (hr != 0) { - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "GetPropertyStore returned hresult={0}", hr)); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "GetPropertyStore returned hresult={0}, errorMessage={1}", hr, Marshal.GetExceptionForHR(hr)!.Message)); } return new DisposablePropertyStore((IPropertyStore)Marshal.GetObjectForIUnknown(ppPropertyStore)); diff --git a/src/modules/peek/Peek.Common/Models/IFileSystemItem.cs b/src/modules/peek/Peek.Common/Models/IFileSystemItem.cs index e455ca243d..d030cf90cb 100644 --- a/src/modules/peek/Peek.Common/Models/IFileSystemItem.cs +++ b/src/modules/peek/Peek.Common/Models/IFileSystemItem.cs @@ -5,7 +5,6 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Peek.Common.Extensions; using Peek.Common.Helpers; using Windows.Storage; @@ -39,10 +38,6 @@ namespace Peek.Common.Models public string Path { get; init; } - public uint? Width => PropertyStoreHelper.TryGetUintProperty(Path, PropertyKey.ImageHorizontalSize); - - public uint? Height => PropertyStoreHelper.TryGetUintProperty(Path, PropertyKey.ImageVerticalSize); - public ulong FileSizeBytes => PropertyStoreHelper.TryGetUlongProperty(Path, PropertyKey.FileSizeBytes) ?? 0; public string FileType => PropertyStoreHelper.TryGetStringProperty(Path, PropertyKey.FileType) ?? string.Empty; diff --git a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs index e21c9c7820..3611f3c82d 100644 --- a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs +++ b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs @@ -61,5 +61,7 @@ namespace Peek.Common.Models public static readonly PropertyKey ImageVerticalSize = new PropertyKey(new Guid(0x6444048F, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 4); public static readonly PropertyKey FileSizeBytes = new PropertyKey(new Guid(0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac), 12); public static readonly PropertyKey FileType = new PropertyKey(new Guid(0xd5cdd502, 0x2e9c, 0x101b, 0x93, 0x97, 0x08, 0x00, 0x2b, 0x2c, 0xf9, 0xae), 26); + public static readonly PropertyKey FrameWidth = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 3); + public static readonly PropertyKey FrameHeight = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 4); } } diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml index c64410551d..f5035fab7f 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml @@ -26,6 +26,22 @@ ToolTipService.ToolTip="{x:Bind ImageInfoTooltip, Mode=OneWay}" Visibility="{x:Bind IsPreviewVisible(ImagePreviewer, Previewer.State), Mode=OneWay}" /> + + + + + + Previewer as IImagePreviewer; + public IVideoPreviewer? VideoPreviewer => Previewer as IVideoPreviewer; + public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer; public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer; @@ -140,6 +143,7 @@ namespace Peek.FilePreviewer { Previewer = null; ImagePreview.Visibility = Visibility.Collapsed; + VideoPreview.Visibility = Visibility.Collapsed; BrowserPreview.Visibility = Visibility.Collapsed; UnsupportedFilePreview.Visibility = Visibility.Collapsed; return; @@ -199,6 +203,8 @@ namespace Peek.FilePreviewer partial void OnPreviewerChanging(IPreviewer? value) { + VideoPreview.MediaPlayer.Pause(); + if (Previewer != null) { Previewer.PropertyChanged -= Previewer_PropertyChanged; diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IVideoPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IVideoPreviewer.cs new file mode 100644 index 0000000000..ae7d26d1b1 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IVideoPreviewer.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.Media.Core; + +namespace Peek.FilePreviewer.Previewers.Interfaces +{ + public interface IVideoPreviewer : IPreviewer + { + public MediaSource? Preview { get; } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/NativeMethods.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/NativeMethods.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/ThumbnailHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/ThumbnailHelper.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/ThumbnailHelper.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/ThumbnailHelper.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/WICHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/WICHelper.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/WICHelper.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/WICHelper.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs new file mode 100644 index 0000000000..7f5e6ae87f --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.UI.Dispatching; +using Peek.Common.Extensions; +using Peek.Common.Helpers; +using Peek.Common.Models; +using Peek.FilePreviewer.Previewers.Interfaces; +using Windows.Foundation; +using Windows.Media.Core; +using Windows.Storage; + +namespace Peek.FilePreviewer.Previewers +{ + public partial class VideoPreviewer : ObservableObject, IVideoPreviewer, IDisposable + { + [ObservableProperty] + private MediaSource? preview; + + [ObservableProperty] + private PreviewState state; + + [ObservableProperty] + private Size videoSize; + + public VideoPreviewer(IFileSystemItem file) + { + Item = file; + Dispatcher = DispatcherQueue.GetForCurrentThread(); + } + + private IFileSystemItem Item { get; } + + private DispatcherQueue Dispatcher { get; } + + private Task? VideoTask { get; set; } + + public static bool IsFileTypeSupported(string fileExt) + { + return _supportedFileTypes.Contains(fileExt); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public async Task LoadPreviewAsync(CancellationToken cancellationToken) + { + State = PreviewState.Loading; + VideoTask = LoadVideoAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + await VideoTask; + + if (Preview == null && HasFailedLoadingPreview()) + { + State = PreviewState.Error; + } + } + + partial void OnPreviewChanged(MediaSource? value) + { + if (Preview != null) + { + State = PreviewState.Loaded; + } + } + + public async Task GetPreviewSizeAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.Run(Item.GetVideoSize); + } + + public async Task CopyAsync() + { + await Dispatcher.RunOnUiThread(async () => + { + var storageItem = await Item.GetStorageItemAsync(); + ClipboardHelper.SaveToClipboard(storageItem); + }); + } + + private Task LoadVideoAsync(CancellationToken cancellationToken) + { + return TaskExtension.RunSafe(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + var storageFile = await Item.GetStorageItemAsync() as StorageFile; + + await Dispatcher.RunOnUiThread(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + Preview = MediaSource.CreateFromStorageFile(storageFile); + }); + }); + } + + private bool HasFailedLoadingPreview() + { + return !(VideoTask?.Result ?? true); + } + + private static readonly HashSet _supportedFileTypes = new() + { + ".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts", + ".m4v", ".mkv", ".mov", ".mp4", ".mp4v", ".mts", ".wm", ".wmv", + }; + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index 8a8b0becdc..f03a8435be 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -16,6 +16,10 @@ namespace Peek.FilePreviewer.Previewers { return new ImagePreviewer(file); } + else if (VideoPreviewer.IsFileTypeSupported(file.Extension)) + { + return new VideoPreviewer(file); + } else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension)) { return new WebBrowserPreviewer(file);