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

Commit

Permalink
Extract requestMediaPermissions (#9568)
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 authored Nov 11, 2022
1 parent f2f00f6 commit fca6ff2
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 53 deletions.
38 changes: 3 additions & 35 deletions src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,16 @@ limitations under the License.
*/

import React from 'react';
import { logger } from "matrix-js-sdk/src/logger";

import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from '../../../elements/SettingsFlag';
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import ErrorDialog from '../../../dialogs/ErrorDialog';
import { requestMediaPermissions } from '../../../../../utils/media/requestMediaPermissions';

const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
// Note we're looking for a device with deviceId 'default' but adding a device
Expand Down Expand Up @@ -90,37 +87,8 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
};

private requestMediaPermissions = async (): Promise<void> => {
let constraints;
let stream;
let error;
try {
constraints = { video: true, audio: true };
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
logger.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
Modal.createDialog(ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
const stream = await requestMediaPermissions();
if (stream) {
this.refreshMediaDevices(stream);
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,8 @@
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Unknown App": "Unknown App",
"No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
"This homeserver is not configured to display maps.": "This homeserver is not configured to display maps.",
"This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.",
"Toggle attribution": "Toggle attribution",
Expand Down Expand Up @@ -1618,8 +1620,6 @@
"Rooms outside of a space": "Rooms outside of a space",
"Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.",
"Default Device": "Default Device",
"No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
"Request media permissions": "Request media permissions",
"Audio Output": "Audio Output",
Expand Down
59 changes: 59 additions & 0 deletions src/utils/media/requestMediaPermissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { logger } from "matrix-js-sdk/src/logger";

import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
import { _t } from "../../languageHandler";
import Modal from "../../Modal";
import SdkConfig from "../../SdkConfig";

export const requestMediaPermissions = async (video = true): Promise<MediaStream | undefined> => {
let stream: MediaStream | undefined;
let error: any;

try {
stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video,
});
} catch (err: any) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (video && err.name === "NotFoundError") {
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
logger.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
Modal.createDialog(ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
}

return stream;
};
1 change: 0 additions & 1 deletion test/components/views/messages/CallEvent-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const CallEvent = wrapInMatrixClientContext(UnwrappedCallEvent);

describe("CallEvent", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});

let client: Mocked<MatrixClient>;
Expand Down
3 changes: 0 additions & 3 deletions test/components/views/rooms/RoomTile-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ describe("RoomTile", () => {
jest.spyOn(PlatformPeg, "get")
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", {
value: { enumerateDevices: async () => [] },
});

let client: Mocked<MatrixClient>;

Expand Down
6 changes: 0 additions & 6 deletions test/components/views/voip/CallView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ const CallView = wrapInMatrixClientContext(_CallView);

describe("CallLobby", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", {
value: {
enumerateDevices: jest.fn(),
getUserMedia: () => null,
},
});
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});

let client: Mocked<MatrixClient>;
Expand Down
1 change: 0 additions & 1 deletion test/components/views/voip/PipView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/t

describe("PipView", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});

let sdkContext: TestSdkContext;
Expand Down
8 changes: 8 additions & 0 deletions test/setup/setupManualMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,11 @@ fetchMock.catch("");
fetchMock.get("/image-file-stub", "image file stub");
// @ts-ignore
window.fetch = fetchMock.sandbox();

// set up mediaDevices mock
Object.defineProperty(navigator, "mediaDevices", {
value: {
enumerateDevices: jest.fn().mockResolvedValue([]),
getUserMedia: jest.fn(),
},
});
4 changes: 0 additions & 4 deletions test/stores/room-list/algorithms/Algorithm-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ describe("Algorithm", () => {
stop: () => {},
} as unknown as ClientWidgetApi);

Object.defineProperty(navigator, "mediaDevices", {
value: { enumerateDevices: async () => [] },
});

// End of setup

expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
Expand Down
1 change: 0 additions & 1 deletion test/toasts/IncomingCallToast-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import { getIncomingCallToastKey, IncomingCallToast } from "../../src/toasts/Inc

describe("IncomingCallEvent", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => { });

let client: Mocked<MatrixClient>;
Expand Down
124 changes: 124 additions & 0 deletions test/utils/media/requestMediaPermissions-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { mocked } from "jest-mock";
import { logger } from "matrix-js-sdk/src/logger";
import { screen } from "@testing-library/react";

import { requestMediaPermissions } from "../../../src/utils/media/requestMediaPermissions";
import { flushPromises } from "../../test-utils";

describe("requestMediaPermissions", () => {
let error: Error;
const audioVideoStream = {} as MediaStream;
const audioStream = {} as MediaStream;

const itShouldLogTheErrorAndShowTheNoMediaPermissionsModal = () => {
it("should log the error and show the »No media permissions« modal", () => {
expect(logger.log).toHaveBeenCalledWith(
"Failed to list userMedia devices",
error,
);
screen.getByText("No media permissions");
});
};

beforeEach(() => {
error = new Error();
jest.spyOn(logger, "log");
});

describe("when an audio and video device is available", () => {
beforeEach(() => {
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
async ({ audio, video }): Promise<MediaStream> => {
if (audio && video) return audioVideoStream;
return audioStream;
},
);
});

it("should return the audio/video stream", async () => {
expect(await requestMediaPermissions()).toBe(audioVideoStream);
});
});

describe("when calling with video = false and an audio device is available", () => {
beforeEach(() => {
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
async ({ audio, video }): Promise<MediaStream> => {
if (audio && !video) return audioStream;
return audioVideoStream;
},
);
});

it("should return the audio stream", async () => {
expect(await requestMediaPermissions(false)).toBe(audioStream);
});
});

describe("when only an audio stream is available", () => {
beforeEach(() => {
error.name = "NotFoundError";
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
async ({ audio, video }): Promise<MediaStream> => {
if (audio && video) throw error;
if (audio) return audioStream;
return audioVideoStream;
},
);
});

it("should return the audio stream", async () => {
expect(await requestMediaPermissions()).toBe(audioStream);
});
});

describe("when no device is available", () => {
beforeEach(async () => {
error.name = "NotFoundError";
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
async (): Promise<MediaStream> => {
throw error;
},
);
await requestMediaPermissions();
// required for the modal to settle
await flushPromises();
await flushPromises();
});

itShouldLogTheErrorAndShowTheNoMediaPermissionsModal();
});

describe("when an Error is raised", () => {
beforeEach(async () => {
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
async ({ audio, video }): Promise<MediaStream> => {
if (audio && video) throw error;
return audioVideoStream;
},
);
await requestMediaPermissions();
// required for the modal to settle
await flushPromises();
await flushPromises();
});

itShouldLogTheErrorAndShowTheNoMediaPermissionsModal();
});
});

0 comments on commit fca6ff2

Please sign in to comment.