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,
MatrixEvent,
Room,
RoomMember,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
@@ -33,7 +34,7 @@ import {
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
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 { CallFeed } from "../../src/webrtc/callFeed";
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 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 {
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 {
constructor(
public userId: string,

View File

@@ -1392,7 +1392,7 @@ describe('Call', function() {
it("ends call on onHangupReceived() if state is ringing", async () => {
expect(call.callHasEnded()).toBe(false);
call.state = CallState.Ringing;
(call as any).state = CallState.Ringing;
call.onHangupReceived({} as MCallHangupReject);
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) => {
expect(call.callHasEnded()).toBe(false);
call.state = state;
(call as any).state = state;
call.onRejectReceived({} as MCallHangupReject);
expect(call.callHasEnded()).toBe(

View File

@@ -17,13 +17,30 @@ limitations under the License.
import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { CallFeed } from "../../../src/webrtc/callFeed";
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", () => {
let client;
const roomId = "room1";
let client: TestClient;
let call: MockMatrixCall;
let feed: CallFeed;
beforeEach(() => {
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
call = new MockMatrixCall(roomId);
feed = new CallFeed({
client: client.client,
call: call.typed(),
roomId,
userId: "user1",
// @ts-ignore Mock
stream: new MockMediaStream("stream1"),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
});
});
afterEach(() => {
@@ -31,21 +48,6 @@ describe("CallFeed", () => {
});
describe("muting", () => {
let feed: CallFeed;
beforeEach(() => {
feed = new CallFeed({
client,
roomId: "room1",
userId: "user1",
// @ts-ignore Mock
stream: new MockMediaStream("stream1"),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
});
});
describe("muting by default", () => {
it("should mute audio by default", () => {
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,
MockMediaStreamTrack,
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';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { sleep } from "../../../src/utils";
@@ -41,16 +51,6 @@ import { CallFeed } from '../../../src/webrtc/callFeed';
import { CallEvent, CallState } from '../../../src/webrtc/call';
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 = [
{
getContent: () => ({
@@ -123,42 +123,6 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<G
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() {
beforeEach(function() {
installWebRTCMocks();
@@ -351,7 +315,7 @@ describe('Group Call', function() {
});
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 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");
call = new MockCall(room.roomId, groupCall.groupCallId);
call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
await groupCall.create();
});
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 });
// @ts-ignore Mock
@@ -514,10 +478,11 @@ describe('Group Call', function() {
});
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(
mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]),
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
);
let metadataUpdateResolve: () => void;
@@ -539,10 +504,11 @@ describe('Group Call', function() {
});
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(
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
@@ -698,6 +664,7 @@ describe('Group Call', function() {
expect(client1.sendToDevice).toHaveBeenCalled();
// @ts-ignore
const oldCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
@@ -719,6 +686,7 @@ describe('Group Call', function() {
// to even be created...
let newCall: MatrixCall | undefined;
while (
// @ts-ignore
(newCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)?.get(client2.deviceId)) === undefined
@@ -763,6 +731,7 @@ describe('Group Call', function() {
groupCall1.setMicrophoneMuted(false);
groupCall1.setLocalVideoMuted(false);
// @ts-ignore
const call = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
@@ -874,7 +843,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
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;
// @ts-ignore Mock
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
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;
// @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [
@@ -939,7 +914,7 @@ describe('Group Call', function() {
});
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);
@@ -948,7 +923,7 @@ describe('Group Call', function() {
});
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);
@@ -956,7 +931,7 @@ describe('Group Call', function() {
});
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;
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 () => {
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);
expect(mockCall.reject).not.toHaveBeenCalled();
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
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 () => {
const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockCall(room.roomId, groupCall.groupCallId);
const oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
newMockCall.callId = "not " + oldMockCall.callId;
@@ -989,6 +965,7 @@ describe('Group Call', function() {
expect(oldMockCall.hangup).toHaveBeenCalled();
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
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
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>([
[call.callId, call.typed()],
]);
@@ -1072,7 +1049,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
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.onNegotiateReceived({
getContent: () => ({

View File

@@ -334,7 +334,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public roomId?: string;
public callId: string;
public invitee?: string;
public state = CallState.Fledgling;
public hangupParty?: CallParty;
public hangupReason?: string;
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.
public isPtt = false;
private _state = CallState.Fledgling;
private readonly client: MatrixClient;
private readonly forceTURN?: boolean;
private readonly turnServers: Array<TurnServer>;
@@ -482,6 +482,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
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 {
return (this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack)
? CallType.Video
@@ -646,6 +656,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.feeds.push(new CallFeed({
client: this.client,
call: this,
roomId: this.roomId,
userId,
deviceId: this.getOpponentDeviceId(),
@@ -689,6 +700,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.feeds.push(new CallFeed({
client: this.client,
call: this,
roomId: this.roomId,
audioMuted: false,
videoMuted: false,
@@ -928,7 +940,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return;
}
this.setState(CallState.Ringing);
this.state = CallState.Ringing;
if (event.getLocalAge()) {
// 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) {
logger.debug(`Call ${this.callId} invite has expired. Hanging up.`);
this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended);
this.state = CallState.Ended;
this.stopAllMedia();
if (this.peerConn!.signalingState != 'closed') {
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
// 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)
this.setState(CallState.Ended);
this.state = CallState.Ended;
}
private shouldAnswerWithMediaType(
@@ -1004,7 +1016,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio");
const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video");
this.setState(CallState.WaitLocalMedia);
this.state = CallState.WaitLocalMedia;
this.waitForLocalAVStream = true;
try {
@@ -1034,7 +1046,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (answerWithVideo) {
// Try to answer without video
logger.warn(`Call ${this.callId} Failed to getUserMedia(), trying to getUserMedia() without video`);
this.setState(prevState);
this.state = prevState;
this.waitForLocalAVStream = false;
await this.answer(answerWithAudio, false);
} else {
@@ -1043,7 +1055,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}
}
} 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`);
// Now we wait for the negotiationneeded event
@@ -1530,7 +1542,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.inviteOrAnswerSent = true;
} catch (error) {
// 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);
let code = CallErrorCode.SendAnswer;
@@ -1627,7 +1639,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.pushLocalFeed(feed);
}
this.setState(CallState.CreateAnswer);
this.state = CallState.CreateAnswer;
let answer: RTCSessionDescriptionInit;
try {
@@ -1645,7 +1657,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// make sure we're still going
if (this.callHasEnded()) return;
this.setState(CallState.Connecting);
this.state = CallState.Connecting;
// Allow a short time for initial candidates to be gathered
await new Promise(resolve => {
@@ -1762,7 +1774,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.chooseOpponent(event);
await this.addBufferedIceCandidates();
this.setState(CallState.Connecting);
this.state = CallState.Connecting;
const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) {
@@ -2034,7 +2046,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.state = CallState.InviteSent;
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = undefined;
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
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? '')) {
clearTimeout(this.iceDisconnectedTimeout);
this.setState(CallState.Connected);
this.state = CallState.Connected;
if (!this.callLengthInterval) {
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)`);
this.hangup(CallErrorCode.IceFailed, false);
}, 30 * 1000);
this.setState(CallState.Connecting);
this.state = CallState.Connecting;
}
// 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);
};
private setState(state: CallState): void {
const oldState = this.state;
this.state = state;
this.emit(CallEvent.State, state, oldState);
}
/**
* Internal
* @param {string} eventType
@@ -2444,7 +2450,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.hangupParty = hangupParty;
this.hangupReason = hangupReason;
this.setState(CallState.Ended);
this.state = CallState.Ended;
if (this.inviteTimeout) {
clearTimeout(this.inviteTimeout);
@@ -2578,7 +2584,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (!audio) {
throw new Error("You CANNOT start a call without audio");
}
this.setState(CallState.WaitLocalMedia);
this.state = CallState.WaitLocalMedia;
try {
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 { logger } from "../logger";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { CallEvent, CallState, MatrixCall } from "./call";
const POLLING_INTERVAL = 200; // ms
export const SPEAKING_THRESHOLD = -60; // dB
@@ -40,6 +41,10 @@ export interface ICallFeedOpts {
* Whether or not the remote SDPStreamMetadata says video is muted
*/
videoMuted: boolean;
/**
* The MatrixCall which is the source of this CallFeed
*/
call?: MatrixCall;
}
export enum CallFeedEvent {
@@ -47,6 +52,7 @@ export enum CallFeedEvent {
MuteStateChanged = "mute_state_changed",
LocalVolumeChanged = "local_volume_changed",
VolumeChanged = "volume_changed",
ConnectedChanged = "connected_changed",
Speaking = "speaking",
Disposed = "disposed",
}
@@ -56,6 +62,7 @@ type EventHandlerMap = {
[CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
[CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void;
[CallFeedEvent.VolumeChanged]: (volume: number) => void;
[CallFeedEvent.ConnectedChanged]: (connected: boolean) => void;
[CallFeedEvent.Speaking]: (speaking: boolean) => void;
[CallFeedEvent.Disposed]: () => void;
};
@@ -69,6 +76,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
public speakingVolumeSamples: number[];
private client: MatrixClient;
private call?: MatrixCall;
private roomId?: string;
private audioMuted: boolean;
private videoMuted: boolean;
@@ -81,11 +89,13 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
private speaking = false;
private volumeLooperTimeout?: ReturnType<typeof setTimeout>;
private _disposed = false;
private _connected = false;
public constructor(opts: ICallFeedOpts) {
super();
this.client = opts.client;
this.call = opts.call;
this.roomId = opts.roomId;
this.userId = opts.userId;
this.deviceId = opts.deviceId;
@@ -101,6 +111,21 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
if (this.hasAudioTrack) {
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 {
@@ -145,6 +170,14 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
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 member of the callRoom
@@ -297,6 +330,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
public dispose(): void {
clearTimeout(this.volumeLooperTimeout);
this.stream?.removeEventListener("addtrack", this.onAddTrack);
this.call?.removeListener(CallEvent.State, this.onCallState);
if (this.audioContext) {
this.audioContext = undefined;
this.analyser = undefined;

View File

@@ -168,11 +168,11 @@ export class GroupCall extends TypedEventEmitter<
public localCallFeed?: CallFeed;
public localScreenshareFeed?: CallFeed;
public localDesktopCapturerSourceId?: string;
public readonly calls = new Map<RoomMember, Map<string, MatrixCall>>();
public readonly userMediaFeeds: CallFeed[] = [];
public readonly screenshareFeeds: CallFeed[] = [];
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 activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
private retryCallLoopInterval?: ReturnType<typeof setTimeout>;