diff --git a/apps/electron-backend/build-embedded-mpv.js b/apps/electron-backend/build-embedded-mpv.js index 8e0bbfc52..23db9f536 100644 --- a/apps/electron-backend/build-embedded-mpv.js +++ b/apps/electron-backend/build-embedded-mpv.js @@ -51,6 +51,9 @@ function log(message) { function cleanOutput() { fs.rmSync(outputFile, { force: true }); fs.rmSync(outputLibDir, { recursive: true, force: true }); + for (const windowsDllName of ['mpv-2.dll', 'mpv.dll']) { + fs.rmSync(path.join(outputDir, windowsDllName), { force: true }); + } fs.rmSync(path.join(outputDir, '.deps'), { recursive: true, force: true }); fs.rmSync(path.join(outputDir, 'obj.target'), { recursive: true, @@ -233,6 +236,17 @@ function copyGenericRuntimeToNativeBuild(runtime) { copiedFiles.add(fileName); } + if (targetPlatform === 'win32') { + for (const fileName of copiedFiles) { + if (fileName.endsWith('.dll')) { + copyFile( + path.join(outputLibDir, fileName), + path.join(outputDir, fileName) + ); + } + } + } + if (targetPlatform === 'linux') { const libMpvPath = findLinuxLibMpv(outputLibDir); if (libMpvPath && path.basename(libMpvPath) !== 'libmpv.so') { diff --git a/apps/electron-backend/native/src/embedded_mpv_linux.cc b/apps/electron-backend/native/src/embedded_mpv_linux.cc index e2856a374..6277c935f 100644 --- a/apps/electron-backend/native/src/embedded_mpv_linux.cc +++ b/apps/electron-backend/native/src/embedded_mpv_linux.cc @@ -94,6 +94,7 @@ public: XMoveResizeWindow(display_, window_, x, y, width, height); XRaiseWindow(display_, window_); XFlush(display_); + drainEvents(); } std::string wid() const @@ -117,6 +118,18 @@ public: } private: + void drainEvents() + { + if (!display_) { + return; + } + + while (XPending(display_) > 0) { + XEvent event{}; + XNextEvent(display_, &event); + } + } + void clearInputShape() { if (!display_ || !window_) { 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 d8c155a4b..b17940bc4 100644 --- a/apps/electron-backend/native/src/embedded_mpv_wid_common.h +++ b/apps/electron-backend/native/src/embedded_mpv_wid_common.h @@ -76,6 +76,7 @@ struct Session { std::string pendingRecordingTargetPath; std::string pendingRecordingStartedAt; std::string pendingRecordingStopStartedAt; + uint64_t pendingPlaybackLoadRequestId = 0; }; std::atomic gNextSessionId{1}; @@ -468,6 +469,26 @@ bool reconcileRecordingPropertyReply( return false; } +bool reconcilePlaybackLoadReply( + const std::shared_ptr& session, + uint64_t requestId, + int error) +{ + if ( + requestId == 0 || + requestId != session->pendingPlaybackLoadRequestId + ) { + return false; + } + + session->pendingPlaybackLoadRequestId = 0; + if (error < 0) { + session->snapshot.status = SessionStatus::Error; + session->snapshot.error = mpv_error_string(error); + } + return true; +} + void runEventLoop(std::shared_ptr session) { while (session->running.load()) { @@ -563,6 +584,13 @@ void runEventLoop(std::shared_ptr session) } case MPV_EVENT_COMMAND_REPLY: case MPV_EVENT_SET_PROPERTY_REPLY: + if (reconcilePlaybackLoadReply( + session, + event->reply_userdata, + event->error + )) { + break; + } if (reconcileRecordingPropertyReply( session, event->reply_userdata, @@ -571,7 +599,6 @@ void runEventLoop(std::shared_ptr session) break; } if (event->error < 0) { - session->snapshot.status = SessionStatus::Error; session->snapshot.error = mpv_error_string(event->error); } break; @@ -844,20 +871,7 @@ Napi::Value LoadPlayback(const Napi::CallbackInfo& info) command.format = MPV_FORMAT_NODE_ARRAY; command.u.list = &commandList; - const int commandResult = mpv_command_node_async( - session->handle, - nextAsyncRequestId(), - &command - ); - if (commandResult < 0) { - updateSessionError(session, mpv_error_string(commandResult)); - throw Napi::Error::New( - env, - std::string("Failed to load playback: ") + - mpv_error_string(commandResult) - ); - } - + const uint64_t loadPlaybackRequestId = nextAsyncRequestId(); { std::lock_guard lock(session->mutex); session->snapshot.streamUrl = streamUrl; @@ -867,6 +881,25 @@ Napi::Value LoadPlayback(const Napi::CallbackInfo& info) session->snapshot.recordingTargetPath.clear(); session->snapshot.recordingStartedAt.clear(); session->snapshot.recordingError.clear(); + session->pendingPlaybackLoadRequestId = loadPlaybackRequestId; + } + + const int commandResult = mpv_command_node_async( + session->handle, + loadPlaybackRequestId, + &command + ); + if (commandResult < 0) { + { + std::lock_guard lock(session->mutex); + session->pendingPlaybackLoadRequestId = 0; + } + updateSessionError(session, mpv_error_string(commandResult)); + throw Napi::Error::New( + env, + std::string("Failed to load playback: ") + + mpv_error_string(commandResult) + ); } return env.Undefined(); diff --git a/apps/electron-backend/native/src/embedded_mpv_win32.cc b/apps/electron-backend/native/src/embedded_mpv_win32.cc index a50dd3def..5b419bd5b 100644 --- a/apps/electron-backend/native/src/embedded_mpv_win32.cc +++ b/apps/electron-backend/native/src/embedded_mpv_win32.cc @@ -18,10 +18,21 @@ public: } static std::string lastError() + { + if (!lastErrorMessage_.empty()) { + return lastErrorMessage_; + } + + return formatLastError( + "Failed to create embedded MPV child window." + ); + } + + static std::string formatLastError(const char* fallbackMessage) { DWORD error = GetLastError(); if (error == 0) { - return "Failed to create embedded MPV child window."; + return fallbackMessage; } LPSTR messageBuffer = nullptr; @@ -37,7 +48,7 @@ public: nullptr ); - std::string message = "Failed to create embedded MPV child window."; + std::string message = fallbackMessage; if (length > 0 && messageBuffer) { message += " "; message += messageBuffer; @@ -50,12 +61,17 @@ public: bool create(uintptr_t parentHandle, const Bounds& bounds) { + lastErrorMessage_.clear(); parentWindow_ = reinterpret_cast(parentHandle); if (!parentWindow_ || !IsWindow(parentWindow_)) { + lastErrorMessage_ = + "Unable to resolve Electron native window handle."; return false; } - registerWindowClass(); + if (!registerWindowClass()) { + return false; + } window_ = CreateWindowExW( WS_EX_TRANSPARENT, windowClassName(), @@ -71,6 +87,9 @@ public: nullptr ); if (!window_) { + lastErrorMessage_ = formatLastError( + "Failed to create embedded MPV child window." + ); return false; } @@ -127,11 +146,11 @@ private: return L"IPTVnatorEmbeddedMpvHostWindow"; } - static void registerWindowClass() + static bool registerWindowClass() { static bool registered = false; if (registered) { - return; + return true; } WNDCLASSEXW windowClass{}; @@ -142,8 +161,19 @@ private: windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.hbrBackground = reinterpret_cast(GetStockObject(BLACK_BRUSH)); - RegisterClassExW(&windowClass); + const ATOM classAtom = RegisterClassExW(&windowClass); + if (classAtom == 0) { + const DWORD error = GetLastError(); + if (error != ERROR_CLASS_ALREADY_EXISTS) { + lastErrorMessage_ = formatLastError( + "Failed to register embedded MPV child window class." + ); + return false; + } + } + registered = true; + return true; } static LRESULT CALLBACK windowProc( @@ -165,6 +195,7 @@ private: HWND parentWindow_ = nullptr; HWND window_ = nullptr; + static inline std::string lastErrorMessage_; }; } // namespace 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 fa8587fc0..999d0fc8e 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 @@ -6,6 +6,25 @@ describe('Embedded MPV native source recording invariants', () => { path.resolve(__dirname, '../../../native/src/embedded_mpv.mm'), 'utf8' ); + const widCommonSource = readFileSync( + path.resolve( + __dirname, + '../../../native/src/embedded_mpv_wid_common.h' + ), + 'utf8' + ); + const win32Source = readFileSync( + path.resolve(__dirname, '../../../native/src/embedded_mpv_win32.cc'), + 'utf8' + ); + const linuxSource = readFileSync( + path.resolve(__dirname, '../../../native/src/embedded_mpv_linux.cc'), + 'utf8' + ); + const buildScriptSource = readFileSync( + path.resolve(__dirname, '../../../build-embedded-mpv.js'), + 'utf8' + ); function functionBody(name: string): string { const start = nativeSource.indexOf(`Napi::Value ${name}(`); @@ -67,6 +86,44 @@ describe('Embedded MPV native source recording invariants', () => { expect(endFileCase).toContain('SessionStatus::Ended'); expect(endFileCase).toContain('MPV_END_FILE_REASON_EOF'); }); + + it('keeps Windows/Linux non-load async MPV command failures non-fatal', () => { + expect(widCommonSource).toContain('pendingPlaybackLoadRequestId'); + expect(widCommonSource).toContain('reconcilePlaybackLoadReply'); + expect(widCommonSource).toContain( + 'session->snapshot.error = mpv_error_string(event->error);' + ); + expect(widCommonSource).not.toContain( + 'if (event->error < 0) {\n' + + ' session->snapshot.status = SessionStatus::Error;' + ); + }); + + it('copies Windows runtime DLLs next to the addon for Windows loader lookup', () => { + expect(buildScriptSource).toContain( + "for (const windowsDllName of ['mpv-2.dll', 'mpv.dll'])" + ); + expect(buildScriptSource).toContain( + 'path.join(outputDir, windowsDllName)' + ); + expect(buildScriptSource).toContain("fileName.endsWith('.dll')"); + expect(buildScriptSource).toContain('path.join(outputDir, fileName)'); + }); + + it('checks Win32 window class registration failures explicitly', () => { + expect(win32Source).toContain('const ATOM classAtom = RegisterClassExW'); + expect(win32Source).toContain('ERROR_CLASS_ALREADY_EXISTS'); + expect(win32Source).toContain( + 'Failed to register embedded MPV child window class.' + ); + }); + + it('drains Linux X11 events after resizing the embedded window', () => { + expect(linuxSource).toContain('void drainEvents()'); + expect(linuxSource).toContain('XPending(display_)'); + expect(linuxSource).toContain('XNextEvent(display_, &event)'); + expect(linuxSource).toContain('drainEvents();'); + }); }); describe('Embedded MPV native build configuration', () => { diff --git a/tools/packaging/electron-package-identity.test.mjs b/tools/packaging/electron-package-identity.test.mjs index ae70c0d0b..ffba9a33a 100644 --- a/tools/packaging/electron-package-identity.test.mjs +++ b/tools/packaging/electron-package-identity.test.mjs @@ -190,7 +190,8 @@ test('embedded MPV package validation accepts Windows and Linux runtime files', try { for (const [platform, runtimeFile] of [ - ['windows', join('lib', 'mpv-2.dll')], + ['windows', 'mpv-2.dll'], + ['windows', join('lib', 'mpv.dll')], ['linux', join('lib', 'libmpv.so.2')], ]) { const resourceDir = join(tempDir, platform); diff --git a/tools/packaging/embedded-mpv-macos.cjs b/tools/packaging/embedded-mpv-macos.cjs index b78d5db24..29a150c87 100644 --- a/tools/packaging/embedded-mpv-macos.cjs +++ b/tools/packaging/embedded-mpv-macos.cjs @@ -399,7 +399,7 @@ function validateNoForbiddenRuntimeLinks(binaryPaths) { return errors; } -function getPackagedRuntimeCandidates(libDir, platform) { +function getPackagedRuntimeCandidates(libDir, platform, nativeDir) { switch (platform) { case 'darwin': return [ @@ -407,11 +407,12 @@ function getPackagedRuntimeCandidates(libDir, platform) { path.join(libDir, 'libmpv.dylib'), ]; case 'win32': - case 'windows': return [ + nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null, + nativeDir ? path.join(nativeDir, 'mpv.dll') : null, path.join(libDir, 'mpv-2.dll'), path.join(libDir, 'mpv.dll'), - ]; + ].filter(Boolean); case 'linux': return [ path.join(libDir, 'libmpv.so.2'), @@ -467,7 +468,11 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) { } } - const runtimeCandidates = getPackagedRuntimeCandidates(libDir, platform); + const runtimeCandidates = getPackagedRuntimeCandidates( + libDir, + platform, + unpackedNativeDir + ); if ( runtimeCandidates.length > 0 && !runtimeCandidates.some((candidate) => fs.existsSync(candidate))