2022-05-04 17:09:48 +01:00
|
|
|
/*
|
2023-01-03 16:55:26 +00:00
|
|
|
Copyright 2022 New Vector Ltd
|
2022-05-04 17:09:48 +01:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2022-01-05 15:35:12 -08:00
|
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
|
|
import { useHistory } from "react-router-dom";
|
2022-08-02 00:46:16 +02:00
|
|
|
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
2022-08-12 16:46:53 -04:00
|
|
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
2022-09-27 16:19:48 +01:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-10-10 09:19:10 -04:00
|
|
|
import { useTranslation } from "react-i18next";
|
2022-08-02 00:46:16 +02:00
|
|
|
|
2022-09-09 02:10:45 -04:00
|
|
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
|
|
|
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
2022-04-07 14:22:36 -07:00
|
|
|
import { useGroupCall } from "./useGroupCall";
|
2022-01-05 15:35:12 -08:00
|
|
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
|
|
|
import { LobbyView } from "./LobbyView";
|
|
|
|
import { InCallView } from "./InCallView";
|
2022-04-22 18:05:48 -07:00
|
|
|
import { PTTCallView } from "./PTTCallView";
|
2022-01-05 15:35:12 -08:00
|
|
|
import { CallEndedView } from "./CallEndedView";
|
2022-05-18 19:00:59 -04:00
|
|
|
import { useRoomAvatar } from "./useRoomAvatar";
|
2022-01-05 15:35:12 -08:00
|
|
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
2022-02-03 16:56:13 -08:00
|
|
|
import { useLocationNavigation } from "../useLocationNavigation";
|
2023-03-01 13:47:36 +01:00
|
|
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
2022-09-09 02:10:45 -04:00
|
|
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
Make Element Call work in Firefox's resist fingerprinting mode
This one is gonna take some explaining:
When in resist fingerprinting mode, Firefox exhibits some funny behavior: when we ask for the the list of media devices, it gives us fake device IDs. But when the js-sdk requests a stream for any of those devices, Firefox associates the stream with the real device ID.
Now, in order to get the names of devices included in their metadata when you query the device list, you need to be holding a stream. For this reason, useMediaHandler was set up to reload the device list whenever matrix-js-sdk got a new local stream. But because of the inconsistency in device IDs, it would enter an infinite cycle telling matrix-js-sdk to request a stream for the fake device ID, but with matrix-js-sdk always responding with the real device ID.
I already wasn't happy with useMediaHandler's use of @ts-ignore comments to inspect private js-sdk fields, and in the meantime we've come up with a simpler function for requesting device names, so I decided to refactor useMediaHandler to use it instead. Importantly, it doesn't break in resist fingerprinting mode.
This created a new UX issue though: now, when on the lobby screen, useMediaHandler would request microphone access so it could get device names, followed immediately by a *second* pop-up for the lobby screen to request camera access. That's 1 pop-up too many, so I changed useMediaHandler to only request device names when a component is mounted that actually wants to show them. Currently, the settings modal is the only such component, and users normally only open it *after* granting full audio/video access, so this solution works out quite nicely.
2023-05-15 22:03:26 -04:00
|
|
|
import { findDeviceByName, getNamedDevices } from "../media-utils";
|
2022-09-09 02:10:45 -04:00
|
|
|
|
2022-08-02 00:46:16 +02:00
|
|
|
declare global {
|
|
|
|
interface Window {
|
2022-09-09 02:04:53 -04:00
|
|
|
groupCall?: GroupCall;
|
2022-08-02 00:46:16 +02:00
|
|
|
}
|
|
|
|
}
|
2022-09-09 02:04:53 -04:00
|
|
|
|
2022-08-02 00:46:16 +02:00
|
|
|
interface Props {
|
|
|
|
client: MatrixClient;
|
|
|
|
isPasswordlessUser: boolean;
|
|
|
|
isEmbedded: boolean;
|
2022-09-09 02:10:45 -04:00
|
|
|
preload: boolean;
|
2022-09-09 02:04:53 -04:00
|
|
|
hideHeader: boolean;
|
2022-08-05 16:16:59 -04:00
|
|
|
roomIdOrAlias: string;
|
2022-08-02 00:46:16 +02:00
|
|
|
groupCall: GroupCall;
|
|
|
|
}
|
2022-09-09 02:04:53 -04:00
|
|
|
|
2022-01-05 15:35:12 -08:00
|
|
|
export function GroupCallView({
|
|
|
|
client,
|
|
|
|
isPasswordlessUser,
|
2022-06-28 15:08:14 +01:00
|
|
|
isEmbedded,
|
2022-09-09 02:10:45 -04:00
|
|
|
preload,
|
2022-09-09 02:04:53 -04:00
|
|
|
hideHeader,
|
2022-07-27 16:14:05 -04:00
|
|
|
roomIdOrAlias,
|
2022-01-05 15:35:12 -08:00
|
|
|
groupCall,
|
2022-08-02 00:46:16 +02:00
|
|
|
}: Props) {
|
2022-01-05 15:35:12 -08:00
|
|
|
const {
|
|
|
|
state,
|
|
|
|
error,
|
|
|
|
activeSpeaker,
|
|
|
|
userMediaFeeds,
|
|
|
|
microphoneMuted,
|
|
|
|
localVideoMuted,
|
|
|
|
localCallFeed,
|
|
|
|
initLocalCallFeed,
|
|
|
|
enter,
|
|
|
|
leave,
|
|
|
|
toggleLocalVideoMuted,
|
|
|
|
toggleMicrophoneMuted,
|
|
|
|
toggleScreensharing,
|
2023-01-12 17:31:19 +00:00
|
|
|
setMicrophoneMuted,
|
2022-02-03 16:56:13 -08:00
|
|
|
requestingScreenshare,
|
2022-01-05 15:35:12 -08:00
|
|
|
isScreensharing,
|
|
|
|
screenshareFeeds,
|
2022-04-22 18:05:48 -07:00
|
|
|
participants,
|
2022-06-09 21:56:58 +01:00
|
|
|
unencryptedEventsFromUsers,
|
2023-03-16 14:41:55 +00:00
|
|
|
otelGroupCallMembership,
|
|
|
|
} = useGroupCall(groupCall, client);
|
2022-01-05 15:35:12 -08:00
|
|
|
|
2022-10-10 09:19:10 -04:00
|
|
|
const { t } = useTranslation();
|
2022-09-09 02:10:45 -04:00
|
|
|
const { setAudioInput, setVideoInput } = useMediaHandler();
|
2022-05-18 19:00:59 -04:00
|
|
|
const avatarUrl = useRoomAvatar(groupCall.room);
|
|
|
|
|
2022-01-05 15:35:12 -08:00
|
|
|
useEffect(() => {
|
|
|
|
window.groupCall = groupCall;
|
2022-09-09 02:10:45 -04:00
|
|
|
return () => {
|
|
|
|
delete window.groupCall;
|
|
|
|
};
|
|
|
|
}, [groupCall]);
|
2022-07-08 20:55:18 +01:00
|
|
|
|
2022-09-09 02:10:45 -04:00
|
|
|
useEffect(() => {
|
|
|
|
if (widget && preload) {
|
|
|
|
// In preload mode, wait for a join action before entering
|
|
|
|
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
2022-09-29 13:19:46 +01:00
|
|
|
// 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.
|
Make Element Call work in Firefox's resist fingerprinting mode
This one is gonna take some explaining:
When in resist fingerprinting mode, Firefox exhibits some funny behavior: when we ask for the the list of media devices, it gives us fake device IDs. But when the js-sdk requests a stream for any of those devices, Firefox associates the stream with the real device ID.
Now, in order to get the names of devices included in their metadata when you query the device list, you need to be holding a stream. For this reason, useMediaHandler was set up to reload the device list whenever matrix-js-sdk got a new local stream. But because of the inconsistency in device IDs, it would enter an infinite cycle telling matrix-js-sdk to request a stream for the fake device ID, but with matrix-js-sdk always responding with the real device ID.
I already wasn't happy with useMediaHandler's use of @ts-ignore comments to inspect private js-sdk fields, and in the meantime we've come up with a simpler function for requesting device names, so I decided to refactor useMediaHandler to use it instead. Importantly, it doesn't break in resist fingerprinting mode.
This created a new UX issue though: now, when on the lobby screen, useMediaHandler would request microphone access so it could get device names, followed immediately by a *second* pop-up for the lobby screen to request camera access. That's 1 pop-up too many, so I changed useMediaHandler to only request device names when a component is mounted that actually wants to show them. Currently, the settings modal is the only such component, and users normally only open it *after* granting full audio/video access, so this solution works out quite nicely.
2023-05-15 22:03:26 -04:00
|
|
|
const devices = await getNamedDevices();
|
2022-09-29 13:19:46 +01:00
|
|
|
|
2022-09-09 02:10:45 -04:00
|
|
|
const { audioInput, videoInput } = ev.detail
|
|
|
|
.data as unknown as JoinCallData;
|
2022-09-27 16:19:48 +01:00
|
|
|
|
|
|
|
if (audioInput !== null) {
|
2022-09-29 17:07:10 +01:00
|
|
|
const deviceId = await findDeviceByName(
|
|
|
|
audioInput,
|
|
|
|
"audioinput",
|
|
|
|
devices
|
|
|
|
);
|
2022-09-27 16:19:48 +01:00
|
|
|
if (!deviceId) {
|
|
|
|
logger.warn("Unknown audio input: " + audioInput);
|
|
|
|
} else {
|
|
|
|
logger.debug(
|
|
|
|
`Found audio input ID ${deviceId} for name ${audioInput}`
|
|
|
|
);
|
|
|
|
setAudioInput(deviceId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (videoInput !== null) {
|
2022-09-29 17:07:10 +01:00
|
|
|
const deviceId = await findDeviceByName(
|
|
|
|
videoInput,
|
|
|
|
"videoinput",
|
|
|
|
devices
|
|
|
|
);
|
2022-09-27 16:19:48 +01:00
|
|
|
if (!deviceId) {
|
|
|
|
logger.warn("Unknown video input: " + videoInput);
|
|
|
|
} else {
|
|
|
|
logger.debug(
|
|
|
|
`Found video input ID ${deviceId} for name ${videoInput}`
|
|
|
|
);
|
|
|
|
setVideoInput(deviceId);
|
|
|
|
}
|
|
|
|
}
|
2022-09-09 02:10:45 -04:00
|
|
|
await Promise.all([
|
|
|
|
groupCall.setMicrophoneMuted(audioInput === null),
|
|
|
|
groupCall.setLocalVideoMuted(videoInput === null),
|
|
|
|
]);
|
|
|
|
|
2023-04-17 18:47:46 +01:00
|
|
|
await enter();
|
2022-12-19 12:16:59 +01:00
|
|
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
|
|
|
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
|
|
|
|
2022-09-09 02:10:45 -04:00
|
|
|
await Promise.all([
|
|
|
|
widget.api.setAlwaysOnScreen(true),
|
|
|
|
widget.api.transport.reply(ev.detail, {}),
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
|
|
|
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
|
|
|
return () => {
|
|
|
|
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
|
|
|
};
|
|
|
|
}
|
2023-04-17 18:47:46 +01:00
|
|
|
}, [groupCall, preload, setAudioInput, setVideoInput, enter]);
|
2022-09-09 02:10:45 -04:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (isEmbedded && !preload) {
|
|
|
|
// In embedded mode, bypass the lobby and just enter the call straight away
|
2023-04-17 18:47:46 +01:00
|
|
|
enter();
|
2022-12-19 12:16:59 +01:00
|
|
|
|
|
|
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
|
|
|
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
2022-09-09 02:10:45 -04:00
|
|
|
}
|
2023-04-17 18:47:46 +01:00
|
|
|
}, [groupCall, isEmbedded, preload, enter]);
|
2022-01-05 15:35:12 -08:00
|
|
|
|
|
|
|
useSentryGroupCallHandler(groupCall);
|
|
|
|
|
2022-02-03 16:56:13 -08:00
|
|
|
useLocationNavigation(requestingScreenshare);
|
|
|
|
|
2022-01-05 15:35:12 -08:00
|
|
|
const [left, setLeft] = useState(false);
|
|
|
|
const history = useHistory();
|
|
|
|
|
2023-01-03 17:09:21 +01:00
|
|
|
const onLeave = useCallback(async () => {
|
2022-09-23 15:35:05 +01:00
|
|
|
setLeft(true);
|
2022-11-04 13:07:14 +01:00
|
|
|
|
2022-11-21 12:39:48 -05:00
|
|
|
let participantCount = 0;
|
|
|
|
for (const deviceMap of groupCall.participants.values()) {
|
|
|
|
participantCount += deviceMap.size;
|
|
|
|
}
|
|
|
|
|
2023-01-03 17:09:21 +01:00
|
|
|
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
|
|
|
// therefore we want the event to be sent instantly without getting queued/batched.
|
|
|
|
const sendInstantly = !!widget;
|
2022-11-04 13:07:14 +01:00
|
|
|
PosthogAnalytics.instance.eventCallEnded.track(
|
2022-12-19 12:16:59 +01:00
|
|
|
groupCall.groupCallId,
|
2023-01-03 17:09:21 +01:00
|
|
|
participantCount,
|
|
|
|
sendInstantly
|
2022-11-04 13:07:14 +01:00
|
|
|
);
|
|
|
|
|
2022-01-05 15:35:12 -08:00
|
|
|
leave();
|
2022-09-09 02:10:45 -04:00
|
|
|
if (widget) {
|
2023-01-05 00:01:57 +01:00
|
|
|
// we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked.
|
|
|
|
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
|
|
|
|
widget.api.setAlwaysOnScreen(false);
|
2023-01-03 17:09:21 +01:00
|
|
|
PosthogAnalytics.instance.logout();
|
2022-09-09 02:10:45 -04:00
|
|
|
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
|
|
|
}
|
2022-01-05 15:35:12 -08:00
|
|
|
|
2022-09-23 15:38:35 +01:00
|
|
|
if (!isPasswordlessUser && !isEmbedded) {
|
2022-01-05 15:35:12 -08:00
|
|
|
history.push("/");
|
|
|
|
}
|
2022-11-21 12:39:48 -05:00
|
|
|
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
|
2022-09-09 02:10:45 -04:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (widget && state === GroupCallState.Entered) {
|
|
|
|
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
|
|
leave();
|
|
|
|
await widget.api.transport.reply(ev.detail, {});
|
2022-09-12 22:53:30 -04:00
|
|
|
widget.api.setAlwaysOnScreen(false);
|
2022-09-09 02:10:45 -04:00
|
|
|
};
|
|
|
|
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
|
|
|
return () => {
|
|
|
|
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, [groupCall, state, leave]);
|
2022-01-05 15:35:12 -08:00
|
|
|
|
|
|
|
if (error) {
|
|
|
|
return <ErrorView error={error} />;
|
|
|
|
} else if (state === GroupCallState.Entered) {
|
2022-04-22 18:05:48 -07:00
|
|
|
if (groupCall.isPtt) {
|
|
|
|
return (
|
|
|
|
<PTTCallView
|
|
|
|
client={client}
|
2022-07-27 16:14:05 -04:00
|
|
|
roomIdOrAlias={roomIdOrAlias}
|
2022-04-22 18:05:48 -07:00
|
|
|
roomName={groupCall.room.name}
|
2022-05-18 19:00:59 -04:00
|
|
|
avatarUrl={avatarUrl}
|
2022-04-28 17:44:50 -07:00
|
|
|
groupCall={groupCall}
|
|
|
|
participants={participants}
|
2022-04-22 18:05:48 -07:00
|
|
|
userMediaFeeds={userMediaFeeds}
|
|
|
|
onLeave={onLeave}
|
2022-06-28 15:08:14 +01:00
|
|
|
isEmbedded={isEmbedded}
|
2022-09-09 02:04:53 -04:00
|
|
|
hideHeader={hideHeader}
|
2023-03-16 14:41:55 +00:00
|
|
|
otelGroupCallMembership={otelGroupCallMembership}
|
2022-04-22 18:05:48 -07:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<InCallView
|
|
|
|
groupCall={groupCall}
|
|
|
|
client={client}
|
|
|
|
roomName={groupCall.room.name}
|
2022-05-18 19:00:59 -04:00
|
|
|
avatarUrl={avatarUrl}
|
2022-10-21 17:24:56 +01:00
|
|
|
participants={participants}
|
2022-04-22 18:05:48 -07:00
|
|
|
microphoneMuted={microphoneMuted}
|
|
|
|
localVideoMuted={localVideoMuted}
|
|
|
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
|
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
2023-01-12 17:31:19 +00:00
|
|
|
setMicrophoneMuted={setMicrophoneMuted}
|
2022-04-22 18:05:48 -07:00
|
|
|
userMediaFeeds={userMediaFeeds}
|
|
|
|
activeSpeaker={activeSpeaker}
|
|
|
|
onLeave={onLeave}
|
|
|
|
toggleScreensharing={toggleScreensharing}
|
|
|
|
isScreensharing={isScreensharing}
|
|
|
|
screenshareFeeds={screenshareFeeds}
|
2022-07-27 16:14:05 -04:00
|
|
|
roomIdOrAlias={roomIdOrAlias}
|
2022-06-09 21:56:58 +01:00
|
|
|
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
2022-09-09 02:04:53 -04:00
|
|
|
hideHeader={hideHeader}
|
2023-03-16 14:41:55 +00:00
|
|
|
otelGroupCallMembership={otelGroupCallMembership}
|
2022-04-22 18:05:48 -07:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2022-01-05 15:35:12 -08:00
|
|
|
} else if (left) {
|
2022-09-23 15:35:05 +01:00
|
|
|
if (isPasswordlessUser) {
|
|
|
|
return <CallEndedView client={client} />;
|
|
|
|
} else {
|
|
|
|
// If the user is a regular user, we'll have sent them back to the homepage,
|
|
|
|
// so just sit here & do nothing: otherwise we would (briefly) mount the
|
|
|
|
// LobbyView again which would open capture devices again.
|
|
|
|
return null;
|
|
|
|
}
|
2022-09-09 02:10:45 -04:00
|
|
|
} else if (preload) {
|
|
|
|
return null;
|
|
|
|
} else if (isEmbedded) {
|
|
|
|
return (
|
|
|
|
<FullScreenView>
|
2023-02-13 09:55:32 +05:30
|
|
|
<h1>{t("Loading…")}</h1>
|
2022-09-09 02:10:45 -04:00
|
|
|
</FullScreenView>
|
|
|
|
);
|
2022-01-05 15:35:12 -08:00
|
|
|
} else {
|
2022-09-09 02:10:45 -04:00
|
|
|
return (
|
|
|
|
<LobbyView
|
|
|
|
client={client}
|
|
|
|
groupCall={groupCall}
|
|
|
|
roomName={groupCall.room.name}
|
|
|
|
avatarUrl={avatarUrl}
|
|
|
|
state={state}
|
|
|
|
onInitLocalCallFeed={initLocalCallFeed}
|
|
|
|
localCallFeed={localCallFeed}
|
|
|
|
onEnter={enter}
|
|
|
|
microphoneMuted={microphoneMuted}
|
|
|
|
localVideoMuted={localVideoMuted}
|
|
|
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
|
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
|
|
roomIdOrAlias={roomIdOrAlias}
|
|
|
|
isEmbedded={isEmbedded}
|
|
|
|
hideHeader={hideHeader}
|
|
|
|
/>
|
|
|
|
);
|
2022-01-05 15:35:12 -08:00
|
|
|
}
|
|
|
|
}
|