Use new matrixRTC calling (#11792)
* initial Signed-off-by: Timo K <toger5@hotmail.de> * cleanup1 Signed-off-by: Timo K <toger5@hotmail.de> * bring back call timer Signed-off-by: Timo K <toger5@hotmail.de> * more cleanup and test removals Signed-off-by: Timo K <toger5@hotmail.de> * remove event Signed-off-by: Timo K <toger5@hotmail.de> * cleanups and minor fixes Signed-off-by: Timo K <toger5@hotmail.de> * add matrixRTC to stubClient Signed-off-by: Timo K <toger5@hotmail.de> * update tests (some got removed) The removal is a consequence of EW now doing less call logic. More logic is done by the js sdk (MatrixRTCSession) And therefore in EC itself. Signed-off-by: Timo K <toger5@hotmail.de> * cleanups Signed-off-by: Timo K <toger5@hotmail.de> * mock the session Signed-off-by: Timo K <toger5@hotmail.de> * lint Signed-off-by: Timo K <toger5@hotmail.de> * remove GroupCallDuration Signed-off-by: Timo K <toger5@hotmail.de> * review and fixing tests Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
parent
c4852dd216
commit
860764c057
13 changed files with 176 additions and 255 deletions
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { EventType, Room } from "matrix-js-sdk/src/matrix";
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -27,7 +27,7 @@ import { ConnectionState, ElementCall } from "../../../models/Call";
|
||||||
import { useCall } from "../../../hooks/useCall";
|
import { useCall } from "../../../hooks/useCall";
|
||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
|
||||||
import { GroupCallDuration } from "../voip/CallDuration";
|
import { SessionDuration } from "../voip/CallDuration";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
|
||||||
interface RoomCallBannerProps {
|
interface RoomCallBannerProps {
|
||||||
|
@ -49,12 +49,13 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
|
||||||
[roomId],
|
[roomId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO matrix rtc
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
const event = call.groupCall.room.currentState.getStateEvents(
|
logger.log("clicking on the call banner is not supported anymore - there are no timeline events anymore.");
|
||||||
EventType.GroupCallPrefix,
|
let messageLikeEventId: string | undefined;
|
||||||
call.groupCall.groupCallId,
|
if (!messageLikeEventId) {
|
||||||
);
|
// Until we have a timeline event for calls this will always be true.
|
||||||
if (event === null) {
|
// We will never jump to the non existing timeline event.
|
||||||
logger.error("Couldn't find a group call event to jump to");
|
logger.error("Couldn't find a group call event to jump to");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -63,17 +64,17 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
metricsTrigger: undefined,
|
metricsTrigger: undefined,
|
||||||
event_id: event.getId(),
|
event_id: messageLikeEventId,
|
||||||
scroll_into_view: true,
|
scroll_into_view: true,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
});
|
});
|
||||||
}, [call, roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomCallBanner" onClick={onClick}>
|
<div className="mx_RoomCallBanner" onClick={onClick}>
|
||||||
<div className="mx_RoomCallBanner_text">
|
<div className="mx_RoomCallBanner_text">
|
||||||
<span className="mx_RoomCallBanner_label">{_t("voip|video_call")}</span>
|
<span className="mx_RoomCallBanner_label">{_t("voip|video_call")}</span>
|
||||||
<GroupCallDuration groupCall={call.groupCall} />
|
<SessionDuration session={call.session} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccessibleButton onClick={connect} kind="primary" element="button" disabled={false}>
|
<AccessibleButton onClick={connect} kind="primary" element="button" disabled={false}>
|
||||||
|
|
|
@ -33,7 +33,7 @@ import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
|
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
|
||||||
import FacePile from "../elements/FacePile";
|
import FacePile from "../elements/FacePile";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { CallDuration, GroupCallDuration } from "../voip/CallDuration";
|
import { CallDuration, SessionDuration } from "../voip/CallDuration";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
const MAX_FACES = 8;
|
const MAX_FACES = 8;
|
||||||
|
@ -77,7 +77,7 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
||||||
/>
|
/>
|
||||||
<FacePile members={facePileMembers} size="24px" overflow={facePileOverflow} />
|
<FacePile members={facePileMembers} size="24px" overflow={facePileOverflow} />
|
||||||
</div>
|
</div>
|
||||||
{call && <GroupCallDuration groupCall={call.groupCall} />}
|
{call && <SessionDuration session={call.session} />}
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_CallEvent_button"
|
className="mx_CallEvent_button"
|
||||||
kind={buttonKind}
|
kind={buttonKind}
|
||||||
|
|
|
@ -65,7 +65,7 @@ import IconizedContextMenu, {
|
||||||
IconizedContextMenuRadio,
|
IconizedContextMenuRadio,
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { GroupCallDuration } from "../voip/CallDuration";
|
import { SessionDuration } from "../voip/CallDuration";
|
||||||
import { Alignment } from "../elements/Tooltip";
|
import { Alignment } from "../elements/Tooltip";
|
||||||
import RoomCallBanner from "../beacon/RoomCallBanner";
|
import RoomCallBanner from "../beacon/RoomCallBanner";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
@ -787,7 +787,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
{icon}
|
{icon}
|
||||||
{name}
|
{name}
|
||||||
{this.props.activeCall instanceof ElementCall && (
|
{this.props.activeCall instanceof ElementCall && (
|
||||||
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
<SessionDuration session={this.props.activeCall?.session} />
|
||||||
)}
|
)}
|
||||||
{/* Empty topic element to fill out space */}
|
{/* Empty topic element to fill out space */}
|
||||||
<div className="mx_LegacyRoomHeader_topic" />
|
<div className="mx_LegacyRoomHeader_topic" />
|
||||||
|
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useState, useEffect, memo } from "react";
|
import React, { FC, useState, useEffect, memo } from "react";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/matrix";
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { formatPreciseDuration } from "../../../DateUtils";
|
import { formatPreciseDuration } from "../../../DateUtils";
|
||||||
|
|
||||||
|
@ -32,20 +33,25 @@ export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => {
|
||||||
return <div className="mx_CallDuration">{formatPreciseDuration(delta)}</div>;
|
return <div className="mx_CallDuration">{formatPreciseDuration(delta)}</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GroupCallDurationProps {
|
interface SessionDurationProps {
|
||||||
groupCall: GroupCall;
|
session: MatrixRTCSession | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A call duration counter that automatically counts up, given a live GroupCall
|
* A call duration counter that automatically counts up, given a matrixRTC session
|
||||||
* object.
|
* object.
|
||||||
*/
|
*/
|
||||||
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
|
export const SessionDuration: FC<SessionDurationProps> = ({ session }) => {
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return groupCall.creationTs === null ? null : <CallDuration delta={now - groupCall.creationTs} />;
|
// This is a temporal solution.
|
||||||
|
// Using the oldest membership will update when this user leaves.
|
||||||
|
// This implies that the displayed call duration will also update consequently.
|
||||||
|
const createdTs = session?.getOldestMembership()?.createdTs();
|
||||||
|
return createdTs ? <CallDuration delta={now - createdTs} /> : <CallDuration delta={0} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,20 +20,21 @@ import {
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
EventType,
|
EventType,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
GroupCall,
|
IMyDevice,
|
||||||
GroupCallEvent,
|
Room,
|
||||||
GroupCallIntent,
|
RoomMember,
|
||||||
GroupCallState,
|
|
||||||
GroupCallType,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
|
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||||
import { IWidgetApiRequest, MatrixWidgetType } from "matrix-widget-api";
|
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSession, MatrixRTCSessionEvent } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||||
|
|
||||||
import type EventEmitter from "events";
|
import type EventEmitter from "events";
|
||||||
import type { IMyDevice, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
||||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||||
import type { IApp } from "../stores/WidgetStore";
|
import type { IApp } from "../stores/WidgetStore";
|
||||||
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
||||||
|
@ -615,12 +616,13 @@ export class JitsiCall extends Call {
|
||||||
* (somewhat cheekily named)
|
* (somewhat cheekily named)
|
||||||
*/
|
*/
|
||||||
export class ElementCall extends Call {
|
export class ElementCall extends Call {
|
||||||
|
// TODO this is only there to support backwards compatiblity in timeline rendering
|
||||||
|
// this should not be part of this class since it has nothing to do with it.
|
||||||
public static readonly CALL_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallPrefix);
|
public static readonly CALL_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallPrefix);
|
||||||
public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix);
|
public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix);
|
||||||
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
||||||
|
|
||||||
private terminationTimer: number | null = null;
|
private terminationTimer: number | null = null;
|
||||||
|
|
||||||
private _layout = Layout.Tile;
|
private _layout = Layout.Tile;
|
||||||
public get layout(): Layout {
|
public get layout(): Layout {
|
||||||
return this._layout;
|
return this._layout;
|
||||||
|
@ -630,7 +632,13 @@ export class ElementCall extends Call {
|
||||||
this.emit(CallEvent.Layout, value);
|
this.emit(CallEvent.Layout, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(public readonly groupCall: GroupCall, client: MatrixClient) {
|
private static createCallWidget(roomId: string, client: MatrixClient): IApp {
|
||||||
|
const ecWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.CALL.matches(app.type));
|
||||||
|
if (ecWidget) {
|
||||||
|
logger.log("There is already a widget in this room, so we recreate it");
|
||||||
|
ActiveWidgetStore.instance.destroyPersistentWidget(ecWidget.id, ecWidget.roomId);
|
||||||
|
WidgetStore.instance.removeVirtualWidget(ecWidget.id, ecWidget.roomId);
|
||||||
|
}
|
||||||
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
|
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
|
||||||
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
|
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
|
||||||
// We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible).
|
// We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible).
|
||||||
|
@ -639,7 +647,6 @@ export class ElementCall extends Call {
|
||||||
const analyticsID: string = accountAnalyticsData?.getContent().pseudonymousAnalyticsOptIn
|
const analyticsID: string = accountAnalyticsData?.getContent().pseudonymousAnalyticsOptIn
|
||||||
? accountAnalyticsData?.getContent().id
|
? accountAnalyticsData?.getContent().id
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Splice together the Element Call URL for this call
|
// Splice together the Element Call URL for this call
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
embed: "true", // We're embedding EC within another application
|
embed: "true", // We're embedding EC within another application
|
||||||
|
@ -648,14 +655,14 @@ export class ElementCall extends Call {
|
||||||
hideHeader: "true", // Hide the header since our room header is enough
|
hideHeader: "true", // Hide the header since our room header is enough
|
||||||
userId: client.getUserId()!,
|
userId: client.getUserId()!,
|
||||||
deviceId: client.getDeviceId()!,
|
deviceId: client.getDeviceId()!,
|
||||||
roomId: groupCall.room.roomId,
|
roomId: roomId,
|
||||||
baseUrl: client.baseUrl,
|
baseUrl: client.baseUrl,
|
||||||
lang: getCurrentLanguage().replace("_", "-"),
|
lang: getCurrentLanguage().replace("_", "-"),
|
||||||
fontScale: `${(SettingsStore.getValue("baseFontSizeV2") ?? 16) / FontWatcher.DEFAULT_SIZE}`,
|
fontScale: `${(SettingsStore.getValue("baseFontSizeV2") ?? 16) / FontWatcher.DEFAULT_SIZE}`,
|
||||||
analyticsID,
|
analyticsID,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (client.isRoomEncrypted(groupCall.room.roomId)) params.append("perParticipantE2EE", "");
|
if (client.isRoomEncrypted(roomId)) params.append("perParticipantE2EE", "");
|
||||||
if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "");
|
if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "");
|
||||||
if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) params.append("allowVoipWithNoMedia", "");
|
if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) params.append("allowVoipWithNoMedia", "");
|
||||||
|
|
||||||
|
@ -678,30 +685,23 @@ export class ElementCall extends Call {
|
||||||
|
|
||||||
// To use Element Call without touching room state, we create a virtual
|
// To use Element Call without touching room state, we create a virtual
|
||||||
// widget (one that doesn't have a corresponding state event)
|
// widget (one that doesn't have a corresponding state event)
|
||||||
super(
|
return WidgetStore.instance.addVirtualWidget(
|
||||||
WidgetStore.instance.addVirtualWidget(
|
|
||||||
{
|
{
|
||||||
id: randomString(24), // So that it's globally unique
|
id: randomString(24), // So that it's globally unique
|
||||||
creatorUserId: client.getUserId()!,
|
creatorUserId: client.getUserId()!,
|
||||||
name: "Element Call",
|
name: "Element Call",
|
||||||
type: MatrixWidgetType.Custom,
|
type: WidgetType.CALL.preferred,
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
// This option makes the widget API wait for the 'contentLoaded' event instead
|
// waitForIframeLoad: false,
|
||||||
// of waiting for a 'load' event from the iframe, which means the widget code isn't
|
|
||||||
// racing to set up its listener before the 'load' event is fired. EC sends this event
|
|
||||||
// of of https://github.com/matrix-org/matrix-js-sdk/pull/3556 so we should uncomment
|
|
||||||
// the line below once we've made both livekit and full-mesh releases that include that
|
|
||||||
// PR, and everything will be less racy.
|
|
||||||
//waitForIframeLoad: false,
|
|
||||||
},
|
},
|
||||||
groupCall.room.roomId,
|
roomId,
|
||||||
),
|
|
||||||
client,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
private constructor(public session: MatrixRTCSession, widget: IApp, client: MatrixClient) {
|
||||||
|
super(widget, client);
|
||||||
|
|
||||||
this.on(CallEvent.Participants, this.onParticipants);
|
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
||||||
groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
|
||||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
|
||||||
|
|
||||||
this.updateParticipants();
|
this.updateParticipants();
|
||||||
}
|
}
|
||||||
|
@ -714,8 +714,18 @@ export class ElementCall extends Call {
|
||||||
SettingsStore.getValue("feature_element_call_video_rooms") &&
|
SettingsStore.getValue("feature_element_call_video_rooms") &&
|
||||||
room.isCallRoom())
|
room.isCallRoom())
|
||||||
) {
|
) {
|
||||||
const groupCall = room.client.groupCallEventHandler!.groupCalls.get(room.roomId);
|
const apps = WidgetStore.instance.getApps(room.roomId);
|
||||||
if (groupCall !== undefined) return new ElementCall(groupCall, room.client);
|
const ecWidget = apps.find((app) => WidgetType.CALL.matches(app.type));
|
||||||
|
const session = room.client.matrixRTC.getRoomSession(room);
|
||||||
|
|
||||||
|
// A call is present if we
|
||||||
|
// - have a widget: This means the create function was called
|
||||||
|
// - or there is a running session where we have not yet created a widget for.
|
||||||
|
if (ecWidget || session.memberships.length !== 0) {
|
||||||
|
// create a widget for the case we are joining a running call and don't have on yet.
|
||||||
|
const availableOrCreatedWidget = ecWidget ?? ElementCall.createCallWidget(room.roomId, room.client);
|
||||||
|
return new ElementCall(session, availableOrCreatedWidget, room.client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -727,19 +737,8 @@ export class ElementCall extends Call {
|
||||||
SettingsStore.getValue("feature_element_call_video_rooms") &&
|
SettingsStore.getValue("feature_element_call_video_rooms") &&
|
||||||
room.isCallRoom();
|
room.isCallRoom();
|
||||||
|
|
||||||
const groupCall = new GroupCall(
|
console.log("Intend is ", isVideoRoom ? "VideoRoom" : "Prompt", " TODO, handle intent appropriately");
|
||||||
room.client,
|
ElementCall.createCallWidget(room.roomId, room.client);
|
||||||
room,
|
|
||||||
GroupCallType.Video,
|
|
||||||
false,
|
|
||||||
isVideoRoom ? GroupCallIntent.Room : GroupCallIntent.Prompt,
|
|
||||||
);
|
|
||||||
|
|
||||||
await groupCall.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public clean(): Promise<void> {
|
|
||||||
return this.groupCall.cleanMemberState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async performConnection(
|
protected async performConnection(
|
||||||
|
@ -755,7 +754,6 @@ export class ElementCall extends Call {
|
||||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.groupCall.enteredViaAnotherSession = true;
|
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
|
@ -774,15 +772,14 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
super.setDisconnected();
|
super.setDisconnected();
|
||||||
this.groupCall.enteredViaAnotherSession = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.groupCall.room.roomId);
|
ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.widget.roomId);
|
||||||
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId);
|
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.widget.roomId);
|
||||||
this.off(CallEvent.Participants, this.onParticipants);
|
|
||||||
this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
||||||
this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
|
||||||
|
|
||||||
if (this.terminationTimer !== null) {
|
if (this.terminationTimer !== null) {
|
||||||
clearTimeout(this.terminationTimer);
|
clearTimeout(this.terminationTimer);
|
||||||
|
@ -792,70 +789,41 @@ export class ElementCall extends Call {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onRTCSessionEnded = (roomId: string, session: MatrixRTCSession): void => {
|
||||||
|
if (roomId == this.roomId) {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the call's layout.
|
* Sets the call's layout.
|
||||||
* @param layout The layout to switch to.
|
* @param layout The layout to switch to.
|
||||||
*/
|
*/
|
||||||
public async setLayout(layout: Layout): Promise<void> {
|
public async setLayout(layout: Layout): Promise<void> {
|
||||||
const action = layout === Layout.Tile ? ElementWidgetActions.TileLayout : ElementWidgetActions.SpotlightLayout;
|
const action = layout === Layout.Tile ? ElementWidgetActions.TileLayout : ElementWidgetActions.SpotlightLayout;
|
||||||
|
|
||||||
await this.messaging!.transport.send(action, {});
|
await this.messaging!.transport.send(action, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMembershipChanged = (): void => this.updateParticipants();
|
||||||
|
|
||||||
private updateParticipants(): void {
|
private updateParticipants(): void {
|
||||||
const participants = new Map<RoomMember, Set<string>>();
|
const participants = new Map<RoomMember, Set<string>>();
|
||||||
|
|
||||||
for (const [member, deviceMap] of this.groupCall.participants) {
|
for (const m of this.session.memberships) {
|
||||||
participants.set(member, new Set(deviceMap.keys()));
|
if (!m.sender) continue;
|
||||||
|
const member = this.room.getMember(m.sender);
|
||||||
|
if (member) {
|
||||||
|
if (participants.has(member)) {
|
||||||
|
participants.get(member)?.add(m.deviceId);
|
||||||
|
} else {
|
||||||
|
participants.set(member, new Set([m.deviceId]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.participants = participants;
|
this.participants = participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get mayTerminate(): boolean {
|
|
||||||
return (
|
|
||||||
this.groupCall.intent !== GroupCallIntent.Room &&
|
|
||||||
this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onParticipants = async (
|
|
||||||
participants: Map<RoomMember, Set<string>>,
|
|
||||||
prevParticipants: Map<RoomMember, Set<string>>,
|
|
||||||
): Promise<void> => {
|
|
||||||
let participantCount = 0;
|
|
||||||
for (const devices of participants.values()) participantCount += devices.size;
|
|
||||||
|
|
||||||
let prevParticipantCount = 0;
|
|
||||||
for (const devices of prevParticipants.values()) prevParticipantCount += devices.size;
|
|
||||||
|
|
||||||
// If the last participant disconnected, terminate the call
|
|
||||||
if (participantCount === 0 && prevParticipantCount > 0 && this.mayTerminate) {
|
|
||||||
if (prevParticipants.get(this.room.getMember(this.client.getUserId()!)!)?.has(this.client.getDeviceId()!)) {
|
|
||||||
// If we were that last participant, do the termination ourselves
|
|
||||||
await this.groupCall.terminate();
|
|
||||||
} else {
|
|
||||||
// We don't appear to have been the last participant, but because of
|
|
||||||
// the potential for races, users lacking permission, and a myriad of
|
|
||||||
// other reasons, we can't rely on other clients to terminate the call.
|
|
||||||
// Since it's likely that other clients are using this same logic, we wait
|
|
||||||
// randomly between 2 and 8 seconds before terminating the call, to
|
|
||||||
// probabilistically reduce event spam. If someone else beats us to it,
|
|
||||||
// this timer will be automatically cleared upon the call's destruction.
|
|
||||||
this.terminationTimer = window.setTimeout(
|
|
||||||
() => this.groupCall.terminate(),
|
|
||||||
Math.random() * 6000 + 2000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onGroupCallParticipants = (): void => this.updateParticipants();
|
|
||||||
|
|
||||||
private onGroupCallState = (state: GroupCallState): void => {
|
|
||||||
if (state === GroupCallState.Ended) this.destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
|
@ -873,4 +841,8 @@ export class ElementCall extends Call {
|
||||||
this.layout = Layout.Spotlight;
|
this.layout = Layout.Spotlight;
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public clean(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import type { GroupCall, Room } from "matrix-js-sdk/src/matrix";
|
import type { GroupCall, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
|
@ -61,6 +65,8 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
||||||
}
|
}
|
||||||
this.matrixClient.on(GroupCallEventHandlerEvent.Incoming, this.onGroupCall);
|
this.matrixClient.on(GroupCallEventHandlerEvent.Incoming, this.onGroupCall);
|
||||||
this.matrixClient.on(GroupCallEventHandlerEvent.Outgoing, this.onGroupCall);
|
this.matrixClient.on(GroupCallEventHandlerEvent.Outgoing, this.onGroupCall);
|
||||||
|
this.matrixClient.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSession);
|
||||||
|
this.matrixClient.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSession);
|
||||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgets);
|
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgets);
|
||||||
|
|
||||||
// If the room ID of a previously connected call is still in settings at
|
// If the room ID of a previously connected call is still in settings at
|
||||||
|
@ -94,6 +100,8 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
||||||
this.matrixClient.off(GroupCallEventHandlerEvent.Incoming, this.onGroupCall);
|
this.matrixClient.off(GroupCallEventHandlerEvent.Incoming, this.onGroupCall);
|
||||||
this.matrixClient.off(GroupCallEventHandlerEvent.Outgoing, this.onGroupCall);
|
this.matrixClient.off(GroupCallEventHandlerEvent.Outgoing, this.onGroupCall);
|
||||||
this.matrixClient.off(GroupCallEventHandlerEvent.Ended, this.onGroupCall);
|
this.matrixClient.off(GroupCallEventHandlerEvent.Ended, this.onGroupCall);
|
||||||
|
this.matrixClient.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSession);
|
||||||
|
this.matrixClient.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSession);
|
||||||
}
|
}
|
||||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
||||||
}
|
}
|
||||||
|
@ -191,4 +199,7 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGroupCall = (groupCall: GroupCall): void => this.updateRoom(groupCall.room);
|
private onGroupCall = (groupCall: GroupCall): void => this.updateRoom(groupCall.room);
|
||||||
|
private onRTCSession = (roomId: string, session: MatrixRTCSession): void => {
|
||||||
|
this.updateRoom(session.room);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,6 +202,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
const app = WidgetUtils.makeAppConfig(widget.id, widget, widget.creatorUserId, roomId, undefined);
|
const app = WidgetUtils.makeAppConfig(widget.id, widget, widget.creatorUserId, roomId, undefined);
|
||||||
this.widgetMap.set(WidgetUtils.getWidgetUid(app), app);
|
this.widgetMap.set(WidgetUtils.getWidgetUid(app), app);
|
||||||
this.roomMap.get(roomId)!.widgets.push(app);
|
this.roomMap.get(roomId)!.widgets.push(app);
|
||||||
|
this.emit(UPDATE_EVENT, roomId);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ export class WidgetType {
|
||||||
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
||||||
public static readonly INTEGRATION_MANAGER = new WidgetType("m.integration_manager", "m.integration_manager");
|
public static readonly INTEGRATION_MANAGER = new WidgetType("m.integration_manager", "m.integration_manager");
|
||||||
public static readonly CUSTOM = new WidgetType("m.custom", "m.custom");
|
public static readonly CUSTOM = new WidgetType("m.custom", "m.custom");
|
||||||
|
public static readonly CALL = new WidgetType("m.call", "m.call");
|
||||||
|
|
||||||
public constructor(public readonly preferred: string, public readonly legacy: string) {}
|
public constructor(public readonly preferred: string, public readonly legacy: string) {}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { UserTab } from "../../../src/components/views/dialogs/UserTab";
|
import { UserTab } from "../../../src/components/views/dialogs/UserTab";
|
||||||
import {
|
import {
|
||||||
clearAllModals,
|
clearAllModals,
|
||||||
|
createStubMatrixRTC,
|
||||||
filterConsole,
|
filterConsole,
|
||||||
flushPromises,
|
flushPromises,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
|
@ -109,6 +110,7 @@ describe("<MatrixChat />", () => {
|
||||||
secretStorage: {
|
secretStorage: {
|
||||||
isStored: jest.fn().mockReturnValue(null),
|
isStored: jest.fn().mockReturnValue(null),
|
||||||
},
|
},
|
||||||
|
matrixRTC: createStubMatrixRTC(),
|
||||||
getDehydratedDevice: jest.fn(),
|
getDehydratedDevice: jest.fn(),
|
||||||
whoami: jest.fn(),
|
whoami: jest.fn(),
|
||||||
isRoomEncrypted: jest.fn(),
|
isRoomEncrypted: jest.fn(),
|
||||||
|
|
|
@ -141,7 +141,6 @@ describe("CallEvent", () => {
|
||||||
|
|
||||||
screen.getByText("@alice:example.org started a video call");
|
screen.getByText("@alice:example.org started a video call");
|
||||||
screen.getByLabelText("2 participants");
|
screen.getByLabelText("2 participants");
|
||||||
screen.getByText("1m 30s");
|
|
||||||
|
|
||||||
// Test that the join button works
|
// Test that the join button works
|
||||||
const dispatcherSpy = jest.fn();
|
const dispatcherSpy = jest.fn();
|
||||||
|
|
|
@ -17,16 +17,14 @@ limitations under the License.
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { waitFor } from "@testing-library/react";
|
import { waitFor } from "@testing-library/react";
|
||||||
import {
|
import { RoomType, Room, RoomEvent, MatrixEvent, RoomStateEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||||
RoomType,
|
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
MatrixEvent,
|
|
||||||
RoomStateEvent,
|
|
||||||
PendingEventOrdering,
|
|
||||||
GroupCallIntent,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { Widget } from "matrix-widget-api";
|
import { Widget } from "matrix-widget-api";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSession, MatrixRTCSessionEvent } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import type { Mocked } from "jest-mock";
|
import type { Mocked } from "jest-mock";
|
||||||
import type { MatrixClient, IMyDevice, RoomMember } from "matrix-js-sdk/src/matrix";
|
import type { MatrixClient, IMyDevice, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
@ -96,9 +94,16 @@ const setUpClientRoomAndStores = (): {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(room, "getMyMembership").mockReturnValue("join");
|
jest.spyOn(room, "getMyMembership").mockReturnValue("join");
|
||||||
|
|
||||||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||||
|
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||||
|
client.matrixRTC.getRoomSession.mockImplementation((roomId) => {
|
||||||
|
const session = new EventEmitter() as MatrixRTCSession;
|
||||||
|
session.memberships = [];
|
||||||
|
return session;
|
||||||
|
});
|
||||||
client.getRooms.mockReturnValue([room]);
|
client.getRooms.mockReturnValue([room]);
|
||||||
client.getUserId.mockReturnValue(alice.userId);
|
client.getUserId.mockReturnValue(alice.userId);
|
||||||
client.getDeviceId.mockReturnValue("alices_device");
|
client.getDeviceId.mockReturnValue("alices_device");
|
||||||
|
@ -576,11 +581,9 @@ describe("ElementCall", () => {
|
||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let alice: RoomMember;
|
let alice: RoomMember;
|
||||||
let bob: RoomMember;
|
|
||||||
let carol: RoomMember;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ client, room, alice, bob, carol } = setUpClientRoomAndStores());
|
({ client, room, alice } = setUpClientRoomAndStores());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => cleanUpClientRoomAndStores(client, room));
|
afterEach(() => cleanUpClientRoomAndStores(client, room));
|
||||||
|
@ -595,15 +598,14 @@ describe("ElementCall", () => {
|
||||||
expect(Call.get(room)).toBeInstanceOf(ElementCall);
|
expect(Call.get(room)).toBeInstanceOf(ElementCall);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores terminated calls", async () => {
|
it("finds ongoing calls that are created by the session manager", async () => {
|
||||||
await ElementCall.create(room);
|
// There is an existing session created by another user in this room.
|
||||||
|
client.matrixRTC.getRoomSession.mockReturnValue({
|
||||||
|
on: (ev: any, fn: any) => {},
|
||||||
|
memberships: [{ fakeVal: "fake membership" }],
|
||||||
|
} as unknown as MatrixRTCSession);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
// Terminate the call
|
|
||||||
await call.groupCall.terminate();
|
|
||||||
|
|
||||||
expect(Call.get(room)).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes font settings through widget URL", async () => {
|
it("passes font settings through widget URL", async () => {
|
||||||
|
@ -731,10 +733,6 @@ describe("ElementCall", () => {
|
||||||
|
|
||||||
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
||||||
|
|
||||||
it("has prompt intent", () => {
|
|
||||||
expect(call.groupCall.intent).toBe(GroupCallIntent.Prompt);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("connects muted", async () => {
|
it("connects muted", async () => {
|
||||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
audioMutedSpy.mockReturnValue(true);
|
audioMutedSpy.mockReturnValue(true);
|
||||||
|
@ -828,57 +826,6 @@ describe("ElementCall", () => {
|
||||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tracks participants in room state", async () => {
|
|
||||||
expect(call.participants).toEqual(new Map());
|
|
||||||
|
|
||||||
// A participant with multiple devices (should only show up once)
|
|
||||||
await client.sendStateEvent(
|
|
||||||
room.roomId,
|
|
||||||
ElementCall.MEMBER_EVENT_TYPE.name,
|
|
||||||
{
|
|
||||||
"m.calls": [
|
|
||||||
{
|
|
||||||
"m.call_id": call.groupCall.groupCallId,
|
|
||||||
"m.devices": [
|
|
||||||
{ device_id: "bobweb", session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10 },
|
|
||||||
{ device_id: "bobdesktop", session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
bob.userId,
|
|
||||||
);
|
|
||||||
// A participant with an expired device (should not show up)
|
|
||||||
await client.sendStateEvent(
|
|
||||||
room.roomId,
|
|
||||||
ElementCall.MEMBER_EVENT_TYPE.name,
|
|
||||||
{
|
|
||||||
"m.calls": [
|
|
||||||
{
|
|
||||||
"m.call_id": call.groupCall.groupCallId,
|
|
||||||
"m.devices": [
|
|
||||||
{ device_id: "carolandroid", session_id: "1", feeds: [], expires_ts: -1000 * 60 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
carol.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Now, stub out client.sendStateEvent so we can test our local echo
|
|
||||||
client.sendStateEvent.mockReset();
|
|
||||||
await call.connect();
|
|
||||||
expect(call.participants).toEqual(
|
|
||||||
new Map([
|
|
||||||
[alice, new Set(["alices_device"])],
|
|
||||||
[bob, new Set(["bobweb", "bobdesktop"])],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await call.disconnect();
|
|
||||||
expect(call.participants).toEqual(new Map([[bob, new Set(["bobweb", "bobdesktop"])]]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks layout", async () => {
|
it("tracks layout", async () => {
|
||||||
await call.connect();
|
await call.connect();
|
||||||
expect(call.layout).toBe(Layout.Tile);
|
expect(call.layout).toBe(Layout.Tile);
|
||||||
|
@ -924,14 +871,11 @@ describe("ElementCall", () => {
|
||||||
|
|
||||||
it("emits events when participants change", async () => {
|
it("emits events when participants change", async () => {
|
||||||
const onParticipants = jest.fn();
|
const onParticipants = jest.fn();
|
||||||
|
call.session.memberships = [{ sender: alice.userId, deviceId: "alices_device" } as CallMembership];
|
||||||
call.on(CallEvent.Participants, onParticipants);
|
call.on(CallEvent.Participants, onParticipants);
|
||||||
|
call.session.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []);
|
||||||
|
|
||||||
await call.connect();
|
expect(onParticipants.mock.calls).toEqual([[new Map([[alice, new Set(["alices_device"])]]), new Map()]]);
|
||||||
await call.disconnect();
|
|
||||||
expect(onParticipants.mock.calls).toEqual([
|
|
||||||
[new Map([[alice, new Set(["alices_device"])]]), new Map()],
|
|
||||||
[new Map(), new Map([[alice, new Set(["alices_device"])]])],
|
|
||||||
]);
|
|
||||||
|
|
||||||
call.off(CallEvent.Participants, onParticipants);
|
call.off(CallEvent.Participants, onParticipants);
|
||||||
});
|
});
|
||||||
|
@ -954,60 +898,19 @@ describe("ElementCall", () => {
|
||||||
call.off(CallEvent.Layout, onLayout);
|
call.off(CallEvent.Layout, onLayout);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ends the call immediately if we're the last participant to leave", async () => {
|
it("ends the call immediately if the session ended", async () => {
|
||||||
await call.connect();
|
await call.connect();
|
||||||
const onDestroy = jest.fn();
|
const onDestroy = jest.fn();
|
||||||
call.on(CallEvent.Destroy, onDestroy);
|
call.on(CallEvent.Destroy, onDestroy);
|
||||||
await call.disconnect();
|
await call.disconnect();
|
||||||
expect(onDestroy).toHaveBeenCalled();
|
// this will be called automatically
|
||||||
call.off(CallEvent.Destroy, onDestroy);
|
// disconnect -> widget sends state event -> session manager notices no-one left
|
||||||
});
|
client.matrixRTC.emit(
|
||||||
|
MatrixRTCSessionManagerEvents.SessionEnded,
|
||||||
it("ends the call after a random delay if the last participant leaves without ending it", async () => {
|
|
||||||
// Bob connects
|
|
||||||
await client.sendStateEvent(
|
|
||||||
room.roomId,
|
room.roomId,
|
||||||
ElementCall.MEMBER_EVENT_TYPE.name,
|
{} as unknown as MatrixRTCSession,
|
||||||
{
|
|
||||||
"m.calls": [
|
|
||||||
{
|
|
||||||
"m.call_id": call.groupCall.groupCallId,
|
|
||||||
"m.devices": [
|
|
||||||
{ device_id: "bobweb", session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
bob.userId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDestroy = jest.fn();
|
|
||||||
call.on(CallEvent.Destroy, onDestroy);
|
|
||||||
|
|
||||||
// Bob disconnects
|
|
||||||
await client.sendStateEvent(
|
|
||||||
room.roomId,
|
|
||||||
ElementCall.MEMBER_EVENT_TYPE.name,
|
|
||||||
{
|
|
||||||
"m.calls": [
|
|
||||||
{
|
|
||||||
"m.call_id": call.groupCall.groupCallId,
|
|
||||||
"m.devices": [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
bob.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Nothing should happen for at least a second, to give Bob a chance
|
|
||||||
// to end the call on his own
|
|
||||||
jest.advanceTimersByTime(1000);
|
|
||||||
expect(onDestroy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Within 10 seconds, our client should end the call on behalf of Bob
|
|
||||||
jest.advanceTimersByTime(9000);
|
|
||||||
expect(onDestroy).toHaveBeenCalled();
|
expect(onDestroy).toHaveBeenCalled();
|
||||||
|
|
||||||
call.off(CallEvent.Destroy, onDestroy);
|
call.off(CallEvent.Destroy, onDestroy);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1040,10 +943,6 @@ describe("ElementCall", () => {
|
||||||
|
|
||||||
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
|
||||||
|
|
||||||
it("has room intent", () => {
|
|
||||||
expect(call.groupCall.intent).toBe(GroupCallIntent.Room);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't end the call when the last participant leaves", async () => {
|
it("doesn't end the call when the last participant leaves", async () => {
|
||||||
await call.connect();
|
await call.connect();
|
||||||
const onDestroy = jest.fn();
|
const onDestroy = jest.fn();
|
||||||
|
|
|
@ -35,6 +35,9 @@ export class MockedCall extends Call {
|
||||||
url: "https://example.org",
|
url: "https://example.org",
|
||||||
name: "Group call",
|
name: "Group call",
|
||||||
creatorUserId: "@alice:example.org",
|
creatorUserId: "@alice:example.org",
|
||||||
|
// waitForIframeLoad = false, makes the widget API wait for the 'contentLoaded' event instead.
|
||||||
|
// This is how the EC is designed, but for backwards compatibility (full mesh) we currently need to use waitForIframeLoad = true
|
||||||
|
// waitForIframeLoad: false
|
||||||
},
|
},
|
||||||
room.client,
|
room.client,
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,10 @@ import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
|
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
|
||||||
import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
|
import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSessionManager } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/matrix";
|
import type { GroupCall } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
||||||
|
@ -89,6 +93,7 @@ export function stubClient(): MatrixClient {
|
||||||
*/
|
*/
|
||||||
export function createTestClient(): MatrixClient {
|
export function createTestClient(): MatrixClient {
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
let txnId = 1;
|
let txnId = 1;
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
|
@ -256,6 +261,7 @@ export function createTestClient(): MatrixClient {
|
||||||
submitMsisdnToken: jest.fn(),
|
submitMsisdnToken: jest.fn(),
|
||||||
getMediaConfig: jest.fn(),
|
getMediaConfig: jest.fn(),
|
||||||
baseUrl: "https://matrix-client.matrix.org",
|
baseUrl: "https://matrix-client.matrix.org",
|
||||||
|
matrixRTC: createStubMatrixRTC(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
@ -272,6 +278,26 @@ export function createTestClient(): MatrixClient {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createStubMatrixRTC(): MatrixRTCSessionManager {
|
||||||
|
const eventEmitterMatrixRTCSessionManager = new EventEmitter();
|
||||||
|
const mockGetRoomSession = jest.fn();
|
||||||
|
mockGetRoomSession.mockImplementation((roomId) => {
|
||||||
|
const session = new EventEmitter() as MatrixRTCSession;
|
||||||
|
session.memberships = [];
|
||||||
|
session.getOldestMembership = () => undefined;
|
||||||
|
return session;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
start: jest.fn(),
|
||||||
|
stop: jest.fn(),
|
||||||
|
getActiveRoomSession: jest.fn(),
|
||||||
|
getRoomSession: mockGetRoomSession,
|
||||||
|
on: eventEmitterMatrixRTCSessionManager.on.bind(eventEmitterMatrixRTCSessionManager),
|
||||||
|
off: eventEmitterMatrixRTCSessionManager.off.bind(eventEmitterMatrixRTCSessionManager),
|
||||||
|
removeListener: eventEmitterMatrixRTCSessionManager.removeListener.bind(eventEmitterMatrixRTCSessionManager),
|
||||||
|
emit: eventEmitterMatrixRTCSessionManager.emit.bind(eventEmitterMatrixRTCSessionManager),
|
||||||
|
} as unknown as MatrixRTCSessionManager;
|
||||||
|
}
|
||||||
type MakeEventPassThruProps = {
|
type MakeEventPassThruProps = {
|
||||||
user: User["userId"];
|
user: User["userId"];
|
||||||
relatesTo?: IEventRelation;
|
relatesTo?: IEventRelation;
|
||||||
|
|
Loading…
Reference in a new issue