diff --git a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs index 311c725c7..f672b074d 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs @@ -19,26 +19,57 @@ using QuickLook.MediaInfo; using QuickLook.MediaInfo.Core; using System; +using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using System.Windows; namespace QuickLook.Plugin.VideoViewer; public sealed class Plugin : IViewer { + /// + /// Extensions opened directly with a full MediaInfo parse. Anything else is probed first, + /// so media with unusual extensions still works while arbitrary binaries stay cheap. + /// + private static readonly HashSet WellKnownExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".3g2", ".3gp", ".asf", ".avi", ".flv", ".m2ts", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", + ".mts", ".ogv", ".rm", ".rmvb", ".ts", ".vob", ".webm", ".wmv", + ".aac", ".ac3", ".aif", ".aiff", ".amr", ".ape", ".dts", ".flac", ".m4a", ".mka", ".mp3", ".oga", + ".ogg", ".opus", ".wav", ".wma", ".wv", + }; + + /// Upper bound of bytes read when probing a file with an unknown extension. + private const long MaxProbeBytes = 4 * 1024 * 1024; + private static readonly MediaInfoNative _mediaInfo; + /// + /// Separate handle for : calling Open() on a handle previously + /// used in buffer mode crashes MediaInfo with an access violation. + /// + private static readonly MediaInfoNative _probeMediaInfo; + private ViewerPanel _vp; public int Priority => -3; static Plugin() { - _mediaInfo = new(Path.Combine( + var nativePath = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), - Environment.Is64BitProcess ? @"runtimes\win-x64\native\" : @"runtimes\win-x86\native\")); + Environment.Is64BitProcess ? @"runtimes\win-x64\native\" : @"runtimes\win-x86\native\"); + + _mediaInfo = new(nativePath); _mediaInfo.Option("Cover_Data", "base64"); + // Only header fields (format, size, rotation, tags, cover) are used. The default parse + // speed also samples the stream, reading tens of MB per file and stalling on slow disks. + _mediaInfo.Option("ParseSpeed", "0"); + + _probeMediaInfo = new(nativePath); + _probeMediaInfo.Option("ParseSpeed", "0"); } public void Init() @@ -56,6 +87,12 @@ public bool CanHandle(string path) { try { + // MediaInfo.Open() on an arbitrary binary can read the whole file several times + // (sync-word scanning for headerless formats like MPEG-TS or raw MP3), which freezes + // the UI on large files. Probe unknown extensions with a bounded read first. + if (!WellKnownExtensions.Contains(Path.GetExtension(path)) && !ProbeIsMedia(path)) + return false; + _mediaInfo.Open(path); string videoCodec = _mediaInfo.Get(StreamKind.Video, 0, "Format"); string audioCodec = _mediaInfo.Get(StreamKind.Audio, 0, "Format"); @@ -83,6 +120,48 @@ public bool CanHandle(string path) return false; } + /// + /// Feeds MediaInfo through its buffer API, honoring its seek requests but reading at most + /// , so non-media files are rejected without scanning them entirely. + /// + private static bool ProbeIsMedia(string path) + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var buffer = new byte[64 * 1024]; + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + long total = 0; + _probeMediaInfo.OpenBufferInit(fs.Length, 0); + while (total < MaxProbeBytes) + { + int read = fs.Read(buffer, 0, (int)Math.Min(buffer.Length, MaxProbeBytes - total)); + if (read <= 0) + break; + total += read; + + // Bit 3 (0x08): MediaInfo has finished parsing + if ((_probeMediaInfo.OpenBufferContinue(handle.AddrOfPinnedObject(), (IntPtr)read) & 0x08) != 0) + break; + + long goTo = _probeMediaInfo.OpenBufferContinueGoToGet(); + if (goTo != -1) + { + fs.Seek(goTo, SeekOrigin.Begin); + _probeMediaInfo.OpenBufferInit(fs.Length, fs.Position); + } + } + _probeMediaInfo.OpenBufferFinalize(); + } + finally + { + handle.Free(); + } + + return !string.IsNullOrWhiteSpace(_probeMediaInfo.Get(StreamKind.Video, 0, "Format")) + || !string.IsNullOrWhiteSpace(_probeMediaInfo.Get(StreamKind.Audio, 0, "Format")); + } + public void Prepare(string path, ContextObject context) { string videoCodec = _mediaInfo.Get(StreamKind.Video, 0, "Format");