Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 902263d

Browse files
author
Enrico Schwendig
authored
force to allow calls without video and audio in embedded mode (#11131)
* force to allow calls without video and audio in embedded mode * Check device access permission and introduce a only screen share call mode * Fix strict typ check issue * Fix i18n check issue * Add unit tests for device selection * Fix mocked media device query
1 parent 3c81f30 commit 902263d

File tree

5 files changed

+71
-4
lines changed

5 files changed

+71
-4
lines changed

src/components/views/voip/CallView.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
3232
import AppTile from "../elements/AppTile";
3333
import { _t } from "../../../languageHandler";
3434
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
35-
import MediaDeviceHandler from "../../../MediaDeviceHandler";
35+
import MediaDeviceHandler, { IMediaDevices } from "../../../MediaDeviceHandler";
3636
import { CallStore } from "../../../stores/CallStore";
3737
import IconizedContextMenu, {
3838
IconizedContextMenuOption,
@@ -149,14 +149,32 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, con
149149
setVideoMuted(!videoMuted);
150150
}, [videoMuted, setVideoMuted]);
151151

152+
// In case we can not fetch media devices we should mute the devices
153+
const handleMediaDeviceFailing = (message: string): void => {
154+
MediaDeviceHandler.startWithAudioMuted = true;
155+
MediaDeviceHandler.startWithVideoMuted = true;
156+
logger.warn(message);
157+
};
158+
152159
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(
153160
async (): Promise<[MediaStream | null, MediaDeviceInfo[], MediaDeviceInfo[]]> => {
154-
let devices = await MediaDeviceHandler.getDevices();
161+
let devices: IMediaDevices | undefined;
162+
try {
163+
devices = await MediaDeviceHandler.getDevices();
164+
if (devices === undefined) {
165+
handleMediaDeviceFailing("Could not access devices!");
166+
return [null, [], []];
167+
}
168+
} catch (error) {
169+
handleMediaDeviceFailing(`Unable to get Media Devices: ${error}`);
170+
return [null, [], []];
171+
}
155172

156173
// We get the preview stream before requesting devices: this is because
157174
// we need (in some browsers) an active media stream in order to get
158175
// non-blank labels for the devices.
159176
let stream: MediaStream | null = null;
177+
160178
try {
161179
if (devices!.audioinput.length > 0) {
162180
// Holding just an audio stream will be enough to get us all device labels, so
@@ -170,7 +188,8 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, con
170188
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoInputId } });
171189
}
172190
} catch (e) {
173-
logger.error(`Failed to get stream for device ${videoInputId}`, e);
191+
logger.warn(`Failed to get stream for device ${videoInputId}`, e);
192+
handleMediaDeviceFailing(`Have access to Device list but unable to read from Media Devices`);
174193
}
175194

176195
// Refresh the devices now that we hold a stream

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,14 +986,15 @@
986986
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
987987
"Element Call video rooms": "Element Call video rooms",
988988
"New group call experience": "New group call experience",
989+
"Under active development.": "Under active development.",
990+
"Allow screen share only mode": "Allow screen share only mode",
989991
"Live Location Sharing": "Live Location Sharing",
990992
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
991993
"Dynamic room predecessors": "Dynamic room predecessors",
992994
"Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)",
993995
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
994996
"Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)",
995997
"Rust cryptography implementation": "Rust cryptography implementation",
996-
"Under active development.": "Under active development.",
997998
"Font size": "Font size",
998999
"Use custom size": "Use custom size",
9991000
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",

src/models/Call.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ export class ElementCall extends Call {
660660
});
661661

662662
if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "");
663+
if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) params.append("allowVoipWithNoMedia", "");
663664

664665
// Set custom fonts
665666
if (SettingsStore.getValue("useSystemFont")) {

src/settings/Settings.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,15 @@ export const SETTINGS: { [setting: string]: ISetting } = {
433433
controller: new ReloadOnChangeController(),
434434
default: false,
435435
},
436+
"feature_allow_screen_share_only_mode": {
437+
isFeature: true,
438+
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
439+
description: _td("Under active development."),
440+
labsGroup: LabGroup.VoiceAndVideo,
441+
displayName: _td("Allow screen share only mode"),
442+
controller: new ReloadOnChangeController(),
443+
default: false,
444+
},
436445
"feature_location_share_live": {
437446
isFeature: true,
438447
labsGroup: LabGroup.Messaging,

test/components/views/voip/CallView-test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessa
3939
import { CallStore } from "../../../../src/stores/CallStore";
4040
import { Call, ConnectionState } from "../../../../src/models/Call";
4141
import SdkConfig from "../../../../src/SdkConfig";
42+
import MediaDeviceHandler from "../../../../src/MediaDeviceHandler";
4243

4344
const CallView = wrapInMatrixClientContext(_CallView);
4445

@@ -247,6 +248,26 @@ describe("CallLobby", () => {
247248
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
248249
});
249250

251+
it("hide when no access to device list", async () => {
252+
mocked(navigator.mediaDevices.enumerateDevices).mockRejectedValue("permission denied");
253+
await renderView();
254+
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
255+
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
256+
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
257+
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
258+
});
259+
260+
it("hide when unknown error with device list", async () => {
261+
const originalGetDevices = MediaDeviceHandler.getDevices;
262+
MediaDeviceHandler.getDevices = () => Promise.reject("unknown error");
263+
await renderView();
264+
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
265+
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
266+
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
267+
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
268+
MediaDeviceHandler.getDevices = originalGetDevices;
269+
});
270+
250271
it("show without dropdown when only one device is available", async () => {
251272
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1]);
252273

@@ -286,5 +307,21 @@ describe("CallLobby", () => {
286307

287308
expect(client.getMediaHandler().setAudioInput).toHaveBeenCalledWith(fakeAudioInput2.deviceId);
288309
});
310+
311+
it("set media muted if no access to audio device", async () => {
312+
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeAudioInput1, fakeAudioInput2]);
313+
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
314+
await renderView();
315+
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
316+
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
317+
});
318+
319+
it("set media muted if no access to video device", async () => {
320+
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1, fakeVideoInput2]);
321+
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
322+
await renderView();
323+
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
324+
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
325+
});
289326
});
290327
});

0 commit comments

Comments
 (0)