Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be465fd297 |
@@ -3,7 +3,6 @@ import { readDesktopSettings } from '../desktop-settings';
|
|||||||
|
|
||||||
export function configureAppFlags(): void {
|
export function configureAppFlags(): void {
|
||||||
linuxSpecificFlags();
|
linuxSpecificFlags();
|
||||||
audioFlags();
|
|
||||||
networkFlags();
|
networkFlags();
|
||||||
setupGpuEncodingFlags();
|
setupGpuEncodingFlags();
|
||||||
chromiumFlags();
|
chromiumFlags();
|
||||||
@@ -15,21 +14,40 @@ function chromiumFlags(): void {
|
|||||||
|
|
||||||
// Suppress Autofill devtools errors
|
// Suppress Autofill devtools errors
|
||||||
app.commandLine.appendSwitch('disable-features', 'Autofill,AutofillAssistant,AutofillServerCommunication');
|
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') {
|
if (process.platform === 'linux') {
|
||||||
// Use the new PipeWire-based audio pipeline on Linux for better screen share audio capture support
|
// PipeWire-based audio pipeline for screen share audio capture
|
||||||
app.commandLine.appendSwitch('enable-features', 'AudioServiceOutOfProcess');
|
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 {
|
function linuxSpecificFlags(): void {
|
||||||
// Disable sandbox on Linux to avoid SUID / /tmp shared-memory issues
|
if (process.platform !== 'linux') {
|
||||||
if (process.platform === 'linux') {
|
return;
|
||||||
app.commandLine.appendSwitch('no-sandbox');
|
|
||||||
app.commandLine.appendSwitch('disable-dev-shm-usage');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
function networkFlags(): void {
|
||||||
@@ -46,11 +64,6 @@ function setupGpuEncodingFlags(): void {
|
|||||||
app.disableHardwareAcceleration();
|
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-gpu-rasterization');
|
||||||
app.commandLine.appendSwitch('enable-zero-copy');
|
app.commandLine.appendSwitch('enable-zero-copy');
|
||||||
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
|
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
|
||||||
|
|||||||
@@ -204,26 +204,32 @@ export function setupSystemHandlers(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-sources', async () => {
|
ipcMain.handle('get-sources', async () => {
|
||||||
const thumbnailSize = { width: 240, height: 150 };
|
try {
|
||||||
const [screenSources, windowSources] = await Promise.all([
|
const thumbnailSize = { width: 240, height: 150 };
|
||||||
desktopCapturer.getSources({
|
const [screenSources, windowSources] = await Promise.all([
|
||||||
types: ['screen'],
|
desktopCapturer.getSources({
|
||||||
thumbnailSize
|
types: ['screen'],
|
||||||
}),
|
thumbnailSize
|
||||||
desktopCapturer.getSources({
|
}),
|
||||||
types: ['window'],
|
desktopCapturer.getSources({
|
||||||
thumbnailSize,
|
types: ['window'],
|
||||||
fetchWindowIcons: true
|
thumbnailSize,
|
||||||
})
|
fetchWindowIcons: true
|
||||||
]);
|
})
|
||||||
const sources = [...screenSources, ...windowSources];
|
]);
|
||||||
const uniqueSources = new Map(sources.map((source) => [source.id, source]));
|
const sources = [...screenSources, ...windowSources];
|
||||||
|
const uniqueSources = new Map(sources.map((source) => [source.id, source]));
|
||||||
|
|
||||||
return [...uniqueSources.values()].map((source) => ({
|
return [...uniqueSources.values()].map((source) => ({
|
||||||
id: source.id,
|
id: source.id,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
thumbnail: source.thumbnail.toDataURL()
|
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 () => {
|
ipcMain.handle('prepare-linux-screen-share-audio-routing', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
|
desktopCapturer,
|
||||||
|
session,
|
||||||
shell
|
shell
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import * as fs from 'fs';
|
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') {
|
if (process.env['NODE_ENV'] === 'development') {
|
||||||
const devUrl = process.env['SSL'] === 'true'
|
const devUrl = process.env['SSL'] === 'true'
|
||||||
? 'https://localhost:4200'
|
? 'https://localhost:4200'
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"electron:build:all": "npm run build:prod && npm run build:electron && electron-builder --win --mac --linux",
|
"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: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",
|
"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",
|
"dev:app": "npm run electron:dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "npm run format && npm run sort:props && eslint . --fix",
|
"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);
|
this.activeScreenStream = await this.startWithLinuxElectronAudioRouting(shareOptions, preset);
|
||||||
captureMethod = 'linux-electron';
|
captureMethod = 'linux-electron';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.rethrowIfScreenShareAborted(error);
|
||||||
this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error);
|
this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +242,7 @@ export class ScreenShareManager {
|
|||||||
captureMethod = null;
|
captureMethod = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.rethrowIfScreenShareAborted(error);
|
||||||
this.logger.warn('getDisplayMedia with system audio failed; falling back to Electron desktop capture', 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;
|
shareOptions.includeSystemAudio = electronCapture.includeSystemAudio;
|
||||||
captureMethod = 'electron-desktop';
|
captureMethod = 'electron-desktop';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isScreenShareSelectionAborted(error)) {
|
this.rethrowIfScreenShareAborted(error);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error);
|
this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +391,15 @@ export class ScreenShareManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isElectronDesktopCaptureAvailable(): boolean {
|
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 {
|
private isWindowsElectron(): boolean {
|
||||||
@@ -725,7 +732,14 @@ export class ScreenShareManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isScreenShareSelectionAborted(error: unknown): boolean {
|
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 {
|
private isLinuxElectronAudioRoutingSupported(): boolean {
|
||||||
@@ -766,13 +780,11 @@ export class ScreenShareManager {
|
|||||||
throw new Error(activation.reason || 'Failed to activate Linux Electron audio routing.');
|
throw new Error(activation.reason || 'Failed to activate Linux Electron audio routing.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const desktopCapture = await this.startWithElectronDesktopCapturer({
|
desktopStream = await this.startWithDisplayMedia({
|
||||||
...options,
|
...options,
|
||||||
includeSystemAudio: false
|
includeSystemAudio: false
|
||||||
}, preset);
|
}, preset);
|
||||||
|
|
||||||
desktopStream = desktopCapture.stream;
|
|
||||||
|
|
||||||
const { audioTrack, captureInfo } = await this.startLinuxScreenShareMonitorTrack();
|
const { audioTrack, captureInfo } = await this.startLinuxScreenShareMonitorTrack();
|
||||||
const stream = new MediaStream([...desktopStream.getVideoTracks(), audioTrack]);
|
const stream = new MediaStream([...desktopStream.getVideoTracks(), audioTrack]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user