import { readFileSync } from 'fs'; import path from 'path'; describe('Embedded MPV native source recording invariants', () => { const nativeSource = readFileSync( 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' ); const buildAndMakeWorkflowSource = readFileSync( path.resolve( __dirname, '../../../../../.github/workflows/build-and-make.yaml' ), 'utf8' ); const stageRuntimeSource = readFileSync( path.resolve( __dirname, '../../../../../tools/embedded-mpv/stage-runtime.mjs' ), 'utf8' ); function functionBody(name: string): string { return sourceFunctionBody(nativeSource, `Napi::Value ${name}(`, name); } function sourceFunctionBody( source: string, signature: string, name: string ): string { const start = source.indexOf(signature); expect(start).toBeGreaterThanOrEqual(0); const bodyStart = source.indexOf('{', start); expect(bodyStart).toBeGreaterThanOrEqual(0); let depth = 0; for (let index = bodyStart; index < source.length; index += 1) { if (source[index] === '{') { depth += 1; } if (source[index] === '}') { depth -= 1; } if (depth === 0) { return source.slice(bodyStart, index + 1); } } throw new Error(`Unable to read ${name} body.`); } function eventCase( source: string, eventName: string, nextEventName: string ): string { const eventLoopBodyStart = source.indexOf('void runEventLoop('); expect(eventLoopBodyStart).toBeGreaterThanOrEqual(0); const eventCaseStart = source.indexOf( `case ${eventName}`, eventLoopBodyStart ); expect(eventCaseStart).toBeGreaterThanOrEqual(0); const nextCaseStart = source.indexOf( `case ${nextEventName}`, eventCaseStart ); expect(nextCaseStart).toBeGreaterThan(eventCaseStart); return source.slice(eventCaseStart, nextCaseStart); } it('tracks LoadPlayback recording auto-stop replies through the reconciler', () => { const body = functionBody('LoadPlayback'); expect(body).toContain( 'const uint64_t stopRecordingRequestId = nextAsyncRequestId();' ); expect(body).toContain( 'session->pendingRecordingStopRequestId = stopRecordingRequestId;' ); expect(body).toContain( 'session->pendingRecordingStopStartedAt =\n' + ' session->snapshot.recordingStartedAt;' ); expect(body).toContain('stopRecordingRequestId,'); }); it('maps successful MPV end-file events to an ended session status', () => { expect(nativeSource).toContain('Ended,'); expect(nativeSource).toContain('case SessionStatus::Ended:'); expect(widCommonSource).toContain('Ended,'); expect(widCommonSource).toContain('case SessionStatus::Ended:'); const endFileCase = eventCase( nativeSource, 'MPV_EVENT_END_FILE', 'MPV_EVENT_PROPERTY_CHANGE' ); expect(endFileCase).toContain('SessionStatus::Ended'); expect(endFileCase).toContain('MPV_END_FILE_REASON_EOF'); const widEndFileCase = eventCase( widCommonSource, 'MPV_EVENT_END_FILE', 'MPV_EVENT_PROPERTY_CHANGE' ); expect(widEndFileCase).toContain('SessionStatus::Ended'); expect(widEndFileCase).toContain('MPV_END_FILE_REASON_EOF'); }); it('keeps MPV redirect end-file events in loading state', () => { for (const endFileCase of [ eventCase( nativeSource, 'MPV_EVENT_END_FILE', 'MPV_EVENT_PROPERTY_CHANGE' ), eventCase( widCommonSource, 'MPV_EVENT_END_FILE', 'MPV_EVENT_PROPERTY_CHANGE' ), ]) { const redirectBranchStart = endFileCase.indexOf( 'MPV_END_FILE_REASON_REDIRECT' ); expect(redirectBranchStart).toBeGreaterThanOrEqual(0); const idleFallbackStart = endFileCase.indexOf( 'SessionStatus::Idle', redirectBranchStart ); expect(idleFallbackStart).toBeGreaterThan(redirectBranchStart); const redirectBranch = endFileCase.slice( redirectBranchStart, idleFallbackStart ); expect(redirectBranch).toContain('SessionStatus::Loading'); expect(redirectBranch).toContain( 'session->snapshot.error.clear();' ); } }); it('maps keep-open eof-reached property changes to an ended session status', () => { expect(nativeSource).toContain( 'mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);' ); expect(widCommonSource).toContain( 'mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);' ); const nativeEofBranchStart = nativeSource.indexOf( 'propertyName == "eof-reached"' ); expect(nativeEofBranchStart).toBeGreaterThanOrEqual(0); const nativeVolumeBranchStart = nativeSource.indexOf( 'propertyName == "volume"', nativeEofBranchStart ); expect(nativeVolumeBranchStart).toBeGreaterThan(nativeEofBranchStart); const nativeEofBranch = nativeSource.slice( nativeEofBranchStart, nativeVolumeBranchStart ); expect(nativeEofBranch).toContain('SessionStatus::Ended'); expect(nativeEofBranch).toContain('session->loadedPath'); const widEofBranchStart = widCommonSource.indexOf( 'name == "eof-reached"' ); expect(widEofBranchStart).toBeGreaterThanOrEqual(0); const widVolumeBranchStart = widCommonSource.indexOf( 'name == "volume"', widEofBranchStart ); expect(widVolumeBranchStart).toBeGreaterThan(widEofBranchStart); const widEofBranch = widCommonSource.slice( widEofBranchStart, widVolumeBranchStart ); expect(widEofBranch).toContain('SessionStatus::Ended'); }); 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('clears transient Windows/Linux MPV operation errors after healthy playback states', () => { const fileLoadedCaseStart = widCommonSource.indexOf( 'case MPV_EVENT_FILE_LOADED:' ); expect(fileLoadedCaseStart).toBeGreaterThanOrEqual(0); const endFileCaseStart = widCommonSource.indexOf( 'case MPV_EVENT_END_FILE:', fileLoadedCaseStart ); expect(endFileCaseStart).toBeGreaterThan(fileLoadedCaseStart); const fileLoadedCase = widCommonSource.slice( fileLoadedCaseStart, endFileCaseStart ); expect(fileLoadedCase).toContain('session->snapshot.error.clear();'); const pausePropertyStart = widCommonSource.indexOf( 'const bool paused = *static_cast(property->data) != 0;' ); expect(pausePropertyStart).toBeGreaterThanOrEqual(0); const volumePropertyStart = widCommonSource.indexOf( '} else if (name == "volume"', pausePropertyStart ); expect(volumePropertyStart).toBeGreaterThan(pausePropertyStart); const pausePropertyBranch = widCommonSource.slice( pausePropertyStart, volumePropertyStart ); expect(pausePropertyBranch).toContain( 'session->snapshot.error.clear();' ); }); 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();'); }); it('defers Linux X11 create failure cleanup until after the error trap scope closes', () => { const trapStart = linuxSource.indexOf( 'ScopedX11ErrorTrap x11Errors(display_);' ); expect(trapStart).toBeGreaterThanOrEqual(0); const deferredCleanupStart = linuxSource.indexOf( 'if (createWindowFailed) {', trapStart ); expect(deferredCleanupStart).toBeGreaterThan(trapStart); const trappedCreateBlock = linuxSource.slice( trapStart, deferredCleanupStart ); const nextWindowGuardStart = linuxSource.indexOf( 'if (!window_) {', deferredCleanupStart ); expect(nextWindowGuardStart).toBeGreaterThan(deferredCleanupStart); const deferredCleanupBlock = linuxSource.slice( deferredCleanupStart, nextWindowGuardStart ); expect(trappedCreateBlock).toContain('createWindowFailed = true;'); expect(trappedCreateBlock).not.toContain('destroy();'); expect(deferredCleanupBlock).toContain('destroy();'); }); it('keeps Linux mpv child processes isolated from Wayland and inherited Electron descriptors', () => { expect(widCommonSource).toContain( 'if (hasEnvPrefix(entry, "WAYLAND_DISPLAY="))' ); expect(widCommonSource).toContain( 'environment.push_back("XDG_SESSION_TYPE=x11");' ); expect(widCommonSource).toContain( 'arguments.push_back("--vo=gpu,x11");' ); expect(widCommonSource).toContain( 'arguments.push_back("--gpu-context=x11egl");' ); 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', () => { expect(widCommonSource).toContain('mpvIpcSocketPath'); expect(widCommonSource).toContain( 'arguments.push_back("--input-ipc-server=" + ipcSocketPath);' ); expect(widCommonSource).toContain('refreshLinuxMpvSnapshot(session);'); expect(widCommonSource).toContain( 'queryLinuxMpvNumber(socketPath, "time-pos")' ); expect(widCommonSource).toContain( 'queryLinuxMpvNumber(socketPath, "duration")' ); expect(widCommonSource).toContain( 'std::string("{\\"command\\":[\\"set_property\\",\\"pause\\",")' ); expect(widCommonSource).toContain( '"{\\"command\\":[\\"seek\\"," + seconds + ",\\"absolute\\"]}\\n"' ); }); it('keeps Linux MPV snapshot IPC off the NAPI snapshot read path', () => { const getSnapshotBody = sourceFunctionBody( widCommonSource, 'Napi::Value GetSessionSnapshot(', 'GetSessionSnapshot' ); expect(getSnapshotBody).not.toContain('refreshLinuxMpvSnapshot'); expect(widCommonSource).toContain('void runLinuxProcessPollLoop'); expect(widCommonSource).toContain('refreshLinuxMpvSnapshot(session);'); expect(widCommonSource).toContain('startLinuxProcessPolling(session)'); expect(widCommonSource).toContain( 'session->eventThread = std::thread(runLinuxProcessPollLoop, session);' ); }); it('terminates Linux MPV processes away from the NAPI teardown path', () => { const destroyBody = sourceFunctionBody( widCommonSource, 'void destroySession(', 'destroySession' ); expect(destroyBody).toContain( 'terminateLinuxMpvProcessAsync(session);' ); expect(destroyBody).toContain('session->eventThread.detach();'); expect(destroyBody).not.toContain('std::this_thread::sleep_for'); expect(destroyBody).not.toContain('SIGKILL'); const asyncTerminationBody = sourceFunctionBody( widCommonSource, 'void terminateLinuxMpvProcessAsync(', 'terminateLinuxMpvProcessAsync' ); expect(asyncTerminationBody).toContain( 'kill(process.processId, SIGTERM);' ); expect(asyncTerminationBody).toContain('std::thread('); expect(asyncTerminationBody).toContain('waitForLinuxMpvProcessExit'); }); it('uses generation-unique Linux MPV IPC socket paths', () => { expect(widCommonSource).toContain('gNextLinuxIpcSocketId'); expect(widCommonSource).toContain( 'std::to_string(gNextLinuxIpcSocketId.fetch_add(1))' ); }); it('keeps dynamic libmpv symbol declarations compatible with distro headers', () => { expect(widCommonSource).not.toContain('MPV_CPLUGIN_DYNAMIC_SYM'); expect(widCommonSource).toContain('#ifdef IPTVNATOR_DYNAMIC_LIBMPV'); expect(widCommonSource).toContain('#ifdef MPV_SELECTANY'); expect(widCommonSource).toContain( '#define IPTVNATOR_MPV_SELECTANY MPV_SELECTANY' ); expect(widCommonSource).toContain( 'IPTVNATOR_MPV_SELECTANY decltype(&name) pfn_##name = nullptr;' ); expect(widCommonSource).toContain( 'IPTVNATOR_DECLARE_MPV_DYNAMIC_SYMBOL(mpv_command_async)' ); 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 }}';" ); expect(buildAndMakeWorkflowSource).toContain( "if (targetPlatform === 'darwin')" ); expect(buildAndMakeWorkflowSource).toContain( "'tools/embedded-mpv/build-macos-runtime.mjs'" ); expect(buildAndMakeWorkflowSource).toContain( "'tools/embedded-mpv/stage-runtime.mjs'" ); expect(buildAndMakeWorkflowSource).not.toContain( '`macos${safeDeploymentTarget}`,\n' + ' `xcode${xcodeHash}`,' ); }); it('requires Linux embedded MPV build inputs and validates process isolation in CI', () => { expect(buildAndMakeWorkflowSource).toContain( 'libmpv-dev mpv pkg-config' ); expect(buildAndMakeWorkflowSource).toContain( 'Stage Linux embedded MPV build inputs' ); expect(buildAndMakeWorkflowSource).toContain( "linuxBackend: 'process-isolated mpv --wid'" ); expect(buildAndMakeWorkflowSource).toContain("matrix.os == 'linux'"); expect(buildAndMakeWorkflowSource).toContain( 'Linux embedded MPV addon must not link directly to libmpv' ); expect(buildScriptSource).toContain("origin: 'external-mpv-process'"); expect(buildScriptSource).toContain('writeLinuxProcessRuntimeManifest'); expect(buildScriptSource).toContain('runtimeFiles: []'); }); }); describe('Embedded MPV native build configuration', () => { const bindingGyp = JSON.parse( readFileSync( path.resolve(__dirname, '../../../native/binding.gyp'), 'utf8' ) ); const target = bindingGyp.targets.find( (candidate: { target_name?: string }) => candidate.target_name === 'embedded_mpv' ); it('declares platform-specific native sources for macOS, Windows, and Linux', () => { expect(target).toBeDefined(); expect(JSON.stringify(target)).toContain('src/embedded_mpv.mm'); expect(JSON.stringify(target)).toContain('src/embedded_mpv_win32.cc'); expect(JSON.stringify(target)).toContain('src/embedded_mpv_linux.cc'); }); });