diff --git a/src/media-utils.ts b/src/media-utils.ts index 0c01b5d..d00fc72 100644 --- a/src/media-utils.ts +++ b/src/media-utils.ts @@ -14,10 +14,55 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { logger } from "matrix-js-sdk/src/logger"; + +/** + * Finds a media device with label matching 'deviceName' + * @param deviceName The label of the device to look for + * @param devices The list of devices to search + * @returns A matching media device or undefined if no matching device was found + */ export async function findDeviceByName( - deviceName: string + deviceName: string, + devices: MediaDeviceInfo[] ): Promise { - const devices = await navigator.mediaDevices.enumerateDevices(); const deviceInfo = devices.find((d) => d.label === deviceName); return deviceInfo?.deviceId; } + +/** + * Gets the available audio input/output and video input devices + * from the browser: a wrapper around mediaDevices.enumerateDevices() + * that requests a stream and holds it while calling enumerateDevices(). + * This is because some browsers (Firefox) only return device labels when + * the app has an active user media stream. In Chrome, this will get a + * stream from the default camera which can mean, for example, that the + * light for the FaceTime camera turns on briefly even if you selected + * another camera. Once the Permissions API + * (https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + * is ready for primetime, this should allow us to avoid this. + * + * @return The available media devices + */ +export async function getDevices(): Promise { + let stream; + try { + stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: true, + }); + } catch (e) { + logger.info("Couldn't get media stream for enumerateDevices: failing"); + throw e; + } + + try { + return await navigator.mediaDevices.enumerateDevices(); + } catch (error) { + logger.warn("Unable to refresh WebRTC Devices: ", error); + } finally { + for (const track of stream.getTracks()) { + track.stop(); + } + } +} diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 0598e4d..be436c5 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -32,7 +32,7 @@ import { useRoomAvatar } from "./useRoomAvatar"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useLocationNavigation } from "../useLocationNavigation"; import { useMediaHandler } from "../settings/useMediaHandler"; -import { findDeviceByName } from "../media-utils"; +import { findDeviceByName, getDevices } from "../media-utils"; declare global { interface Window { @@ -96,11 +96,16 @@ export function GroupCallView({ if (widget && preload) { // In preload mode, wait for a join action before entering const onJoin = async (ev: CustomEvent) => { + // Get the available devices so we can match the selected device + // to its ID. This involves getting a media stream (see docs on + // the function) so we only do it once and re-use the result. + const devices = await getDevices(); + const { audioInput, videoInput } = ev.detail .data as unknown as JoinCallData; if (audioInput !== null) { - const deviceId = await findDeviceByName(audioInput); + const deviceId = await findDeviceByName(audioInput, devices); if (!deviceId) { logger.warn("Unknown audio input: " + audioInput); } else { @@ -112,7 +117,7 @@ export function GroupCallView({ } if (videoInput !== null) { - const deviceId = await findDeviceByName(videoInput); + const deviceId = await findDeviceByName(videoInput, devices); if (!deviceId) { logger.warn("Unknown video input: " + videoInput); } else {