Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be465fd297 |
@@ -3,7 +3,6 @@ import { readDesktopSettings } from '../desktop-settings';
|
||||
|
||||
export function configureAppFlags(): void {
|
||||
linuxSpecificFlags();
|
||||
audioFlags();
|
||||
networkFlags();
|
||||
setupGpuEncodingFlags();
|
||||
chromiumFlags();
|
||||
@@ -15,21 +14,40 @@ function chromiumFlags(): void {
|
||||
|
||||
// Suppress Autofill devtools errors
|
||||
app.commandLine.appendSwitch('disable-features', 'Autofill,AutofillAssistant,AutofillServerCommunication');
|
||||
}
|
||||
|
||||
function audioFlags(): void {
|
||||
// Collect all enabled features into a single switch to avoid later calls overwriting earlier ones
|
||||
const enabledFeatures: string[] = [];
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
// Use the new PipeWire-based audio pipeline on Linux for better screen share audio capture support
|
||||
app.commandLine.appendSwitch('enable-features', 'AudioServiceOutOfProcess');
|
||||
// PipeWire-based audio pipeline for screen share audio capture
|
||||
enabledFeatures.push('AudioServiceOutOfProcess');
|
||||
// PipeWire-based screen capture so the xdg-desktop-portal system picker works
|
||||
enabledFeatures.push('WebRTCPipeWireCapturer');
|
||||
}
|
||||
|
||||
const desktopSettings = readDesktopSettings();
|
||||
|
||||
if (process.platform === 'linux' && desktopSettings.vaapiVideoEncode) {
|
||||
enabledFeatures.push('VaapiVideoEncode');
|
||||
}
|
||||
|
||||
if (enabledFeatures.length > 0) {
|
||||
app.commandLine.appendSwitch('enable-features', enabledFeatures.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
function linuxSpecificFlags(): void {
|
||||
// Disable sandbox on Linux to avoid SUID / /tmp shared-memory issues
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('no-sandbox');
|
||||
app.commandLine.appendSwitch('disable-dev-shm-usage');
|
||||
if (process.platform !== 'linux') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable sandbox on Linux to avoid SUID / /tmp shared-memory issues
|
||||
app.commandLine.appendSwitch('no-sandbox');
|
||||
app.commandLine.appendSwitch('disable-dev-shm-usage');
|
||||
|
||||
// Auto-detect Wayland vs X11 so the xdg-desktop-portal system picker
|
||||
// works for screen capture on Wayland compositors
|
||||
app.commandLine.appendSwitch('ozone-platform-hint', 'auto');
|
||||
}
|
||||
|
||||
function networkFlags(): void {
|
||||
@@ -46,11 +64,6 @@ function setupGpuEncodingFlags(): void {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && desktopSettings.vaapiVideoEncode) {
|
||||
// Enable VA-API hardware video encoding on Linux
|
||||
app.commandLine.appendSwitch('enable-features', 'VaapiVideoEncode');
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('enable-zero-copy');
|
||||
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
|
||||
|
||||
@@ -204,26 +204,32 @@ export function setupSystemHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('get-sources', async () => {
|
||||
const thumbnailSize = { width: 240, height: 150 };
|
||||
const [screenSources, windowSources] = await Promise.all([
|
||||
desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize
|
||||
}),
|
||||
desktopCapturer.getSources({
|
||||
types: ['window'],
|
||||
thumbnailSize,
|
||||
fetchWindowIcons: true
|
||||
})
|
||||
]);
|
||||
const sources = [...screenSources, ...windowSources];
|
||||
const uniqueSources = new Map(sources.map((source) => [source.id, source]));
|
||||
try {
|
||||
const thumbnailSize = { width: 240, height: 150 };
|
||||
const [screenSources, windowSources] = await Promise.all([
|
||||
desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize
|
||||
}),
|
||||
desktopCapturer.getSources({
|
||||
types: ['window'],
|
||||
thumbnailSize,
|
||||
fetchWindowIcons: true
|
||||
})
|
||||
]);
|
||||
const sources = [...screenSources, ...windowSources];
|
||||
const uniqueSources = new Map(sources.map((source) => [source.id, source]));
|
||||
|
||||
return [...uniqueSources.values()].map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL()
|
||||
}));
|
||||
return [...uniqueSources.values()].map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL()
|
||||
}));
|
||||
} catch {
|
||||
// desktopCapturer.getSources fails on Wayland; return empty so the
|
||||
// renderer falls through to getDisplayMedia with the system picker.
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('prepare-linux-screen-share-audio-routing', async () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
desktopCapturer,
|
||||
session,
|
||||
shell
|
||||
} from 'electron';
|
||||
import * as fs from 'fs';
|
||||
@@ -61,6 +63,34 @@ export async function createWindow(): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
async (_request, respond) => {
|
||||
// On Linux/Wayland the system picker (useSystemPicker: true) handles
|
||||
// the portal. This handler is only reached if the system picker is
|
||||
// unavailable (e.g. X11 without a portal). Fall back to
|
||||
// desktopCapturer so the user still gets something.
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['window', 'screen'],
|
||||
thumbnailSize: { width: 150, height: 150 }
|
||||
});
|
||||
const firstSource = sources[0];
|
||||
|
||||
if (firstSource) {
|
||||
respond({ video: firstSource });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// desktopCapturer also unavailable
|
||||
}
|
||||
|
||||
respond({});
|
||||
},
|
||||
{ useSystemPicker: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env['NODE_ENV'] === 'development') {
|
||||
const devUrl = process.env['SSL'] === 'true'
|
||||
? 'https://localhost:4200'
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"electron:build:all": "npm run build:prod && npm run build:electron && electron-builder --win --mac --linux",
|
||||
"build:prod:all": "npm run build:prod && npm run build:electron && cd server && npm run build",
|
||||
"build:prod:win": "npm run build:prod:all && electron-builder --win",
|
||||
"dev": "npm run electron:full",
|
||||
"dev": "npm run build:electron && npm run electron:full",
|
||||
"dev:app": "npm run electron:dev",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "npm run format && npm run sort:props && eslint . --fix",
|
||||
|
||||
@@ -225,6 +225,7 @@ export class ScreenShareManager {
|
||||
this.activeScreenStream = await this.startWithLinuxElectronAudioRouting(shareOptions, preset);
|
||||
captureMethod = 'linux-electron';
|
||||
} catch (error) {
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error);
|
||||
}
|
||||
}
|
||||
@@ -241,6 +242,7 @@ export class ScreenShareManager {
|
||||
captureMethod = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('getDisplayMedia with system audio failed; falling back to Electron desktop capture', error);
|
||||
}
|
||||
}
|
||||
@@ -253,10 +255,7 @@ export class ScreenShareManager {
|
||||
shareOptions.includeSystemAudio = electronCapture.includeSystemAudio;
|
||||
captureMethod = 'electron-desktop';
|
||||
} catch (error) {
|
||||
if (this.isScreenShareSelectionAborted(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error);
|
||||
}
|
||||
}
|
||||
@@ -392,7 +391,15 @@ export class ScreenShareManager {
|
||||
}
|
||||
|
||||
private isElectronDesktopCaptureAvailable(): boolean {
|
||||
return !!this.getElectronApi()?.getSources;
|
||||
return !!this.getElectronApi()?.getSources && !this.isLinuxElectron();
|
||||
}
|
||||
|
||||
private isLinuxElectron(): boolean {
|
||||
if (!this.getElectronApi() || typeof navigator === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /linux/i.test(`${navigator.userAgent} ${navigator.platform}`);
|
||||
}
|
||||
|
||||
private isWindowsElectron(): boolean {
|
||||
@@ -725,7 +732,14 @@ export class ScreenShareManager {
|
||||
}
|
||||
|
||||
private isScreenShareSelectionAborted(error: unknown): boolean {
|
||||
return error instanceof Error && error.name === 'AbortError';
|
||||
return error instanceof Error
|
||||
&& (error.name === 'AbortError' || error.name === 'NotAllowedError');
|
||||
}
|
||||
|
||||
private rethrowIfScreenShareAborted(error: unknown): void {
|
||||
if (this.isScreenShareSelectionAborted(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private isLinuxElectronAudioRoutingSupported(): boolean {
|
||||
@@ -766,13 +780,11 @@ export class ScreenShareManager {
|
||||
throw new Error(activation.reason || 'Failed to activate Linux Electron audio routing.');
|
||||
}
|
||||
|
||||
const desktopCapture = await this.startWithElectronDesktopCapturer({
|
||||
desktopStream = await this.startWithDisplayMedia({
|
||||
...options,
|
||||
includeSystemAudio: false
|
||||
}, preset);
|
||||
|
||||
desktopStream = desktopCapture.stream;
|
||||
|
||||
const { audioTrack, captureInfo } = await this.startLinuxScreenShareMonitorTrack();
|
||||
const stream = new MediaStream([...desktopStream.getVideoTracks(), audioTrack]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user