1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-07-28 15:22:05 +03:00

New group call experience: Starting and ending calls (#9318)

* Create m.room calls in video rooms, and m.prompt calls otherwise

* Terminate a call when the last person leaves

* Hook up the room header button to a unified CallView component

* Write more tests
This commit is contained in:
Robin
2022-09-27 07:54:51 -04:00
committed by GitHub
parent 54b79c7667
commit ace6591f43
15 changed files with 695 additions and 512 deletions

View File

@ -51,11 +51,12 @@ jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
jest.spyOn(MediaDeviceHandler, "getAudioInput").mockReturnValue("1");
jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");
jest.spyOn(SettingsStore, "getValue").mockImplementation(settingName =>
settingName === "feature_video_rooms" || settingName === "feature_element_call_video_rooms" ? true : undefined,
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
jest.spyOn(SettingsStore, "getValue").mockImplementation(
settingName => enabledSettings.has(settingName) || undefined,
);
const setUpClientRoomAndStores = (roomType: RoomType): {
const setUpClientRoomAndStores = (): {
client: Mocked<MatrixClient>;
room: Room;
alice: RoomMember;
@ -68,7 +69,6 @@ const setUpClientRoomAndStores = (roomType: RoomType): {
const room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(room, "getType").mockReturnValue(roomType);
const alice = mkRoomMember(room.roomId, "@alice:example.org");
const bob = mkRoomMember(room.roomId, "@bob:example.org");
@ -165,7 +165,8 @@ describe("JitsiCall", () => {
let carol: RoomMember;
beforeEach(() => {
({ client, room, alice, bob, carol } = setUpClientRoomAndStores(RoomType.ElementVideo));
({ client, room, alice, bob, carol } = setUpClientRoomAndStores());
jest.spyOn(room, "getType").mockReturnValue(RoomType.ElementVideo);
});
afterEach(() => cleanUpClientRoomAndStores(client, room));
@ -191,7 +192,7 @@ describe("JitsiCall", () => {
});
});
describe("instance", () => {
describe("instance in a video room", () => {
let call: JitsiCall;
let widget: Widget;
let messaging: Mocked<ClientWidgetApi>;
@ -542,7 +543,7 @@ describe("ElementCall", () => {
let carol: RoomMember;
beforeEach(() => {
({ client, room, alice, bob, carol } = setUpClientRoomAndStores(RoomType.UnstableCall));
({ client, room, alice, bob, carol } = setUpClientRoomAndStores());
});
afterEach(() => cleanUpClientRoomAndStores(client, room));
@ -569,7 +570,7 @@ describe("ElementCall", () => {
});
});
describe("instance", () => {
describe("instance in a non-video room", () => {
let call: ElementCall;
let widget: Widget;
let messaging: Mocked<ClientWidgetApi>;
@ -590,6 +591,10 @@ describe("ElementCall", () => {
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
it("has intent m.prompt", () => {
expect(call.groupCall.getContent()["m.intent"]).toBe("m.prompt");
});
it("connects muted", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
audioMutedSpy.mockReturnValue(true);
@ -747,6 +752,59 @@ describe("ElementCall", () => {
expect(events).toEqual([new Set([alice]), new Set()]);
});
it("ends the call immediately if we're the last participant to leave", async () => {
await call.connect();
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
expect(onDestroy).toHaveBeenCalled();
call.off(CallEvent.Destroy, onDestroy);
});
it("ends the call after a random delay if the last participant leaves without ending it", async () => {
// Bob connects
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
{
"m.expires_ts": 1000 * 60 * 10,
"m.calls": [{
"m.call_id": call.groupCall.getStateKey()!,
"m.devices": [{ device_id: "bobweb", session_id: "1", feeds: [] }],
}],
},
bob.userId,
);
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
// Bob disconnects
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
{
"m.expires_ts": 1000 * 60 * 10,
"m.calls": [{
"m.call_id": call.groupCall.getStateKey()!,
"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();
call.off(CallEvent.Destroy, onDestroy);
});
describe("clean", () => {
const aliceWeb: IMyDevice = {
device_id: "aliceweb",
@ -848,4 +906,40 @@ describe("ElementCall", () => {
});
});
});
describe("instance in a video room", () => {
let call: ElementCall;
let widget: Widget;
let audioMutedSpy: jest.SpyInstance<boolean, []>;
let videoMutedSpy: jest.SpyInstance<boolean, []>;
beforeEach(async () => {
jest.useFakeTimers();
jest.setSystemTime(0);
jest.spyOn(room, "getType").mockReturnValue(RoomType.UnstableCall);
await ElementCall.create(room);
const maybeCall = ElementCall.get(room);
if (maybeCall === null) throw new Error("Failed to create call");
call = maybeCall;
({ widget, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
});
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
it("has intent m.room", () => {
expect(call.groupCall.getContent()["m.intent"]).toBe("m.room");
});
it("doesn't end the call when the last participant leaves", async () => {
await call.connect();
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
expect(onDestroy).not.toHaveBeenCalled();
call.off(CallEvent.Destroy, onDestroy);
});
});
});