Skip to content

Commit 6ae741e

Browse files
committed
Watch for a 'join' action to know when the call is connected
Previously we were watching for changes to the room state to know when you become connected to a call. However, the room state might not change if you had a stuck membership event prior to re-joining the call. It's going to be more reliable to watch for the 'join' action that Element Call sends, and use that to track the connection state.
1 parent 589ae14 commit 6ae741e

File tree

2 files changed

+137
-387
lines changed

2 files changed

+137
-387
lines changed

src/models/Call.ts

Lines changed: 56 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import type EventEmitter from "events";
2828
import type { IApp } from "../stores/WidgetStore";
2929
import SdkConfig, { DEFAULTS } from "../SdkConfig";
3030
import SettingsStore from "../settings/SettingsStore";
31-
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
3231
import { timeout } from "../utils/promise";
3332
import WidgetUtils from "../utils/WidgetUtils";
3433
import { WidgetType } from "../widgets/WidgetType";
@@ -188,47 +187,17 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
188187
*/
189188
public abstract clean(): Promise<void>;
190189

191-
/**
192-
* Contacts the widget to connect to the call or prompt the user to connect to the call.
193-
* @param {MediaDeviceInfo | null} audioInput The audio input to use, or
194-
* null to start muted.
195-
* @param {MediaDeviceInfo | null} audioInput The video input to use, or
196-
* null to start muted.
197-
*/
198-
protected abstract performConnection(
199-
audioInput: MediaDeviceInfo | null,
200-
videoInput: MediaDeviceInfo | null,
201-
): Promise<void>;
202-
203190
/**
204191
* Contacts the widget to disconnect from the call.
205192
*/
206193
protected abstract performDisconnection(): Promise<void>;
207194

208195
/**
209196
* Starts the communication between the widget and the call.
210-
* The call then waits for the necessary requirements to actually perform the connection
211-
* or connects right away depending on the call type. (Jitsi, Legacy, ElementCall...)
212-
* It uses the media devices set in MediaDeviceHandler.
213-
* The widget associated with the call must be active
214-
* for this to succeed.
197+
* The widget associated with the call must be active for this to succeed.
215198
* Only call this if the call state is: ConnectionState.Disconnected.
216199
*/
217200
public async start(): Promise<void> {
218-
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
219-
(await MediaDeviceHandler.getDevices())!;
220-
221-
let audioInput: MediaDeviceInfo | null = null;
222-
if (!MediaDeviceHandler.startWithAudioMuted) {
223-
const deviceId = MediaDeviceHandler.getAudioInput();
224-
audioInput = audioInputs.find((d) => d.deviceId === deviceId) ?? audioInputs[0] ?? null;
225-
}
226-
let videoInput: MediaDeviceInfo | null = null;
227-
if (!MediaDeviceHandler.startWithVideoMuted) {
228-
const deviceId = MediaDeviceHandler.getVideoInput();
229-
videoInput = videoInputs.find((d) => d.deviceId === deviceId) ?? videoInputs[0] ?? null;
230-
}
231-
232201
const messagingStore = WidgetMessagingStore.instance;
233202
this.messaging = messagingStore.getMessagingForUid(this.widgetUid) ?? null;
234203
if (!this.messaging) {
@@ -249,13 +218,23 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
249218
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
250219
}
251220
}
252-
await this.performConnection(audioInput, videoInput);
221+
}
253222

223+
protected setConnected(): void {
254224
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
255225
window.addEventListener("beforeunload", this.beforeUnload);
256226
this.connectionState = ConnectionState.Connected;
257227
}
258228

229+
/**
230+
* Manually marks the call as disconnected.
231+
*/
232+
protected setDisconnected(): void {
233+
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
234+
window.removeEventListener("beforeunload", this.beforeUnload);
235+
this.connectionState = ConnectionState.Disconnected;
236+
}
237+
259238
/**
260239
* Disconnects the user from the call.
261240
*/
@@ -268,15 +247,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
268247
this.close();
269248
}
270249

