|
8 | 8 | * @ai-context: 帧数据经 sender 窗口 webContents.send 推回(非广播), |
9 | 9 | * 窗口销毁时静默丢帧。 |
10 | 10 | */ |
11 | | -import { BrowserWindow, desktopCapturer } from 'electron'; |
| 11 | +import { BrowserWindow, desktopCapturer, screen } from 'electron'; |
12 | 12 | import { ScreenCapture } from './screenCapture.js'; |
13 | 13 | import type { ScreenCaptureOptions, ScreenshotFrameData } from './screenCapture.js'; |
14 | 14 | import { safeHandle, getMainWindowId } from './ipcUtils.js'; |
15 | 15 | import { logger } from './logger.js'; |
16 | 16 | import { scoreAndFilterWindows } from './windowScorer.js'; |
17 | 17 | import type { WindowSignalInput } from './windowRules.js'; |
18 | 18 | import { getCaptureRateScale, onPerformanceModeChange } from './performanceMode.js'; |
| 19 | +import { loadProcessAudioNative } from './audio/processAudioNative.js'; |
| 20 | +import { parseHwndFromSourceId, buildNativeIndex, resolveGeometrySignals } from './windowSignals.js'; |
| 21 | +import type { NativeWindowSignal } from './windowSignals.js'; |
19 | 22 |
|
20 | 23 | // ================================================================ |
21 | 24 | // 模块级状态 |
@@ -67,12 +70,63 @@ function applyRateScale(options: ScreenCaptureOptions): ScreenCaptureOptions { |
67 | 70 |
|
68 | 71 | /** |
69 | 72 | * 构建 source id → 评分信号的映射。 |
70 | | - * @ai-context: 阶段一为空映射(纯标题评分);阶段二接入 native 窗口枚举后填充 |
71 | | - * 进程名/几何/前台信号。信号源缺失时返回空 Map,评分自动降级。 |
| 73 | + * @ai-context: native 缺失时返回空 Map,评分自动降级为纯标题(与旧版行为一致)。 |
| 74 | + * 前台窗口判定:native.getForegroundHwnd() 命中当前 source 的 HWND。 |
72 | 75 | */ |
73 | 76 | function buildSignalMap(sources: Electron.DesktopCapturerSource[]): Map<string, WindowSignalInput> { |
74 | | - void sources; |
75 | | - return new Map(); |
| 77 | + const signals = new Map<string, WindowSignalInput>(); |
| 78 | + const native = loadProcessAudioNative(); |
| 79 | + if (!native) return signals; |
| 80 | + |
| 81 | + let nativeWindows: NativeWindowSignal[]; |
| 82 | + try { |
| 83 | + nativeWindows = native.listAudioWindows().map((w) => ({ |
| 84 | + hwnd: String(w.hwnd), |
| 85 | + processName: w.processName, |
| 86 | + width: w.width, |
| 87 | + height: w.height, |
| 88 | + alwaysOnTop: w.alwaysOnTop, |
| 89 | + })); |
| 90 | + } catch (err) { |
| 91 | + logger.warn(`[IPC] native listAudioWindows 失败,降级纯标题评分: ${err instanceof Error ? err.message : String(err)}`); |
| 92 | + return signals; |
| 93 | + } |
| 94 | + |
| 95 | + const index = buildNativeIndex(nativeWindows); |
| 96 | + |
| 97 | + // 显示器总面积(像素²;无显示器时置 0,面积占比信号自动跳过) |
| 98 | + let displayArea = 0; |
| 99 | + try { |
| 100 | + const bounds = screen.getPrimaryDisplay().bounds; |
| 101 | + displayArea = bounds.width * bounds.height; |
| 102 | + } catch { |
| 103 | + displayArea = 0; |
| 104 | + } |
| 105 | + |
| 106 | + // 前台窗口 hwnd(失败时为空串 → 无窗口命中前台) |
| 107 | + let foregroundHwnd = ''; |
| 108 | + try { |
| 109 | + foregroundHwnd = native.getForegroundHwnd(); |
| 110 | + } catch { |
| 111 | + foregroundHwnd = ''; |
| 112 | + } |
| 113 | + |
| 114 | + for (const src of sources) { |
| 115 | + const hwnd = parseHwndFromSourceId(src.id); |
| 116 | + if (!hwnd) continue; |
| 117 | + const nativeWin = index.get(hwnd); |
| 118 | + if (!nativeWin) continue; |
| 119 | + const geo = resolveGeometrySignals(nativeWin, displayArea); |
| 120 | + signals.set(src.id, { |
| 121 | + title: src.name, |
| 122 | + processName: nativeWin.processName, |
| 123 | + aspectRatio: geo.aspectRatio, |
| 124 | + areaRatio: geo.areaRatio, |
| 125 | + alwaysOnTop: geo.alwaysOnTop, |
| 126 | + isForeground: hwnd === foregroundHwnd, |
| 127 | + }); |
| 128 | + } |
| 129 | + return signals; |
76 | 130 | } |
77 | 131 |
|
78 | 132 | /** 销毁旧实例并按其参数/推送目标重建采集(start 与性能模式重启共用) */ |
|
0 commit comments