Skip to content

Commit

Permalink
Merge pull request matrix-org#2935 from robintown/entered-via-widget
Browse files Browse the repository at this point in the history
Make GroupCall work better with widgets

(cherry picked from commit 79ccd7c)
  • Loading branch information
robintown committed Dec 2, 2022
1 parent 31c4f6c commit 36deabb
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 15 deletions.
3 changes: 3 additions & 0 deletions spec/test-utils/webrtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();

public supportsExperimentalThreads(): boolean { return true; }
public async decryptEventIfNeeded(): Promise<void> {}

public typed(): MatrixClient { return this as unknown as MatrixClient; }

public emitRoomState(event: MatrixEvent, state: RoomState): void {
Expand Down
179 changes: 167 additions & 12 deletions spec/unit/webrtc/groupCall.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '../../../src';
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
import { MatrixClient } from "../../../src/client";
import { IMyDevice, MatrixClient } from "../../../src/client";
import {
installWebRTCMocks,
MockCallFeed,
Expand Down Expand Up @@ -180,13 +180,13 @@ describe('Group Call', function() {

room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
});

it("does not initialize local call feed, if it already is", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
});

it("does not initialize local call feed, if it already is", async () => {
await groupCall.initLocalCallFeed();
jest.spyOn(groupCall, "initLocalCallFeed");
await groupCall.enter();
Expand Down Expand Up @@ -216,10 +216,6 @@ describe('Group Call', function() {
});

it("sends member state event to room on enter", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;

await groupCall.create();

try {
Expand Down Expand Up @@ -249,10 +245,6 @@ describe('Group Call', function() {
});

it("sends member state event to room on leave", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;

await groupCall.create();
await groupCall.enter();
mockSendState.mockClear();
Expand All @@ -267,6 +259,18 @@ describe('Group Call', function() {
);
});

it("includes local device in participants when entered via another session", async () => {
const hasLocalParticipant = () => groupCall.participants.get(
room.getMember(mockClient.getUserId()!)!,
)?.has(mockClient.getDeviceId()!) ?? false;

expect(groupCall.enteredViaAnotherSession).toBe(false);
expect(hasLocalParticipant()).toBe(false);

groupCall.enteredViaAnotherSession = true;
expect(hasLocalParticipant()).toBe(true);
});

it("starts with mic unmuted in regular calls", async () => {
try {
await groupCall.create();
Expand Down Expand Up @@ -1270,4 +1274,155 @@ describe('Group Call', function() {
});
});
});

describe("cleaning member state", () => {
const bobWeb: IMyDevice = {
device_id: "bobweb",
last_seen_ts: 0,
};
const bobDesktop: IMyDevice = {
device_id: "bobdesktop",
last_seen_ts: 0,
};
const bobDesktopOffline: IMyDevice = {
device_id: "bobdesktopoffline",
last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago
};
const bobDesktopNeverOnline: IMyDevice = {
device_id: "bobdesktopneveronline",
};

const mkContent = (devices: IMyDevice[]) => ({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10,
})),
}],
});

const expectDevices = (devices: IMyDevice[]) => expect(
room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)?.getContent(),
).toEqual({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number),
})),
}],
});

let mockClient: MatrixClient;
let room: Room;
let groupCall: GroupCall;

beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});

afterAll(() => jest.useRealTimers());

beforeEach(async () => {
const typedMockClient = new MockCallMatrixClient(
FAKE_USER_ID_2, bobWeb.device_id, FAKE_SESSION_ID_2,
);
jest.spyOn(typedMockClient, "sendStateEvent").mockImplementation(
async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`;
if (roomId === room.roomId) {
room.addLiveEvents([new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
})]);
}
return { event_id: eventId };
},
);
mockClient = typedMockClient as unknown as MatrixClient;

room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));

groupCall = new GroupCall(
mockClient,
room,
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
FAKE_CONF_ID,
);
await groupCall.create();

mockClient.getDevices = async () => ({
devices: [
bobWeb,
bobDesktop,
bobDesktopOffline,
bobDesktopNeverOnline,
],
});
});

afterEach(() => groupCall.leave());

it("doesn't clean up valid devices", async () => {
await groupCall.enter();
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);

await groupCall.cleanMemberState();
expectDevices([bobWeb, bobDesktop]);
});

it("cleans up our own device if we're disconnected", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);

await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});

it("doesn't clean up the local device if entered via another session", async () => {
groupCall.enteredViaAnotherSession = true;
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb]),
FAKE_USER_ID_2,
);

await groupCall.cleanMemberState();
expectDevices([bobWeb]);
});

it("cleans up devices that have never been online", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobDesktop, bobDesktopNeverOnline]),
FAKE_USER_ID_2,
);

await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});

it("no-ops if there are no state events", async () => {
await groupCall.cleanMemberState();
expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null);
});
});
});
24 changes: 21 additions & 3 deletions src/webrtc/groupCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,21 @@ export class GroupCall extends TypedEventEmitter<
this._creationTs = value;
}

private _enteredViaAnotherSession = false;

/**
* Whether the local device has entered this call via another session, such
* as a widget.
*/
public get enteredViaAnotherSession(): boolean {
return this._enteredViaAnotherSession;
}

public set enteredViaAnotherSession(value: boolean) {
this._enteredViaAnotherSession = value;
this.updateParticipants();
}

/**
* Executes the given callback on all calls in this group call.
* @param f The callback.
Expand Down Expand Up @@ -1170,7 +1185,7 @@ export class GroupCall extends TypedEventEmitter<

const participants = new Map<RoomMember, Map<string, ParticipantState>>();
const now = Date.now();
const entered = this.state === GroupCallState.Entered;
const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession;
let nextExpiration = Infinity;

for (const e of this.getMemberStateEvents()) {
Expand Down Expand Up @@ -1344,8 +1359,11 @@ export class GroupCall extends TypedEventEmitter<
await this.updateDevices(devices => {
const newDevices = devices.filter(d => {
const device = deviceMap.get(d.device_id);
return device?.last_seen_ts !== undefined
&& !(d.device_id === this.client.getDeviceId()! && this.state !== GroupCallState.Entered);
return device?.last_seen_ts !== undefined && !(
d.device_id === this.client.getDeviceId()!
&& this.state !== GroupCallState.Entered
&& !this.enteredViaAnotherSession
);
});

// Skip the update if the devices are unchanged
Expand Down

0 comments on commit 36deabb

Please sign in to comment.