Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ type ChannelToArgsMap = {
'video-call-window/open-url': (url: string) => void;
'video-call-window/web-contents-id': (webContentsId: number) => void;
'video-call-window/open-screen-picker': () => { success: boolean };
'video-call-window/screen-sharing-source-responded': (source: string) => void;
'video-call-window/screen-sharing-source-responded': (
source:
| string
| null
| {
sourceId: string | null;
shareAudio?: boolean;
}
) => void;
'video-call-window/screen-recording-is-permission-granted': () => boolean;
'video-call-window/close-requested': () => { success: boolean };
'video-call-window/open-webview-dev-tools': () => boolean;
Expand Down
29 changes: 26 additions & 3 deletions src/screenSharing/ScreenSharingRequestTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { desktopCapturer, ipcMain } from 'electron';
import type { DisplayMediaCallback } from './screenPicker/types';

const DEFAULT_TIMEOUT = 60000;
type ScreenSharingSelectionPayload = {
sourceId: string | null;
shareAudio?: boolean;
};

export class ScreenSharingRequestTracker {
private activeListener:
| ((event: Event, sourceId: string | null) => void)
| ((
event: Event,
payload: string | null | ScreenSharingSelectionPayload
) => void)
| null = null;

private activeRequestId: string | null = null;
Expand Down Expand Up @@ -73,7 +80,10 @@ export class ScreenSharingRequestTracker {

let callbackInvoked = false;

const listener = async (_event: Event, sourceId: string | null) => {
const listener = async (
_event: Event,
payload: string | null | ScreenSharingSelectionPayload
) => {
if (this.activeRequestId !== requestId) {
return;
}
Expand All @@ -86,6 +96,15 @@ export class ScreenSharingRequestTracker {
this.removeListenerOnly();
this.markComplete();

const sourceId =
typeof payload === 'object' && payload !== null
? payload.sourceId
: payload;
const shareAudio =
typeof payload === 'object' && payload !== null
? payload.shareAudio === true
: false;

if (!sourceId) {
cb({ video: false } as any);
return;
Expand All @@ -107,7 +126,11 @@ export class ScreenSharingRequestTracker {
return;
}

cb({ video: selectedSource });
cb(
shareAudio
? ({ video: selectedSource, audio: 'loopback' } as any)
: { video: selectedSource }
);
} catch (error) {
console.error(`${this.label}: error validating source:`, error);
cb({ video: false } as any);
Expand Down
44 changes: 41 additions & 3 deletions src/screenSharing/screenSharePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Box,
Button,
Callout,
CheckBox,
Label,
Tabs,
Scrollable,
Expand All @@ -13,6 +14,7 @@ import type {
SourcesOptions,
} from 'electron';
import { ipcRenderer } from 'electron';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -31,6 +33,11 @@ interface IScreenSharePickerProps {
includeTheme?: boolean;
}

type ScreenSharingSelectionPayload = {
sourceId: string | null;
shareAudio?: boolean;
};

export function ScreenSharePicker({
onMounted,
responseChannel = 'video-call-window/screen-sharing-source-responded',
Expand All @@ -43,6 +50,7 @@ export function ScreenSharePicker({
const [sources, setSources] = useState<DesktopCapturerSource[]>([]);
const [currentTab, setCurrentTab] = useState<'screen' | 'window'>('screen');
const [selectedSourceId, setSelectedSourceId] = useState<string | null>(null);
const [shareAudio, setShareAudio] = useState(false);
const [
isScreenRecordingPermissionGranted,
setIsScreenRecordingPermissionGranted,
Expand Down Expand Up @@ -116,11 +124,16 @@ export function ScreenSharePicker({
if (visible) {
responseSentRef.current = false;
wasVisibleRef.current = true;
setShareAudio(false);
} else if (wasVisibleRef.current) {
wasVisibleRef.current = false;
if (!responseSentRef.current) {
responseSentRef.current = true;
ipcRenderer.send(responseChannel, null);
const payload: ScreenSharingSelectionPayload = {
sourceId: null,
shareAudio: false,
};
ipcRenderer.send(responseChannel, payload);
}
}
}, [visible, responseChannel]);
Expand Down Expand Up @@ -175,7 +188,11 @@ export function ScreenSharePicker({

responseSentRef.current = true;
setVisible(false);
ipcRenderer.send(responseChannel, selectedSourceId);
const payload: ScreenSharingSelectionPayload = {
sourceId: selectedSourceId,
shareAudio,
};
ipcRenderer.send(responseChannel, payload);
}
};

Expand All @@ -186,7 +203,11 @@ export function ScreenSharePicker({
}
responseSentRef.current = true;
setVisible(false);
ipcRenderer.send(responseChannel, null);
const payload: ScreenSharingSelectionPayload = {
sourceId: null,
shareAudio: false,
};
ipcRenderer.send(responseChannel, payload);
};

// Filter sources based on the current tab
Expand Down Expand Up @@ -383,8 +404,25 @@ export function ScreenSharePicker({
<Box
display='flex'
justifyContent='space-between'
alignItems='center'
marginBlockStart='auto'
>
<Box
display='flex'
alignItems='center'
style={{ columnGap: '8px' }}
>
<CheckBox
id='share-system-audio'
checked={shareAudio}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setShareAudio(event.currentTarget.checked)
}
/>
<Label htmlFor='share-system-audio'>
{t('screenSharing.shareSystemAudio', 'Share system audio')}
</Label>
</Box>
<Button onClick={handleClose}>{t('screenSharing.cancel')}</Button>
<Button primary onClick={handleShare} disabled={!selectedSourceId}>
{t('screenSharing.share')}
Expand Down
8 changes: 6 additions & 2 deletions src/videoCallWindow/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
getDesktopCapturerCacheStatus,
prewarmDesktopCapturerCache,
} from '../screenSharing/desktopCapturerCache';
type ScreenSharingSelectionPayload = {
sourceId: string | null;
shareAudio?: boolean;
};
import type {
DisplayMediaCallback,
ScreenPickerProvider,
Expand Down Expand Up @@ -296,11 +300,11 @@ export const startVideoCallWindowHandler = (): void => {
// jitsiBridge's ipcRenderer.on listener fires correctly.
ipcMain.once(
'video-call-window/screen-sharing-source-responded',
(_event, sourceId: string | null) => {
(_event, payload: string | null | ScreenSharingSelectionPayload) => {
if (!callerWebContents.isDestroyed()) {
callerWebContents.send(
'video-call-window/screen-sharing-source-responded',
sourceId
payload
);
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/videoCallWindow/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { contextBridge, ipcRenderer } from 'electron';
import './jitsiBridge';

type ScreenSharingSelectionPayload = {
sourceId: string | null;
shareAudio?: boolean;
};

// Expose any necessary APIs to the webview content
contextBridge.exposeInMainWorld('videoCallWindow', {
// Add methods here if needed for communication with the main process
Expand All @@ -10,8 +15,12 @@ contextBridge.exposeInMainWorld('videoCallWindow', {
return new Promise<string | null>((resolve) => {
ipcRenderer.once(
'video-call-window/screen-sharing-source-responded',
(_event, id) => {
resolve(id);
(_event, payload: string | null | ScreenSharingSelectionPayload) => {
if (typeof payload === 'object' && payload !== null) {
resolve(payload.sourceId);
return;
}
resolve(payload);
}
);
});
Expand Down
17 changes: 15 additions & 2 deletions src/videoCallWindow/preload/jitsiBridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ipcRenderer } from 'electron';
type ScreenSharingSelectionPayload = {
sourceId: string | null;
shareAudio?: boolean;
};

/**
* Jitsi Meet External API Interface
Expand Down Expand Up @@ -404,9 +408,18 @@ class JitsiBridgeImpl implements JitsiBridge {

ipcRenderer.on(
'video-call-window/screen-sharing-source-responded',
(_event, sourceId: string | null) => {
(_event, payload: string | null | ScreenSharingSelectionPayload) => {
cleanup();

const sourceId =
typeof payload === 'object' && payload !== null
? payload.sourceId
: payload;
const shareAudio =
typeof payload === 'object' && payload !== null
? payload.shareAudio === true
: false;

if (!sourceId) {
console.log('JitsiBridge: Screen sharing cancelled by user');
errorCb(new Error('gum.screensharing_user_canceled'));
Expand All @@ -417,7 +430,7 @@ class JitsiBridgeImpl implements JitsiBridge {
const sourceType = sourceId.startsWith('window:')
? 'window'
: 'screen';
successCb(sourceId, sourceType);
successCb(sourceId, sourceType, shareAudio);
}
);

Expand Down