// Screen / window source enumeration + on-demand high-res thumbnail fetch. // The picker in the renderer calls `SCREEN_GET_SOURCES` once to populate // the grid (thumbnails come back inline as data URLs from desktopCapturer, // so no second round-trip is needed for the initial paint). When the user // hovers a tile we can optionally refresh the thumb at a higher resolution // via `SCREEN_GET_THUMBNAIL` — same API but a single id and 640x360 size. import { desktopCapturer, ipcMain } from 'electron'; import { CHANNELS, type ScreenSource } from '../ipc-types'; // Renderer-driven pending source: the picker modal sets this BEFORE calling // getDisplayMedia so our display-media handler can route the chosen source // to LiveKit. Stored module-level (single capture in flight at a time — // the renderer enforces this since only one picker can be open). Cleared // on consume or on explicit null-set (cancel/error path). let pendingShareSourceId: string | null = null; /** Read-and-clear: returns the pending id and resets it to null in one * step so the main-process display-media handler can't accidentally apply * the same id twice (e.g. if a stray getDisplayMedia call fires while * the picker is closed). */ export function consumePendingShareSourceId(): string | null { const id = pendingShareSourceId; pendingShareSourceId = null; return id; } /** Non-destructive read for callers that just want to know if a pending * selection exists. */ export function getPendingShareSourceId(): string | null { return pendingShareSourceId; } function parseDisplayId(raw: string): number | null { // desktopCapturer ids for screens look like "screen::0". We // index monitors 0-based in the UI so reduce the opaque id to a small // integer per primary-display order. For windows there is no display // association, return null. if (!raw.startsWith('screen:')) return null; const parts = raw.split(':'); const n = Number(parts[1]); return Number.isFinite(n) ? n : null; } async function enumerate(thumbWidth: number, thumbHeight: number): Promise { const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: thumbWidth, height: thumbHeight }, fetchWindowIcons: true, }); return sources.map((src): ScreenSource => { const kind: 'screen' | 'window' = src.id.startsWith('screen:') ? 'screen' : 'window'; const thumbnailDataUrl = src.thumbnail && !src.thumbnail.isEmpty() ? src.thumbnail.toDataURL() : null; const iconDataUrl = src.appIcon && !src.appIcon.isEmpty() ? src.appIcon.toDataURL() : null; return { id: src.id, name: src.name, kind, displayId: kind === 'screen' ? parseDisplayId(src.id) : null, thumbnailDataUrl, iconDataUrl, }; }); } export function register(): void { ipcMain.handle(CHANNELS.SCREEN_GET_SOURCES, async (): Promise => { return enumerate(320, 180); }); ipcMain.handle( CHANNELS.SCREEN_GET_THUMBNAIL, async (_evt, sourceId: string): Promise => { // Re-enumerate — desktopCapturer has no "fetch one by id" API. Done // at 640x360 so the detail view looks crisp without paying the full // enumeration cost more than once per hover-debounce. const list = await enumerate(640, 360); const found = list.find((s) => s.id === sourceId); return found?.thumbnailDataUrl ?? null; }, ); ipcMain.handle( CHANNELS.SCREEN_SET_PENDING_SOURCE, (_evt, sourceId: string | null): void => { // Renderer signals the chosen source id (or null to clear on cancel/ // error). Stored until the next getDisplayMedia request comes in via // the display-media handler, which calls consumePendingShareSourceId // to read-and-clear it. pendingShareSourceId = sourceId; }, ); }