1 Commits

Author SHA1 Message Date
Myx
be465fd297 Fix screenshare portals linux
All checks were successful
Queue Release Build / prepare (push) Successful in 14s
Queue Release Build / build-windows (push) Successful in 34m25s
Queue Release Build / build-linux (push) Successful in 40m26s
Queue Release Build / finalize (push) Successful in 3m44s
2026-03-11 17:54:04 +01:00
5 changed files with 104 additions and 43 deletions

View File

@@ -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');

View File

@@ -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 () => {

View File

@@ -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'

View File

@@ -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",

View File

@@ -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]);