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:
byGalax
2026-05-06 23:35:01 +02:00
parent 72d02e385f
commit 825160ee46
504 changed files with 14217 additions and 14366 deletions
@@ -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;
},
);
}