feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// 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:<display-id>: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<ScreenSource[]> {
|
||||
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<ScreenSource[]> => {
|
||||
return enumerate(320, 180);
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
CHANNELS.SCREEN_GET_THUMBNAIL,
|
||||
async (_evt, sourceId: string): Promise<string | null> => {
|
||||
// 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user