Refactor screen share picker internals
This commit is contained in:
parent
855b1c03b0
commit
d0b8a2991f
25 changed files with 547 additions and 244 deletions
|
@ -1913,6 +1913,10 @@
|
||||||
"messageformat": "Signal is sharing {window}.",
|
"messageformat": "Signal is sharing {window}.",
|
||||||
"description": "Text that appears in the screen sharing controller to inform person that they are presenting"
|
"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": {
|
"icu:calling__presenting--reconnecting": {
|
||||||
"messageformat": "Reconnecting...",
|
"messageformat": "Reconnecting...",
|
||||||
"description": "Text that appears in the screen sharing controller to inform person that the call is in reconnecting state"
|
"description": "Text that appears in the screen sharing controller to inform person that the call is in reconnecting state"
|
||||||
|
|
27
app/main.ts
27
app/main.ts
|
@ -16,7 +16,6 @@ import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
clipboard,
|
clipboard,
|
||||||
desktopCapturer,
|
|
||||||
dialog,
|
dialog,
|
||||||
ipcMain as ipc,
|
ipcMain as ipc,
|
||||||
Menu,
|
Menu,
|
||||||
|
@ -1229,7 +1228,7 @@ function setupAsStandalone() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let screenShareWindow: BrowserWindow | undefined;
|
let screenShareWindow: BrowserWindow | undefined;
|
||||||
async function showScreenShareWindow(sourceName: string) {
|
async function showScreenShareWindow(sourceName: string | undefined) {
|
||||||
if (screenShareWindow) {
|
if (screenShareWindow) {
|
||||||
screenShareWindow.showInactive();
|
screenShareWindow.showInactive();
|
||||||
return;
|
return;
|
||||||
|
@ -1981,7 +1980,7 @@ app.on('ready', async () => {
|
||||||
realpath(app.getAppPath()),
|
realpath(app.getAppPath()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updateDefaultSession(session.defaultSession);
|
updateDefaultSession(session.defaultSession, getLogger);
|
||||||
|
|
||||||
if (getEnvironment() !== Environment.Test) {
|
if (getEnvironment() !== Environment.Test) {
|
||||||
installFileHandler({
|
installFileHandler({
|
||||||
|
@ -2621,9 +2620,12 @@ ipc.on('stop-screen-share', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('show-screen-share', (_event: Electron.Event, sourceName: string) => {
|
ipc.on(
|
||||||
|
'show-screen-share',
|
||||||
|
(_event: Electron.Event, sourceName: string | undefined) => {
|
||||||
drop(showScreenShareWindow(sourceName));
|
drop(showScreenShareWindow(sourceName));
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
|
ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
|
||||||
if (systemTrayService) {
|
if (systemTrayService) {
|
||||||
|
@ -2895,8 +2897,8 @@ function handleSignalRoute(route: ParsedSignalRoute) {
|
||||||
});
|
});
|
||||||
} else if (route.key === 'showWindow') {
|
} else if (route.key === 'showWindow') {
|
||||||
mainWindow.webContents.send('show-window');
|
mainWindow.webContents.send('show-window');
|
||||||
} else if (route.key === 'setIsPresenting') {
|
} else if (route.key === 'cancelPresenting') {
|
||||||
mainWindow.webContents.send('set-is-presenting');
|
mainWindow.webContents.send('cancel-presenting');
|
||||||
} else if (route.key === 'captcha') {
|
} else if (route.key === 'captcha') {
|
||||||
challengeHandler.handleCaptcha(route.args.captchaId);
|
challengeHandler.handleCaptcha(route.args.captchaId);
|
||||||
// Show window after handling captcha
|
// Show window after handling captcha
|
||||||
|
@ -3023,17 +3025,6 @@ ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
|
||||||
return { canceled: false, filePath: finalFilePath };
|
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) => {
|
ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => {
|
||||||
const role = untypedRole as MenuItemConstructorOptions['role'];
|
const role = untypedRole as MenuItemConstructorOptions['role'];
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { WindowsNotificationData } from '../ts/services/notifications';
|
||||||
import { NotificationType } from '../ts/services/notifications';
|
import { NotificationType } from '../ts/services/notifications';
|
||||||
import { missingCaseError } from '../ts/util/missingCaseError';
|
import { missingCaseError } from '../ts/util/missingCaseError';
|
||||||
import {
|
import {
|
||||||
setIsPresentingRoute,
|
cancelPresentingRoute,
|
||||||
showConversationRoute,
|
showConversationRoute,
|
||||||
showWindowRoute,
|
showWindowRoute,
|
||||||
startCallLobbyRoute,
|
startCallLobbyRoute,
|
||||||
|
@ -69,7 +69,7 @@ export function renderWindowsToast({
|
||||||
} else if (type === NotificationType.IncomingCall) {
|
} else if (type === NotificationType.IncomingCall) {
|
||||||
launch = showWindowRoute.toAppUrl({});
|
launch = showWindowRoute.toAppUrl({});
|
||||||
} else if (type === NotificationType.IsPresenting) {
|
} else if (type === NotificationType.IsPresenting) {
|
||||||
launch = setIsPresentingRoute.toAppUrl({});
|
launch = cancelPresentingRoute.toAppUrl({});
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(type);
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,72 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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}/`;
|
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(
|
session.setSpellCheckerDictionaryDownloadURL(
|
||||||
SPELL_CHECKER_DICTIONARY_DOWNLOAD_URL
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
|
@ -120,17 +120,18 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
playRingtone: action('play-ringtone'),
|
playRingtone: action('play-ringtone'),
|
||||||
removeClient: action('remove-client'),
|
removeClient: action('remove-client'),
|
||||||
blockClient: action('block-client'),
|
blockClient: action('block-client'),
|
||||||
|
cancelPresenting: action('cancel-presenting'),
|
||||||
renderDeviceSelection: () => <div />,
|
renderDeviceSelection: () => <div />,
|
||||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||||
|
selectPresentingSource: action('select-presenting-source'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setIsCallActive: action('set-is-call-active'),
|
setIsCallActive: action('set-is-call-active'),
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setPresenting: action('toggle-presenting'),
|
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
setOutgoingRing: action('set-outgoing-ring'),
|
setOutgoingRing: action('set-outgoing-ring'),
|
||||||
showContactModal: action('show-contact-modal'),
|
showContactModal: action('show-contact-modal'),
|
||||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
||||||
CallingConversationType,
|
CallingConversationType,
|
||||||
CallViewMode,
|
CallViewMode,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
PresentedSource,
|
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import {
|
import {
|
||||||
CallEndedReason,
|
CallEndedReason,
|
||||||
|
@ -105,6 +104,7 @@ export type PropsType = {
|
||||||
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||||
bounceAppIconStart: () => unknown;
|
bounceAppIconStart: () => unknown;
|
||||||
bounceAppIconStop: () => unknown;
|
bounceAppIconStop: () => unknown;
|
||||||
|
cancelPresenting: () => void;
|
||||||
declineCall: (_: DeclineCallType) => void;
|
declineCall: (_: DeclineCallType) => void;
|
||||||
denyUser: (payload: PendingUserActionPayloadType) => void;
|
denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||||
hasInitialLoadCompleted: boolean;
|
hasInitialLoadCompleted: boolean;
|
||||||
|
@ -120,6 +120,7 @@ export type PropsType = {
|
||||||
playRingtone: () => unknown;
|
playRingtone: () => unknown;
|
||||||
removeClient: (payload: RemoveClientType) => void;
|
removeClient: (payload: RemoveClientType) => void;
|
||||||
blockClient: (payload: RemoveClientType) => void;
|
blockClient: (payload: RemoveClientType) => void;
|
||||||
|
selectPresentingSource: (id: string) => void;
|
||||||
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||||
|
@ -128,7 +129,6 @@ export type PropsType = {
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
setOutgoingRing: (_: boolean) => void;
|
setOutgoingRing: (_: boolean) => void;
|
||||||
setPresenting: (_?: PresentedSource) => void;
|
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
showShareCallLinkViaSignal: (
|
showShareCallLinkViaSignal: (
|
||||||
callLink: CallLinkType,
|
callLink: CallLinkType,
|
||||||
|
@ -171,6 +171,7 @@ function ActiveCallManager({
|
||||||
blockClient,
|
blockClient,
|
||||||
callLink,
|
callLink,
|
||||||
cancelCall,
|
cancelCall,
|
||||||
|
cancelPresenting,
|
||||||
changeCallView,
|
changeCallView,
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
denyUser,
|
denyUser,
|
||||||
|
@ -186,13 +187,13 @@ function ActiveCallManager({
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
removeClient,
|
removeClient,
|
||||||
|
selectPresentingSource,
|
||||||
sendGroupCallRaiseHand,
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setPresenting,
|
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
setOutgoingRing,
|
setOutgoingRing,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
@ -452,6 +453,7 @@ function ActiveCallManager({
|
||||||
activeCall={activeCall}
|
activeCall={activeCall}
|
||||||
approveUser={approveUser}
|
approveUser={approveUser}
|
||||||
batchUserAction={batchUserAction}
|
batchUserAction={batchUserAction}
|
||||||
|
cancelPresenting={cancelPresenting}
|
||||||
changeCallView={changeCallView}
|
changeCallView={changeCallView}
|
||||||
denyUser={denyUser}
|
denyUser={denyUser}
|
||||||
getPresentingSources={getPresentingSources}
|
getPresentingSources={getPresentingSources}
|
||||||
|
@ -473,7 +475,6 @@ function ActiveCallManager({
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
setPresenting={setPresenting}
|
|
||||||
stickyControls={showParticipantsList}
|
stickyControls={showParticipantsList}
|
||||||
switchToPresentationView={switchToPresentationView}
|
switchToPresentationView={switchToPresentationView}
|
||||||
switchFromPresentationView={switchFromPresentationView}
|
switchFromPresentationView={switchFromPresentationView}
|
||||||
|
@ -491,7 +492,8 @@ function ActiveCallManager({
|
||||||
<CallingSelectPresentingSourcesModal
|
<CallingSelectPresentingSourcesModal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
presentingSourcesAvailable={presentingSourcesAvailable}
|
presentingSourcesAvailable={presentingSourcesAvailable}
|
||||||
setPresenting={setPresenting}
|
selectPresentingSource={selectPresentingSource}
|
||||||
|
cancelPresenting={cancelPresenting}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{settingsDialogOpen && renderDeviceSelection()}
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
|
@ -536,6 +538,7 @@ export function CallManager({
|
||||||
bounceAppIconStop,
|
bounceAppIconStop,
|
||||||
callLink,
|
callLink,
|
||||||
cancelCall,
|
cancelCall,
|
||||||
|
cancelPresenting,
|
||||||
changeCallView,
|
changeCallView,
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
declineCall,
|
declineCall,
|
||||||
|
@ -558,6 +561,7 @@ export function CallManager({
|
||||||
renderDeviceSelection,
|
renderDeviceSelection,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
|
selectPresentingSource,
|
||||||
sendGroupCallRaiseHand,
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
|
@ -566,7 +570,6 @@ export function CallManager({
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setOutgoingRing,
|
setOutgoingRing,
|
||||||
setPresenting,
|
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
showShareCallLinkViaSignal,
|
showShareCallLinkViaSignal,
|
||||||
|
@ -635,6 +638,7 @@ export function CallManager({
|
||||||
blockClient={blockClient}
|
blockClient={blockClient}
|
||||||
callLink={callLink}
|
callLink={callLink}
|
||||||
cancelCall={cancelCall}
|
cancelCall={cancelCall}
|
||||||
|
cancelPresenting={cancelPresenting}
|
||||||
changeCallView={changeCallView}
|
changeCallView={changeCallView}
|
||||||
closeNeedPermissionScreen={closeNeedPermissionScreen}
|
closeNeedPermissionScreen={closeNeedPermissionScreen}
|
||||||
denyUser={denyUser}
|
denyUser={denyUser}
|
||||||
|
@ -653,6 +657,7 @@ export function CallManager({
|
||||||
renderDeviceSelection={renderDeviceSelection}
|
renderDeviceSelection={renderDeviceSelection}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
renderReactionPicker={renderReactionPicker}
|
renderReactionPicker={renderReactionPicker}
|
||||||
|
selectPresentingSource={selectPresentingSource}
|
||||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||||
sendGroupCallReaction={sendGroupCallReaction}
|
sendGroupCallReaction={sendGroupCallReaction}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||||
|
@ -660,7 +665,6 @@ export function CallManager({
|
||||||
setLocalPreview={setLocalPreview}
|
setLocalPreview={setLocalPreview}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
setOutgoingRing={setOutgoingRing}
|
setOutgoingRing={setOutgoingRing}
|
||||||
setPresenting={setPresenting}
|
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
showContactModal={showContactModal}
|
showContactModal={showContactModal}
|
||||||
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
|
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
|
||||||
|
|
|
@ -206,13 +206,13 @@ const createProps = (
|
||||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
|
cancelPresenting: action('cancel-presenting'),
|
||||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setPresenting: action('toggle-presenting'),
|
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
stickyControls: false,
|
stickyControls: false,
|
||||||
switchToPresentationView: action('switch-to-presentation-view'),
|
switchToPresentationView: action('switch-to-presentation-view'),
|
||||||
|
|
|
@ -29,7 +29,6 @@ import type {
|
||||||
ActiveCallReactionsType,
|
ActiveCallReactionsType,
|
||||||
ConversationsByDemuxIdType,
|
ConversationsByDemuxIdType,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
PresentedSource,
|
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import {
|
import {
|
||||||
CALLING_REACTIONS_LIFETIME,
|
CALLING_REACTIONS_LIFETIME,
|
||||||
|
@ -97,6 +96,7 @@ export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
approveUser: (payload: PendingUserActionPayloadType) => void;
|
approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||||
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||||
|
cancelPresenting: () => void;
|
||||||
denyUser: (payload: PendingUserActionPayloadType) => void;
|
denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
getPresentingSources: () => void;
|
getPresentingSources: () => void;
|
||||||
|
@ -120,7 +120,6 @@ export type PropsType = {
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
setPresenting: (_?: PresentedSource) => void;
|
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
stickyControls: boolean;
|
stickyControls: boolean;
|
||||||
switchToPresentationView: () => void;
|
switchToPresentationView: () => void;
|
||||||
|
@ -190,6 +189,7 @@ export function CallScreen({
|
||||||
activeCall,
|
activeCall,
|
||||||
approveUser,
|
approveUser,
|
||||||
batchUserAction,
|
batchUserAction,
|
||||||
|
cancelPresenting,
|
||||||
changeCallView,
|
changeCallView,
|
||||||
denyUser,
|
denyUser,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
|
@ -210,7 +210,6 @@ export function CallScreen({
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setPresenting,
|
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
stickyControls,
|
stickyControls,
|
||||||
switchToPresentationView,
|
switchToPresentationView,
|
||||||
|
@ -260,11 +259,11 @@ export function CallScreen({
|
||||||
|
|
||||||
const togglePresenting = useCallback(() => {
|
const togglePresenting = useCallback(() => {
|
||||||
if (presentingSource) {
|
if (presentingSource) {
|
||||||
setPresenting();
|
cancelPresenting();
|
||||||
} else {
|
} else {
|
||||||
getPresentingSources();
|
getPresentingSources();
|
||||||
}
|
}
|
||||||
}, [getPresentingSources, presentingSource, setPresenting]);
|
}, [getPresentingSources, presentingSource, cancelPresenting]);
|
||||||
|
|
||||||
const hangUp = useCallback(() => {
|
const hangUp = useCallback(() => {
|
||||||
hangUpActiveCall('button click');
|
hangUpActiveCall('button click');
|
||||||
|
|
|
@ -11,7 +11,7 @@ export type PropsType = {
|
||||||
onCloseController: () => unknown;
|
onCloseController: () => unknown;
|
||||||
onStopSharing: () => unknown;
|
onStopSharing: () => unknown;
|
||||||
status: ScreenShareStatus;
|
status: ScreenShareStatus;
|
||||||
presentedSourceName: string;
|
presentedSourceName: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CallingScreenSharingController({
|
export function CallingScreenSharingController({
|
||||||
|
@ -25,10 +25,12 @@ export function CallingScreenSharingController({
|
||||||
|
|
||||||
if (status === ScreenShareStatus.Reconnecting) {
|
if (status === ScreenShareStatus.Reconnecting) {
|
||||||
text = i18n('icu:calling__presenting--reconnecting');
|
text = i18n('icu:calling__presenting--reconnecting');
|
||||||
} else {
|
} else if (presentedSourceName) {
|
||||||
text = i18n('icu:calling__presenting--info', {
|
text = i18n('icu:calling__presenting--info', {
|
||||||
window: presentedSourceName,
|
window: presentedSourceName,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
text = i18n('icu:calling__presenting--info--unknown');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -51,7 +51,8 @@ const createProps = (): PropsType => ({
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
setPresenting: action('set-presenting'),
|
selectPresentingSource: action('select-presenting-source'),
|
||||||
|
cancelPresenting: action('cancel-presenting'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -9,11 +9,13 @@ import type { LocalizerType } from '../types/Util';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import type { PresentedSource, PresentableSource } from '../types/Calling';
|
import type { PresentedSource, PresentableSource } from '../types/Calling';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
import { strictAssert } from '../util/assert';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
presentingSourcesAvailable: Array<PresentableSource>;
|
presentingSourcesAvailable: ReadonlyArray<PresentableSource>;
|
||||||
setPresenting: (_?: PresentedSource) => void;
|
selectPresentingSource: (id: string) => void;
|
||||||
|
cancelPresenting: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Source({
|
function Source({
|
||||||
|
@ -67,7 +69,8 @@ function Source({
|
||||||
export function CallingSelectPresentingSourcesModal({
|
export function CallingSelectPresentingSourcesModal({
|
||||||
i18n,
|
i18n,
|
||||||
presentingSourcesAvailable,
|
presentingSourcesAvailable,
|
||||||
setPresenting,
|
selectPresentingSource,
|
||||||
|
cancelPresenting,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
const [sourceToPresent, setSourceToPresent] = useState<
|
const [sourceToPresent, setSourceToPresent] = useState<
|
||||||
PresentedSource | undefined
|
PresentedSource | undefined
|
||||||
|
@ -84,12 +87,15 @@ export function CallingSelectPresentingSourcesModal({
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setPresenting()} variant={ButtonVariant.Secondary}>
|
<Button onClick={cancelPresenting} variant={ButtonVariant.Secondary}>
|
||||||
{i18n('icu:cancel')}
|
{i18n('icu:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!sourceToPresent}
|
disabled={!sourceToPresent}
|
||||||
onClick={() => setPresenting(sourceToPresent)}
|
onClick={() => {
|
||||||
|
strictAssert(sourceToPresent, 'No source to present');
|
||||||
|
selectPresentingSource(sourceToPresent.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{i18n('icu:calling__SelectPresentingSourcesModal--confirm')}
|
{i18n('icu:calling__SelectPresentingSourcesModal--confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -102,9 +108,7 @@ export function CallingSelectPresentingSourcesModal({
|
||||||
hasXButton
|
hasXButton
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
moduleClassName="module-CallingSelectPresentingSourcesModal"
|
moduleClassName="module-CallingSelectPresentingSourcesModal"
|
||||||
onClose={() => {
|
onClose={cancelPresenting}
|
||||||
setPresenting();
|
|
||||||
}}
|
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
title={i18n('icu:calling__SelectPresentingSourcesModal--title')}
|
title={i18n('icu:calling__SelectPresentingSourcesModal--title')}
|
||||||
modalFooter={footer}
|
modalFooter={footer}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { DesktopCapturerSource } from 'electron';
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import type {
|
import type {
|
||||||
AudioDevice,
|
AudioDevice,
|
||||||
|
@ -64,7 +63,6 @@ import type {
|
||||||
AvailableIODevicesType,
|
AvailableIODevicesType,
|
||||||
CallEndedReason,
|
CallEndedReason,
|
||||||
MediaDeviceSettings,
|
MediaDeviceSettings,
|
||||||
PresentableSource,
|
|
||||||
PresentedSource,
|
PresentedSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import {
|
import {
|
||||||
|
@ -77,7 +75,6 @@ import {
|
||||||
findBestMatchingAudioDeviceIndex,
|
findBestMatchingAudioDeviceIndex,
|
||||||
findBestMatchingCameraId,
|
findBestMatchingCameraId,
|
||||||
} from '../calling/findBestMatchingDevice';
|
} from '../calling/findBestMatchingDevice';
|
||||||
import type { LocalizerType } from '../types/Util';
|
|
||||||
import { normalizeAci } from '../util/normalizeAci';
|
import { normalizeAci } from '../util/normalizeAci';
|
||||||
import { isAciString } from '../util/isAciString';
|
import { isAciString } from '../util/isAciString';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
|
@ -102,7 +99,6 @@ import {
|
||||||
} from '../calling/constants';
|
} from '../calling/constants';
|
||||||
import { callingMessageToProto } from '../util/callingMessageToProto';
|
import { callingMessageToProto } from '../util/callingMessageToProto';
|
||||||
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
|
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
|
||||||
import OS from '../util/os/osMain';
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import { DataReader, DataWriter } from '../sql/Client';
|
import { DataReader, DataWriter } from '../sql/Client';
|
||||||
import {
|
import {
|
||||||
|
@ -192,6 +188,7 @@ type CallingReduxInterface = Pick<
|
||||||
CallingReduxActionsType,
|
CallingReduxActionsType,
|
||||||
| 'callStateChange'
|
| 'callStateChange'
|
||||||
| 'cancelIncomingGroupCallRing'
|
| 'cancelIncomingGroupCallRing'
|
||||||
|
| 'cancelPresenting'
|
||||||
| 'groupCallAudioLevelsChange'
|
| 'groupCallAudioLevelsChange'
|
||||||
| 'groupCallEnded'
|
| 'groupCallEnded'
|
||||||
| 'groupCallRaisedHandsChange'
|
| 'groupCallRaisedHandsChange'
|
||||||
|
@ -204,7 +201,6 @@ type CallingReduxInterface = Pick<
|
||||||
| 'refreshIODevices'
|
| 'refreshIODevices'
|
||||||
| 'remoteSharingScreenChange'
|
| 'remoteSharingScreenChange'
|
||||||
| 'remoteVideoChange'
|
| 'remoteVideoChange'
|
||||||
| 'setPresenting'
|
|
||||||
| 'startCallingLobby'
|
| 'startCallingLobby'
|
||||||
| 'startCallLinkLobby'
|
| 'startCallLinkLobby'
|
||||||
| 'startCallLinkLobbyByRoomId'
|
| 'startCallLinkLobbyByRoomId'
|
||||||
|
@ -213,9 +209,13 @@ type CallingReduxInterface = Pick<
|
||||||
areAnyCallsActiveOrRinging(): boolean;
|
areAnyCallsActiveOrRinging(): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isScreenSource(source: PresentedSource): boolean {
|
export type SetPresentingOptionsType = Readonly<{
|
||||||
return source.id.startsWith('screen');
|
conversationId: string;
|
||||||
}
|
hasLocalVideo: boolean;
|
||||||
|
mediaStream?: MediaStream;
|
||||||
|
source?: PresentedSource;
|
||||||
|
callLinkRootKey?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
function truncateForLogging(name: string | undefined): string | undefined {
|
function truncateForLogging(name: string | undefined): string | undefined {
|
||||||
if (!name || name.length <= 4) {
|
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({
|
function protoToCallingMessage({
|
||||||
offer,
|
offer,
|
||||||
answer,
|
answer,
|
||||||
|
@ -416,7 +393,7 @@ export class CallingClass {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('stop-screen-share', () => {
|
ipcRenderer.on('stop-screen-share', () => {
|
||||||
reduxInterface.setPresenting();
|
reduxInterface.cancelPresenting();
|
||||||
});
|
});
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
'calling:set-rtc-stats-interval',
|
'calling:set-rtc-stats-interval',
|
||||||
|
@ -2046,51 +2023,13 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPresentingSources(): Promise<Array<PresentableSource>> {
|
async setPresenting({
|
||||||
// There's a Linux Wayland Electron bug where requesting desktopCapturer.
|
conversationId,
|
||||||
// getSources() with types as ['screen', 'window'] (the default) pops 2
|
hasLocalVideo,
|
||||||
// OS permissions dialogs in an unusable state (Dialog 1 for Share Window
|
mediaStream,
|
||||||
// is the foreground and ignores input; Dialog 2 for Share Screen is background
|
source,
|
||||||
// and requires input. As a workaround, request both sources sequentially.
|
callLinkRootKey,
|
||||||
// https://github.com/signalapp/Signal-Desktop/issues/5350#issuecomment-1688614149
|
}: SetPresentingOptionsType): Promise<void> {
|
||||||
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> {
|
|
||||||
const call = getOwn(this.callsLookup, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
log.warn('Trying to set presenting for a non-existent call');
|
log.warn('Trying to set presenting for a non-existent call');
|
||||||
|
@ -2098,7 +2037,8 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.videoCapturer.disable();
|
this.videoCapturer.disable();
|
||||||
if (source) {
|
const isPresenting = mediaStream != null;
|
||||||
|
if (isPresenting) {
|
||||||
this.hadLocalVideoBeforePresenting = hasLocalVideo;
|
this.hadLocalVideoBeforePresenting = hasLocalVideo;
|
||||||
drop(
|
drop(
|
||||||
this.enableCaptureAndSend(call, {
|
this.enableCaptureAndSend(call, {
|
||||||
|
@ -2106,7 +2046,7 @@ export class CallingClass {
|
||||||
maxFramerate: 5,
|
maxFramerate: 5,
|
||||||
maxHeight: 1800,
|
maxHeight: 1800,
|
||||||
maxWidth: 2880,
|
maxWidth: 2880,
|
||||||
screenShareSourceId: source.id,
|
mediaStream,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.setOutgoingVideo(conversationId, true);
|
this.setOutgoingVideo(conversationId, true);
|
||||||
|
@ -2118,11 +2058,10 @@ export class CallingClass {
|
||||||
this.hadLocalVideoBeforePresenting = undefined;
|
this.hadLocalVideoBeforePresenting = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPresenting = Boolean(source);
|
|
||||||
this.setOutgoingVideoIsScreenShare(call, isPresenting);
|
this.setOutgoingVideoIsScreenShare(call, isPresenting);
|
||||||
|
|
||||||
if (source) {
|
if (isPresenting) {
|
||||||
ipcRenderer.send('show-screen-share', source.name);
|
ipcRenderer.send('show-screen-share', source?.name);
|
||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
let absolutePath: string | undefined;
|
let absolutePath: string | undefined;
|
||||||
|
|
|
@ -218,7 +218,7 @@ class NotificationService extends EventEmitter {
|
||||||
isVideoCall: true,
|
isVideoCall: true,
|
||||||
});
|
});
|
||||||
} else if (type === NotificationType.IsPresenting) {
|
} else if (type === NotificationType.IsPresenting) {
|
||||||
window.reduxActions?.calling?.setPresenting();
|
window.reduxActions?.calling?.cancelPresenting();
|
||||||
} else if (type === NotificationType.IncomingCall) {
|
} else if (type === NotificationType.IncomingCall) {
|
||||||
window.IPC.showWindow();
|
window.IPC.showWindow();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,6 +19,10 @@ import { getIntl, getPlatform } from '../selectors/user';
|
||||||
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
|
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { drop } from '../../util/drop';
|
import { drop } from '../../util/drop';
|
||||||
|
import {
|
||||||
|
DesktopCapturer,
|
||||||
|
type DesktopCapturerBaton,
|
||||||
|
} from '../../util/desktopCapturer';
|
||||||
import { calling } from '../../services/calling';
|
import { calling } from '../../services/calling';
|
||||||
import { truncateAudioLevel } from '../../calling/truncateAudioLevel';
|
import { truncateAudioLevel } from '../../calling/truncateAudioLevel';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
|
@ -179,7 +183,8 @@ export type ActiveCallStateType = {
|
||||||
outgoingRing: boolean;
|
outgoingRing: boolean;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
presentingSource?: PresentedSource;
|
presentingSource?: PresentedSource;
|
||||||
presentingSourcesAvailable?: Array<PresentableSource>;
|
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
|
||||||
|
capturerBaton?: DesktopCapturerBaton;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||||
showParticipantsList: 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 REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
const REMOVE_CLIENT = 'calling/REMOVE_CLIENT';
|
const REMOVE_CLIENT = 'calling/REMOVE_CLIENT';
|
||||||
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
|
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 SEND_GROUP_CALL_REACTION = 'calling/SEND_GROUP_CALL_REACTION';
|
||||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_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: 'calling/RETURN_TO_ACTIVE_CALL';
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type SelectPresentingSourceActionType = ReadonlyDeep<{
|
||||||
|
type: 'calling/SELECT_PRESENTING_SOURCE';
|
||||||
|
payload: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
type SetLocalAudioActionType = ReadonlyDeep<{
|
type SetLocalAudioActionType = ReadonlyDeep<{
|
||||||
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
payload: SetLocalAudioType;
|
payload: SetLocalAudioType;
|
||||||
|
@ -881,11 +892,13 @@ type SetPresentingFulfilledActionType = ReadonlyDeep<{
|
||||||
payload?: PresentedSource;
|
payload?: PresentedSource;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
type SetPresentingSourcesActionType = ReadonlyDeep<{
|
||||||
type SetPresentingSourcesActionType = {
|
|
||||||
type: 'calling/SET_PRESENTING_SOURCES';
|
type: 'calling/SET_PRESENTING_SOURCES';
|
||||||
payload: Array<PresentableSource>;
|
payload: {
|
||||||
};
|
presentableSources: ReadonlyArray<PresentableSource>;
|
||||||
|
capturerBaton: DesktopCapturerBaton;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
type SetOutgoingRingActionType = ReadonlyDeep<{
|
type SetOutgoingRingActionType = ReadonlyDeep<{
|
||||||
type: 'calling/SET_OUTGOING_RING';
|
type: 'calling/SET_OUTGOING_RING';
|
||||||
|
@ -962,6 +975,7 @@ export type CallingActionType =
|
||||||
| RemoveClientActionType
|
| RemoveClientActionType
|
||||||
| ReturnToActiveCallActionType
|
| ReturnToActiveCallActionType
|
||||||
| SendGroupCallReactionActionType
|
| SendGroupCallReactionActionType
|
||||||
|
| SelectPresentingSourceActionType
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
| SetPresentingSourcesActionType
|
| SetPresentingSourcesActionType
|
||||||
|
@ -1275,6 +1289,8 @@ function declineCall(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalCapturers = new WeakMap<DesktopCapturerBaton, DesktopCapturer>();
|
||||||
|
|
||||||
function getPresentingSources(): ThunkAction<
|
function getPresentingSources(): ThunkAction<
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
|
@ -1283,6 +1299,8 @@ function getPresentingSources(): ThunkAction<
|
||||||
| ToggleNeedsScreenRecordingPermissionsActionType
|
| ToggleNeedsScreenRecordingPermissionsActionType
|
||||||
> {
|
> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
|
const i18n = getIntl(getState());
|
||||||
|
|
||||||
// We check if the user has permissions first before calling desktopCapturer
|
// We check if the user has permissions first before calling desktopCapturer
|
||||||
// Next we call getPresentingSources so that one gets the prompt for permissions,
|
// Next we call getPresentingSources so that one gets the prompt for permissions,
|
||||||
// if necessary.
|
// if necessary.
|
||||||
|
@ -1294,19 +1312,51 @@ function getPresentingSources(): ThunkAction<
|
||||||
const needsPermission =
|
const needsPermission =
|
||||||
platform === 'darwin' && !hasScreenCapturePermission();
|
platform === 'darwin' && !hasScreenCapturePermission();
|
||||||
|
|
||||||
const sources = await calling.getPresentingSources();
|
const capturer = new DesktopCapturer({
|
||||||
|
i18n,
|
||||||
|
onPresentableSources(presentableSources) {
|
||||||
if (needsPermission) {
|
if (needsPermission) {
|
||||||
dispatch({
|
// Abort
|
||||||
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
|
capturer.selectSource(undefined);
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_PRESENTING_SOURCES,
|
type: SET_PRESENTING_SOURCES,
|
||||||
payload: 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1774,6 +1824,13 @@ function returnToActiveCall(): ReturnToActiveCallActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectPresentingSource(id: string): SelectPresentingSourceActionType {
|
||||||
|
return {
|
||||||
|
type: SELECT_PRESENTING_SOURCE,
|
||||||
|
payload: id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function setIsCallActive(
|
function setIsCallActive(
|
||||||
isCallActive: boolean
|
isCallActive: boolean
|
||||||
): ThunkAction<void, RootStateType, unknown, never> {
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
|
@ -1871,8 +1928,9 @@ function setGroupCallVideoRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPresenting(
|
function _setPresenting(
|
||||||
sourceToPresent?: PresentedSource
|
sourceToPresent?: PresentedSource,
|
||||||
|
mediaStream?: MediaStream
|
||||||
): ThunkAction<void, RootStateType, unknown, SetPresentingFulfilledActionType> {
|
): ThunkAction<void, RootStateType, unknown, SetPresentingFulfilledActionType> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -1898,24 +1956,34 @@ function setPresenting(
|
||||||
rootKey = callLink?.rootKey;
|
rootKey = callLink?.rootKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
await calling.setPresenting(
|
await calling.setPresenting({
|
||||||
activeCall.conversationId,
|
conversationId: activeCall.conversationId,
|
||||||
activeCallState.hasLocalVideo,
|
hasLocalVideo: activeCallState.hasLocalVideo,
|
||||||
sourceToPresent,
|
mediaStream,
|
||||||
rootKey
|
source: sourceToPresent,
|
||||||
);
|
callLinkRootKey: rootKey,
|
||||||
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_PRESENTING,
|
type: SET_PRESENTING,
|
||||||
payload: sourceToPresent,
|
payload: sourceToPresent,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sourceToPresent) {
|
if (mediaStream) {
|
||||||
await callingTones.someonePresenting();
|
await callingTones.someonePresenting();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelPresenting(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
SetPresentingFulfilledActionType
|
||||||
|
> {
|
||||||
|
return _setPresenting(undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
|
function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
|
||||||
return {
|
return {
|
||||||
type: SET_OUTGOING_RING,
|
type: SET_OUTGOING_RING,
|
||||||
|
@ -2557,6 +2625,7 @@ export const actions = {
|
||||||
callStateChange,
|
callStateChange,
|
||||||
cancelCall,
|
cancelCall,
|
||||||
cancelIncomingGroupCallRing,
|
cancelIncomingGroupCallRing,
|
||||||
|
cancelPresenting,
|
||||||
changeCallView,
|
changeCallView,
|
||||||
changeIODevice,
|
changeIODevice,
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
|
@ -2592,13 +2661,13 @@ export const actions = {
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
sendGroupCallRaiseHand,
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
|
selectPresentingSource,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setIsCallActive,
|
setIsCallActive,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setOutgoingRing,
|
setOutgoingRing,
|
||||||
setPresenting,
|
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
startCall,
|
startCall,
|
||||||
startCallLinkLobby,
|
startCallLinkLobby,
|
||||||
|
@ -2612,6 +2681,9 @@ export const actions = {
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
updateCallLinkName,
|
updateCallLinkName,
|
||||||
updateCallLinkRestrictions,
|
updateCallLinkRestrictions,
|
||||||
|
|
||||||
|
// Exported only for tests
|
||||||
|
_setPresenting,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCallingActions = (): BoundActionCreatorsMapObject<
|
export const useCallingActions = (): BoundActionCreatorsMapObject<
|
||||||
|
@ -3677,12 +3749,20 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel source selection if running
|
||||||
|
const { capturerBaton } = activeCallState;
|
||||||
|
if (capturerBaton != null) {
|
||||||
|
const capturer = globalCapturers.get(capturerBaton);
|
||||||
|
capturer?.selectSource(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
...activeCallState,
|
...activeCallState,
|
||||||
presentingSource: action.payload,
|
presentingSource: action.payload,
|
||||||
presentingSourcesAvailable: undefined,
|
presentingSourcesAvailable: undefined,
|
||||||
|
capturerBaton: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3698,7 +3778,43 @@ export function reducer(
|
||||||
...state,
|
...state,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
...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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -441,15 +441,16 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
||||||
openSystemPreferencesAction,
|
openSystemPreferencesAction,
|
||||||
removeClient,
|
removeClient,
|
||||||
blockClient,
|
blockClient,
|
||||||
|
cancelPresenting,
|
||||||
sendGroupCallRaiseHand,
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
|
selectPresentingSource,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setIsCallActive,
|
setIsCallActive,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setOutgoingRing,
|
setOutgoingRing,
|
||||||
setPresenting,
|
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
switchToPresentationView,
|
switchToPresentationView,
|
||||||
switchFromPresentationView,
|
switchFromPresentationView,
|
||||||
|
@ -477,6 +478,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
||||||
bounceAppIconStop={bounceAppIconStop}
|
bounceAppIconStop={bounceAppIconStop}
|
||||||
callLink={callLink}
|
callLink={callLink}
|
||||||
cancelCall={cancelCall}
|
cancelCall={cancelCall}
|
||||||
|
cancelPresenting={cancelPresenting}
|
||||||
changeCallView={changeCallView}
|
changeCallView={changeCallView}
|
||||||
closeNeedPermissionScreen={closeNeedPermissionScreen}
|
closeNeedPermissionScreen={closeNeedPermissionScreen}
|
||||||
declineCall={declineCall}
|
declineCall={declineCall}
|
||||||
|
@ -503,13 +505,13 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
||||||
renderReactionPicker={renderReactionPicker}
|
renderReactionPicker={renderReactionPicker}
|
||||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||||
sendGroupCallReaction={sendGroupCallReaction}
|
sendGroupCallReaction={sendGroupCallReaction}
|
||||||
|
selectPresentingSource={selectPresentingSource}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||||
setIsCallActive={setIsCallActive}
|
setIsCallActive={setIsCallActive}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalPreview={setLocalPreview}
|
setLocalPreview={setLocalPreview}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
setOutgoingRing={setOutgoingRing}
|
setOutgoingRing={setOutgoingRing}
|
||||||
setPresenting={setPresenting}
|
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
showContactModal={showContactModal}
|
showContactModal={showContactModal}
|
||||||
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
|
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
|
||||||
|
|
|
@ -240,46 +240,6 @@ describe('calling duck', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('actions', () => {
|
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', () => {
|
describe('remoteSharingScreenChange', () => {
|
||||||
it("updates whether someone's screen is being shared", () => {
|
it("updates whether someone's screen is being shared", () => {
|
||||||
const { remoteSharingScreenChange } = actions;
|
const { remoteSharingScreenChange } = actions;
|
||||||
|
@ -308,7 +268,7 @@ describe('calling duck', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setPresenting', () => {
|
describe('_setPresenting', () => {
|
||||||
beforeEach(function (this: Mocha.Context) {
|
beforeEach(function (this: Mocha.Context) {
|
||||||
this.callingServiceSetPresenting = this.sandbox.stub(
|
this.callingServiceSetPresenting = this.sandbox.stub(
|
||||||
callingService,
|
callingService,
|
||||||
|
@ -316,8 +276,8 @@ describe('calling duck', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls setPresenting on the calling service', async function (this: Mocha.Context) {
|
it('calls _setPresenting on the calling service', async function (this: Mocha.Context) {
|
||||||
const { setPresenting } = actions;
|
const { _setPresenting } = actions;
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const presentedSource = {
|
const presentedSource = {
|
||||||
id: 'window:786',
|
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.calledOnce(this.callingServiceSetPresenting);
|
||||||
sinon.assert.calledWith(
|
sinon.assert.calledWith(this.callingServiceSetPresenting, {
|
||||||
this.callingServiceSetPresenting,
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
'fake-group-call-conversation-id',
|
hasLocalVideo: false,
|
||||||
false,
|
mediaStream: undefined,
|
||||||
presentedSource
|
source: presentedSource,
|
||||||
);
|
callLinkRootKey: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches SET_PRESENTING', async () => {
|
it('dispatches SET_PRESENTING', async () => {
|
||||||
const { setPresenting } = actions;
|
const { _setPresenting } = actions;
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const presentedSource = {
|
const presentedSource = {
|
||||||
id: 'window:786',
|
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.calledOnce(dispatch);
|
||||||
sinon.assert.calledWith(dispatch, {
|
sinon.assert.calledWith(dispatch, {
|
||||||
|
@ -366,7 +327,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it('turns off presenting when no value is passed in', async () => {
|
it('turns off presenting when no value is passed in', async () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const { setPresenting } = actions;
|
const { _setPresenting } = actions;
|
||||||
const presentedSource = {
|
const presentedSource = {
|
||||||
id: 'window:786',
|
id: 'window:786',
|
||||||
name: 'Application',
|
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];
|
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 () => {
|
it('sets the presenting value when one is passed in', async () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const { setPresenting } = actions;
|
const { _setPresenting } = actions;
|
||||||
|
|
||||||
const getState = (): RootStateType => ({
|
const getState = (): RootStateType => ({
|
||||||
...getEmptyRootState(),
|
...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];
|
const action = dispatch.getCall(0).args[0];
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ describe('renderWindowsToast', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected =
|
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);
|
assert.strictEqual(xml, expected);
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,13 +21,15 @@ describe('updateDefaultSession', () => {
|
||||||
it('sets the spellcheck URL', () => {
|
it('sets the spellcheck URL', () => {
|
||||||
const sesh = session.fromPartition(uuid());
|
const sesh = session.fromPartition(uuid());
|
||||||
const stub = sandbox.stub(sesh, 'setSpellCheckerDictionaryDownloadURL');
|
const stub = sandbox.stub(sesh, 'setSpellCheckerDictionaryDownloadURL');
|
||||||
|
const getLogger = sandbox.stub();
|
||||||
|
|
||||||
updateDefaultSession(sesh);
|
updateDefaultSession(sesh, getLogger);
|
||||||
|
|
||||||
sinon.assert.calledOnce(stub);
|
sinon.assert.calledOnce(stub);
|
||||||
sinon.assert.calledWith(
|
sinon.assert.calledWith(
|
||||||
stub,
|
stub,
|
||||||
`https://updates.signal.org/desktop/hunspell_dictionaries/${process.versions.electron}/`
|
`https://updates.signal.org/desktop/hunspell_dictionaries/${process.versions.electron}/`
|
||||||
);
|
);
|
||||||
|
sinon.assert.notCalled(getLogger);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -205,13 +205,13 @@ describe('signalRoutes', () => {
|
||||||
check('sgnl://show-window', result);
|
check('sgnl://show-window', result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setIsPresenting', () => {
|
it('cancelPresenting', () => {
|
||||||
const result: ParsedSignalRoute = {
|
const result: ParsedSignalRoute = {
|
||||||
key: 'setIsPresenting',
|
key: 'cancelPresenting',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
};
|
||||||
const check = createCheck({ isRoute: true, hasWebUrl: false });
|
const check = createCheck({ isRoute: true, hasWebUrl: false });
|
||||||
check('sgnl://set-is-presenting/', result);
|
check('sgnl://cancel-presenting/', result);
|
||||||
check('sgnl://set-is-presenting', result);
|
check('sgnl://cancel-presenting', result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
|
import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { AciString, ServiceIdString } from './ServiceId';
|
import type { AciString, ServiceIdString } from './ServiceId';
|
||||||
|
@ -19,13 +20,13 @@ export enum CallViewMode {
|
||||||
Presentation = 'Presentation',
|
Presentation = 'Presentation',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PresentableSource = {
|
export type PresentableSource = ReadonlyDeep<{
|
||||||
appIcon?: string;
|
appIcon?: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
isScreen: boolean;
|
isScreen: boolean;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type PresentedSource = {
|
export type PresentedSource = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -54,7 +55,7 @@ export type ActiveCallBaseType = {
|
||||||
outgoingRing: boolean;
|
outgoingRing: boolean;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
presentingSource?: PresentedSource;
|
presentingSource?: PresentedSource;
|
||||||
presentingSourcesAvailable?: Array<PresentableSource>;
|
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
|
|
229
ts/util/desktopCapturer.ts
Normal file
229
ts/util/desktopCapturer.ts
Normal 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');
|
||||||
|
}
|
|
@ -49,7 +49,7 @@ type AllHostnamePatterns =
|
||||||
| 'show-conversation'
|
| 'show-conversation'
|
||||||
| 'start-call-lobby'
|
| 'start-call-lobby'
|
||||||
| 'show-window'
|
| 'show-window'
|
||||||
| 'set-is-presenting'
|
| 'cancel-presenting'
|
||||||
| ':captchaId(.+)'
|
| ':captchaId(.+)'
|
||||||
| '';
|
| '';
|
||||||
|
|
||||||
|
@ -535,18 +535,18 @@ export const showWindowRoute = _route('showWindow', {
|
||||||
* Set is presenting
|
* Set is presenting
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* setIsPresentingRoute.toAppUrl({})
|
* cancelPresentingRoute.toAppUrl({})
|
||||||
* // URL { "sgnl://set-is-presenting" }
|
* // URL { "sgnl://cancel-presenting" }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const setIsPresentingRoute = _route('setIsPresenting', {
|
export const cancelPresentingRoute = _route('cancelPresenting', {
|
||||||
patterns: [_pattern('sgnl:', 'set-is-presenting', '{/}?', {})],
|
patterns: [_pattern('sgnl:', 'cancel-presenting', '{/}?', {})],
|
||||||
schema: z.object({}),
|
schema: z.object({}),
|
||||||
parse() {
|
parse() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
toAppUrl() {
|
toAppUrl() {
|
||||||
return new URL('sgnl://set-is-presenting');
|
return new URL('sgnl://cancel-presenting');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -565,7 +565,7 @@ const _allSignalRoutes = [
|
||||||
showConversationRoute,
|
showConversationRoute,
|
||||||
startCallLobbyRoute,
|
startCallLobbyRoute,
|
||||||
showWindowRoute,
|
showWindowRoute,
|
||||||
setIsPresentingRoute,
|
cancelPresentingRoute,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -121,7 +121,7 @@ type PermissionsWindowPropsType = {
|
||||||
|
|
||||||
type ScreenShareWindowPropsType = {
|
type ScreenShareWindowPropsType = {
|
||||||
onStopSharing: () => void;
|
onStopSharing: () => void;
|
||||||
presentedSourceName: string;
|
presentedSourceName: string | undefined;
|
||||||
getStatus: () => ScreenShareStatus;
|
getStatus: () => ScreenShareStatus;
|
||||||
setRenderCallback: (cb: () => void) => void;
|
setRenderCallback: (cb: () => void) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -360,8 +360,8 @@ ipc.on('show-window', () => {
|
||||||
window.IPC.showWindow();
|
window.IPC.showWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('set-is-presenting', () => {
|
ipc.on('cancel-presenting', () => {
|
||||||
window.reduxActions?.calling?.setPresenting();
|
window.reduxActions?.calling?.cancelPresenting();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on(
|
ipc.on(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue