diff --git a/apps/electron-backend/native/src/embedded_mpv_wid_common.h b/apps/electron-backend/native/src/embedded_mpv_wid_common.h index 14c467b11..c9a806f20 100644 --- a/apps/electron-backend/native/src/embedded_mpv_wid_common.h +++ b/apps/electron-backend/native/src/embedded_mpv_wid_common.h @@ -60,9 +60,6 @@ IPTVNATOR_DECLARE_MPV_DYNAMIC_SYMBOL(mpv_wakeup) #include #include #include -#ifdef IPTVNATOR_DYNAMIC_LIBMPV -#include -#endif #include #include #include @@ -75,7 +72,6 @@ IPTVNATOR_DECLARE_MPV_DYNAMIC_SYMBOL(mpv_wakeup) #include #ifdef __linux__ #include -#include #include #include #include @@ -152,157 +148,6 @@ std::atomic gNextAsyncRequestId{1}; std::mutex gSessionsMutex; std::unordered_map> gSessions; -#ifdef IPTVNATOR_DYNAMIC_LIBMPV -std::once_flag gMpvApiLoadOnce; -bool gMpvApiLoaded = false; -std::string gMpvApiLoadError; -void* gMpvLibrary = nullptr; - -std::string directoryName(const std::string& filePath) -{ - const auto separator = filePath.find_last_of('/'); - if (separator == std::string::npos) { - return "."; - } - - if (separator == 0) { - return "/"; - } - - return filePath.substr(0, separator); -} - -std::string currentAddonDirectory() -{ - Dl_info info{}; - if (dladdr(reinterpret_cast(¤tAddonDirectory), &info) && - info.dli_fname) { - return directoryName(info.dli_fname); - } - - return "."; -} - -std::vector mpvLibraryCandidates() -{ - const auto addonDirectory = currentAddonDirectory(); - const auto bundledLibraryDirectory = addonDirectory + "/lib"; - - return { - bundledLibraryDirectory + "/libmpv.so.2", - bundledLibraryDirectory + "/libmpv.so.1", - bundledLibraryDirectory + "/libmpv.so", - "libmpv.so.2", - "libmpv.so", - }; -} - -void* openMpvLibrary(const std::string& libraryPath, std::string& error) -{ - constexpr int flags = RTLD_NOW | RTLD_LOCAL; - - dlerror(); - void* library = dlopen(libraryPath.c_str(), flags); - const char* loadError = dlerror(); - if (library) { - return library; - } - - std::ostringstream message; - message << libraryPath << ": "; - message << (loadError ? loadError : "unknown dlopen error"); - error = message.str(); - return nullptr; -} - -template -bool loadMpvSymbol( - void* library, - SymbolPointer& target, - const char* name, - std::string& error) -{ - dlerror(); - void* symbol = dlsym(library, name); - const char* symbolError = dlerror(); - if (symbolError || !symbol) { - error = std::string("Unable to resolve libmpv symbol ") + name + - ": " + (symbolError ? symbolError : "symbol not found"); - return false; - } - - target = reinterpret_cast(symbol); - return true; -} - -bool loadMpvSymbols(void* library, std::string& error) -{ -#define LOAD_MPV_SYMBOL(name) \ - if (!loadMpvSymbol(library, pfn_##name, #name, error)) { \ - return false; \ - } - - LOAD_MPV_SYMBOL(mpv_command_async); - LOAD_MPV_SYMBOL(mpv_command_node_async); - LOAD_MPV_SYMBOL(mpv_create); - LOAD_MPV_SYMBOL(mpv_error_string); - LOAD_MPV_SYMBOL(mpv_initialize); - LOAD_MPV_SYMBOL(mpv_observe_property); - LOAD_MPV_SYMBOL(mpv_request_log_messages); - LOAD_MPV_SYMBOL(mpv_set_option_string); - LOAD_MPV_SYMBOL(mpv_set_property); - LOAD_MPV_SYMBOL(mpv_set_property_async); - LOAD_MPV_SYMBOL(mpv_terminate_destroy); - LOAD_MPV_SYMBOL(mpv_wait_event); - LOAD_MPV_SYMBOL(mpv_wakeup); - -#undef LOAD_MPV_SYMBOL - return true; -} - -void loadMpvApiOnce() -{ - std::vector errors; - - for (const auto& candidate : mpvLibraryCandidates()) { - std::string openError; - void* library = openMpvLibrary(candidate, openError); - if (!library) { - errors.push_back(openError); - continue; - } - - std::string symbolError; - if (loadMpvSymbols(library, symbolError)) { - gMpvLibrary = library; - gMpvApiLoaded = true; - return; - } - - errors.push_back(candidate + ": " + symbolError); - dlclose(library); - } - - std::ostringstream message; - message << "Unable to load the embedded libmpv runtime."; - for (const auto& error : errors) { - message << "\n- " << error; - } - gMpvApiLoadError = message.str(); -} - -[[maybe_unused]] bool ensureMpvApiLoaded(std::string& error) -{ - std::call_once(gMpvApiLoadOnce, loadMpvApiOnce); - if (!gMpvApiLoaded) { - error = gMpvApiLoadError; - return false; - } - - return true; -} -#endif - void traceMpvCommon(const std::string& message) { if (!std::getenv("IPTVNATOR_TRACE_EMBEDDED_MPV")) { @@ -696,36 +541,14 @@ std::vector buildLinuxMpvArguments( return arguments; } -void closeInheritedFileDescriptors() +long inheritedFileDescriptorLimit() { - DIR* fdDirectory = opendir("/proc/self/fd"); - if (fdDirectory) { - const int directoryFd = dirfd(fdDirectory); - - while (dirent* entry = readdir(fdDirectory)) { - char* end = nullptr; - const long fileDescriptor = std::strtol(entry->d_name, &end, 10); - if ( - end == entry->d_name || - (end && *end != '\0') || - fileDescriptor <= STDERR_FILENO || - fileDescriptor == directoryFd - ) { - continue; - } - const int descriptor = static_cast(fileDescriptor); - const int flags = fcntl(descriptor, F_GETFD); - if (flags >= 0) { - fcntl(descriptor, F_SETFD, flags | FD_CLOEXEC); - } - } - - closedir(fdDirectory); - return; - } - const long maxOpenFiles = sysconf(_SC_OPEN_MAX); - const long fileDescriptorLimit = maxOpenFiles > 0 ? maxOpenFiles : 1024; + return maxOpenFiles > 0 ? maxOpenFiles : 1024; +} + +void closeInheritedFileDescriptors(long fileDescriptorLimit) +{ for (long fileDescriptor = STDERR_FILENO + 1; fileDescriptor < fileDescriptorLimit; fileDescriptor += 1) { @@ -1026,6 +849,7 @@ pid_t spawnLinuxMpvProcess( envp.push_back(nullptr); const bool traceEnabled = std::getenv("IPTVNATOR_TRACE_EMBEDDED_MPV"); + const long fileDescriptorLimit = inheritedFileDescriptorLimit(); const pid_t processId = fork(); if (processId != 0) { return processId; @@ -1042,7 +866,7 @@ pid_t spawnLinuxMpvProcess( } } - closeInheritedFileDescriptors(); + closeInheritedFileDescriptors(fileDescriptorLimit); execvpe(argv[0], argv.data(), envp.data()); _exit(127); } @@ -1503,9 +1327,6 @@ Napi::Value IsSupported(const Napi::CallbackInfo& info) bool supported = NativeVideoHost::isAvailable(); #ifdef __linux__ supported = supported && isExecutableAvailable("mpv"); -#elif defined(IPTVNATOR_DYNAMIC_LIBMPV) - std::string loadError; - supported = supported && ensureMpvApiLoaded(loadError); #endif return Napi::Boolean::New(info.Env(), supported); } @@ -1537,13 +1358,6 @@ Napi::Value CreateSession(const Napi::CallbackInfo& info) } const auto bounds = readBounds(info[1].As()); -#if defined(IPTVNATOR_DYNAMIC_LIBMPV) && !defined(__linux__) - std::string mpvLoadError; - if (!ensureMpvApiLoaded(mpvLoadError)) { - throw Napi::Error::New(env, mpvLoadError); - } -#endif - const auto session = std::make_shared(); session->id = "embedded-mpv-" + std::to_string(gNextSessionId.fetch_add(1)); session->snapshot.volumePercent = diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts index 57331ea24..834193526 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts @@ -32,6 +32,13 @@ describe('Embedded MPV native source recording invariants', () => { ), 'utf8' ); + const stageRuntimeSource = readFileSync( + path.resolve( + __dirname, + '../../../../../tools/embedded-mpv/stage-runtime.mjs' + ), + 'utf8' + ); function functionBody(name: string): string { const start = nativeSource.indexOf(`Napi::Value ${name}(`); @@ -183,8 +190,14 @@ describe('Embedded MPV native source recording invariants', () => { expect(widCommonSource).toContain( 'arguments.push_back("--gpu-context=x11egl");' ); - expect(widCommonSource).toContain('closeInheritedFileDescriptors();'); + expect(widCommonSource).toContain('inheritedFileDescriptorLimit()'); + expect(widCommonSource).toContain( + 'closeInheritedFileDescriptors(fileDescriptorLimit);' + ); expect(widCommonSource).toContain('flags | FD_CLOEXEC'); + expect(widCommonSource).not.toContain('opendir('); + expect(widCommonSource).not.toContain('readdir('); + expect(widCommonSource).not.toContain('closedir('); }); it('drives Linux out-of-process MPV state and controls over JSON IPC', () => { @@ -223,9 +236,20 @@ describe('Embedded MPV native source recording invariants', () => { expect(widCommonSource).toContain( '#define mpv_command_async pfn_mpv_command_async' ); + expect(widCommonSource).not.toContain('mpvLibraryCandidates'); + expect(widCommonSource).not.toContain('ensureMpvApiLoaded'); + expect(widCommonSource).not.toContain('"libmpv.so"'); expect(buildScriptSource).toContain('cleanNativeBuildIntermediates();'); }); + it('does not stage Linux libmpv runtime libraries', () => { + expect(stageRuntimeSource).toContain( + "if (platform !== 'linux') {\n" + + ' copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);\n' + + ' }' + ); + }); + it('uses platform-specific embedded MPV runtime cache key inputs in CI', () => { expect(buildAndMakeWorkflowSource).toContain( "const targetPlatform = '${{ matrix.embedded_mpv_platform }}';" diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts index 02605a2ed..d16d2318a 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts @@ -414,6 +414,26 @@ describe('EmbeddedMpvNativeService power blocker', () => { expect(support.supported).toBe(false); expect(support.reason).toContain('requires the mpv executable on PATH'); + expect(mockSpawnSync).toHaveBeenCalledWith('mpv', ['--version'], { + stdio: 'ignore', + timeout: 3000, + }); + }); + + it('caches the Linux mpv executable probe result', () => { + Object.defineProperty(process, 'platform', { + value: 'linux', + }); + delete process.env.WAYLAND_DISPLAY; + + service.getSupport(); + service.getSupport(); + + expect(mockSpawnSync).toHaveBeenCalledTimes(1); + expect(mockSpawnSync).toHaveBeenCalledWith('mpv', ['--version'], { + stdio: 'ignore', + timeout: 3000, + }); }); it('rejects Electron native Wayland placeholder handles before calling the addon', () => { @@ -432,6 +452,22 @@ describe('EmbeddedMpvNativeService power blocker', () => { expect(addon.createSession).not.toHaveBeenCalled(); }); + it('rejects 64-bit Electron native Wayland placeholder handles before calling the addon', () => { + Object.defineProperty(process, 'platform', { + value: 'linux', + }); + process.env.DISPLAY = ':0'; + process.env.WAYLAND_DISPLAY = 'wayland-0'; + mainWindowGetNativeWindowHandleMock.mockReturnValueOnce( + Buffer.alloc(8) + ); + + expect(() => service.createSession(BOUNDS, '', 1)).toThrow( + 'Embedded MPV on Linux requires Electron to run under X11 or Xwayland.' + ); + expect(addon.createSession).not.toHaveBeenCalled(); + }); + it.each([ { platform: 'darwin' as NodeJS.Platform, diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index 253e26cc0..716b54d2a 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -97,6 +97,7 @@ export class EmbeddedMpvNativeService { private readonly pollFailuresLogged = new Set(); private powerBlockerId: number | null = null; private readonly loadAddonModule = createRequire(__filename); + private cachedLinuxMpvExecutableReason: string | null | undefined; private detectCapabilities(): EmbeddedMpvCapabilities { const addon = this.addon; @@ -793,12 +794,19 @@ export class EmbeddedMpvNativeService { return null; } - const result = spawnSync('mpv', ['--version'], { stdio: 'ignore' }); - if (result.status === 0) { - return null; + if (this.cachedLinuxMpvExecutableReason !== undefined) { + return this.cachedLinuxMpvExecutableReason; } - return 'Embedded MPV on Linux requires the mpv executable on PATH. Install the mpv package for your distribution and restart IPTVnator.'; + const result = spawnSync('mpv', ['--version'], { + stdio: 'ignore', + timeout: 3000, + }); + this.cachedLinuxMpvExecutableReason = + result.status === 0 + ? null + : 'Embedded MPV on Linux requires the mpv executable on PATH. Install the mpv package for your distribution and restart IPTVnator.'; + return this.cachedLinuxMpvExecutableReason; } private isInvalidLinuxWaylandWindowHandle(windowHandle: Buffer): boolean { @@ -810,11 +818,15 @@ export class EmbeddedMpvNativeService { return false; } - if (windowHandle.length === 0 || windowHandle.length > 4) { + if (windowHandle.length === 0) { return false; } - return windowHandle.readUIntLE(0, windowHandle.length) <= 1; + const windowHandleValue = + windowHandle.length >= 4 + ? windowHandle.readUInt32LE(0) + : windowHandle.readUIntLE(0, windowHandle.length); + return windowHandleValue <= 1; } private getAddon(): NativeEmbeddedMpvAddon { diff --git a/tools/embedded-mpv/stage-runtime.mjs b/tools/embedded-mpv/stage-runtime.mjs index 21912fcf6..35836cc5b 100644 --- a/tools/embedded-mpv/stage-runtime.mjs +++ b/tools/embedded-mpv/stage-runtime.mjs @@ -181,7 +181,9 @@ try { path.join(sourceIncludeDir, 'mpv'), path.join(destinationIncludeDir, 'mpv') ); - copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter); + if (platform !== 'linux') { + copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter); + } if (platform === 'win32') { copyDirectory(sourceBinDir, destinationLibDir, runtimeFileFilter); }