Refactor screen share picker internals

This commit is contained in:
Fedor Indutny 2024-09-19 18:03:44 -07:00 committed by GitHub
parent 855b1c03b0
commit d0b8a2991f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 547 additions and 244 deletions

View file

@ -1913,6 +1913,10 @@
"messageformat": "Signal is sharing {window}.",
"description": "Text that appears in the screen sharing controller to inform person that they are presenting"
},
"icu:calling__presenting--info--unknown": {
"messageformat": "Signal screen sharing is active",
"description": "Text that appears in the screen sharing controller to inform person that they are presenting"
},
"icu:calling__presenting--reconnecting": {
"messageformat": "Reconnecting...",
"description": "Text that appears in the screen sharing controller to inform person that the call is in reconnecting state"

View file

@ -16,7 +16,6 @@ import {
app,
BrowserWindow,
clipboard,
desktopCapturer,
dialog,
ipcMain as ipc,
Menu,
@ -1229,7 +1228,7 @@ function setupAsStandalone() {
}
let screenShareWindow: BrowserWindow | undefined;
async function showScreenShareWindow(sourceName: string) {
async function showScreenShareWindow(sourceName: string | undefined) {
if (screenShareWindow) {
screenShareWindow.showInactive();
return;
@ -1981,7 +1980,7 @@ app.on('ready', async () => {
realpath(app.getAppPath()),
]);
updateDefaultSession(session.defaultSession);
updateDefaultSession(session.defaultSession, getLogger);
if (getEnvironment() !== Environment.Test) {
installFileHandler({
@ -2621,9 +2620,12 @@ ipc.on('stop-screen-share', () => {
}
});
ipc.on('show-screen-share', (_event: Electron.Event, sourceName: string) => {
drop(showScreenShareWindow(sourceName));
});
ipc.on(
'show-screen-share',
(_event: Electron.Event, sourceName: string | undefined) => {
drop(showScreenShareWindow(sourceName));
}
);
ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
if (systemTrayService) {
@ -2895,8 +2897,8 @@ function handleSignalRoute(route: ParsedSignalRoute) {
});
} else if (route.key === 'showWindow') {
mainWindow.webContents.send('show-window');
} else if (route.key === 'setIsPresenting') {
mainWindow.webContents.send('set-is-presenting');
} else if (route.key === 'cancelPresenting') {
mainWindow.webContents.send('cancel-presenting');
} else if (route.key === 'captcha') {
challengeHandler.handleCaptcha(route.args.captchaId);
// Show window after handling captcha
@ -3023,17 +3025,6 @@ ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
return { canceled: false, filePath: finalFilePath };
});
ipc.handle(
'getScreenCaptureSources',
async (_event, types: Array<'screen' | 'window'> = ['screen', 'window']) => {
return desktopCapturer.getSources({
fetchWindowIcons: true,
thumbnailSize: { height: 102, width: 184 },
types,
});
}
);
ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => {
const role = untypedRole as MenuItemConstructorOptions['role'];

View file

@ -9,7 +9,7 @@ import type { WindowsNotificationData } from '../ts/services/notifications';
import { NotificationType } from '../ts/services/notifications';
import { missingCaseError } from '../ts/util/missingCaseError';
import {
setIsPresentingRoute,
cancelPresentingRoute,
showConversationRoute,
showWindowRoute,
startCallLobbyRoute,
@ -69,7 +69,7 @@ export function renderWindowsToast({
} else if (type === NotificationType.IncomingCall) {
launch = showWindowRoute.toAppUrl({});
} else if (type === NotificationType.IsPresenting) {
launch = setIsPresentingRoute.toAppUrl({});
launch = cancelPresentingRoute.toAppUrl({});
} else {
throw missingCaseError(type);
}

View file

@ -1,12 +1,72 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Session } from 'electron';
import type { Session, DesktopCapturerSource, IpcMainEvent } from 'electron';
import { desktopCapturer, ipcMain } from 'electron';
import { v4 as generateUuid } from 'uuid';
import OS from '../ts/util/os/osMain';
import type { LoggerType } from '../ts/types/Logging';
import { strictAssert } from '../ts/util/assert';
import { type IpcResponseType } from '../ts/util/desktopCapturer';
const SPELL_CHECKER_DICTIONARY_DOWNLOAD_URL = `https://updates.signal.org/desktop/hunspell_dictionaries/${process.versions.electron}/`;
export function updateDefaultSession(session: Session): void {
export function updateDefaultSession(
session: Session,
getLogger: () => LoggerType
): void {
session.setSpellCheckerDictionaryDownloadURL(
SPELL_CHECKER_DICTIONARY_DOWNLOAD_URL
);
session.setDisplayMediaRequestHandler(
async (request, callback) => {
const { frame, videoRequested, audioRequested } = request;
try {
strictAssert(videoRequested, 'Not requesting video');
strictAssert(!audioRequested, 'Requesting audio');
const sources = await desktopCapturer.getSources({
fetchWindowIcons: true,
thumbnailSize: { height: 102, width: 184 },
types: ['screen', 'window'],
});
// Wayland already shows a window/screen selection modal so we just
// have to go with the source that we were given.
if (OS.isLinux() && OS.isWaylandEnabled() && sources.length === 1) {
callback({ video: sources[0] });
return;
}
const id = generateUuid();
ipcMain.once(
`select-capture-sources:${id}:response`,
(_event: IpcMainEvent, stream: DesktopCapturerSource | undefined) => {
try {
callback({ video: stream });
} catch {
// Don't let Electron errors crash the app
}
}
);
frame.send('select-capture-sources', {
id,
sources,
} satisfies IpcResponseType);
} catch (error) {
try {
callback({});
} catch {
// Electron throws error here, but this is the only way to cancel the
// request.
}
getLogger().error('Failed to get desktopCapturer sources', error);
}
},
{ useSystemPicker: false }
);
}

View file

@ -1,13 +0,0 @@
diff --git a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
index d9d6a00..78fa83f 100644
--- a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
+++ b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
@@ -2,6 +2,8 @@
#import <Foundation/Foundation.h>
#include <node_api.h>
+CG_EXTERN bool CGPreflightScreenCaptureAccess(void) CG_AVAILABLE_STARTING(10.15);
+
static napi_value hasPermissions(napi_env env, napi_callback_info info) {
napi_status status;
bool hasPermissions;

View file

@ -120,17 +120,18 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
playRingtone: action('play-ringtone'),
removeClient: action('remove-client'),
blockClient: action('block-client'),
cancelPresenting: action('cancel-presenting'),
renderDeviceSelection: () => <div />,
renderEmojiPicker: () => <>EmojiPicker</>,
renderReactionPicker: () => <div />,
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
sendGroupCallReaction: action('send-group-call-reaction'),
selectPresentingSource: action('select-presenting-source'),
setGroupCallVideoRequest: action('set-group-call-video-request'),
setIsCallActive: action('set-is-call-active'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
setOutgoingRing: action('set-outgoing-ring'),
showContactModal: action('show-contact-modal'),

View file

@ -16,7 +16,6 @@ import type {
CallingConversationType,
CallViewMode,
GroupCallVideoRequest,
PresentedSource,
} from '../types/Calling';
import {
CallEndedReason,
@ -105,6 +104,7 @@ export type PropsType = {
batchUserAction: (payload: BatchUserActionPayloadType) => void;
bounceAppIconStart: () => unknown;
bounceAppIconStop: () => unknown;
cancelPresenting: () => void;
declineCall: (_: DeclineCallType) => void;
denyUser: (payload: PendingUserActionPayloadType) => void;
hasInitialLoadCompleted: boolean;
@ -120,6 +120,7 @@ export type PropsType = {
playRingtone: () => unknown;
removeClient: (payload: RemoveClientType) => void;
blockClient: (payload: RemoveClientType) => void;
selectPresentingSource: (id: string) => void;
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
@ -128,7 +129,6 @@ export type PropsType = {
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setOutgoingRing: (_: boolean) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
showShareCallLinkViaSignal: (
callLink: CallLinkType,
@ -171,6 +171,7 @@ function ActiveCallManager({
blockClient,
callLink,
cancelCall,
cancelPresenting,
changeCallView,
closeNeedPermissionScreen,
denyUser,
@ -186,13 +187,13 @@ function ActiveCallManager({
renderEmojiPicker,
renderReactionPicker,
removeClient,
selectPresentingSource,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setPresenting,
setRendererCanvas,
setOutgoingRing,
showContactModal,
@ -452,6 +453,7 @@ function ActiveCallManager({
activeCall={activeCall}
approveUser={approveUser}
batchUserAction={batchUserAction}
cancelPresenting={cancelPresenting}
changeCallView={changeCallView}
denyUser={denyUser}
getPresentingSources={getPresentingSources}
@ -473,7 +475,6 @@ function ActiveCallManager({
setRendererCanvas={setRendererCanvas}
setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo}
setPresenting={setPresenting}
stickyControls={showParticipantsList}
switchToPresentationView={switchToPresentationView}
switchFromPresentationView={switchFromPresentationView}
@ -491,7 +492,8 @@ function ActiveCallManager({
<CallingSelectPresentingSourcesModal
i18n={i18n}
presentingSourcesAvailable={presentingSourcesAvailable}
setPresenting={setPresenting}
selectPresentingSource={selectPresentingSource}
cancelPresenting={cancelPresenting}
/>
) : null}
{settingsDialogOpen && renderDeviceSelection()}
@ -536,6 +538,7 @@ export function CallManager({
bounceAppIconStop,
callLink,
cancelCall,
cancelPresenting,
changeCallView,
closeNeedPermissionScreen,
declineCall,
@ -558,6 +561,7 @@ export function CallManager({
renderDeviceSelection,
renderEmojiPicker,
renderReactionPicker,
selectPresentingSource,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
@ -566,7 +570,6 @@ export function CallManager({
setLocalPreview,
setLocalVideo,
setOutgoingRing,
setPresenting,
setRendererCanvas,
showContactModal,
showShareCallLinkViaSignal,
@ -635,6 +638,7 @@ export function CallManager({
blockClient={blockClient}
callLink={callLink}
cancelCall={cancelCall}
cancelPresenting={cancelPresenting}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
denyUser={denyUser}
@ -653,6 +657,7 @@ export function CallManager({
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}
selectPresentingSource={selectPresentingSource}
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest}
@ -660,7 +665,6 @@ export function CallManager({
setLocalPreview={setLocalPreview}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showContactModal={showContactModal}
showShareCallLinkViaSignal={showShareCallLinkViaSignal}

View file

@ -206,13 +206,13 @@ const createProps = (
openSystemPreferencesAction: action('open-system-preferences-action'),
renderEmojiPicker: () => <>EmojiPicker</>,
renderReactionPicker: () => <div />,
cancelPresenting: action('cancel-presenting'),
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
sendGroupCallReaction: action('send-group-call-reaction'),
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
stickyControls: false,
switchToPresentationView: action('switch-to-presentation-view'),

View file

@ -29,7 +29,6 @@ import type {
ActiveCallReactionsType,
ConversationsByDemuxIdType,
GroupCallVideoRequest,
PresentedSource,
} from '../types/Calling';
import {
CALLING_REACTIONS_LIFETIME,
@ -97,6 +96,7 @@ export type PropsType = {
activeCall: ActiveCallType;
approveUser: (payload: PendingUserActionPayloadType) => void;
batchUserAction: (payload: BatchUserActionPayloadType) => void;
cancelPresenting: () => void;
denyUser: (payload: PendingUserActionPayloadType) => void;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
getPresentingSources: () => void;
@ -120,7 +120,6 @@ export type PropsType = {
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
stickyControls: boolean;
switchToPresentationView: () => void;
@ -190,6 +189,7 @@ export function CallScreen({
activeCall,
approveUser,
batchUserAction,
cancelPresenting,
changeCallView,
denyUser,
getGroupCallVideoFrameSource,
@ -210,7 +210,6 @@ export function CallScreen({
setLocalAudio,
setLocalVideo,
setLocalPreview,
setPresenting,
setRendererCanvas,
stickyControls,
switchToPresentationView,
@ -260,11 +259,11 @@ export function CallScreen({
const togglePresenting = useCallback(() => {
if (presentingSource) {
setPresenting();
cancelPresenting();
} else {
getPresentingSources();
}
}, [getPresentingSources, presentingSource, setPresenting]);
}, [getPresentingSources, presentingSource, cancelPresenting]);
const hangUp = useCallback(() => {
hangUpActiveCall('button click');

View file

@ -11,7 +11,7 @@ export type PropsType = {
onCloseController: () => unknown;
onStopSharing: () => unknown;
status: ScreenShareStatus;
presentedSourceName: string;
presentedSourceName: string | undefined;
};
export function CallingScreenSharingController({
@ -25,10 +25,12 @@ export function CallingScreenSharingController({
if (status === ScreenShareStatus.Reconnecting) {
text = i18n('icu:calling__presenting--reconnecting');
} else {
} else if (presentedSourceName) {
text = i18n('icu:calling__presenting--info', {
window: presentedSourceName,
});
} else {
text = i18n('icu:calling__presenting--info--unknown');
}
return (

View file

@ -51,7 +51,8 @@ const createProps = (): PropsType => ({
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
},
],
setPresenting: action('set-presenting'),
selectPresentingSource: action('select-presenting-source'),
cancelPresenting: action('cancel-presenting'),
});
export default {

View file

@ -9,11 +9,13 @@ import type { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import type { PresentedSource, PresentableSource } from '../types/Calling';
import { Theme } from '../util/theme';
import { strictAssert } from '../util/assert';
export type PropsType = {
i18n: LocalizerType;
presentingSourcesAvailable: Array<PresentableSource>;
setPresenting: (_?: PresentedSource) => void;
presentingSourcesAvailable: ReadonlyArray<PresentableSource>;
selectPresentingSource: (id: string) => void;
cancelPresenting: () => void;
};
function Source({
@ -67,7 +69,8 @@ function Source({
export function CallingSelectPresentingSourcesModal({
i18n,
presentingSourcesAvailable,
setPresenting,
selectPresentingSource,
cancelPresenting,
}: PropsType): JSX.Element | null {
const [sourceToPresent, setSourceToPresent] = useState<
PresentedSource | undefined
@ -84,12 +87,15 @@ export function CallingSelectPresentingSourcesModal({
const footer = (
<>
<Button onClick={() => setPresenting()} variant={ButtonVariant.Secondary}>
<Button onClick={cancelPresenting} variant={ButtonVariant.Secondary}>
{i18n('icu:cancel')}
</Button>
<Button
disabled={!sourceToPresent}
onClick={() => setPresenting(sourceToPresent)}
onClick={() => {
strictAssert(sourceToPresent, 'No source to present');
selectPresentingSource(sourceToPresent.id);
}}
>
{i18n('icu:calling__SelectPresentingSourcesModal--confirm')}
</Button>
@ -102,9 +108,7 @@ export function CallingSelectPresentingSourcesModal({
hasXButton
i18n={i18n}
moduleClassName="module-CallingSelectPresentingSourcesModal"
onClose={() => {
setPresenting();
}}
onClose={cancelPresenting}
theme={Theme.Dark}
title={i18n('icu:calling__SelectPresentingSourcesModal--title')}
modalFooter={footer}

View file

@ -1,7 +1,6 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { DesktopCapturerSource } from 'electron';
import { ipcRenderer } from 'electron';
import type {
AudioDevice,
@ -64,7 +63,6 @@ import type {
AvailableIODevicesType,
CallEndedReason,
MediaDeviceSettings,
PresentableSource,
PresentedSource,
} from '../types/Calling';
import {
@ -77,7 +75,6 @@ import {
findBestMatchingAudioDeviceIndex,
findBestMatchingCameraId,
} from '../calling/findBestMatchingDevice';
import type { LocalizerType } from '../types/Util';
import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString';
import * as Errors from '../types/errors';
@ -102,7 +99,6 @@ import {
} from '../calling/constants';
import { callingMessageToProto } from '../util/callingMessageToProto';
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
import OS from '../util/os/osMain';
import { SignalService as Proto } from '../protobuf';
import { DataReader, DataWriter } from '../sql/Client';
import {
@ -192,6 +188,7 @@ type CallingReduxInterface = Pick<
CallingReduxActionsType,
| 'callStateChange'
| 'cancelIncomingGroupCallRing'
| 'cancelPresenting'
| 'groupCallAudioLevelsChange'
| 'groupCallEnded'
| 'groupCallRaisedHandsChange'
@ -204,7 +201,6 @@ type CallingReduxInterface = Pick<
| 'refreshIODevices'
| 'remoteSharingScreenChange'
| 'remoteVideoChange'
| 'setPresenting'
| 'startCallingLobby'
| 'startCallLinkLobby'
| 'startCallLinkLobbyByRoomId'
@ -213,9 +209,13 @@ type CallingReduxInterface = Pick<
areAnyCallsActiveOrRinging(): boolean;
};
function isScreenSource(source: PresentedSource): boolean {
return source.id.startsWith('screen');
}
export type SetPresentingOptionsType = Readonly<{
conversationId: string;
hasLocalVideo: boolean;
mediaStream?: MediaStream;
source?: PresentedSource;
callLinkRootKey?: string;
}>;
function truncateForLogging(name: string | undefined): string | undefined {
if (!name || name.length <= 4) {
@ -251,29 +251,6 @@ function cleanForLogging(settings?: MediaDeviceSettings): unknown {
};
}
function translateSourceName(
i18n: LocalizerType,
source: PresentedSource
): string {
const { name } = source;
if (!isScreenSource(source)) {
return name;
}
if (name === 'Entire Screen') {
return i18n('icu:calling__SelectPresentingSourcesModal--entireScreen');
}
const match = name.match(/^Screen (\d+)$/);
if (match) {
return i18n('icu:calling__SelectPresentingSourcesModal--screen', {
id: match[1],
});
}
return name;
}
function protoToCallingMessage({
offer,
answer,
@ -416,7 +393,7 @@ export class CallingClass {
});
ipcRenderer.on('stop-screen-share', () => {
reduxInterface.setPresenting();
reduxInterface.cancelPresenting();
});
ipcRenderer.on(
'calling:set-rtc-stats-interval',
@ -2046,51 +2023,13 @@ export class CallingClass {
}
}
async getPresentingSources(): Promise<Array<PresentableSource>> {
// There's a Linux Wayland Electron bug where requesting desktopCapturer.
// getSources() with types as ['screen', 'window'] (the default) pops 2
// OS permissions dialogs in an unusable state (Dialog 1 for Share Window
// is the foreground and ignores input; Dialog 2 for Share Screen is background
// and requires input. As a workaround, request both sources sequentially.
// https://github.com/signalapp/Signal-Desktop/issues/5350#issuecomment-1688614149
const sources: ReadonlyArray<DesktopCapturerSource> =
OS.isLinux() && OS.isWaylandEnabled()
? (
await ipcRenderer.invoke('getScreenCaptureSources', ['screen'])
).concat(
await ipcRenderer.invoke('getScreenCaptureSources', ['window'])
)
: await ipcRenderer.invoke('getScreenCaptureSources');
const presentableSources: Array<PresentableSource> = [];
sources.forEach(source => {
// If electron can't retrieve a thumbnail then it won't be able to
// present this source so we filter these out.
if (source.thumbnail.isEmpty()) {
return;
}
presentableSources.push({
appIcon:
source.appIcon && !source.appIcon.isEmpty()
? source.appIcon.toDataURL()
: undefined,
id: source.id,
name: translateSourceName(window.i18n, source),
isScreen: isScreenSource(source),
thumbnail: source.thumbnail.toDataURL(),
});
});
return presentableSources;
}
async setPresenting(
conversationId: string,
hasLocalVideo: boolean,
source?: PresentedSource,
callLinkRootKey?: string
): Promise<void> {
async setPresenting({
conversationId,
hasLocalVideo,
mediaStream,
source,
callLinkRootKey,
}: SetPresentingOptionsType): Promise<void> {
const call = getOwn(this.callsLookup, conversationId);
if (!call) {
log.warn('Trying to set presenting for a non-existent call');
@ -2098,7 +2037,8 @@ export class CallingClass {
}
this.videoCapturer.disable();
if (source) {
const isPresenting = mediaStream != null;
if (isPresenting) {
this.hadLocalVideoBeforePresenting = hasLocalVideo;
drop(
this.enableCaptureAndSend(call, {
@ -2106,7 +2046,7 @@ export class CallingClass {
maxFramerate: 5,
maxHeight: 1800,
maxWidth: 2880,
screenShareSourceId: source.id,
mediaStream,
})
);
this.setOutgoingVideo(conversationId, true);
@ -2118,11 +2058,10 @@ export class CallingClass {
this.hadLocalVideoBeforePresenting = undefined;
}
const isPresenting = Boolean(source);
this.setOutgoingVideoIsScreenShare(call, isPresenting);
if (source) {
ipcRenderer.send('show-screen-share', source.name);
if (isPresenting) {
ipcRenderer.send('show-screen-share', source?.name);
let url: string;
let absolutePath: string | undefined;

View file

@ -218,7 +218,7 @@ class NotificationService extends EventEmitter {
isVideoCall: true,
});
} else if (type === NotificationType.IsPresenting) {
window.reduxActions?.calling?.setPresenting();
window.reduxActions?.calling?.cancelPresenting();
} else if (type === NotificationType.IncomingCall) {
window.IPC.showWindow();
} else {

View file

@ -19,6 +19,10 @@ import { getIntl, getPlatform } from '../selectors/user';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import { missingCaseError } from '../../util/missingCaseError';
import { drop } from '../../util/drop';
import {
DesktopCapturer,
type DesktopCapturerBaton,
} from '../../util/desktopCapturer';
import { calling } from '../../services/calling';
import { truncateAudioLevel } from '../../calling/truncateAudioLevel';
import type { StateType as RootStateType } from '../reducer';
@ -179,7 +183,8 @@ export type ActiveCallStateType = {
outgoingRing: boolean;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
capturerBaton?: DesktopCapturerBaton;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
@ -636,6 +641,7 @@ const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
const REMOVE_CLIENT = 'calling/REMOVE_CLIENT';
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
const SELECT_PRESENTING_SOURCE = 'calling/SELECT_PRESENTING_SOURCE';
const SEND_GROUP_CALL_REACTION = 'calling/SEND_GROUP_CALL_REACTION';
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
@ -866,6 +872,11 @@ type ReturnToActiveCallActionType = ReadonlyDeep<{
type: 'calling/RETURN_TO_ACTIVE_CALL';
}>;
type SelectPresentingSourceActionType = ReadonlyDeep<{
type: 'calling/SELECT_PRESENTING_SOURCE';
payload: string;
}>;
type SetLocalAudioActionType = ReadonlyDeep<{
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
payload: SetLocalAudioType;
@ -881,11 +892,13 @@ type SetPresentingFulfilledActionType = ReadonlyDeep<{
payload?: PresentedSource;
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type SetPresentingSourcesActionType = {
type SetPresentingSourcesActionType = ReadonlyDeep<{
type: 'calling/SET_PRESENTING_SOURCES';
payload: Array<PresentableSource>;
};
payload: {
presentableSources: ReadonlyArray<PresentableSource>;
capturerBaton: DesktopCapturerBaton;
};
}>;
type SetOutgoingRingActionType = ReadonlyDeep<{
type: 'calling/SET_OUTGOING_RING';
@ -962,6 +975,7 @@ export type CallingActionType =
| RemoveClientActionType
| ReturnToActiveCallActionType
| SendGroupCallReactionActionType
| SelectPresentingSourceActionType
| SetLocalAudioActionType
| SetLocalVideoFulfilledActionType
| SetPresentingSourcesActionType
@ -1275,6 +1289,8 @@ function declineCall(
};
}
const globalCapturers = new WeakMap<DesktopCapturerBaton, DesktopCapturer>();
function getPresentingSources(): ThunkAction<
void,
RootStateType,
@ -1283,6 +1299,8 @@ function getPresentingSources(): ThunkAction<
| ToggleNeedsScreenRecordingPermissionsActionType
> {
return async (dispatch, getState) => {
const i18n = getIntl(getState());
// We check if the user has permissions first before calling desktopCapturer
// Next we call getPresentingSources so that one gets the prompt for permissions,
// if necessary.
@ -1294,19 +1312,51 @@ function getPresentingSources(): ThunkAction<
const needsPermission =
platform === 'darwin' && !hasScreenCapturePermission();
const sources = await calling.getPresentingSources();
const capturer = new DesktopCapturer({
i18n,
onPresentableSources(presentableSources) {
if (needsPermission) {
// Abort
capturer.selectSource(undefined);
return;
}
dispatch({
type: SET_PRESENTING_SOURCES,
payload: {
presentableSources,
capturerBaton: capturer.baton,
},
});
},
onMediaStream(mediaStream) {
let presentingSource: PresentedSource | undefined;
const { activeCallState } = getState().calling;
if (activeCallState?.state === 'Active') {
({ presentingSource } = activeCallState);
}
dispatch(
_setPresenting(
presentingSource || {
id: 'media-stream',
name: '',
},
mediaStream
)
);
},
onError(error) {
log.error('getPresentingSources: got error', Errors.toLogFormat(error));
},
});
globalCapturers.set(capturer.baton, capturer);
if (needsPermission) {
dispatch({
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
});
return;
}
dispatch({
type: SET_PRESENTING_SOURCES,
payload: sources,
});
};
}
@ -1774,6 +1824,13 @@ function returnToActiveCall(): ReturnToActiveCallActionType {
};
}
function selectPresentingSource(id: string): SelectPresentingSourceActionType {
return {
type: SELECT_PRESENTING_SOURCE,
payload: id,
};
}
function setIsCallActive(
isCallActive: boolean
): ThunkAction<void, RootStateType, unknown, never> {
@ -1871,8 +1928,9 @@ function setGroupCallVideoRequest(
};
}
function setPresenting(
sourceToPresent?: PresentedSource
function _setPresenting(
sourceToPresent?: PresentedSource,
mediaStream?: MediaStream
): ThunkAction<void, RootStateType, unknown, SetPresentingFulfilledActionType> {
return async (dispatch, getState) => {
const state = getState();
@ -1898,24 +1956,34 @@ function setPresenting(
rootKey = callLink?.rootKey;
}
await calling.setPresenting(
activeCall.conversationId,
activeCallState.hasLocalVideo,
sourceToPresent,
rootKey
);
await calling.setPresenting({
conversationId: activeCall.conversationId,
hasLocalVideo: activeCallState.hasLocalVideo,
mediaStream,
source: sourceToPresent,
callLinkRootKey: rootKey,
});
dispatch({
type: SET_PRESENTING,
payload: sourceToPresent,
});
if (sourceToPresent) {
if (mediaStream) {
await callingTones.someonePresenting();
}
};
}
function cancelPresenting(): ThunkAction<
void,
RootStateType,
unknown,
SetPresentingFulfilledActionType
> {
return _setPresenting(undefined, undefined);
}
function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
return {
type: SET_OUTGOING_RING,
@ -2557,6 +2625,7 @@ export const actions = {
callStateChange,
cancelCall,
cancelIncomingGroupCallRing,
cancelPresenting,
changeCallView,
changeIODevice,
closeNeedPermissionScreen,
@ -2592,13 +2661,13 @@ export const actions = {
returnToActiveCall,
sendGroupCallRaiseHand,
sendGroupCallReaction,
selectPresentingSource,
setGroupCallVideoRequest,
setIsCallActive,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setOutgoingRing,
setPresenting,
setRendererCanvas,
startCall,
startCallLinkLobby,
@ -2612,6 +2681,9 @@ export const actions = {
toggleSettings,
updateCallLinkName,
updateCallLinkRestrictions,
// Exported only for tests
_setPresenting,
};
export const useCallingActions = (): BoundActionCreatorsMapObject<
@ -3677,12 +3749,20 @@ export function reducer(
return state;
}
// Cancel source selection if running
const { capturerBaton } = activeCallState;
if (capturerBaton != null) {
const capturer = globalCapturers.get(capturerBaton);
capturer?.selectSource(undefined);
}
return {
...state,
activeCallState: {
...activeCallState,
presentingSource: action.payload,
presentingSourcesAvailable: undefined,
capturerBaton: undefined,
},
};
}
@ -3698,7 +3778,43 @@ export function reducer(
...state,
activeCallState: {
...activeCallState,
presentingSourcesAvailable: action.payload,
presentingSourcesAvailable: action.payload.presentableSources,
capturerBaton: action.payload.capturerBaton,
},
};
}
if (action.type === SELECT_PRESENTING_SOURCE) {
const { activeCallState } = state;
if (activeCallState?.state !== 'Active') {
log.warn('Cannot set presenting sources when there is no active call');
return state;
}
const { capturerBaton, presentingSourcesAvailable } = activeCallState;
if (!capturerBaton || !presentingSourcesAvailable) {
log.warn(
'Cannot set presenting sources when there is no presenting modal'
);
return state;
}
const capturer = globalCapturers.get(capturerBaton);
if (!capturer) {
log.warn('Cannot toggle presenting when there is no capturer');
return state;
}
capturer.selectSource(action.payload);
return {
...state,
activeCallState: {
...activeCallState,
presentingSource: presentingSourcesAvailable.find(
source => source.id === action.payload
),
presentingSourcesAvailable: undefined,
capturerBaton: undefined,
},
};
}

View file

@ -441,15 +441,16 @@ export const SmartCallManager = memo(function SmartCallManager() {
openSystemPreferencesAction,
removeClient,
blockClient,
cancelPresenting,
sendGroupCallRaiseHand,
sendGroupCallReaction,
selectPresentingSource,
setGroupCallVideoRequest,
setIsCallActive,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setOutgoingRing,
setPresenting,
setRendererCanvas,
switchToPresentationView,
switchFromPresentationView,
@ -477,6 +478,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
bounceAppIconStop={bounceAppIconStop}
callLink={callLink}
cancelCall={cancelCall}
cancelPresenting={cancelPresenting}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
declineCall={declineCall}
@ -503,13 +505,13 @@ export const SmartCallManager = memo(function SmartCallManager() {
renderReactionPicker={renderReactionPicker}
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction}
selectPresentingSource={selectPresentingSource}
setGroupCallVideoRequest={setGroupCallVideoRequest}
setIsCallActive={setIsCallActive}
setLocalAudio={setLocalAudio}
setLocalPreview={setLocalPreview}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showContactModal={showContactModal}
showShareCallLinkViaSignal={showShareCallLinkViaSignal}

View file

@ -240,46 +240,6 @@ describe('calling duck', () => {
});
describe('actions', () => {
describe('getPresentingSources', () => {
beforeEach(function (this: Mocha.Context) {
this.callingServiceGetPresentingSources = this.sandbox
.stub(callingService, 'getPresentingSources')
.resolves([
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
]);
});
it('retrieves sources from the calling service', async function (this: Mocha.Context) {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(this.callingServiceGetPresentingSources);
});
it('dispatches SET_PRESENTING_SOURCES', async () => {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_PRESENTING_SOURCES',
payload: [
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
],
});
});
});
describe('remoteSharingScreenChange', () => {
it("updates whether someone's screen is being shared", () => {
const { remoteSharingScreenChange } = actions;
@ -308,7 +268,7 @@ describe('calling duck', () => {
});
});
describe('setPresenting', () => {
describe('_setPresenting', () => {
beforeEach(function (this: Mocha.Context) {
this.callingServiceSetPresenting = this.sandbox.stub(
callingService,
@ -316,8 +276,8 @@ describe('calling duck', () => {
);
});
it('calls setPresenting on the calling service', async function (this: Mocha.Context) {
const { setPresenting } = actions;
it('calls _setPresenting on the calling service', async function (this: Mocha.Context) {
const { _setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
@ -330,19 +290,20 @@ describe('calling duck', () => {
},
});
await setPresenting(presentedSource)(dispatch, getState, null);
await _setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(this.callingServiceSetPresenting);
sinon.assert.calledWith(
this.callingServiceSetPresenting,
'fake-group-call-conversation-id',
false,
presentedSource
);
sinon.assert.calledWith(this.callingServiceSetPresenting, {
conversationId: 'fake-group-call-conversation-id',
hasLocalVideo: false,
mediaStream: undefined,
source: presentedSource,
callLinkRootKey: undefined,
});
});
it('dispatches SET_PRESENTING', async () => {
const { setPresenting } = actions;
const { _setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
@ -355,7 +316,7 @@ describe('calling duck', () => {
},
});
await setPresenting(presentedSource)(dispatch, getState, null);
await _setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
@ -366,7 +327,7 @@ describe('calling duck', () => {
it('turns off presenting when no value is passed in', async () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const { _setPresenting } = actions;
const presentedSource = {
id: 'window:786',
name: 'Application',
@ -379,7 +340,7 @@ describe('calling duck', () => {
},
});
await setPresenting(presentedSource)(dispatch, getState, null);
await _setPresenting(presentedSource)(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
@ -401,7 +362,7 @@ describe('calling duck', () => {
it('sets the presenting value when one is passed in', async () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const { _setPresenting } = actions;
const getState = (): RootStateType => ({
...getEmptyRootState(),
@ -410,7 +371,7 @@ describe('calling duck', () => {
},
});
await setPresenting()(dispatch, getState, null);
await _setPresenting()(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];

View file

@ -89,7 +89,7 @@ describe('renderWindowsToast', () => {
});
const expected =
'<toast launch="sgnl://set-is-presenting" activationType="protocol"><visual><binding template="ToastText02"><text id="1">Alice</text><text id="2">Hi there!</text></binding></visual></toast>';
'<toast launch="sgnl://cancel-presenting" activationType="protocol"><visual><binding template="ToastText02"><text id="1">Alice</text><text id="2">Hi there!</text></binding></visual></toast>';
assert.strictEqual(xml, expected);
});

View file

@ -21,13 +21,15 @@ describe('updateDefaultSession', () => {
it('sets the spellcheck URL', () => {
const sesh = session.fromPartition(uuid());
const stub = sandbox.stub(sesh, 'setSpellCheckerDictionaryDownloadURL');
const getLogger = sandbox.stub();
updateDefaultSession(sesh);
updateDefaultSession(sesh, getLogger);
sinon.assert.calledOnce(stub);
sinon.assert.calledWith(
stub,
`https://updates.signal.org/desktop/hunspell_dictionaries/${process.versions.electron}/`
);
sinon.assert.notCalled(getLogger);
});
});

View file

@ -205,13 +205,13 @@ describe('signalRoutes', () => {
check('sgnl://show-window', result);
});
it('setIsPresenting', () => {
it('cancelPresenting', () => {
const result: ParsedSignalRoute = {
key: 'setIsPresenting',
key: 'cancelPresenting',
args: {},
};
const check = createCheck({ isRoute: true, hasWebUrl: false });
check('sgnl://set-is-presenting/', result);
check('sgnl://set-is-presenting', result);
check('sgnl://cancel-presenting/', result);
check('sgnl://cancel-presenting', result);
});
});

View file

@ -1,6 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
import type { ConversationType } from '../state/ducks/conversations';
import type { AciString, ServiceIdString } from './ServiceId';
@ -19,13 +20,13 @@ export enum CallViewMode {
Presentation = 'Presentation',
}
export type PresentableSource = {
export type PresentableSource = ReadonlyDeep<{
appIcon?: string;
id: string;
name: string;
isScreen: boolean;
thumbnail: string;
};
}>;
export type PresentedSource = {
id: string;
@ -54,7 +55,7 @@ export type ActiveCallBaseType = {
outgoingRing: boolean;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;

229
ts/util/desktopCapturer.ts Normal file
View file

@ -0,0 +1,229 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer, type DesktopCapturerSource } from 'electron';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import type { PresentableSource } from '../types/Calling';
import type { LocalizerType } from '../types/Util';
import {
REQUESTED_VIDEO_WIDTH,
REQUESTED_VIDEO_HEIGHT,
REQUESTED_VIDEO_FRAMERATE,
} from '../calling/constants';
import { strictAssert } from './assert';
import { explodePromise } from './explodePromise';
import { isNotNil } from './isNotNil';
enum Step {
RequestingMedia = 'RequestingMedia',
Done = 'Done',
Error = 'Error',
// Skipped on macOS Sequoia
SelectingSource = 'SelectingSource',
SelectedSource = 'SelectedSource',
}
type State = Readonly<
| {
step: Step.RequestingMedia;
promise: Promise<void>;
}
| {
step: Step.SelectingSource;
promise: Promise<void>;
sources: ReadonlyArray<DesktopCapturerSource>;
onSource: (source: DesktopCapturerSource | undefined) => void;
}
| {
step: Step.SelectedSource;
promise: Promise<void>;
}
| {
step: Step.Done;
}
| {
step: Step.Error;
}
>;
export const liveCapturers = new Set<DesktopCapturer>();
export type IpcResponseType = Readonly<{
id: string;
sources: ReadonlyArray<DesktopCapturerSource>;
}>;
export type DesktopCapturerOptionsType = Readonly<{
i18n: LocalizerType;
onPresentableSources(sources: ReadonlyArray<PresentableSource>): void;
onMediaStream(stream: MediaStream): void;
onError(error: Error): void;
}>;
export type DesktopCapturerBaton = Readonly<{
__desktop_capturer_baton: never;
}>;
export class DesktopCapturer {
private state: State;
private static isInitialized = false;
// For use as a key in weak maps
public readonly baton = {} as DesktopCapturerBaton;
constructor(private readonly options: DesktopCapturerOptionsType) {
if (!DesktopCapturer.isInitialized) {
DesktopCapturer.initialize();
}
this.state = { step: Step.RequestingMedia, promise: this.getStream() };
}
public selectSource(id: string | undefined): void {
strictAssert(
this.state.step === Step.SelectingSource,
`Invalid state in "selectSource" ${this.state.step}`
);
const { promise, sources, onSource } = this.state;
const source = id == null ? undefined : sources.find(s => s.id === id);
this.state = { step: Step.SelectedSource, promise };
onSource(source);
}
/** @internal */
private onSources(
sources: ReadonlyArray<DesktopCapturerSource>
): Promise<DesktopCapturerSource | undefined> {
strictAssert(
this.state.step === Step.RequestingMedia,
`Invalid state in "onSources" ${this.state.step}`
);
const presentableSources = sources
.map(source => {
// If electron can't retrieve a thumbnail then it won't be able to
// present this source so we filter these out.
if (source.thumbnail.isEmpty()) {
return undefined;
}
return {
appIcon:
source.appIcon && !source.appIcon.isEmpty()
? source.appIcon.toDataURL()
: undefined,
id: source.id,
name: this.translateSourceName(source),
isScreen: isScreenSource(source),
thumbnail: source.thumbnail.toDataURL(),
};
})
.filter(isNotNil);
const { promise } = this.state;
const { promise: source, resolve: onSource } = explodePromise<
DesktopCapturerSource | undefined
>();
this.state = { step: Step.SelectingSource, promise, sources, onSource };
this.options.onPresentableSources(presentableSources);
return source;
}
private async getStream(): Promise<void> {
liveCapturers.add(this);
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
width: {
max: REQUESTED_VIDEO_WIDTH,
ideal: REQUESTED_VIDEO_WIDTH,
},
height: {
max: REQUESTED_VIDEO_HEIGHT,
ideal: REQUESTED_VIDEO_HEIGHT,
},
frameRate: {
max: REQUESTED_VIDEO_FRAMERATE,
ideal: REQUESTED_VIDEO_FRAMERATE,
},
},
});
strictAssert(
this.state.step === Step.RequestingMedia ||
this.state.step === Step.SelectedSource,
`Invalid state in "getStream.success" ${this.state.step}`
);
this.options.onMediaStream(stream);
this.state = { step: Step.Done };
} catch (error) {
strictAssert(
this.state.step === Step.RequestingMedia ||
this.state.step === Step.SelectedSource,
`Invalid state in "getStream.error" ${this.state.step}`
);
this.options.onError(error);
this.state = { step: Step.Error };
} finally {
liveCapturers.delete(this);
}
}
private translateSourceName(source: DesktopCapturerSource): string {
const { i18n } = this.options;
const { name } = source;
if (!isScreenSource(source)) {
return name;
}
if (name === 'Entire Screen') {
return i18n('icu:calling__SelectPresentingSourcesModal--entireScreen');
}
const match = name.match(/^Screen (\d+)$/);
if (match) {
return i18n('icu:calling__SelectPresentingSourcesModal--screen', {
id: match[1],
});
}
return name;
}
private static initialize(): void {
DesktopCapturer.isInitialized = true;
ipcRenderer.on(
'select-capture-sources',
async (_, { id, sources }: IpcResponseType) => {
let selected: DesktopCapturerSource | undefined;
try {
const { value: capturer, done } = liveCapturers.values().next();
strictAssert(!done, 'No capturer available for incoming sources');
liveCapturers.delete(capturer);
selected = await capturer.onSources(sources);
} catch (error) {
log.error(
'desktopCapturer: failed to get the source',
Errors.toLogFormat(error)
);
}
ipcRenderer.send(`select-capture-sources:${id}:response`, selected);
}
);
}
}
function isScreenSource(source: DesktopCapturerSource): boolean {
return source.id.startsWith('screen');
}

View file

@ -49,7 +49,7 @@ type AllHostnamePatterns =
| 'show-conversation'
| 'start-call-lobby'
| 'show-window'
| 'set-is-presenting'
| 'cancel-presenting'
| ':captchaId(.+)'
| '';
@ -535,18 +535,18 @@ export const showWindowRoute = _route('showWindow', {
* Set is presenting
* @example
* ```ts
* setIsPresentingRoute.toAppUrl({})
* // URL { "sgnl://set-is-presenting" }
* cancelPresentingRoute.toAppUrl({})
* // URL { "sgnl://cancel-presenting" }
* ```
*/
export const setIsPresentingRoute = _route('setIsPresenting', {
patterns: [_pattern('sgnl:', 'set-is-presenting', '{/}?', {})],
export const cancelPresentingRoute = _route('cancelPresenting', {
patterns: [_pattern('sgnl:', 'cancel-presenting', '{/}?', {})],
schema: z.object({}),
parse() {
return {};
},
toAppUrl() {
return new URL('sgnl://set-is-presenting');
return new URL('sgnl://cancel-presenting');
},
});
@ -565,7 +565,7 @@ const _allSignalRoutes = [
showConversationRoute,
startCallLobbyRoute,
showWindowRoute,
setIsPresentingRoute,
cancelPresentingRoute,
] as const;
strictAssert(

2
ts/window.d.ts vendored
View file

@ -121,7 +121,7 @@ type PermissionsWindowPropsType = {
type ScreenShareWindowPropsType = {
onStopSharing: () => void;
presentedSourceName: string;
presentedSourceName: string | undefined;
getStatus: () => ScreenShareStatus;
setRenderCallback: (cb: () => void) => void;
};

View file

@ -360,8 +360,8 @@ ipc.on('show-window', () => {
window.IPC.showWindow();
});
ipc.on('set-is-presenting', () => {
window.reduxActions?.calling?.setPresenting();
ipc.on('cancel-presenting', () => {
window.reduxActions?.calling?.cancelPresenting();
});
ipc.on(