feat!: [Experimental hotfix 2] Fix Audio stream handling on windows electron

Experimental fix for solving replacement of mic audio when enabling screenshare audio on windows electron

On Windows Electron, startWithElectronDesktopCapturer uses getUserMedia({ chromeMediaSource: 'desktop' }) for both video and audio. This getUserMedia desktop audio call can interfere with / replace the existing mic getUserMedia stream, killing voice audio.

BREAKING CHANGE: possibly streaming
This commit is contained in:
2026-03-12 23:55:38 +01:00
parent 778e75bef5
commit 781c05294f

View File

@@ -190,10 +190,12 @@ export class ScreenShareManager {
/** /**
* Begin screen sharing. * Begin screen sharing.
* *
* On Linux Electron builds, prefers a dedicated PulseAudio/PipeWire routing * On Linux Electron builds, prefers a dedicated PulseAudio/PipeWire routing
* path so remote voice playback is kept out of captured system audio. * path so remote voice playback is kept out of captured system audio.
* On other Electron builds, uses desktop capture. In browser contexts, uses * On Windows Electron builds, prefers `getDisplayMedia` with system audio
* `getDisplayMedia`. * so the separate mic `getUserMedia` stream is not disrupted; falls back to
* Electron desktop capture only when `getDisplayMedia` fails entirely.
* In browser contexts, uses `getDisplayMedia`.
* *
* @param options - Screen-share capture options. * @param options - Screen-share capture options.
* @returns The captured screen {@link MediaStream}. * @returns The captured screen {@link MediaStream}.
@@ -230,16 +232,27 @@ export class ScreenShareManager {
} }
} }
if (!this.activeScreenStream && shareOptions.includeSystemAudio && !electronDesktopCaptureAvailable) { if (!this.activeScreenStream && shareOptions.includeSystemAudio) {
try { try {
this.activeScreenStream = await this.startWithDisplayMedia(shareOptions, preset); this.activeScreenStream = await this.startWithDisplayMedia(shareOptions, preset);
captureMethod = 'display-media'; captureMethod = 'display-media';
if (this.activeScreenStream.getAudioTracks().length === 0) { if (this.activeScreenStream.getAudioTracks().length === 0) {
this.logger.warn('getDisplayMedia did not provide system audio; trying Electron desktop capture'); if (electronDesktopCaptureAvailable) {
this.activeScreenStream.getTracks().forEach((track) => track.stop()); // On Windows Electron, keep the getDisplayMedia stream for video
this.activeScreenStream = null; // rather than falling through to getUserMedia desktop audio which
captureMethod = null; // can replace or kill the active mic stream.
this.logger.warn(
'getDisplayMedia did not provide system audio; '
+ 'continuing without system audio to preserve mic stream'
);
shareOptions.includeSystemAudio = false;
} else {
this.logger.warn('getDisplayMedia did not provide system audio; trying next capture method');
this.activeScreenStream.getTracks().forEach((track) => track.stop());
this.activeScreenStream = null;
captureMethod = null;
}
} }
} catch (error) { } catch (error) {
this.rethrowIfScreenShareAborted(error); this.rethrowIfScreenShareAborted(error);
@@ -426,9 +439,12 @@ export class ScreenShareManager {
private shouldSuppressRemotePlaybackDuringShare( private shouldSuppressRemotePlaybackDuringShare(
includeSystemAudio: boolean, includeSystemAudio: boolean,
captureMethod: ScreenShareCaptureMethod | null _captureMethod: ScreenShareCaptureMethod | null
): boolean { ): boolean {
return includeSystemAudio && captureMethod === 'electron-desktop' && this.isWindowsElectron(); // On Windows Electron, system audio capture (via getDisplayMedia or
// desktop capturer) includes all output audio. Remote voice playback
// must be suppressed to avoid a feedback loop regardless of capture method.
return includeSystemAudio && this.isWindowsElectron();
} }
private getRequiredLinuxElectronApi(): Required<Pick< private getRequiredLinuxElectronApi(): Required<Pick<