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

force to allow calls without video and audio in embedded mode #11131

25 changes: 22 additions & 3 deletions src/components/views/voip/CallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
import { _t } from "../../../languageHandler";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import MediaDeviceHandler from "../../../MediaDeviceHandler";
import MediaDeviceHandler, { IMediaDevices } from "../../../MediaDeviceHandler";
import { CallStore } from "../../../stores/CallStore";
import IconizedContextMenu, {
IconizedContextMenuOption,
Expand Down Expand Up @@ -149,14 +149,32 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, con
setVideoMuted(!videoMuted);
}, [videoMuted, setVideoMuted]);

// In case we can not fetch media devices we should mute the devices
const handleMediaDeviceFailing = (message: string): void => {
MediaDeviceHandler.startWithAudioMuted = true;
MediaDeviceHandler.startWithVideoMuted = true;
logger.warn(message);
};

const [videoStream, audioInputs, videoInputs] = useAsyncMemo(
async (): Promise<[MediaStream | null, MediaDeviceInfo[], MediaDeviceInfo[]]> => {
let devices = await MediaDeviceHandler.getDevices();
let devices: IMediaDevices | undefined;
try {
devices = await MediaDeviceHandler.getDevices();
if (devices === undefined) {
handleMediaDeviceFailing("Could not access devices!");
return [null, [], []];
}
} catch (error) {
handleMediaDeviceFailing(`Unable to get Media Devices: ${error}`);
return [null, [], []];
}

// We get the preview stream before requesting devices: this is because
// we need (in some browsers) an active media stream in order to get
// non-blank labels for the devices.
let stream: MediaStream | null = null;

try {
if (devices!.audioinput.length > 0) {
// Holding just an audio stream will be enough to get us all device labels, so
Expand All @@ -170,7 +188,8 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, con
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoInputId } });
}
} catch (e) {
logger.error(`Failed to get stream for device ${videoInputId}`, e);
logger.warn(`Failed to get stream for device ${videoInputId}`, e);
handleMediaDeviceFailing(`Have access to Device list but unable to read from Media Devices`);
}

// Refresh the devices now that we hold a stream
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -980,14 +980,15 @@
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
"Element Call video rooms": "Element Call video rooms",
"New group call experience": "New group call experience",
"Under active development.": "Under active development.",
"Allow screen share only mode": "Allow screen share only mode",
"Live Location Sharing": "Live Location Sharing",
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
"Dynamic room predecessors": "Dynamic room predecessors",
"Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)",
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
"Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)",
"Rust cryptography implementation": "Rust cryptography implementation",
"Under active development.": "Under active development.",
"Font size": "Font size",
"Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
Expand Down
1 change: 1 addition & 0 deletions src/models/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ export class ElementCall extends Call {
});

if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "");
if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) params.append("allowVoipWithNoMedia", "");

// Set custom fonts
if (SettingsStore.getValue("useSystemFont")) {
Expand Down
9 changes: 9 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ export const SETTINGS: { [setting: string]: ISetting } = {
controller: new ReloadOnChangeController(),
default: false,
},
"feature_allow_screen_share_only_mode": {
isFeature: true,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
description: _td("Under active development."),
labsGroup: LabGroup.VoiceAndVideo,
displayName: _td("Allow screen share only mode"),
controller: new ReloadOnChangeController(),
default: false,
},
"feature_location_share_live": {
isFeature: true,
labsGroup: LabGroup.Messaging,
Expand Down
37 changes: 37 additions & 0 deletions test/components/views/voip/CallView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessa
import { CallStore } from "../../../../src/stores/CallStore";
import { Call, ConnectionState } from "../../../../src/models/Call";
import SdkConfig from "../../../../src/SdkConfig";
import MediaDeviceHandler from "../../../../src/MediaDeviceHandler";

const CallView = wrapInMatrixClientContext(_CallView);

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

it("hide when no access to device list", async () => {
mocked(navigator.mediaDevices.enumerateDevices).mockRejectedValue("permission denied");
await renderView();
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
});

it("hide when unknown error with device list", async () => {
const originalGetDevices = MediaDeviceHandler.getDevices;
MediaDeviceHandler.getDevices = () => Promise.reject("unknown error");
await renderView();
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
MediaDeviceHandler.getDevices = originalGetDevices;
});

it("show without dropdown when only one device is available", async () => {
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1]);

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

expect(client.getMediaHandler().setAudioInput).toHaveBeenCalledWith(fakeAudioInput2.deviceId);
});

it("set media muted if no access to audio device", async () => {
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeAudioInput1, fakeAudioInput2]);
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
await renderView();
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
});

it("set media muted if no access to video device", async () => {
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1, fakeVideoInput2]);
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
await renderView();
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
});
});
});