271-
/**
272-
* Manually marks the call as disconnected.
273-
*/
274-
public setDisconnected(): void {
275-
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
276-
window.removeEventListener("beforeunload", this.beforeUnload);
277-
this.connectionState = ConnectionState.Disconnected;
278-
}
279-
280250
/**
281251
* Stops further communication with the widget and tells the UI to close.
282252
*/
@@ -462,66 +432,10 @@ export class JitsiCall extends Call {
462432
});
463433
}
464434

465-
protected async performConnection(
466-
audioInput: MediaDeviceInfo | null,
467-
videoInput: MediaDeviceInfo | null,
468-
): Promise<void> {
469-
// Ensure that the messaging doesn't get stopped while we're waiting for responses
470-
const dontStopMessaging = new Promise<void>((resolve, reject) => {
471-
const messagingStore = WidgetMessagingStore.instance;
472-
473-
const listener = (uid: string): void => {
474-
if (uid === this.widgetUid) {
475-
cleanup();
476-
reject(new Error("Messaging stopped"));
477-
}
478-
};
479-
const done = (): void => {
480-
cleanup();
481-
resolve();
482-
};
483-
const cleanup = (): void => {
484-
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
485-
this.off(CallEvent.ConnectionState, done);
486-
};
487-
488-
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
489-
this.on(CallEvent.ConnectionState, done);
490-
});
491-
492-
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
493-
// sending a hangup event that races with the rest of this method, so we need
494-
// to add the hangup listener now rather than later
435+
public async start(): Promise<void> {
436+
await super.start();
437+
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
495438
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
496-
497-
// Actually perform the join
498-
const response = waitForEvent(
499-
this.messaging!,
500-
`action:${ElementWidgetActions.JoinCall}`,
501-
(ev: CustomEvent<IWidgetApiRequest>) => {
502-
ev.preventDefault();
503-
this.messaging!.transport.reply(ev.detail, {}); // ack
504-
return true;
505-
},
506-
);
507-
const request = this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
508-
audioInput: audioInput?.label ?? null,
509-
videoInput: videoInput?.label ?? null,
510-
});
511-
try {
512-
await Promise.race([Promise.all([request, response]), dontStopMessaging]);
513-
} catch (e) {
514-
// If it timed out, clean up our advance preparations
515-
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
516-
517-
if (this.messaging!.transport.ready) {
518-
// The messaging still exists, which means Jitsi might still be going in the background
519-
this.messaging!.transport.send(ElementWidgetActions.HangupCall, { force: true });
520-
}
521-
522-
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
523-
}
524-
525439
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
526440
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
527441
}
@@ -544,18 +458,17 @@ export class JitsiCall extends Call {
544458
}
545459
}
546460

547-
public setDisconnected(): void {
548-
// During tests this.messaging can be undefined
549-
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
461+
public close(): void {
462+
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
463+
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
550464
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
551465
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
552-
553-
super.setDisconnected();
466+
super.close();
554467
}
555468

556469
public destroy(): void {
557470
this.room.off(RoomStateEvent.Update, this.onRoomState);
558-
this.on(CallEvent.ConnectionState, this.onConnectionState);
471+
this.off(CallEvent.ConnectionState, this.onConnectionState);
559472
if (this.participantsExpirationTimer !== null) {
560473
clearTimeout(this.participantsExpirationTimer);
561474
this.participantsExpirationTimer = null;
@@ -607,27 +520,21 @@ export class JitsiCall extends Call {
607520
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
608521
};
609522

523+
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
524+
ev.preventDefault();
525+
this.messaging!.transport.reply(ev.detail, {}); // ack
526+
this.setConnected();
527+
};
528+
610529
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
611530
// If we're already in the middle of a client-initiated disconnection,
612531
// ignore the event
613532
if (this.connectionState === ConnectionState.Disconnecting) return;
614533

615534
ev.preventDefault();
616-
617-
// In case this hangup is caused by Jitsi Meet crashing at startup,
618-
// wait for the connection event in order to avoid racing
619-
if (this.connectionState === ConnectionState.Disconnected) {
620-
await waitForEvent(this, CallEvent.ConnectionState);
621-
}
622-
623535
this.messaging!.transport.reply(ev.detail, {}); // ack
624536
this.setDisconnected();
625-
this.close();
626-
// In video rooms we immediately want to restart the call after hangup
627-
// The lobby will be shown again and it connects to all signals from Jitsi.
628-
if (isVideoRoom(this.room)) {
629-
this.start();
630-
}
537+
if (!isVideoRoom(this.room)) this.close();
631538
};
632539
}
633540

