1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Merge remote-tracking branch 'upstream/develop' into fix/call-notifs

This commit is contained in:
Šimon Brandner
2021-09-02 16:32:26 +02:00
14 changed files with 575 additions and 169 deletions

View File

@@ -1,3 +1,19 @@
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
===================================================================================================
## 🦖 Deprecations
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
## ✨ Features
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
## 🐛 Bug Fixes
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
===================================================================================================

View File

@@ -1,6 +1,6 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));

View File

@@ -1,6 +1,8 @@
<html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8"/>
<link rel="icon" href="data:,">
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "12.3.1",
"version": "12.4.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",

View File

@@ -55,12 +55,20 @@ export enum JoinRule {
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/
Private = "private",
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
Knock = "knock",
Restricted = "restricted",
}
export enum RestrictedAllowType {
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
RoomMembership = "m.room_membership",
}
export interface IJoinRuleEventContent {
join_rule: JoinRule; // eslint-disable-line camelcase
allow?: {
type: RestrictedAllowType;
room_id: string; // eslint-disable-line camelcase
}[];
}
export enum GuestAccess {

View File

@@ -380,6 +380,11 @@ export interface IStartClientOpts {
* This should be in the order of hours. Default: undefined.
*/
clientWellKnownPollPeriod?: number;
/**
* @experimental
*/
experimentalThreadSupport?: boolean;
}
export interface IStoredClientOpts extends IStartClientOpts {

View File

@@ -28,7 +28,9 @@ import { EventType, MsgType, RelationType } from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';
/**
* Enum for event statuses.
@@ -192,6 +194,12 @@ export class MatrixEvent extends EventEmitter {
*/
private txnId: string = null;
/**
* @experimental
* A reference to the thread this event belongs to
*/
private thread: Thread = null;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
@@ -213,6 +221,8 @@ export class MatrixEvent extends EventEmitter {
*/
public verificationRequest = null;
private readonly reEmitter: ReEmitter;
/**
* Construct a Matrix Event object
* @constructor
@@ -262,6 +272,7 @@ export class MatrixEvent extends EventEmitter {
this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge();
this.reEmitter = new ReEmitter(this);
}
/**
@@ -382,6 +393,15 @@ export class MatrixEvent extends EventEmitter {
return this.event.content || {};
}
/**
* @experimental
* Get the event ID of the replied event
*/
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
}
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
@@ -1285,6 +1305,21 @@ export class MatrixEvent extends EventEmitter {
public getTxnId(): string | undefined {
return this.txnId;
}
/**
* @experimental
*/
public setThread(thread: Thread): void {
this.thread = thread;
this.reEmitter.reEmit(thread, ["Thread.ready", "Thread.update"]);
}
/**
* @experimental
*/
public getThread(): Thread {
return this.thread;
}
}
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted

View File

@@ -26,6 +26,7 @@ import * as utils from "../utils";
import { EventType } from "../@types/event";
import { MatrixEvent } from "./event";
import { MatrixClient } from "../client";
import { IJoinRuleEventContent, JoinRule } from "../@types/partials";
// possible statuses for out-of-band member loading
enum OobStatus {
@@ -728,10 +729,10 @@ export class RoomState extends EventEmitter {
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
public getJoinRule(): string {
public getJoinRule(): JoinRule {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
return joinRuleContent["join_rule"] || "invite";
const joinRuleContent = joinRuleEvent?.getContent<IJoinRuleEventContent>() ?? {};
return joinRuleContent["join_rule"] || JoinRule.Invite;
}
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {

View File

@@ -32,9 +32,10 @@ import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { ResizeMethod } from "../@types/partials";
import { JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
import { Thread } from "./thread";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -145,6 +146,11 @@ export class Room extends EventEmitter {
public oldState: RoomState;
public currentState: RoomState;
/**
* @experimental
*/
public threads = new Set<Thread>();
/**
* Construct a new Room.
*
@@ -857,13 +863,26 @@ export class Room extends EventEmitter {
}
/**
* Get an event which is stored in our unfiltered timeline set
* Get an event which is stored in our unfiltered timeline set or in a thread
*
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
public findEventById(eventId: string): MatrixEvent | undefined {
return this.getUnfilteredTimelineSet().findEventById(eventId);
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
if (event) {
return event;
} else {
const threads = this.getThreads();
for (let i = 0; i < threads.length; i++) {
const thread = threads[i];
event = thread.findEventById(eventId);
if (event) {
return event;
}
}
}
}
/**
@@ -1049,6 +1068,54 @@ export class Room extends EventEmitter {
);
}
/**
* @experimental
*/
public addThread(thread: Thread): Set<Thread> {
this.threads.add(thread);
if (!thread.ready) {
thread.once("Thread.ready", this.dedupeThreads);
this.emit("Thread.update", thread);
this.reEmitter.reEmit(thread, ["Thread.update", "Thread.ready"]);
}
return this.threads;
}
/**
* @experimental
*/
public getThread(eventId: string): Thread {
return this.getThreads().find(thread => {
return thread.id === eventId;
});
}
/**
* @experimental
*/
public getThreads(): Thread[] {
return Array.from(this.threads.values());
}
/**
* Two threads starting from a different child event can end up
* with the same event root. This method ensures that the duplicates
* are removed
* @experimental
*/
private dedupeThreads = (readyThread): void => {
const threads = Array.from(this.threads);
if (threads.includes(readyThread)) {
this.threads = new Set(threads.filter(thread => {
if (readyThread.id === thread.id && readyThread !== thread) {
return false;
} else {
return true;
}
}));
}
};
/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
@@ -1225,6 +1292,28 @@ export class Room extends EventEmitter {
}
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public addThreadedEvent(event: MatrixEvent): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
}
}
let thread = this.findEventById(event.replyEventId)?.getThread();
if (thread) {
thread.addEvent(event);
} else {
thread = new Thread([event], this, this.client);
this.addThread(thread);
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
@@ -1973,7 +2062,7 @@ export class Room extends EventEmitter {
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
public getJoinRule(): string {
public getJoinRule(): JoinRule {
return this.currentState.getJoinRule();
}

186
src/models/thread.ts Normal file
View File

@@ -0,0 +1,186 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { MatrixClient } from "../matrix";
import { MatrixEvent } from "./event";
import { EventTimelineSet } from './event-timeline-set';
import { Room } from './room';
/**
* @experimental
*/
export class Thread extends EventEmitter {
/**
* A reference to the event ID at the top of the thread
*/
private root: string;
/**
* A reference to all the events ID at the bottom of the threads
*/
public tail = new Set<string>();
private _timelineSet: EventTimelineSet;
constructor(
events: MatrixEvent[] = [],
public readonly room: Room,
public readonly client: MatrixClient,
) {
super();
this._timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
});
events.forEach(event => this.addEvent(event));
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
* Will fire "Thread.update"
* @param event The event to add
*/
public async addEvent(event: MatrixEvent): Promise<void> {
if (this._timelineSet.findEventById(event.getId()) || event.status !== null) {
return;
}
if (this.tail.has(event.replyEventId)) {
this.tail.delete(event.replyEventId);
}
this.tail.add(event.getId());
if (!event.replyEventId || !this._timelineSet.findEventById(event.replyEventId)) {
this.root = event.getId();
}
event.setThread(this);
this._timelineSet.addLiveEvent(event);
if (this.ready) {
this.client.decryptEventIfNeeded(event, {});
this.emit("Thread.update", this);
} else {
this.emit("Thread.update", this);
}
}
/**
* Completes the reply chain with all events
* missing from the current sync data
* Will fire "Thread.ready"
*/
public async fetchReplyChain(): Promise<void> {
if (!this.ready) {
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
if (!mxEvent) {
mxEvent = await this.fetchEventById(
this.rootEvent.getRoomId(),
this.rootEvent.replyEventId,
);
}
this.addEvent(mxEvent);
if (mxEvent.replyEventId) {
await this.fetchReplyChain();
} else {
await this.decryptEvents();
this.emit("Thread.ready", this);
}
}
}
private async decryptEvents(): Promise<void> {
await Promise.allSettled(
Array.from(this._timelineSet.getLiveTimeline().getEvents()).map(event => {
return this.client.decryptEventIfNeeded(event, {});
}),
);
}
/**
* Fetches an event over the network
*/
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
const response = await this.client.http.authedRequest(
undefined,
"GET",
`/rooms/${roomId}/event/${eventId}`,
);
return new MatrixEvent(response);
}
/**
* Finds an event by ID in the current thread
*/
public findEventById(eventId: string) {
return this._timelineSet.findEventById(eventId);
}
/**
* Determines thread's ready status
*/
public get ready(): boolean {
return this.rootEvent.replyEventId === undefined;
}
/**
* The thread ID, which is the same as the root event ID
*/
public get id(): string {
return this.root;
}
/**
* The thread root event
*/
public get rootEvent(): MatrixEvent {
return this.findEventById(this.root);
}
/**
* The number of messages in the thread
*/
public get length(): number {
return this._timelineSet.getLiveTimeline().getEvents().length;
}
/**
* A set of mxid participating to the thread
*/
public get participants(): Set<string> {
const participants = new Set<string>();
this._timelineSet.getLiveTimeline().getEvents().forEach(event => {
participants.add(event.getSender());
});
return participants;
}
/**
* A read-only getter to access the timeline set
*/
public get timelineSet(): EventTimelineSet {
return this._timelineSet;
}
/**
* A getter for the last event added to the thread
*/
public get replyToEvent(): MatrixEvent {
const events = this._timelineSet.getLiveTimeline().getEvents();
return events[events.length -1];
}
}

View File

@@ -148,6 +148,7 @@ export class SyncApi {
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000);
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = (roomId: string) => {
@@ -283,8 +284,9 @@ export class SyncApi {
return;
}
leaveObj.timeline = leaveObj.timeline || {};
const timelineEvents =
this.mapSyncEventsFormat(leaveObj.timeline, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
@@ -293,17 +295,39 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
});
return rooms;
});
}
/**
* Split events between the ones that will end up in the main
* room timeline versus the one that need to be processed in a thread
* @experimental
*/
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
if (this.opts.experimentalThreadSupport) {
return events.reduce((memo, event: MatrixEvent) => {
memo[event.replyEventId ? 1 : 0].push(event);
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled
// treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* Peek into a room. This will result in the room in question being synced so it
* is accessible via getRooms(). Live updates for the room will be provided.
@@ -1193,7 +1217,7 @@ export class SyncApi {
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const events = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
@@ -1234,8 +1258,8 @@ export class SyncApi {
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId();
for (let i = events.length - 1; i >= 0; i--) {
const eventId = events[i].getId();
if (room.getTimelineForEvent(eventId)) {
debuglog("Already have event " + eventId + " in limited " +
"sync - not resetting");
@@ -1244,7 +1268,7 @@ export class SyncApi {
// we might still be missing some of the events before i;
// we don't want to be adding them to the end of the
// timeline because that would put them out of order.
timelineEvents.splice(0, i);
events.splice(0, i);
// XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because
@@ -1274,7 +1298,10 @@ export class SyncApi {
}
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
this.processThreadEvents(room, threadedEvents);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1295,7 +1322,7 @@ export class SyncApi {
client.emit("Room", room);
}
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => {
client.emit("event", e);
@@ -1316,6 +1343,7 @@ export class SyncApi {
await utils.promiseMapSeries(stateEvents, processRoomEvent);
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -1335,10 +1363,13 @@ export class SyncApi {
leaveRooms.forEach((leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const timelineEvents = this.mapSyncEventsFormat(leaveObj.timeline, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1347,7 +1378,7 @@ export class SyncApi {
client.emit("Room", room);
}
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
stateEvents.forEach(function(e) {
client.emit("event", e);
@@ -1355,6 +1386,9 @@ export class SyncApi {
timelineEvents.forEach(function(e) {
client.emit("event", e);
});
threadedEvents.forEach(function(e) {
client.emit("event", e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -1674,6 +1708,15 @@ export class SyncApi {
room.addLiveEvents(timelineEventList || [], null, fromCache);
}
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
threadedEvents.forEach(event => {
room.addThreadedEvent(event);
});
}
/**
* Takes a list of timelineEvents and adds and adds to notifEvents
* as appropriate.

View File

@@ -31,14 +31,21 @@ import { randomString } from '../randomstring';
import {
MCallReplacesEvent,
MCallAnswer,
MCallOfferNegotiate,
MCallInviteNegotiate,
CallCapabilities,
SDPStreamMetadataPurpose,
SDPStreamMetadata,
SDPStreamMetadataKey,
MCallSDPStreamMetadataChanged,
MCallSelectAnswer,
MCAllAssertedIdentity,
MCallCandidates,
MCallBase,
MCallHangupReject,
} from './callEventTypes';
import { CallFeed } from './callFeed';
import { MatrixClient } from "../client";
import { ISendEventResponse } from "../@types/requests";
// events: hangup, error(err), replaced(call), state(state, oldState)
@@ -261,30 +268,33 @@ function genCallID(): string {
* @param {MatrixClient} opts.client The Matrix Client instance to send events to.
*/
export class MatrixCall extends EventEmitter {
roomId: string;
type: CallType;
callId: string;
state: CallState;
hangupParty: CallParty;
hangupReason: string;
direction: CallDirection;
ourPartyId: string;
public roomId: string;
public type: CallType = null;
public callId: string;
public state = CallState.Fledgling;
public hangupParty: CallParty;
public hangupReason: string;
public direction: CallDirection;
public ourPartyId: string;
private client: any; // Fix when client is TSified
private client: MatrixClient;
private forceTURN: boolean;
private turnServers: Array<TurnServer>;
private candidateSendQueue: Array<RTCIceCandidate>;
private candidateSendTries: number;
private sentEndOfCandidates: boolean;
// A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where
// possible
private candidateSendQueue: Array<RTCIceCandidate> = [];
private candidateSendTries = 0;
private sentEndOfCandidates = false;
private peerConn: RTCPeerConnection;
private feeds: Array<CallFeed>;
private usermediaSenders: Array<RTCRtpSender>;
private screensharingSenders: Array<RTCRtpSender>;
private inviteOrAnswerSent: boolean;
private feeds: Array<CallFeed> = [];
private usermediaSenders: Array<RTCRtpSender> = [];
private screensharingSenders: Array<RTCRtpSender> = [];
private inviteOrAnswerSent = false;
private waitForLocalAVStream: boolean;
private successor: MatrixCall;
private opponentMember: RoomMember;
private opponentVersion: number;
private opponentVersion: number | string;
// The party ID of the other side: undefined if we haven't chosen a partner
// yet, null if we have but they didn't send a party ID.
private opponentPartyId: string;
@@ -293,7 +303,7 @@ export class MatrixCall extends EventEmitter {
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold
private remoteOnHold;
private remoteOnHold = false;
// the stats for the call at the point it ended. We can't get these after we
// tear the call down, so we just grab a snapshot before we stop the call.
@@ -301,7 +311,7 @@ export class MatrixCall extends EventEmitter {
private callStatsAtEnd: any[];
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer: boolean;
private makingOffer = false;
private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular,
@@ -317,7 +327,6 @@ export class MatrixCall extends EventEmitter {
super();
this.roomId = opts.roomId;
this.client = opts.client;
this.type = null;
this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId;
// Array of Objects with urls, username, credential keys
@@ -330,33 +339,14 @@ export class MatrixCall extends EventEmitter {
for (const server of this.turnServers) {
utils.checkObjectHasKeys(server, ["urls"]);
}
this.callId = genCallID();
this.state = CallState.Fledgling;
// A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where
// possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
this.sentEndOfCandidates = false;
this.inviteOrAnswerSent = false;
this.makingOffer = false;
this.remoteOnHold = false;
this.feeds = [];
this.usermediaSenders = [];
this.screensharingSenders = [];
}
/**
* Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
async placeVoiceCall() {
public async placeVoiceCall(): Promise<void> {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Audio);
@@ -368,7 +358,7 @@ export class MatrixCall extends EventEmitter {
* Place a video call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
async placeVideoCall() {
public async placeVideoCall(): Promise<void> {
logger.debug("placeVideoCall");
this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Video);
@@ -376,11 +366,11 @@ export class MatrixCall extends EventEmitter {
await this.placeCallWithConstraints(constraints);
}
public getOpponentMember() {
public getOpponentMember(): RoomMember {
return this.opponentMember;
}
public opponentCanBeTransferred() {
public opponentCanBeTransferred(): boolean {
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
}
@@ -462,7 +452,7 @@ export class MatrixCall extends EventEmitter {
return !this.feeds.some((feed) => !feed.isLocal());
}
private pushRemoteFeed(stream: MediaStream) {
private pushRemoteFeed(stream: MediaStream): void {
// Fallback to old behavior if the other side doesn't support SDPStreamMetadata
if (!this.opponentSupportsSDPStreamMetadata()) {
this.pushRemoteFeedWithoutMetadata(stream);
@@ -495,7 +485,7 @@ export class MatrixCall extends EventEmitter {
/**
* This method is used ONLY if the other client doesn't support sending SDPStreamMetadata
*/
private pushRemoteFeedWithoutMetadata(stream: MediaStream) {
private pushRemoteFeedWithoutMetadata(stream: MediaStream): void {
const userId = this.getOpponentMember().userId;
// We can guess the purpose here since the other client can only send one stream
const purpose = SDPStreamMetadataPurpose.Usermedia;
@@ -523,7 +513,7 @@ export class MatrixCall extends EventEmitter {
logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`);
}
private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true) {
private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
const userId = this.client.getUserId();
// We try to replace an existing feed if there already is one with the same purpose
@@ -562,12 +552,12 @@ export class MatrixCall extends EventEmitter {
logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`);
}
private deleteAllFeeds() {
private deleteAllFeeds(): void {
this.feeds = [];
this.emit(CallEvent.FeedsChanged, this.feeds);
}
private deleteFeedByStream(stream: MediaStream) {
private deleteFeedByStream(stream: MediaStream): void {
logger.debug(`Removing feed with stream id ${stream.id}`);
const feed = this.getFeedByStreamId(stream.id);
@@ -607,8 +597,8 @@ export class MatrixCall extends EventEmitter {
* Configure this call from an invite event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.invite event
*/
async initWithInvite(event: MatrixEvent) {
const invite = event.getContent();
public async initWithInvite(event: MatrixEvent): Promise<void> {
const invite = event.getContent<MCallInviteNegotiate>();
this.direction = CallDirection.Inbound;
// make sure we have valid turn creds. Unless something's gone wrong, it should
@@ -674,7 +664,7 @@ export class MatrixCall extends EventEmitter {
* Configure this call from a hangup or reject event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.hangup event
*/
initWithHangup(event: MatrixEvent) {
public initWithHangup(event: MatrixEvent): void {
// 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)
@@ -684,7 +674,7 @@ export class MatrixCall extends EventEmitter {
/**
* Answer a call.
*/
async answer() {
public async answer(): Promise<void> {
if (this.inviteOrAnswerSent) {
return;
}
@@ -719,7 +709,7 @@ export class MatrixCall extends EventEmitter {
* MatrixClient.
* @param {MatrixCall} newCall The new call.
*/
replacedBy(newCall: MatrixCall) {
public replacedBy(newCall: MatrixCall): void {
if (this.state === CallState.WaitLocalMedia) {
logger.debug("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true;
@@ -737,7 +727,7 @@ export class MatrixCall extends EventEmitter {
* @param {string} reason The reason why the call is being hung up.
* @param {boolean} suppressEvent True to suppress emitting an event.
*/
hangup(reason: CallErrorCode, suppressEvent: boolean) {
public hangup(reason: CallErrorCode, suppressEvent: boolean): void {
if (this.callHasEnded()) return;
logger.debug("Ending call " + this.callId);
@@ -757,7 +747,7 @@ export class MatrixCall extends EventEmitter {
* This used to be done by calling hangup, but is a separate method and protocol
* event as of MSC2746.
*/
reject() {
public reject(): void {
if (this.state !== CallState.Ringing) {
throw Error("Call must be in 'ringing' state to reject!");
}
@@ -800,7 +790,7 @@ export class MatrixCall extends EventEmitter {
public async setScreensharingEnabled(
enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) {
): Promise<boolean> {
// Skip if there is nothing to do
if (enabled && this.isScreensharing()) {
logger.warn(`There is already a screensharing stream - there is nothing to do!`);
@@ -850,7 +840,7 @@ export class MatrixCall extends EventEmitter {
private async setScreensharingEnabledWithoutMetadataSupport(
enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) {
): Promise<boolean> {
logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`);
if (enabled) {
try {
@@ -896,7 +886,7 @@ export class MatrixCall extends EventEmitter {
* Set whether our outbound video should be muted or not.
* @param {boolean} muted True to mute the outbound video.
*/
setLocalVideoMuted(muted: boolean) {
public setLocalVideoMuted(muted: boolean): void {
this.localUsermediaFeed?.setVideoMuted(muted);
this.updateMuteStatus();
}
@@ -910,7 +900,7 @@ export class MatrixCall extends EventEmitter {
* @return {Boolean} True if the local preview video is muted, else false
* (including if the call is not set up yet).
*/
isLocalVideoMuted(): boolean {
public isLocalVideoMuted(): boolean {
return this.localUsermediaFeed?.isVideoMuted();
}
@@ -918,7 +908,7 @@ export class MatrixCall extends EventEmitter {
* Set whether the microphone should be muted or not.
* @param {boolean} muted True to mute the mic.
*/
setMicrophoneMuted(muted: boolean) {
public setMicrophoneMuted(muted: boolean): void {
this.localUsermediaFeed?.setAudioMuted(muted);
this.updateMuteStatus();
}
@@ -932,7 +922,7 @@ export class MatrixCall extends EventEmitter {
* @return {Boolean} True if the mic is muted, else false (including if the call
* is not set up yet).
*/
isMicrophoneMuted(): boolean {
public isMicrophoneMuted(): boolean {
return this.localUsermediaFeed?.isAudioMuted();
}
@@ -940,11 +930,11 @@ export class MatrixCall extends EventEmitter {
* @returns true if we have put the party on the other side of the call on hold
* (that is, we are signalling to them that we are not listening)
*/
isRemoteOnHold(): boolean {
public isRemoteOnHold(): boolean {
return this.remoteOnHold;
}
setRemoteOnHold(onHold: boolean) {
public setRemoteOnHold(onHold: boolean): void {
if (this.isRemoteOnHold() === onHold) return;
this.remoteOnHold = onHold;
@@ -964,7 +954,7 @@ export class MatrixCall extends EventEmitter {
* they cannot hear us).
* @returns true if the other party has put us on hold
*/
isLocalOnHold(): boolean {
public isLocalOnHold(): boolean {
if (this.state !== CallState.Connected) return false;
let callOnHold = true;
@@ -984,7 +974,7 @@ export class MatrixCall extends EventEmitter {
* Sends a DTMF digit to the other party
* @param digit The digit (nb. string - '#' and '*' are dtmf too)
*/
sendDtmfDigit(digit: string) {
public sendDtmfDigit(digit: string): void {
for (const sender of this.peerConn.getSenders()) {
if (sender.track.kind === 'audio' && sender.dtmf) {
sender.dtmf.insertDTMF(digit);
@@ -995,7 +985,7 @@ export class MatrixCall extends EventEmitter {
throw new Error("Unable to find a track to send DTMF on");
}
private updateMuteStatus() {
private updateMuteStatus(): void {
this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
});
@@ -1011,7 +1001,7 @@ export class MatrixCall extends EventEmitter {
* Internal
* @param {Object} stream
*/
private gotUserMediaForInvite = async (stream: MediaStream) => {
private gotUserMediaForInvite = async (stream: MediaStream): Promise<void> => {
if (this.successor) {
this.successor.gotUserMediaForAnswer(stream);
return;
@@ -1028,7 +1018,7 @@ export class MatrixCall extends EventEmitter {
// Now we wait for the negotiationneeded event
};
private async sendAnswer() {
private async sendAnswer(): Promise<void> {
const answerContent = {
answer: {
sdp: this.peerConn.localDescription.sdp,
@@ -1075,7 +1065,7 @@ export class MatrixCall extends EventEmitter {
this.sendCandidateQueue();
}
private gotUserMediaForAnswer = async (stream: MediaStream) => {
private gotUserMediaForAnswer = async (stream: MediaStream): Promise<void> => {
if (this.callHasEnded()) {
return;
}
@@ -1114,7 +1104,7 @@ export class MatrixCall extends EventEmitter {
* Internal
* @param {Object} event
*/
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => {
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): Promise<void> => {
if (event.candidate) {
logger.debug(
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
@@ -1133,7 +1123,7 @@ export class MatrixCall extends EventEmitter {
}
};
private onIceGatheringStateChange = (event: Event) => {
private onIceGatheringStateChange = (event: Event): void => {
logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState);
if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) {
// If we didn't get an empty-string candidate to signal the end of candidates,
@@ -1151,19 +1141,20 @@ export class MatrixCall extends EventEmitter {
}
};
async onRemoteIceCandidatesReceived(ev: MatrixEvent) {
public async onRemoteIceCandidatesReceived(ev: MatrixEvent): Promise<void> {
if (this.callHasEnded()) {
//debuglog("Ignoring remote ICE candidate because call has ended");
return;
}
const candidates = ev.getContent().candidates;
const content = ev.getContent<MCallCandidates>();
const candidates = content.candidates;
if (!candidates) {
logger.info("Ignoring candidates event with no candidates!");
return;
}
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
const fromPartyId = content.version === 0 ? null : content.party_id || null;
if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates
@@ -1174,9 +1165,9 @@ export class MatrixCall extends EventEmitter {
return;
}
if (!this.partyIdMatches(ev.getContent())) {
if (!this.partyIdMatches(content)) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`Ignoring candidates from party ID ${content.party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
@@ -1190,8 +1181,9 @@ export class MatrixCall extends EventEmitter {
* Used by MatrixClient.
* @param {Object} msg
*/
async onAnswerReceived(event: MatrixEvent) {
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`);
public async onAnswerReceived(event: MatrixEvent): Promise<void> {
const content = event.getContent<MCallAnswer>();
logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`);
if (this.callHasEnded()) {
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
@@ -1200,7 +1192,7 @@ export class MatrixCall extends EventEmitter {
if (this.opponentPartyId !== undefined) {
logger.info(
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
`Ignoring answer from party ID ${content.party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`,
);
return;
@@ -1211,7 +1203,7 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.Connecting);
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey];
const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else {
@@ -1219,7 +1211,7 @@ export class MatrixCall extends EventEmitter {
}
try {
await this.peerConn.setRemoteDescription(event.getContent().answer);
await this.peerConn.setRemoteDescription(content.answer);
} catch (e) {
logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -1242,13 +1234,13 @@ export class MatrixCall extends EventEmitter {
}
}
async onSelectAnswerReceived(event: MatrixEvent) {
public async onSelectAnswerReceived(event: MatrixEvent): Promise<void> {
if (this.direction !== CallDirection.Inbound) {
logger.warn("Got select_answer for an outbound call: ignoring");
return;
}
const selectedPartyId = event.getContent().selected_party_id;
const selectedPartyId = event.getContent<MCallSelectAnswer>().selected_party_id;
if (selectedPartyId === undefined || selectedPartyId === null) {
logger.warn("Got nonsensical select_answer with null/undefined selected_party_id: ignoring");
@@ -1262,8 +1254,9 @@ export class MatrixCall extends EventEmitter {
}
}
async onNegotiateReceived(event: MatrixEvent) {
const description = event.getContent().description;
public async onNegotiateReceived(event: MatrixEvent): Promise<void> {
const content = event.getContent<MCallInviteNegotiate>();
const description = content.description;
if (!description || !description.sdp || !description.type) {
logger.info("Ignoring invalid m.call.negotiate event");
return;
@@ -1288,7 +1281,7 @@ export class MatrixCall extends EventEmitter {
const prevLocalOnHold = this.isLocalOnHold();
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey];
const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else {
@@ -1336,12 +1329,13 @@ export class MatrixCall extends EventEmitter {
this.updateRemoteSDPStreamMetadata(metadata);
}
async onAssertedIdentityReceived(event: MatrixEvent) {
if (!event.getContent().asserted_identity) return;
public async onAssertedIdentityReceived(event: MatrixEvent): Promise<void> {
const content = event.getContent<MCAllAssertedIdentity>();
if (!content.asserted_identity) return;
this.remoteAssertedIdentity = {
id: event.getContent().asserted_identity.id,
displayName: event.getContent().asserted_identity.display_name,
id: content.asserted_identity.id,
displayName: content.asserted_identity.display_name,
};
this.emit(CallEvent.AssertedIdentityChanged);
}
@@ -1353,7 +1347,7 @@ export class MatrixCall extends EventEmitter {
return this.state === CallState.Ended;
}
private gotLocalOffer = async (description: RTCSessionDescriptionInit) => {
private gotLocalOffer = async (description: RTCSessionDescriptionInit): Promise<void> => {
logger.debug("Created offer: ", description);
if (this.callHasEnded()) {
@@ -1383,7 +1377,7 @@ export class MatrixCall extends EventEmitter {
const content = {
lifetime: CALL_TIMEOUT_MS,
} as MCallOfferNegotiate;
} as MCallInviteNegotiate;
// clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) {
@@ -1442,7 +1436,7 @@ export class MatrixCall extends EventEmitter {
}
};
private getLocalOfferFailed = (err: Error) => {
private getLocalOfferFailed = (err: Error): void => {
logger.error("Failed to get local offer", err);
this.emit(
@@ -1455,7 +1449,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
};
private getUserMediaFailed = (err: Error) => {
private getUserMediaFailed = (err: Error): void => {
if (this.successor) {
this.successor.getUserMediaFailed(err);
return;
@@ -1474,7 +1468,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
};
onIceConnectionStateChanged = () => {
private onIceConnectionStateChanged = (): void => {
if (this.callHasEnded()) {
return; // because ICE can still complete as we're ending the call
}
@@ -1490,14 +1484,14 @@ export class MatrixCall extends EventEmitter {
}
};
private onSignallingStateChanged = () => {
private onSignallingStateChanged = (): void => {
logger.debug(
"call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState,
);
};
private onTrack = (ev: RTCTrackEvent) => {
private onTrack = (ev: RTCTrackEvent): void => {
if (ev.streams.length === 0) {
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
return;
@@ -1521,7 +1515,7 @@ export class MatrixCall extends EventEmitter {
* [96685:23:0518/162603.933377:ERROR:webrtc_video_engine.cc(1171)] GetChangedRecvParameters called without any video codecs.
* [96685:23:0518/162603.933430:ERROR:sdp_offer_answer.cc(4302)] Failed to set local video description recv parameters for m-section with mid='2'. (INVALID_PARAMETER)
*/
private getRidOfRTXCodecs() {
private getRidOfRTXCodecs(): void {
// RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF
if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return;
@@ -1549,7 +1543,7 @@ export class MatrixCall extends EventEmitter {
}
}
onNegotiationNeeded = async () => {
private onNegotiationNeeded = async (): Promise<void> => {
logger.info("Negotiation is needed!");
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
@@ -1570,7 +1564,7 @@ export class MatrixCall extends EventEmitter {
}
};
onHangupReceived = (msg) => {
public onHangupReceived = (msg: MCallHangupReject): void => {
logger.debug("Hangup received for call ID " + this.callId);
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
@@ -1583,7 +1577,7 @@ export class MatrixCall extends EventEmitter {
}
};
onRejectReceived = (msg) => {
public onRejectReceived = (msg: MCallHangupReject): void => {
logger.debug("Reject received for call ID " + this.callId);
// No need to check party_id for reject because if we'd received either
@@ -1605,12 +1599,12 @@ export class MatrixCall extends EventEmitter {
}
};
onAnsweredElsewhere = (msg) => {
public onAnsweredElsewhere = (msg: MCallAnswer): void => {
logger.debug("Call ID " + this.callId + " answered elsewhere");
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
};
setState(state: CallState) {
private setState(state: CallState): void {
const oldState = this.state;
this.state = state;
this.emit(CallEvent.State, state, oldState);
@@ -1622,7 +1616,7 @@ export class MatrixCall extends EventEmitter {
* @param {Object} content
* @return {Promise}
*/
private sendVoipEvent(eventType: string, content: object) {
private sendVoipEvent(eventType: string, content: object): Promise<ISendEventResponse> {
return this.client.sendEvent(this.roomId, eventType, Object.assign({}, content, {
version: VOIP_PROTO_VERSION,
call_id: this.callId,
@@ -1630,7 +1624,7 @@ export class MatrixCall extends EventEmitter {
}));
}
queueCandidate(content: RTCIceCandidate) {
private queueCandidate(content: RTCIceCandidate): void {
// We partially de-trickle candidates by waiting for `delay` before sending them
// amalgamated, in order to avoid sending too many m.call.candidates events and hitting
// rate limits in Matrix.
@@ -1664,7 +1658,7 @@ export class MatrixCall extends EventEmitter {
/*
* Transfers this call to another user
*/
async transfer(targetUserId: string) {
public async transfer(targetUserId: string): Promise<void> {
// Fetch the target user's global profile info: their room avatar / displayname
// could be different in whatever room we share with them.
const profileInfo = await this.client.getProfileInfo(targetUserId);
@@ -1675,7 +1669,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(),
target_user: {
id: targetUserId,
display_name: profileInfo.display_name,
display_name: profileInfo.displayname,
avatar_url: profileInfo.avatar_url,
},
create_call: replacementId,
@@ -1690,7 +1684,7 @@ export class MatrixCall extends EventEmitter {
* Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together).
*/
async transferToCall(transferTargetCall?: MatrixCall) {
public async transferToCall(transferTargetCall?: MatrixCall): Promise<void> {
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
@@ -1702,7 +1696,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(),
target_user: {
id: this.getOpponentMember().userId,
display_name: transfereeProfileInfo.display_name,
display_name: transfereeProfileInfo.displayname,
avatar_url: transfereeProfileInfo.avatar_url,
},
await_call: newCallId,
@@ -1714,7 +1708,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(),
target_user: {
id: transferTargetCall.getOpponentMember().userId,
display_name: targetProfileInfo.display_name,
display_name: targetProfileInfo.displayname,
avatar_url: targetProfileInfo.avatar_url,
},
create_call: newCallId,
@@ -1726,7 +1720,7 @@ export class MatrixCall extends EventEmitter {
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true);
}
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
if (this.callHasEnded()) return;
this.callStatsAtEnd = await this.collectCallStats();
@@ -1752,7 +1746,7 @@ export class MatrixCall extends EventEmitter {
}
}
private stopAllMedia() {
private stopAllMedia(): void {
logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
for (const feed of this.feeds) {
@@ -1762,7 +1756,7 @@ export class MatrixCall extends EventEmitter {
}
}
private checkForErrorListener() {
private checkForErrorListener(): void {
if (this.listeners("error").length === 0) {
throw new Error(
"You MUST attach an error listener using call.on('error', function() {})",
@@ -1770,7 +1764,7 @@ export class MatrixCall extends EventEmitter {
}
}
private async sendCandidateQueue() {
private async sendCandidateQueue(): Promise<void> {
if (this.candidateSendQueue.length === 0) {
return;
}
@@ -1819,7 +1813,7 @@ export class MatrixCall extends EventEmitter {
}
}
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
private async placeCallWithConstraints(constraints: MediaStreamConstraints): Promise<void> {
logger.log("Getting user media with constraints", constraints);
// XXX Find a better way to do this
this.client.callEventHandler.calls.set(this.callId, this);
@@ -1864,7 +1858,7 @@ export class MatrixCall extends EventEmitter {
return pc;
}
private partyIdMatches(msg): boolean {
private partyIdMatches(msg: MCallBase): boolean {
// They must either match or both be absent (in which case opponentPartyId will be null)
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
// here and use null if the version is 0 (woe betide any opponent sending messages in the
@@ -1875,9 +1869,9 @@ export class MatrixCall extends EventEmitter {
// Commits to an opponent for the call
// ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) {
private chooseOpponent(ev: MatrixEvent): void {
// I choo-choo-choose you
const msg = ev.getContent();
const msg = ev.getContent<MCallInviteNegotiate | MCallAnswer>();
logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
@@ -1892,11 +1886,11 @@ export class MatrixCall extends EventEmitter {
// party ID
this.opponentPartyId = msg.party_id || null;
}
this.opponentCaps = msg.capabilities || {};
this.opponentCaps = msg.capabilities || {} as CallCapabilities;
this.opponentMember = ev.sender;
}
private async addBufferedIceCandidates() {
private async addBufferedIceCandidates(): Promise<void> {
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCandidates) {
logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
@@ -1905,7 +1899,7 @@ export class MatrixCall extends EventEmitter {
this.remoteCandidateBuffer = null;
}
private async addIceCandidates(candidates: RTCIceCandidate[]) {
private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
for (const candidate of candidates) {
if (
(candidate.sdpMid === null || candidate.sdpMid === undefined) &&
@@ -1927,7 +1921,7 @@ export class MatrixCall extends EventEmitter {
}
}
public get hasPeerConnection() {
public get hasPeerConnection(): boolean {
return Boolean(this.peerConn);
}
}
@@ -1949,13 +1943,13 @@ async function getScreensharingStream(
}
}
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): void {
for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled;
}
}
function getUserMediaContraints(type: ConstraintsType) {
function getUserMediaContraints(type: ConstraintsType): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia;
switch (type) {
@@ -1986,7 +1980,9 @@ function getUserMediaContraints(type: ConstraintsType) {
}
}
async function getScreenshareContraints(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
async function getScreenshareContraints(
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<DesktopCapturerConstraints> {
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
@@ -2020,14 +2016,14 @@ let videoInput: string;
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
export function setAudioInput(deviceId: string) { audioInput = deviceId; }
export function setAudioInput(deviceId: string): void { audioInput = deviceId; }
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
export function setVideoInput(deviceId: string) { videoInput = deviceId; }
export function setVideoInput(deviceId: string): void { videoInput = deviceId; }
/**
* DEPRECATED
@@ -2042,7 +2038,7 @@ export function setVideoInput(deviceId: string) { videoInput = deviceId; }
* since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts) {
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts): MatrixCall {
// typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
// NB. We don't log here as apps try to create a call object as a test for

View File

@@ -19,6 +19,7 @@ import { logger } from '../logger';
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event';
import { MatrixClient } from '../client';
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button
@@ -252,9 +253,9 @@ export class CallEventHandler {
} else {
if (call.state !== CallState.Ended) {
if (type === EventType.CallHangup) {
call.onHangupReceived(content);
call.onHangupReceived(content as MCallHangupReject);
} else {
call.onRejectReceived(content);
call.onRejectReceived(content as MCallHangupReject);
}
this.calls.delete(content.call_id);
}
@@ -274,7 +275,7 @@ export class CallEventHandler {
case EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
call.onAnsweredElsewhere(content as MCallAnswer);
}
} else {
call.onAnswerReceived(event);

View File

@@ -1,6 +1,8 @@
// allow non-camelcase as these are events type that go onto the wire
/* eslint-disable camelcase */
import { CallErrorCode } from "./call";
// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
@@ -19,11 +21,6 @@ export interface SDPStreamMetadata {
[key: string]: SDPStreamMetadataObject;
}
interface CallOfferAnswer {
type: string;
sdp: string;
}
export interface CallCapabilities {
'm.call.transferee': boolean;
'm.call.dtmf': boolean;
@@ -35,29 +32,56 @@ export interface CallReplacesTarget {
avatar_url: string;
}
export interface MCallAnswer {
answer: CallOfferAnswer;
capabilities: CallCapabilities;
export interface MCallBase {
call_id: string;
version: string | number;
party_id?: string;
}
export interface MCallAnswer extends MCallBase {
answer: RTCSessionDescription;
capabilities?: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallOfferNegotiate {
offer: CallOfferAnswer;
description: CallOfferAnswer;
export interface MCallSelectAnswer extends MCallBase {
selected_party_id: string;
}
export interface MCallInviteNegotiate extends MCallBase {
offer: RTCSessionDescription;
description: RTCSessionDescription;
lifetime: number;
capabilities: CallCapabilities;
capabilities?: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallSDPStreamMetadataChanged {
export interface MCallSDPStreamMetadataChanged extends MCallBase {
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallReplacesEvent {
export interface MCallReplacesEvent extends MCallBase {
replacement_id: string;
target_user: CallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
}
export interface MCAllAssertedIdentity extends MCallBase {
asserted_identity: {
id: string;
display_name: string;
avatar_url: string;
};
}
export interface MCallCandidates extends MCallBase {
candidates: RTCIceCandidate[];
}
export interface MCallHangupReject extends MCallBase {
reason?: CallErrorCode;
}
/* eslint-enable camelcase */