1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Don't expose calls on GroupCall (#2941)

This commit is contained in:
Šimon Brandner
2022-12-05 18:44:13 +01:00
committed by GitHub
parent 4a4d493856
commit 2c8eece5ca
7 changed files with 197 additions and 107 deletions

View File

@@ -26,6 +26,7 @@ import {
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
Room, Room,
RoomMember,
RoomState, RoomState,
RoomStateEvent, RoomStateEvent,
RoomStateEventHandlerMap, RoomStateEventHandlerMap,
@@ -33,7 +34,7 @@ import {
import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter"; import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call"; import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call";
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler"; import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { CallFeed } from "../../src/webrtc/callFeed"; import { CallFeed } from "../../src/webrtc/callFeed";
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall"; import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
@@ -83,6 +84,17 @@ export const DUMMY_SDP = (
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler"; export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler"; export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
export const FAKE_ROOM_ID = "!fake:test.dummy";
export const FAKE_CONF_ID = "fakegroupcallid";
export const FAKE_USER_ID_1 = "@alice:test.dummy";
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
export const FAKE_SESSION_ID_1 = "alice1";
export const FAKE_USER_ID_2 = "@bob:test.dummy";
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
export const FAKE_SESSION_ID_2 = "bob1";
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
class MockMediaStreamAudioSourceNode { class MockMediaStreamAudioSourceNode {
public connect() {} public connect() {}
} }
@@ -431,6 +443,43 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
} }
} }
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
constructor(public roomId: string, public groupCallId?: string) {
super();
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"),
};
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public on = jest.fn();
public removeListener = jest.fn();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string | undefined {
return this.opponentDeviceId;
}
public typed(): MatrixCall { return this as unknown as MatrixCall; }
}
export class MockCallFeed { export class MockCallFeed {
constructor( constructor(
public userId: string, public userId: string,

View File

@@ -1392,7 +1392,7 @@ describe('Call', function() {
it("ends call on onHangupReceived() if state is ringing", async () => { it("ends call on onHangupReceived() if state is ringing", async () => {
expect(call.callHasEnded()).toBe(false); expect(call.callHasEnded()).toBe(false);
call.state = CallState.Ringing; (call as any).state = CallState.Ringing;
call.onHangupReceived({} as MCallHangupReject); call.onHangupReceived({} as MCallHangupReject);
expect(call.callHasEnded()).toBe(true); expect(call.callHasEnded()).toBe(true);
@@ -1424,7 +1424,7 @@ describe('Call', function() {
)("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => { )("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => {
expect(call.callHasEnded()).toBe(false); expect(call.callHasEnded()).toBe(false);
call.state = state; (call as any).state = state;
call.onRejectReceived({} as MCallHangupReject); call.onRejectReceived({} as MCallHangupReject);
expect(call.callHasEnded()).toBe( expect(call.callHasEnded()).toBe(

View File

@@ -17,26 +17,23 @@ limitations under the License.
import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { CallFeed } from "../../../src/webrtc/callFeed"; import { CallFeed } from "../../../src/webrtc/callFeed";
import { TestClient } from "../../TestClient"; import { TestClient } from "../../TestClient";
import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc"; import { MockMatrixCall, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
import { CallEvent, CallState } from "../../../src/webrtc/call";
describe("CallFeed", () => { describe("CallFeed", () => {
let client; const roomId = "room1";
let client: TestClient;
beforeEach(() => { let call: MockMatrixCall;
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
});
afterEach(() => {
client.stop();
});
describe("muting", () => {
let feed: CallFeed; let feed: CallFeed;
beforeEach(() => { beforeEach(() => {
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
call = new MockMatrixCall(roomId);
feed = new CallFeed({ feed = new CallFeed({
client, client: client.client,
roomId: "room1", call: call.typed(),
roomId,
userId: "user1", userId: "user1",
// @ts-ignore Mock // @ts-ignore Mock
stream: new MockMediaStream("stream1"), stream: new MockMediaStream("stream1"),
@@ -46,6 +43,11 @@ describe("CallFeed", () => {
}); });
}); });
afterEach(() => {
client.stop();
});
describe("muting", () => {
describe("muting by default", () => { describe("muting by default", () => {
it("should mute audio by default", () => { it("should mute audio by default", () => {
expect(feed.isAudioMuted()).toBeTruthy(); expect(feed.isAudioMuted()).toBeTruthy();
@@ -86,4 +88,23 @@ describe("CallFeed", () => {
}); });
}); });
}); });
describe("connected", () => {
it.each([true, false])("should always be connected, if isLocal()", (val: boolean) => {
// @ts-ignore
feed._connected = val;
jest.spyOn(feed, "isLocal").mockReturnValue(true);
expect(feed.connected).toBeTruthy();
});
it.each([
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
call.emit(CallEvent.State, state);
expect(feed.connected).toBe(expected);
});
});
}); });

View File

@@ -33,6 +33,16 @@ import {
MockMediaStream, MockMediaStream,
MockMediaStreamTrack, MockMediaStreamTrack,
MockRTCPeerConnection, MockRTCPeerConnection,
MockMatrixCall,
FAKE_ROOM_ID,
FAKE_USER_ID_1,
FAKE_CONF_ID,
FAKE_DEVICE_ID_2,
FAKE_SESSION_ID_2,
FAKE_USER_ID_2,
FAKE_DEVICE_ID_1,
FAKE_SESSION_ID_1,
FAKE_USER_ID_3,
} from '../../test-utils/webrtc'; } from '../../test-utils/webrtc';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { sleep } from "../../../src/utils"; import { sleep } from "../../../src/utils";
@@ -41,16 +51,6 @@ import { CallFeed } from '../../../src/webrtc/callFeed';
import { CallEvent, CallState } from '../../../src/webrtc/call'; import { CallEvent, CallState } from '../../../src/webrtc/call';
import { flushPromises } from '../../test-utils/flushPromises'; import { flushPromises } from '../../test-utils/flushPromises';
const FAKE_ROOM_ID = "!fake:test.dummy";
const FAKE_CONF_ID = "fakegroupcallid";
const FAKE_USER_ID_1 = "@alice:test.dummy";
const FAKE_DEVICE_ID_1 = "@AAAAAA";
const FAKE_SESSION_ID_1 = "alice1";
const FAKE_USER_ID_2 = "@bob:test.dummy";
const FAKE_DEVICE_ID_2 = "@BBBBBB";
const FAKE_SESSION_ID_2 = "bob1";
const FAKE_USER_ID_3 = "@charlie:test.dummy";
const FAKE_STATE_EVENTS = [ const FAKE_STATE_EVENTS = [
{ {
getContent: () => ({ getContent: () => ({
@@ -123,42 +123,6 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<G
return groupCall; return groupCall;
}; };
class MockCall {
constructor(public roomId: string, public groupCallId: string) {
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"),
};
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public on = jest.fn();
public removeListener = jest.fn();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string {
return this.opponentDeviceId;
}
public typed(): MatrixCall { return this as unknown as MatrixCall; }
}
describe('Group Call', function() { describe('Group Call', function() {
beforeEach(function() { beforeEach(function() {
installWebRTCMocks(); installWebRTCMocks();
@@ -351,7 +315,7 @@ describe('Group Call', function() {
}); });
describe("call feeds changing", () => { describe("call feeds changing", () => {
let call: MockCall; let call: MockMatrixCall;
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current")); const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new")); const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new"));
@@ -361,13 +325,13 @@ describe('Group Call', function() {
jest.spyOn(groupCall, "emit"); jest.spyOn(groupCall, "emit");
call = new MockCall(room.roomId, groupCall.groupCallId); call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
await groupCall.create(); await groupCall.create();
}); });
it("ignores changes, if we can't get user id of opponent", async () => { it("ignores changes, if we can't get user id of opponent", async () => {
const call = new MockCall(room.roomId, groupCall.groupCallId); const call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined }); jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined });
// @ts-ignore Mock // @ts-ignore Mock
@@ -514,10 +478,11 @@ describe('Group Call', function() {
}); });
it("sends metadata updates before unmuting in PTT mode", async () => { it("sends metadata updates before unmuting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
// @ts-ignore
groupCall.calls.set( groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember, mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
); );
let metadataUpdateResolve: () => void; let metadataUpdateResolve: () => void;
@@ -539,10 +504,11 @@ describe('Group Call', function() {
}); });
it("sends metadata updates after muting in PTT mode", async () => { it("sends metadata updates after muting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
// @ts-ignore
groupCall.calls.set( groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember, mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
); );
// the call starts muted, so unmute to get in the right state to test // the call starts muted, so unmute to get in the right state to test
@@ -698,6 +664,7 @@ describe('Group Call', function() {
expect(client1.sendToDevice).toHaveBeenCalled(); expect(client1.sendToDevice).toHaveBeenCalled();
// @ts-ignore
const oldCall = groupCall1.calls.get( const oldCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!, groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!; )!.get(client2.deviceId)!;
@@ -719,6 +686,7 @@ describe('Group Call', function() {
// to even be created... // to even be created...
let newCall: MatrixCall | undefined; let newCall: MatrixCall | undefined;
while ( while (
// @ts-ignore
(newCall = groupCall1.calls.get( (newCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!, groupCall1.room.getMember(client2.userId)!,
)?.get(client2.deviceId)) === undefined )?.get(client2.deviceId)) === undefined
@@ -763,6 +731,7 @@ describe('Group Call', function() {
groupCall1.setMicrophoneMuted(false); groupCall1.setMicrophoneMuted(false);
groupCall1.setLocalVideoMuted(false); groupCall1.setLocalVideoMuted(false);
// @ts-ignore
const call = groupCall1.calls.get( const call = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!, groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!; )!.get(client2.deviceId)!;
@@ -874,7 +843,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created // It takes a bit of time for the calls to get created
await sleep(10); await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; // @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock // @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [ call.pushRemoteFeed(new MockMediaStream("stream", [
@@ -897,7 +869,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created // It takes a bit of time for the calls to get created
await sleep(10); await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; // @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock // @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [ call.pushRemoteFeed(new MockMediaStream("stream", [
@@ -939,7 +914,7 @@ describe('Group Call', function() {
}); });
it("ignores incoming calls for other rooms", async () => { it("ignores incoming calls for other rooms", async () => {
const mockCall = new MockCall("!someotherroom.fake.dummy", groupCall.groupCallId); const mockCall = new MockMatrixCall("!someotherroom.fake.dummy", groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@@ -948,7 +923,7 @@ describe('Group Call', function() {
}); });
it("rejects incoming calls for the wrong group call", async () => { it("rejects incoming calls for the wrong group call", async () => {
const mockCall = new MockCall(room.roomId, "not " + groupCall.groupCallId); const mockCall = new MockMatrixCall(room.roomId, "not " + groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@@ -956,7 +931,7 @@ describe('Group Call', function() {
}); });
it("ignores incoming calls not in the ringing state", async () => { it("ignores incoming calls not in the ringing state", async () => {
const mockCall = new MockCall(room.roomId, groupCall.groupCallId); const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockCall.state = CallState.Connected; mockCall.state = CallState.Connected;
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@@ -966,12 +941,13 @@ describe('Group Call', function() {
}); });
it("answers calls for the right room & group call ID", async () => { it("answers calls for the right room & group call ID", async () => {
const mockCall = new MockCall(room.roomId, groupCall.groupCallId); const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
expect(mockCall.reject).not.toHaveBeenCalled(); expect(mockCall.reject).not.toHaveBeenCalled();
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled(); expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[ expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!, groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, mockCall]]), new Map([[FAKE_DEVICE_ID_1, mockCall]]),
@@ -979,8 +955,8 @@ describe('Group Call', function() {
}); });
it("replaces calls if it already has one with the same user", async () => { it("replaces calls if it already has one with the same user", async () => {
const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId); const oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockCall(room.roomId, groupCall.groupCallId); const newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
newMockCall.callId = "not " + oldMockCall.callId; newMockCall.callId = "not " + oldMockCall.callId;
@@ -989,6 +965,7 @@ describe('Group Call', function() {
expect(oldMockCall.hangup).toHaveBeenCalled(); expect(oldMockCall.hangup).toHaveBeenCalled();
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled(); expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[ expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!, groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, newMockCall]]), new Map([[FAKE_DEVICE_ID_1, newMockCall]]),
@@ -999,7 +976,7 @@ describe('Group Call', function() {
// First we leave the call since we have already entered // First we leave the call since we have already entered
groupCall.leave(); groupCall.leave();
const call = new MockCall(room.roomId, groupCall.groupCallId); const call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockClient.callEventHandler!.calls = new Map<string, MatrixCall>([ mockClient.callEventHandler!.calls = new Map<string, MatrixCall>([
[call.callId, call.typed()], [call.callId, call.typed()],
]); ]);
@@ -1072,7 +1049,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created // It takes a bit of time for the calls to get created
await sleep(10); await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; // @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
call.onNegotiateReceived({ call.onNegotiateReceived({
getContent: () => ({ getContent: () => ({

View File

@@ -334,7 +334,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public roomId?: string; public roomId?: string;
public callId: string; public callId: string;
public invitee?: string; public invitee?: string;
public state = CallState.Fledgling;
public hangupParty?: CallParty; public hangupParty?: CallParty;
public hangupReason?: string; public hangupReason?: string;
public direction?: CallDirection; public direction?: CallDirection;
@@ -346,6 +345,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// This should be set by the consumer on incoming & outgoing calls. // This should be set by the consumer on incoming & outgoing calls.
public isPtt = false; public isPtt = false;
private _state = CallState.Fledgling;
private readonly client: MatrixClient; private readonly client: MatrixClient;
private readonly forceTURN?: boolean; private readonly forceTURN?: boolean;
private readonly turnServers: Array<TurnServer>; private readonly turnServers: Array<TurnServer>;
@@ -482,6 +482,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return this.remoteAssertedIdentity; return this.remoteAssertedIdentity;
} }
public get state(): CallState {
return this._state;
}
private set state(state: CallState) {
const oldState = this._state;
this._state = state;
this.emit(CallEvent.State, state, oldState);
}
public get type(): CallType { public get type(): CallType {
return (this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack) return (this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack)
? CallType.Video ? CallType.Video
@@ -646,6 +656,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.feeds.push(new CallFeed({ this.feeds.push(new CallFeed({
client: this.client, client: this.client,
call: this,
roomId: this.roomId, roomId: this.roomId,
userId, userId,
deviceId: this.getOpponentDeviceId(), deviceId: this.getOpponentDeviceId(),
@@ -689,6 +700,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.feeds.push(new CallFeed({ this.feeds.push(new CallFeed({
client: this.client, client: this.client,
call: this,
roomId: this.roomId, roomId: this.roomId,
audioMuted: false, audioMuted: false,
videoMuted: false, videoMuted: false,
@@ -928,7 +940,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return; return;
} }
this.setState(CallState.Ringing); this.state = CallState.Ringing;
if (event.getLocalAge()) { if (event.getLocalAge()) {
// Time out the call if it's ringing for too long // Time out the call if it's ringing for too long
@@ -936,7 +948,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (this.state == CallState.Ringing) { if (this.state == CallState.Ringing) {
logger.debug(`Call ${this.callId} invite has expired. Hanging up.`); logger.debug(`Call ${this.callId} invite has expired. Hanging up.`);
this.hangupParty = CallParty.Remote; // effectively this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended); this.state = CallState.Ended;
this.stopAllMedia(); this.stopAllMedia();
if (this.peerConn!.signalingState != 'closed') { if (this.peerConn!.signalingState != 'closed') {
this.peerConn!.close(); this.peerConn!.close();
@@ -963,7 +975,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// perverse as it may seem, sometimes we want to instantiate a call with a // perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events // hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up) // come in reverse order and we want to remember that a call has been hung up)
this.setState(CallState.Ended); this.state = CallState.Ended;
} }
private shouldAnswerWithMediaType( private shouldAnswerWithMediaType(
@@ -1004,7 +1016,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio"); const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio");
const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video"); const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video");
this.setState(CallState.WaitLocalMedia); this.state = CallState.WaitLocalMedia;
this.waitForLocalAVStream = true; this.waitForLocalAVStream = true;
try { try {
@@ -1034,7 +1046,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (answerWithVideo) { if (answerWithVideo) {
// Try to answer without video // Try to answer without video
logger.warn(`Call ${this.callId} Failed to getUserMedia(), trying to getUserMedia() without video`); logger.warn(`Call ${this.callId} Failed to getUserMedia(), trying to getUserMedia() without video`);
this.setState(prevState); this.state = prevState;
this.waitForLocalAVStream = false; this.waitForLocalAVStream = false;
await this.answer(answerWithAudio, false); await this.answer(answerWithAudio, false);
} else { } else {
@@ -1043,7 +1055,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
} }
} else if (this.waitForLocalAVStream) { } else if (this.waitForLocalAVStream) {
this.setState(CallState.WaitLocalMedia); this.state = CallState.WaitLocalMedia;
} }
} }
@@ -1495,7 +1507,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}); });
} }
this.setState(CallState.CreateOffer); this.state = CallState.CreateOffer;
logger.debug(`Call ${this.callId} gotUserMediaForInvite`); logger.debug(`Call ${this.callId} gotUserMediaForInvite`);
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
@@ -1530,7 +1542,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.inviteOrAnswerSent = true; this.inviteOrAnswerSent = true;
} catch (error) { } catch (error) {
// We've failed to answer: back to the ringing state // We've failed to answer: back to the ringing state
this.setState(CallState.Ringing); this.state = CallState.Ringing;
if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event); if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SendAnswer; let code = CallErrorCode.SendAnswer;
@@ -1627,7 +1639,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.pushLocalFeed(feed); this.pushLocalFeed(feed);
} }
this.setState(CallState.CreateAnswer); this.state = CallState.CreateAnswer;
let answer: RTCSessionDescriptionInit; let answer: RTCSessionDescriptionInit;
try { try {
@@ -1645,7 +1657,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// make sure we're still going // make sure we're still going
if (this.callHasEnded()) return; if (this.callHasEnded()) return;
this.setState(CallState.Connecting); this.state = CallState.Connecting;
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
await new Promise(resolve => { await new Promise(resolve => {
@@ -1762,7 +1774,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.chooseOpponent(event); this.chooseOpponent(event);
await this.addBufferedIceCandidates(); await this.addBufferedIceCandidates();
this.setState(CallState.Connecting); this.state = CallState.Connecting;
const sdpStreamMetadata = content[SDPStreamMetadataKey]; const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) { if (sdpStreamMetadata) {
@@ -2034,7 +2046,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.sendCandidateQueue(); this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) { if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true; this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent); this.state = CallState.InviteSent;
this.inviteTimeout = setTimeout(() => { this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = undefined; this.inviteTimeout = undefined;
if (this.state === CallState.InviteSent) { if (this.state === CallState.InviteSent) {
@@ -2088,7 +2100,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// chrome doesn't implement any of the 'onstarted' events yet // chrome doesn't implement any of the 'onstarted' events yet
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? '')) { if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? '')) {
clearTimeout(this.iceDisconnectedTimeout); clearTimeout(this.iceDisconnectedTimeout);
this.setState(CallState.Connected); this.state = CallState.Connected;
if (!this.callLengthInterval) { if (!this.callLengthInterval) {
this.callLengthInterval = setInterval(() => { this.callLengthInterval = setInterval(() => {
@@ -2112,7 +2124,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`); logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`);
this.hangup(CallErrorCode.IceFailed, false); this.hangup(CallErrorCode.IceFailed, false);
}, 30 * 1000); }, 30 * 1000);
this.setState(CallState.Connecting); this.state = CallState.Connecting;
} }
// In PTT mode, override feed status to muted when we lose connection to // In PTT mode, override feed status to muted when we lose connection to
@@ -2244,12 +2256,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true); this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
}; };
private setState(state: CallState): void {
const oldState = this.state;
this.state = state;
this.emit(CallEvent.State, state, oldState);
}
/** /**
* Internal * Internal
* @param {string} eventType * @param {string} eventType
@@ -2444,7 +2450,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.hangupParty = hangupParty; this.hangupParty = hangupParty;
this.hangupReason = hangupReason; this.hangupReason = hangupReason;
this.setState(CallState.Ended); this.state = CallState.Ended;
if (this.inviteTimeout) { if (this.inviteTimeout) {
clearTimeout(this.inviteTimeout); clearTimeout(this.inviteTimeout);
@@ -2578,7 +2584,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (!audio) { if (!audio) {
throw new Error("You CANNOT start a call without audio"); throw new Error("You CANNOT start a call without audio");
} }
this.setState(CallState.WaitLocalMedia); this.state = CallState.WaitLocalMedia;
try { try {
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video); const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);

View File

@@ -20,6 +20,7 @@ import { MatrixClient } from "../client";
import { RoomMember } from "../models/room-member"; import { RoomMember } from "../models/room-member";
import { logger } from "../logger"; import { logger } from "../logger";
import { TypedEventEmitter } from "../models/typed-event-emitter"; import { TypedEventEmitter } from "../models/typed-event-emitter";
import { CallEvent, CallState, MatrixCall } from "./call";
const POLLING_INTERVAL = 200; // ms const POLLING_INTERVAL = 200; // ms
export const SPEAKING_THRESHOLD = -60; // dB export const SPEAKING_THRESHOLD = -60; // dB
@@ -40,6 +41,10 @@ export interface ICallFeedOpts {
* Whether or not the remote SDPStreamMetadata says video is muted * Whether or not the remote SDPStreamMetadata says video is muted
*/ */
videoMuted: boolean; videoMuted: boolean;
/**
* The MatrixCall which is the source of this CallFeed
*/
call?: MatrixCall;
} }
export enum CallFeedEvent { export enum CallFeedEvent {
@@ -47,6 +52,7 @@ export enum CallFeedEvent {
MuteStateChanged = "mute_state_changed", MuteStateChanged = "mute_state_changed",
LocalVolumeChanged = "local_volume_changed", LocalVolumeChanged = "local_volume_changed",
VolumeChanged = "volume_changed", VolumeChanged = "volume_changed",
ConnectedChanged = "connected_changed",
Speaking = "speaking", Speaking = "speaking",
Disposed = "disposed", Disposed = "disposed",
} }
@@ -56,6 +62,7 @@ type EventHandlerMap = {
[CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
[CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void; [CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void;
[CallFeedEvent.VolumeChanged]: (volume: number) => void; [CallFeedEvent.VolumeChanged]: (volume: number) => void;
[CallFeedEvent.ConnectedChanged]: (connected: boolean) => void;
[CallFeedEvent.Speaking]: (speaking: boolean) => void; [CallFeedEvent.Speaking]: (speaking: boolean) => void;
[CallFeedEvent.Disposed]: () => void; [CallFeedEvent.Disposed]: () => void;
}; };
@@ -69,6 +76,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
public speakingVolumeSamples: number[]; public speakingVolumeSamples: number[];
private client: MatrixClient; private client: MatrixClient;
private call?: MatrixCall;
private roomId?: string; private roomId?: string;
private audioMuted: boolean; private audioMuted: boolean;
private videoMuted: boolean; private videoMuted: boolean;
@@ -81,11 +89,13 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
private speaking = false; private speaking = false;
private volumeLooperTimeout?: ReturnType<typeof setTimeout>; private volumeLooperTimeout?: ReturnType<typeof setTimeout>;
private _disposed = false; private _disposed = false;
private _connected = false;
public constructor(opts: ICallFeedOpts) { public constructor(opts: ICallFeedOpts) {
super(); super();
this.client = opts.client; this.client = opts.client;
this.call = opts.call;
this.roomId = opts.roomId; this.roomId = opts.roomId;
this.userId = opts.userId; this.userId = opts.userId;
this.deviceId = opts.deviceId; this.deviceId = opts.deviceId;
@@ -101,6 +111,21 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
if (this.hasAudioTrack) { if (this.hasAudioTrack) {
this.initVolumeMeasuring(); this.initVolumeMeasuring();
} }
if (opts.call) {
opts.call.addListener(CallEvent.State, this.onCallState);
this.onCallState(opts.call.state);
}
}
public get connected(): boolean {
// Local feeds are always considered connected
return this.isLocal() || this._connected;
}
private set connected(connected: boolean) {
this._connected = connected;
this.emit(CallFeedEvent.ConnectedChanged, this.connected);
} }
private get hasAudioTrack(): boolean { private get hasAudioTrack(): boolean {
@@ -145,6 +170,14 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
this.emit(CallFeedEvent.NewStream, this.stream); this.emit(CallFeedEvent.NewStream, this.stream);
}; };
private onCallState = (state: CallState): void => {
if (state === CallState.Connected) {
this.connected = true;
} else if (state === CallState.Connecting) {
this.connected = false;
}
};
/** /**
* Returns callRoom member * Returns callRoom member
* @returns member of the callRoom * @returns member of the callRoom
@@ -297,6 +330,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
public dispose(): void { public dispose(): void {
clearTimeout(this.volumeLooperTimeout); clearTimeout(this.volumeLooperTimeout);
this.stream?.removeEventListener("addtrack", this.onAddTrack); this.stream?.removeEventListener("addtrack", this.onAddTrack);
this.call?.removeListener(CallEvent.State, this.onCallState);
if (this.audioContext) { if (this.audioContext) {
this.audioContext = undefined; this.audioContext = undefined;
this.analyser = undefined; this.analyser = undefined;

View File

@@ -168,11 +168,11 @@ export class GroupCall extends TypedEventEmitter<
public localCallFeed?: CallFeed; public localCallFeed?: CallFeed;
public localScreenshareFeed?: CallFeed; public localScreenshareFeed?: CallFeed;
public localDesktopCapturerSourceId?: string; public localDesktopCapturerSourceId?: string;
public readonly calls = new Map<RoomMember, Map<string, MatrixCall>>();
public readonly userMediaFeeds: CallFeed[] = []; public readonly userMediaFeeds: CallFeed[] = [];
public readonly screenshareFeeds: CallFeed[] = []; public readonly screenshareFeeds: CallFeed[] = [];
public groupCallId: string; public groupCallId: string;
private readonly calls = new Map<RoomMember, Map<string, MatrixCall>>(); // RoomMember -> device ID -> MatrixCall
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // User ID -> device ID -> handlers private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // User ID -> device ID -> handlers
private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>; private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
private retryCallLoopInterval?: ReturnType<typeof setTimeout>; private retryCallLoopInterval?: ReturnType<typeof setTimeout>;