825160ee46
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>
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
// System-audio loopback. Under Electron this is renderer-driven: we
|
|
// ask main for the primary screen's capturer id, then call
|
|
// getUserMedia with Chromium's `chromeMediaSource: 'desktop'`
|
|
// constraint to obtain the OS-mixer MediaStream directly. The whole
|
|
// Tauri WASAPI + AudioWorklet base64 pipeline is gone.
|
|
//
|
|
// Windows-only in practice (loopback audio is a Windows feature of
|
|
// Chromium's desktop source). On other platforms `startSystemAudioCapture`
|
|
// throws `SystemAudioUnavailable`; callers are expected to fall back
|
|
// to the standard getDisplayMedia flow.
|
|
|
|
import { isTauriRuntime } from './globalShortcut';
|
|
|
|
export interface SystemAudioHandle {
|
|
/** Monotonic id, used by callers to correlate stop() with start. */
|
|
captureId: number;
|
|
/** MediaStream with a single audio track carrying the OS mixer. */
|
|
stream: MediaStream;
|
|
/** Teardown — stops the MediaStreamTrack. Idempotent. */
|
|
stop: () => Promise<void>;
|
|
}
|
|
|
|
export class SystemAudioUnavailable extends Error {
|
|
constructor(reason: string) {
|
|
super('system audio unavailable: ' + reason);
|
|
this.name = 'SystemAudioUnavailable';
|
|
}
|
|
}
|
|
|
|
interface ChromiumAudioConstraint {
|
|
mandatory: {
|
|
chromeMediaSource: 'desktop';
|
|
chromeMediaSourceId: string;
|
|
};
|
|
}
|
|
|
|
export async function startSystemAudioCapture(): Promise<SystemAudioHandle> {
|
|
if (!isTauriRuntime()) {
|
|
throw new SystemAudioUnavailable('not an electron runtime');
|
|
}
|
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
|
|
throw new SystemAudioUnavailable('mediaDevices unavailable');
|
|
}
|
|
|
|
const resolved = await window.electronAPI.resolveLoopbackSource();
|
|
if (!resolved) {
|
|
throw new SystemAudioUnavailable('no screen source available');
|
|
}
|
|
|
|
const audioConstraint: ChromiumAudioConstraint = {
|
|
mandatory: {
|
|
chromeMediaSource: 'desktop',
|
|
chromeMediaSourceId: resolved.sourceId,
|
|
},
|
|
};
|
|
|
|
let stream: MediaStream;
|
|
try {
|
|
// Cast: the Chromium `mandatory` constraint is non-standard and
|
|
// not covered by lib.dom.d.ts typings.
|
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
audio: audioConstraint as unknown as MediaTrackConstraints,
|
|
video: false,
|
|
});
|
|
} catch (err: unknown) {
|
|
throw new SystemAudioUnavailable(
|
|
err instanceof Error ? err.message : String(err),
|
|
);
|
|
}
|
|
|
|
const tracks = stream.getAudioTracks();
|
|
if (tracks.length === 0) {
|
|
for (const t of stream.getTracks()) t.stop();
|
|
throw new SystemAudioUnavailable('no audio track in returned stream');
|
|
}
|
|
|
|
const captureId = Date.now();
|
|
let stopped = false;
|
|
const stop = async (): Promise<void> => {
|
|
if (stopped) return;
|
|
stopped = true;
|
|
for (const track of stream.getTracks()) {
|
|
try {
|
|
track.stop();
|
|
} catch {
|
|
/* already stopped */
|
|
}
|
|
}
|
|
};
|
|
|
|
return { captureId, stream, stop };
|
|
}
|