diff --git a/src/ui/Logic/VideoPlayers/LibMpvDynamic/LibMpvDynamicPlayer.cs b/src/ui/Logic/VideoPlayers/LibMpvDynamic/LibMpvDynamicPlayer.cs index 6bd68a60348..0dc59cf9095 100644 --- a/src/ui/Logic/VideoPlayers/LibMpvDynamic/LibMpvDynamicPlayer.cs +++ b/src/ui/Logic/VideoPlayers/LibMpvDynamic/LibMpvDynamicPlayer.cs @@ -365,16 +365,57 @@ private static string[] GetLibraryNames() } } + internal static string[] GetWindowsLibraryPaths( + string mpvPath, + string dataFolder, + string baseDirectory, + string currentDirectory) + { + var paths = new List(); + var comparisonPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + void AddPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + if (comparisonPaths.Add(string.Empty)) + { + paths.Add(path); + } + + return; + } + + var comparisonPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(path)); + if (comparisonPaths.Add(comparisonPath)) + { + paths.Add(path); + } + } + + // A configured override wins. The per-user data folder comes next so a downloaded + // libmpv can override the installer/portable baseline without administrator rights. + if (!string.IsNullOrWhiteSpace(mpvPath)) + { + AddPath(mpvPath); + } + + AddPath(dataFolder); + AddPath(baseDirectory); + AddPath(currentDirectory); + AddPath(string.Empty); + return paths.ToArray(); + } + private static string[] GetLibraryPaths() { if (OperatingSystem.IsWindows()) { - return - [ + return GetWindowsLibraryPaths( MpvPath, - Directory.GetCurrentDirectory(), - string.Empty, - ]; + Se.DataFolder, + AppContext.BaseDirectory, + Directory.GetCurrentDirectory()); } else if (OperatingSystem.IsLinux()) { @@ -456,6 +497,13 @@ private void LoadLibMpvMethods() return address != IntPtr.Zero ? Marshal.GetDelegateForFunctionPointer(address, type) : null; } + internal static bool ShouldAttemptLibraryLoad(string libraryPath, string fullPath) + { + // The empty Windows root is a sentinel for the platform loader's normal search path. + // Do not pre-empt that fallback with File.Exists(), which only checks the process CWD. + return string.IsNullOrEmpty(libraryPath) || File.Exists(fullPath); + } + private bool LoadLibraryInternal() { foreach (var libName in GetLibraryNames()) @@ -463,16 +511,18 @@ private bool LoadLibraryInternal() foreach (var libPath in GetLibraryPaths()) { var fullPath = Path.Combine(libPath, libName); - if (File.Exists(fullPath)) + if (!ShouldAttemptLibraryLoad(libPath, fullPath)) { - var libHandle = NativeMethods.CrossLoadLibrary(fullPath); - if (libHandle != IntPtr.Zero) - { - _library = libHandle; - LoadLibMpvMethods(); - _mpv = _mpvCreate!.Invoke(); - return true; - } + continue; + } + + var libHandle = NativeMethods.CrossLoadLibrary(fullPath); + if (libHandle != IntPtr.Zero) + { + _library = libHandle; + LoadLibMpvMethods(); + _mpv = _mpvCreate!.Invoke(); + return true; } } } diff --git a/tests/UI/Logic/VideoPlayers/LibMpvLibraryPathTests.cs b/tests/UI/Logic/VideoPlayers/LibMpvLibraryPathTests.cs new file mode 100644 index 00000000000..0f079f6bb5c --- /dev/null +++ b/tests/UI/Logic/VideoPlayers/LibMpvLibraryPathTests.cs @@ -0,0 +1,76 @@ +using Nikse.SubtitleEdit.Logic.VideoPlayers.LibMpvDynamic; + +namespace UITests.Logic.VideoPlayers; + +/// +/// The Windows installer and the in-app updater keep libmpv in different roots. Library lookup +/// must name those roots explicitly instead of depending on the process working directory. +/// +public class LibMpvLibraryPathTests +{ + [Fact] + public void GetWindowsLibraryPaths_PutsManualOverrideFirst() + { + var paths = LibMpvDynamicPlayer.GetWindowsLibraryPaths("/manual", "/data", "/app", "/cwd"); + + Assert.Equal("/manual", paths[0]); + } + + [Fact] + public void GetWindowsLibraryPaths_PutsDownloadedCopyBeforeBundledCopy() + { + var paths = LibMpvDynamicPlayer.GetWindowsLibraryPaths(string.Empty, "/data", "/app", "/cwd"); + + Assert.Equal("/data", paths[0]); + Assert.Equal("/app", paths[1]); + } + + [Fact] + public void GetWindowsLibraryPaths_DoesNotDependOnWorkingDirectoryForKnownCopies() + { + var paths = LibMpvDynamicPlayer.GetWindowsLibraryPaths(string.Empty, "/data", "/app", "/cwd"); + + Assert.True(Array.IndexOf(paths, "/data") < Array.IndexOf(paths, "/cwd")); + Assert.True(Array.IndexOf(paths, "/app") < Array.IndexOf(paths, "/cwd")); + Assert.Equal(string.Empty, paths[^1]); + } + + [Fact] + public void GetWindowsLibraryPaths_DeduplicatesPortableRoots() + { + var paths = LibMpvDynamicPlayer.GetWindowsLibraryPaths(string.Empty, "/portable", "/portable", "/portable"); + + Assert.Equal(["/portable", string.Empty], paths); + } + [Fact] + public void GetWindowsLibraryPaths_DeduplicatesEquivalentRootsWithTrailingSeparator() + { + var root = Path.Combine(Path.GetTempPath(), "libmpv-path-test"); + var rootWithSeparator = root + Path.DirectorySeparatorChar; + + var paths = LibMpvDynamicPlayer.GetWindowsLibraryPaths(string.Empty, rootWithSeparator, root, root); + + Assert.Equal([rootWithSeparator, string.Empty], paths); + } + + + [Fact] + public void ShouldAttemptLibraryLoad_DefaultLoaderSentinel_DoesNotRequireLocalFile() + { + var missing = Path.Combine(Path.GetTempPath(), "missing-" + Guid.NewGuid().ToString("N"), "libmpv-2.dll"); + + Assert.False(File.Exists(missing)); + Assert.True(LibMpvDynamicPlayer.ShouldAttemptLibraryLoad(string.Empty, missing)); + } + + [Fact] + public void ShouldAttemptLibraryLoad_ExplicitRoot_RequiresExistingFile() + { + var missing = Path.Combine(Path.GetTempPath(), "missing-" + Guid.NewGuid().ToString("N"), "libmpv-2.dll"); + + Assert.False(File.Exists(missing)); + Assert.False(LibMpvDynamicPlayer.ShouldAttemptLibraryLoad(Path.GetDirectoryName(missing)!, missing)); + } + + +}