@@ -823,55 +730,38 @@ export class ElementCall extends Call {
823730
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room));
824731
}
825732

826-
protected async performConnection(
827-
audioInput: MediaDeviceInfo | null,
828-
videoInput: MediaDeviceInfo | null,
829-
): Promise<void> {
733+
public async start(): Promise<void> {
734+
await super.start();
735+
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
830736
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
831-
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
737+
this.messaging!.on(`action:${ElementWidgetActions.Close}`, this.onClose);
832738
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
833-
834-
// TODO: if the widget informs us when the join button is clicked (widget action), so we can
835-
// - set state to connecting
836-
// - send call notify
837-
const session = this.client.matrixRTC.getActiveRoomSession(this.room);
838-
if (session) {
839-
await waitForEvent(
840-
session,
841-
MatrixRTCSessionEvent.MembershipsChanged,
842-
(_, newMemberships: CallMembership[]) =>
843-
newMemberships.some((m) => m.sender === this.client.getUserId()),
844-
false, // allow user to wait as long as they want (no timeout)
845-
);
846-
} else {
847-
await waitForEvent(
848-
this.client.matrixRTC,
849-
MatrixRTCSessionManagerEvents.SessionStarted,
850-
(roomId: string, session: MatrixRTCSession) =>
851-
this.session.callId === session.callId && roomId === this.roomId,
852-
false, // allow user to wait as long as they want (no timeout)
853-
);
854-
}
855739
}
856740

857741
protected async performDisconnection(): Promise<void> {
742+
const response = waitForEvent(
743+
this.messaging!,
744+
`action:${ElementWidgetActions.HangupCall}`,
745+
(ev: CustomEvent<IWidgetApiRequest>) => {
746+
ev.preventDefault();
747+
this.messaging!.transport.reply(ev.detail, {}); // ack
748+
return true;
749+
},
750+
);
751+
const request = this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
858752
try {
859-
await this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
860-
await waitForEvent(
861-
this.session,
862-
MatrixRTCSessionEvent.MembershipsChanged,
863-
(_, newMemberships: CallMembership[]) =>
864-
!newMemberships.some((m) => m.sender === this.client.getUserId()),
865-
);
753+
await Promise.all([request, response]);
866754
} catch (e) {
867755
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
868756
}
869757
}
870758

871-
public setDisconnected(): void {
759+
public close(): void {
760+
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
872761
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
762+
this.messaging!.off(`action:${ElementWidgetActions.Close}`, this.onClose);
873763
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
874-
super.setDisconnected();
764+
super.close();
875765
}
876766

877767
public destroy(): void {
@@ -918,15 +808,20 @@ export class ElementCall extends Call {
918808
this.messaging!.transport.reply(ev.detail, {}); // ack
919809
};
920810

811+
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
812+
ev.preventDefault();
813+
this.messaging!.transport.reply(ev.detail, {}); // ack
814+
this.setConnected();
815+
};
816+
921817
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
818+
// If we're already in the middle of a client-initiated disconnection,
819+
// ignore the event
820+
if (this.connectionState === ConnectionState.Disconnecting) return;
821+
922822
ev.preventDefault();
923823
this.messaging!.transport.reply(ev.detail, {}); // ack
924824
this.setDisconnected();
925-
// In video rooms we immediately want to reconnect after hangup
926-
// This starts the lobby again and connects to all signals from EC.
927-
if (isVideoRoom(this.room)) {
928-
this.start();
929-
}
930825
};
931826

932827
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {

0 commit comments

Comments
 (0)