Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});