Skip to content

Commit 3eb88ec

Browse files
authored
Merge pull request #3693 from element-hq/toger5/fix-sticky-rejoin-error
Fix rejoin EC crash
2 parents 486c0b8 + 745772f commit 3eb88ec

File tree

2 files changed

+62
-12
lines changed

2 files changed

+62
-12
lines changed

playwright/spa-call-sticky.spec.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE in the repository root for full details.
66
*/
77

8-
import { expect, type Page, test, type Request } from "@playwright/test";
8+
import {
9+
expect,
10+
type Page,
11+
test,
12+
type Request,
13+
type Browser,
14+
} from "@playwright/test";
915

1016
import { SpaHelpers } from "./spa-helpers";
1117

12-
test("One to One call using matrix rtc 2.0 aka sticky events", async ({
13-
browser,
14-
page,
15-
browserName,
16-
}) => {
18+
async function setupTwoUserSpaCall(
19+
browser: Browser,
20+
page: Page,
21+
browserName: string,
22+
): Promise<{ guestPage: Page }> {
1723
test.skip(
1824
browserName === "firefox",
1925
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
@@ -63,13 +69,49 @@ test("One to One call using matrix rtc 2.0 aka sticky events", async ({
6369
"Pevara",
6470
"2_0",
6571
);
72+
// Assert both sides have sent sticky membership events
73+
expect(androlHasSentStickyEvent).toEqual(true);
74+
expect(pevaraHasSentStickyEvent).toEqual(true);
75+
76+
return { guestPage };
77+
}
78+
79+
test("One to One call using matrix rtc 2.0 aka sticky events", async ({
80+
browser,
81+
page,
82+
browserName,
83+
}) => {
84+
const { guestPage } = await setupTwoUserSpaCall(browser, page, browserName);
6685

6786
await SpaHelpers.expectVideoTilesCount(page, 2);
6887
await SpaHelpers.expectVideoTilesCount(guestPage, 2);
88+
});
6989

70-
// Assert both sides have sent sticky membership events
71-
expect(androlHasSentStickyEvent).toEqual(true);
72-
expect(pevaraHasSentStickyEvent).toEqual(true);
90+
// This issue occurs when a member leave but does not clean up their sticky event.
91+
// If they rejoin they will use a new stickye key (stickyKey = member.id = UUID())
92+
// We end up with two memberships with the same user and device id. This previously
93+
// was a impossible case since that would be the same state event. Now its possible.
94+
// We need to ALWAYS key by userId, deviceId and member.id. This test checks that.
95+
test("One to One rejoin after improper leave does not crash EC", async ({
96+
browser,
97+
page,
98+
browserName,
99+
}) => {
100+
const { guestPage } = await setupTwoUserSpaCall(browser, page, browserName);
101+
102+
await SpaHelpers.expectVideoTilesCount(page, 2);
103+
await SpaHelpers.expectVideoTilesCount(guestPage, 2);
104+
105+
await guestPage.reload();
106+
await expect(guestPage.getByTestId("lobby_joinCall")).toBeVisible();
107+
108+
// Check if rejoining with the same browser context (device) breaks EC.
109+
// This has happened on versions that do not consider the member.id as part of the key for a media tile.
110+
await guestPage.getByTestId("lobby_joinCall").click();
111+
112+
// We cannot use the `expectVideoTilesCount` helper here since one of them is expected to show waiting for media
113+
await expect(page.getByTestId("videoTile")).toHaveCount(3);
114+
await expect(guestPage.getByTestId("videoTile")).toHaveCount(2);
73115
});
74116

75117
function isStickySend(url: string): boolean {

src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,23 @@ export function createMatrixLivekitMembers$({
111111
: null;
112112

113113
yield {
114-
keys: [membership.userId, membership.deviceId],
114+
// This could also just be the memberId without the other fields.
115+
// In theory we should never have the same memberId for different userIds (they are UUIDs)
116+
// This still makes us resilient agains someone who intentionally tries to use the same memberId.
117+
// If they want to do this they would now need to also use the same sender which is impossible.
118+
keys: [
119+
membership.userId,
120+
membership.deviceId,
121+
membership.memberId,
122+
],
115123
data: { membership, participant, connection },
116124
};
117125
}
118126
},
119127
// Each update where the key of the generator array do not change will result in updates to the `data$` observable in the factory.
120-
(scope, data$, userId, deviceId) => {
128+
(scope, data$, userId, deviceId, memberId) => {
121129
logger.debug(
122-
`Generating member for livekitIdentity: ${data$.value.membership.rtcBackendIdentity}, userId:deviceId: ${userId}${deviceId}`,
130+
`Generating member for livekitIdentity: ${data$.value.membership.rtcBackendIdentity},keys userId:deviceId:memberId ${userId}:${deviceId}:${memberId}`,
123131
);
124132
const { participant$, ...rest } = scope.splitBehavior(data$);
125133
// will only get called once per `participantId, userId` pair.

0 commit comments

Comments
 (0)