Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 64 additions & 14 deletions src/ui/Logic/VideoPlayers/LibMpvDynamic/LibMpvDynamicPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,16 +365,57 @@ private static string[] GetLibraryNames()
}
}

internal static string[] GetWindowsLibraryPaths(
string mpvPath,
string dataFolder,
string baseDirectory,
string currentDirectory)
{
var paths = new List<string>();
var comparisonPaths = new HashSet<string>(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())
{
Expand Down Expand Up @@ -456,23 +497,32 @@ 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())
{
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;
}
}
}
Expand Down
76 changes: 76 additions & 0 deletions tests/UI/Logic/VideoPlayers/LibMpvLibraryPathTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Nikse.SubtitleEdit.Logic.VideoPlayers.LibMpvDynamic;

namespace UITests.Logic.VideoPlayers;

/// <summary>
/// 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.
/// </summary>
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));
}


}
Loading