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) 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"); console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org"); var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) { client.publicRooms(function (err, data) {
if (err) { if (err) {
console.error("err %s", JSON.stringify(err)); console.error("err %s", JSON.stringify(err));

View File

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

View File

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

View File

@@ -55,12 +55,20 @@ export enum JoinRule {
* @deprecated Reserved keyword. Should not be used. Not yet implemented. * @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/ */
Private = "private", Private = "private",
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time. Knock = "knock",
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time. Restricted = "restricted",
} }
export enum RestrictedAllowType { 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 { export enum GuestAccess {

View File

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

View File

@@ -28,7 +28,9 @@ import { EventType, MsgType, RelationType } from "../@types/event";
import { Crypto } from "../crypto"; import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils"; import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
import { Thread } from "./thread";
import { IActionsObject } from '../pushprocessor'; import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';
/** /**
* Enum for event statuses. * Enum for event statuses.
@@ -192,6 +194,12 @@ export class MatrixEvent extends EventEmitter {
*/ */
private txnId: string = null; 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. /* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account * 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 * 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; public verificationRequest = null;
private readonly reEmitter: ReEmitter;
/** /**
* Construct a Matrix Event object * Construct a Matrix Event object
* @constructor * @constructor
@@ -262,6 +272,7 @@ export class MatrixEvent extends EventEmitter {
this.txnId = event.txn_id || null; this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge(); this.localTimestamp = Date.now() - this.getAge();
this.reEmitter = new ReEmitter(this);
} }
/** /**
@@ -382,6 +393,15 @@ export class MatrixEvent extends EventEmitter {
return this.event.content || {}; 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 * Get the previous event content JSON. This will only return something for
* state events which exist in the timeline. * state events which exist in the timeline.
@@ -1285,6 +1305,21 @@ export class MatrixEvent extends EventEmitter {
public getTxnId(): string | undefined { public getTxnId(): string | undefined {
return this.txnId; 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 /* 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 { EventType } from "../@types/event";
import { MatrixEvent } from "./event"; import { MatrixEvent } from "./event";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { IJoinRuleEventContent, JoinRule } from "../@types/partials";
// possible statuses for out-of-band member loading // possible statuses for out-of-band member loading
enum OobStatus { 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 the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room * @returns {string} the join_rule applied to this room
*/ */
public getJoinRule(): string { public getJoinRule(): JoinRule {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, ""); const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {}; const joinRuleContent = joinRuleEvent?.getContent<IJoinRuleEventContent>() ?? {};
return joinRuleContent["join_rule"] || "invite"; return joinRuleContent["join_rule"] || JoinRule.Invite;
} }
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void { private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {

View File

@@ -32,9 +32,10 @@ import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter'; import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event"; import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { ResizeMethod } from "../@types/partials"; import { JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { Thread } from "./thread";
// These constants are used as sane defaults when the homeserver doesn't support // 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 // 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 oldState: RoomState;
public currentState: RoomState; public currentState: RoomState;
/**
* @experimental
*/
public threads = new Set<Thread>();
/** /**
* Construct a new Room. * 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 * @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/ */
public findEventById(eventId: string): MatrixEvent | undefined { 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. * Get a member from the current room state.
* @param {string} userId The user ID of the member. * @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 * Add an event to the end of this room's live timelines. Will fire
* "Room.timeline". * "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 the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room * @returns {string} the join_rule applied to this room
*/ */
public getJoinRule(): string { public getJoinRule(): JoinRule {
return this.currentState.getJoinRule(); 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.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000); this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000);
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological; this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
if (!opts.canResetEntireTimeline) { if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = (roomId: string) => { opts.canResetEntireTimeline = (roomId: string) => {
@@ -283,8 +284,9 @@ export class SyncApi {
return; return;
} }
leaveObj.timeline = leaveObj.timeline || {}; leaveObj.timeline = leaveObj.timeline || {};
const timelineEvents = const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
this.mapSyncEventsFormat(leaveObj.timeline, room); const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any // set the back-pagination token. Do this *before* adding any
@@ -293,17 +295,39 @@ export class SyncApi {
EventTimeline.BACKWARDS); EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents); this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.recalculate(); room.recalculate();
client.store.storeRoom(room); client.store.storeRoom(room);
client.emit("Room", room); client.emit("Room", room);
this.processEventsForNotifs(room, timelineEvents); this.processEventsForNotifs(room, events);
}); });
return rooms; 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 * 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. * 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 // this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events // room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly // 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 ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data); 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 // which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain). // will stop us linking the empty timeline into the chain).
// //
for (let i = timelineEvents.length - 1; i >= 0; i--) { for (let i = events.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId(); const eventId = events[i].getId();
if (room.getTimelineForEvent(eventId)) { if (room.getTimelineForEvent(eventId)) {
debuglog("Already have event " + eventId + " in limited " + debuglog("Already have event " + eventId + " in limited " +
"sync - not resetting"); "sync - not resetting");
@@ -1244,7 +1268,7 @@ export class SyncApi {
// we might still be missing some of the events before i; // we might still be missing some of the events before i;
// we don't want to be adding them to the end of the // we don't want to be adding them to the end of the
// timeline because that would put them out of order. // 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 // XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because // 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.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
this.processThreadEvents(room, threadedEvents);
// set summary after processing events, // set summary after processing events,
// because it will trigger a name calculation // because it will trigger a name calculation
@@ -1295,7 +1322,7 @@ export class SyncApi {
client.emit("Room", room); client.emit("Room", room);
} }
this.processEventsForNotifs(room, timelineEvents); this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => { const processRoomEvent = async (e) => {
client.emit("event", e); client.emit("event", e);
@@ -1316,6 +1343,7 @@ export class SyncApi {
await utils.promiseMapSeries(stateEvents, processRoomEvent); await utils.promiseMapSeries(stateEvents, processRoomEvent);
await utils.promiseMapSeries(timelineEvents, processRoomEvent); await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) { ephemeralEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
}); });
@@ -1335,10 +1363,13 @@ export class SyncApi {
leaveRooms.forEach((leaveObj) => { leaveRooms.forEach((leaveObj) => {
const room = leaveObj.room; const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, 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 accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents); this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.addAccountData(accountDataEvents); room.addAccountData(accountDataEvents);
room.recalculate(); room.recalculate();
@@ -1347,7 +1378,7 @@ export class SyncApi {
client.emit("Room", room); client.emit("Room", room);
} }
this.processEventsForNotifs(room, timelineEvents); this.processEventsForNotifs(room, events);
stateEvents.forEach(function(e) { stateEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
@@ -1355,6 +1386,9 @@ export class SyncApi {
timelineEvents.forEach(function(e) { timelineEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
}); });
threadedEvents.forEach(function(e) {
client.emit("event", e);
});
accountDataEvents.forEach(function(e) { accountDataEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
}); });
@@ -1674,6 +1708,15 @@ export class SyncApi {
room.addLiveEvents(timelineEventList || [], null, fromCache); 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 * Takes a list of timelineEvents and adds and adds to notifEvents
* as appropriate. * as appropriate.

View File

@@ -31,14 +31,21 @@ import { randomString } from '../randomstring';
import { import {
MCallReplacesEvent, MCallReplacesEvent,
MCallAnswer, MCallAnswer,
MCallOfferNegotiate, MCallInviteNegotiate,
CallCapabilities, CallCapabilities,
SDPStreamMetadataPurpose, SDPStreamMetadataPurpose,
SDPStreamMetadata, SDPStreamMetadata,
SDPStreamMetadataKey, SDPStreamMetadataKey,
MCallSDPStreamMetadataChanged, MCallSDPStreamMetadataChanged,
MCallSelectAnswer,
MCAllAssertedIdentity,
MCallCandidates,
MCallBase,
MCallHangupReject,
} from './callEventTypes'; } from './callEventTypes';
import { CallFeed } from './callFeed'; import { CallFeed } from './callFeed';
import { MatrixClient } from "../client";
import { ISendEventResponse } from "../@types/requests";
// events: hangup, error(err), replaced(call), state(state, oldState) // 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. * @param {MatrixClient} opts.client The Matrix Client instance to send events to.
*/ */
export class MatrixCall extends EventEmitter { export class MatrixCall extends EventEmitter {
roomId: string; public roomId: string;
type: CallType; public type: CallType = null;
callId: string; public callId: string;
state: CallState; public state = CallState.Fledgling;
hangupParty: CallParty; public hangupParty: CallParty;
hangupReason: string; public hangupReason: string;
direction: CallDirection; public direction: CallDirection;
ourPartyId: string; public ourPartyId: string;
private client: any; // Fix when client is TSified private client: MatrixClient;
private forceTURN: boolean; private forceTURN: boolean;
private turnServers: Array<TurnServer>; private turnServers: Array<TurnServer>;
private candidateSendQueue: Array<RTCIceCandidate>; // A queue for candidates waiting to go out.
private candidateSendTries: number; // We try to amalgamate candidates into a single candidate message where
private sentEndOfCandidates: boolean; // possible
private candidateSendQueue: Array<RTCIceCandidate> = [];
private candidateSendTries = 0;
private sentEndOfCandidates = false;
private peerConn: RTCPeerConnection; private peerConn: RTCPeerConnection;
private feeds: Array<CallFeed>; private feeds: Array<CallFeed> = [];
private usermediaSenders: Array<RTCRtpSender>; private usermediaSenders: Array<RTCRtpSender> = [];
private screensharingSenders: Array<RTCRtpSender>; private screensharingSenders: Array<RTCRtpSender> = [];
private inviteOrAnswerSent: boolean; private inviteOrAnswerSent = false;
private waitForLocalAVStream: boolean; private waitForLocalAVStream: boolean;
private successor: MatrixCall; private successor: MatrixCall;
private opponentMember: RoomMember; 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 // 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. // yet, null if we have but they didn't send a party ID.
private opponentPartyId: string; 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 // 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 // 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 // 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. // 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[]; private callStatsAtEnd: any[];
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer: boolean; private makingOffer = false;
private ignoreOffer: boolean; private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular, // If candidates arrive before we've picked an opponent (which, in particular,
@@ -317,7 +327,6 @@ export class MatrixCall extends EventEmitter {
super(); super();
this.roomId = opts.roomId; this.roomId = opts.roomId;
this.client = opts.client; this.client = opts.client;
this.type = null;
this.forceTURN = opts.forceTURN; this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId; this.ourPartyId = this.client.deviceId;
// Array of Objects with urls, username, credential keys // Array of Objects with urls, username, credential keys
@@ -330,33 +339,14 @@ export class MatrixCall extends EventEmitter {
for (const server of this.turnServers) { for (const server of this.turnServers) {
utils.checkObjectHasKeys(server, ["urls"]); utils.checkObjectHasKeys(server, ["urls"]);
} }
this.callId = genCallID(); 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. * Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
async placeVoiceCall() { public async placeVoiceCall(): Promise<void> {
logger.debug("placeVoiceCall"); logger.debug("placeVoiceCall");
this.checkForErrorListener(); this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Audio); const constraints = getUserMediaContraints(ConstraintsType.Audio);
@@ -368,7 +358,7 @@ export class MatrixCall extends EventEmitter {
* Place a video call to this room. * Place a video call to this room.
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
async placeVideoCall() { public async placeVideoCall(): Promise<void> {
logger.debug("placeVideoCall"); logger.debug("placeVideoCall");
this.checkForErrorListener(); this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Video); const constraints = getUserMediaContraints(ConstraintsType.Video);
@@ -376,11 +366,11 @@ export class MatrixCall extends EventEmitter {
await this.placeCallWithConstraints(constraints); await this.placeCallWithConstraints(constraints);
} }
public getOpponentMember() { public getOpponentMember(): RoomMember {
return this.opponentMember; return this.opponentMember;
} }
public opponentCanBeTransferred() { public opponentCanBeTransferred(): boolean {
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]); 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()); 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 // Fallback to old behavior if the other side doesn't support SDPStreamMetadata
if (!this.opponentSupportsSDPStreamMetadata()) { if (!this.opponentSupportsSDPStreamMetadata()) {
this.pushRemoteFeedWithoutMetadata(stream); 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 * 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; const userId = this.getOpponentMember().userId;
// We can guess the purpose here since the other client can only send one stream // We can guess the purpose here since the other client can only send one stream
const purpose = SDPStreamMetadataPurpose.Usermedia; const purpose = SDPStreamMetadataPurpose.Usermedia;
@@ -523,7 +513,7 @@ export class MatrixCall extends EventEmitter {
logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`); 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(); const userId = this.client.getUserId();
// We try to replace an existing feed if there already is one with the same purpose // 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}")`); logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`);
} }
private deleteAllFeeds() { private deleteAllFeeds(): void {
this.feeds = []; this.feeds = [];
this.emit(CallEvent.FeedsChanged, 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}`); logger.debug(`Removing feed with stream id ${stream.id}`);
const feed = this.getFeedByStreamId(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. * Configure this call from an invite event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.invite event * @param {MatrixEvent} event The m.call.invite event
*/ */
async initWithInvite(event: MatrixEvent) { public async initWithInvite(event: MatrixEvent): Promise<void> {
const invite = event.getContent(); const invite = event.getContent<MCallInviteNegotiate>();
this.direction = CallDirection.Inbound; this.direction = CallDirection.Inbound;
// make sure we have valid turn creds. Unless something's gone wrong, it should // 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. * Configure this call from a hangup or reject event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.hangup event * @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 // 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)
@@ -684,7 +674,7 @@ export class MatrixCall extends EventEmitter {
/** /**
* Answer a call. * Answer a call.
*/ */
async answer() { public async answer(): Promise<void> {
if (this.inviteOrAnswerSent) { if (this.inviteOrAnswerSent) {
return; return;
} }
@@ -719,7 +709,7 @@ export class MatrixCall extends EventEmitter {
* MatrixClient. * MatrixClient.
* @param {MatrixCall} newCall The new call. * @param {MatrixCall} newCall The new call.
*/ */
replacedBy(newCall: MatrixCall) { public replacedBy(newCall: MatrixCall): void {
if (this.state === CallState.WaitLocalMedia) { if (this.state === CallState.WaitLocalMedia) {
logger.debug("Telling new call to wait for local media"); logger.debug("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true; 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 {string} reason The reason why the call is being hung up.
* @param {boolean} suppressEvent True to suppress emitting an event. * @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; if (this.callHasEnded()) return;
logger.debug("Ending call " + this.callId); 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 * This used to be done by calling hangup, but is a separate method and protocol
* event as of MSC2746. * event as of MSC2746.
*/ */
reject() { public reject(): void {
if (this.state !== CallState.Ringing) { if (this.state !== CallState.Ringing) {
throw Error("Call must be in 'ringing' state to reject!"); throw Error("Call must be in 'ringing' state to reject!");
} }
@@ -800,7 +790,7 @@ export class MatrixCall extends EventEmitter {
public async setScreensharingEnabled( public async setScreensharingEnabled(
enabled: boolean, enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>, selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) { ): Promise<boolean> {
// Skip if there is nothing to do // Skip if there is nothing to do
if (enabled && this.isScreensharing()) { if (enabled && this.isScreensharing()) {
logger.warn(`There is already a screensharing stream - there is nothing to do!`); 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( private async setScreensharingEnabledWithoutMetadataSupport(
enabled: boolean, enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>, selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) { ): Promise<boolean> {
logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`); logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`);
if (enabled) { if (enabled) {
try { try {
@@ -896,7 +886,7 @@ export class MatrixCall extends EventEmitter {
* Set whether our outbound video should be muted or not. * Set whether our outbound video should be muted or not.
* @param {boolean} muted True to mute the outbound video. * @param {boolean} muted True to mute the outbound video.
*/ */
setLocalVideoMuted(muted: boolean) { public setLocalVideoMuted(muted: boolean): void {
this.localUsermediaFeed?.setVideoMuted(muted); this.localUsermediaFeed?.setVideoMuted(muted);
this.updateMuteStatus(); this.updateMuteStatus();
} }
@@ -910,7 +900,7 @@ export class MatrixCall extends EventEmitter {
* @return {Boolean} True if the local preview video is muted, else false * @return {Boolean} True if the local preview video is muted, else false
* (including if the call is not set up yet). * (including if the call is not set up yet).
*/ */
isLocalVideoMuted(): boolean { public isLocalVideoMuted(): boolean {
return this.localUsermediaFeed?.isVideoMuted(); return this.localUsermediaFeed?.isVideoMuted();
} }
@@ -918,7 +908,7 @@ export class MatrixCall extends EventEmitter {
* Set whether the microphone should be muted or not. * Set whether the microphone should be muted or not.
* @param {boolean} muted True to mute the mic. * @param {boolean} muted True to mute the mic.
*/ */
setMicrophoneMuted(muted: boolean) { public setMicrophoneMuted(muted: boolean): void {
this.localUsermediaFeed?.setAudioMuted(muted); this.localUsermediaFeed?.setAudioMuted(muted);
this.updateMuteStatus(); 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 * @return {Boolean} True if the mic is muted, else false (including if the call
* is not set up yet). * is not set up yet).
*/ */
isMicrophoneMuted(): boolean { public isMicrophoneMuted(): boolean {
return this.localUsermediaFeed?.isAudioMuted(); 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 * @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) * (that is, we are signalling to them that we are not listening)
*/ */
isRemoteOnHold(): boolean { public isRemoteOnHold(): boolean {
return this.remoteOnHold; return this.remoteOnHold;
} }
setRemoteOnHold(onHold: boolean) { public setRemoteOnHold(onHold: boolean): void {
if (this.isRemoteOnHold() === onHold) return; if (this.isRemoteOnHold() === onHold) return;
this.remoteOnHold = onHold; this.remoteOnHold = onHold;
@@ -964,7 +954,7 @@ export class MatrixCall extends EventEmitter {
* they cannot hear us). * they cannot hear us).
* @returns true if the other party has put us on hold * @returns true if the other party has put us on hold
*/ */
isLocalOnHold(): boolean { public isLocalOnHold(): boolean {
if (this.state !== CallState.Connected) return false; if (this.state !== CallState.Connected) return false;
let callOnHold = true; let callOnHold = true;
@@ -984,7 +974,7 @@ export class MatrixCall extends EventEmitter {
* Sends a DTMF digit to the other party * Sends a DTMF digit to the other party
* @param digit The digit (nb. string - '#' and '*' are dtmf too) * @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()) { for (const sender of this.peerConn.getSenders()) {
if (sender.track.kind === 'audio' && sender.dtmf) { if (sender.track.kind === 'audio' && sender.dtmf) {
sender.dtmf.insertDTMF(digit); 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"); throw new Error("Unable to find a track to send DTMF on");
} }
private updateMuteStatus() { private updateMuteStatus(): void {
this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, { this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
}); });
@@ -1011,7 +1001,7 @@ export class MatrixCall extends EventEmitter {
* Internal * Internal
* @param {Object} stream * @param {Object} stream
*/ */
private gotUserMediaForInvite = async (stream: MediaStream) => { private gotUserMediaForInvite = async (stream: MediaStream): Promise<void> => {
if (this.successor) { if (this.successor) {
this.successor.gotUserMediaForAnswer(stream); this.successor.gotUserMediaForAnswer(stream);
return; return;
@@ -1028,7 +1018,7 @@ export class MatrixCall extends EventEmitter {
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
}; };
private async sendAnswer() { private async sendAnswer(): Promise<void> {
const answerContent = { const answerContent = {
answer: { answer: {
sdp: this.peerConn.localDescription.sdp, sdp: this.peerConn.localDescription.sdp,
@@ -1075,7 +1065,7 @@ export class MatrixCall extends EventEmitter {
this.sendCandidateQueue(); this.sendCandidateQueue();
} }
private gotUserMediaForAnswer = async (stream: MediaStream) => { private gotUserMediaForAnswer = async (stream: MediaStream): Promise<void> => {
if (this.callHasEnded()) { if (this.callHasEnded()) {
return; return;
} }
@@ -1114,7 +1104,7 @@ export class MatrixCall extends EventEmitter {
* Internal * Internal
* @param {Object} event * @param {Object} event
*/ */
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => { private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): Promise<void> => {
if (event.candidate) { if (event.candidate) {
logger.debug( logger.debug(
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " + "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); logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState);
if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) {
// If we didn't get an empty-string candidate to signal the end of candidates, // 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()) { if (this.callHasEnded()) {
//debuglog("Ignoring remote ICE candidate because call has ended"); //debuglog("Ignoring remote ICE candidate because call has ended");
return; return;
} }
const candidates = ev.getContent().candidates; const content = ev.getContent<MCallCandidates>();
const candidates = content.candidates;
if (!candidates) { if (!candidates) {
logger.info("Ignoring candidates event with no candidates!"); logger.info("Ignoring candidates event with no candidates!");
return; 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) { if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates // we haven't picked an opponent yet so save the candidates
@@ -1174,9 +1165,9 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
if (!this.partyIdMatches(ev.getContent())) { if (!this.partyIdMatches(content)) {
logger.info( 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}`, `we have chosen party ID ${this.opponentPartyId}`,
); );
@@ -1190,8 +1181,9 @@ export class MatrixCall extends EventEmitter {
* Used by MatrixClient. * Used by MatrixClient.
* @param {Object} msg * @param {Object} msg
*/ */
async onAnswerReceived(event: MatrixEvent) { public async onAnswerReceived(event: MatrixEvent): Promise<void> {
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`); const content = event.getContent<MCallAnswer>();
logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`);
if (this.callHasEnded()) { if (this.callHasEnded()) {
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`); logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
@@ -1200,7 +1192,7 @@ export class MatrixCall extends EventEmitter {
if (this.opponentPartyId !== undefined) { if (this.opponentPartyId !== undefined) {
logger.info( 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}`, `we already have an answer/reject from ${this.opponentPartyId}`,
); );
return; return;
@@ -1211,7 +1203,7 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.Connecting); this.setState(CallState.Connecting);
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) { if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else { } else {
@@ -1219,7 +1211,7 @@ export class MatrixCall extends EventEmitter {
} }
try { try {
await this.peerConn.setRemoteDescription(event.getContent().answer); await this.peerConn.setRemoteDescription(content.answer);
} catch (e) { } catch (e) {
logger.debug("Failed to set remote description", e); logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); 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) { if (this.direction !== CallDirection.Inbound) {
logger.warn("Got select_answer for an outbound call: ignoring"); logger.warn("Got select_answer for an outbound call: ignoring");
return; return;
} }
const selectedPartyId = event.getContent().selected_party_id; const selectedPartyId = event.getContent<MCallSelectAnswer>().selected_party_id;
if (selectedPartyId === undefined || selectedPartyId === null) { if (selectedPartyId === undefined || selectedPartyId === null) {
logger.warn("Got nonsensical select_answer with null/undefined selected_party_id: ignoring"); 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) { public async onNegotiateReceived(event: MatrixEvent): Promise<void> {
const description = event.getContent().description; const content = event.getContent<MCallInviteNegotiate>();
const description = content.description;
if (!description || !description.sdp || !description.type) { if (!description || !description.sdp || !description.type) {
logger.info("Ignoring invalid m.call.negotiate event"); logger.info("Ignoring invalid m.call.negotiate event");
return; return;
@@ -1288,7 +1281,7 @@ export class MatrixCall extends EventEmitter {
const prevLocalOnHold = this.isLocalOnHold(); const prevLocalOnHold = this.isLocalOnHold();
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) { if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else { } else {
@@ -1336,12 +1329,13 @@ export class MatrixCall extends EventEmitter {
this.updateRemoteSDPStreamMetadata(metadata); this.updateRemoteSDPStreamMetadata(metadata);
} }
async onAssertedIdentityReceived(event: MatrixEvent) { public async onAssertedIdentityReceived(event: MatrixEvent): Promise<void> {
if (!event.getContent().asserted_identity) return; const content = event.getContent<MCAllAssertedIdentity>();
if (!content.asserted_identity) return;
this.remoteAssertedIdentity = { this.remoteAssertedIdentity = {
id: event.getContent().asserted_identity.id, id: content.asserted_identity.id,
displayName: event.getContent().asserted_identity.display_name, displayName: content.asserted_identity.display_name,
}; };
this.emit(CallEvent.AssertedIdentityChanged); this.emit(CallEvent.AssertedIdentityChanged);
} }
@@ -1353,7 +1347,7 @@ export class MatrixCall extends EventEmitter {
return this.state === CallState.Ended; return this.state === CallState.Ended;
} }
private gotLocalOffer = async (description: RTCSessionDescriptionInit) => { private gotLocalOffer = async (description: RTCSessionDescriptionInit): Promise<void> => {
logger.debug("Created offer: ", description); logger.debug("Created offer: ", description);
if (this.callHasEnded()) { if (this.callHasEnded()) {
@@ -1383,7 +1377,7 @@ export class MatrixCall extends EventEmitter {
const content = { const content = {
lifetime: CALL_TIMEOUT_MS, 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 // clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) { 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); logger.error("Failed to get local offer", err);
this.emit( this.emit(
@@ -1455,7 +1449,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false); this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
}; };
private getUserMediaFailed = (err: Error) => { private getUserMediaFailed = (err: Error): void => {
if (this.successor) { if (this.successor) {
this.successor.getUserMediaFailed(err); this.successor.getUserMediaFailed(err);
return; return;
@@ -1474,7 +1468,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false); this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
}; };
onIceConnectionStateChanged = () => { private onIceConnectionStateChanged = (): void => {
if (this.callHasEnded()) { if (this.callHasEnded()) {
return; // because ICE can still complete as we're ending the call 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( logger.debug(
"call " + this.callId + ": Signalling state changed to: " + "call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState, this.peerConn.signalingState,
); );
}; };
private onTrack = (ev: RTCTrackEvent) => { private onTrack = (ev: RTCTrackEvent): void => {
if (ev.streams.length === 0) { if (ev.streams.length === 0) {
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`); logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
return; 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.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) * [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 // RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF
if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return; 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!"); logger.info("Negotiation is needed!");
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) { 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); 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 // 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); logger.debug("Reject received for call ID " + this.callId);
// No need to check party_id for reject because if we'd received either // 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"); logger.debug("Call ID " + this.callId + " answered elsewhere");
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true); this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
}; };
setState(state: CallState) { private setState(state: CallState): void {
const oldState = this.state; const oldState = this.state;
this.state = state; this.state = state;
this.emit(CallEvent.State, state, oldState); this.emit(CallEvent.State, state, oldState);
@@ -1622,7 +1616,7 @@ export class MatrixCall extends EventEmitter {
* @param {Object} content * @param {Object} content
* @return {Promise} * @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, { return this.client.sendEvent(this.roomId, eventType, Object.assign({}, content, {
version: VOIP_PROTO_VERSION, version: VOIP_PROTO_VERSION,
call_id: this.callId, 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 // 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 // amalgamated, in order to avoid sending too many m.call.candidates events and hitting
// rate limits in Matrix. // rate limits in Matrix.
@@ -1664,7 +1658,7 @@ export class MatrixCall extends EventEmitter {
/* /*
* Transfers this call to another user * 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 // Fetch the target user's global profile info: their room avatar / displayname
// could be different in whatever room we share with them. // could be different in whatever room we share with them.
const profileInfo = await this.client.getProfileInfo(targetUserId); const profileInfo = await this.client.getProfileInfo(targetUserId);
@@ -1675,7 +1669,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: targetUserId, id: targetUserId,
display_name: profileInfo.display_name, display_name: profileInfo.displayname,
avatar_url: profileInfo.avatar_url, avatar_url: profileInfo.avatar_url,
}, },
create_call: replacementId, create_call: replacementId,
@@ -1690,7 +1684,7 @@ export class MatrixCall extends EventEmitter {
* Transfers this call to the target call, effectively 'joining' the * Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together). * 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 targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId); const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
@@ -1702,7 +1696,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: this.getOpponentMember().userId, id: this.getOpponentMember().userId,
display_name: transfereeProfileInfo.display_name, display_name: transfereeProfileInfo.displayname,
avatar_url: transfereeProfileInfo.avatar_url, avatar_url: transfereeProfileInfo.avatar_url,
}, },
await_call: newCallId, await_call: newCallId,
@@ -1714,7 +1708,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: transferTargetCall.getOpponentMember().userId, id: transferTargetCall.getOpponentMember().userId,
display_name: targetProfileInfo.display_name, display_name: targetProfileInfo.displayname,
avatar_url: targetProfileInfo.avatar_url, avatar_url: targetProfileInfo.avatar_url,
}, },
create_call: newCallId, create_call: newCallId,
@@ -1726,7 +1720,7 @@ export class MatrixCall extends EventEmitter {
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true); 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; if (this.callHasEnded()) return;
this.callStatsAtEnd = await this.collectCallStats(); 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})`); logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
for (const feed of this.feeds) { 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) { if (this.listeners("error").length === 0) {
throw new Error( throw new Error(
"You MUST attach an error listener using call.on('error', function() {})", "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) { if (this.candidateSendQueue.length === 0) {
return; 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); logger.log("Getting user media with constraints", constraints);
// XXX Find a better way to do this // XXX Find a better way to do this
this.client.callEventHandler.calls.set(this.callId, this); this.client.callEventHandler.calls.set(this.callId, this);
@@ -1864,7 +1858,7 @@ export class MatrixCall extends EventEmitter {
return pc; 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) // 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 // 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 // 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 // Commits to an opponent for the call
// ev: An invite or answer event // ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) { private chooseOpponent(ev: MatrixEvent): void {
// I choo-choo-choose you // 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}`); logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
@@ -1892,11 +1886,11 @@ export class MatrixCall extends EventEmitter {
// party ID // party ID
this.opponentPartyId = msg.party_id || null; this.opponentPartyId = msg.party_id || null;
} }
this.opponentCaps = msg.capabilities || {}; this.opponentCaps = msg.capabilities || {} as CallCapabilities;
this.opponentMember = ev.sender; this.opponentMember = ev.sender;
} }
private async addBufferedIceCandidates() { private async addBufferedIceCandidates(): Promise<void> {
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCandidates) { if (bufferedCandidates) {
logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
@@ -1905,7 +1899,7 @@ export class MatrixCall extends EventEmitter {
this.remoteCandidateBuffer = null; this.remoteCandidateBuffer = null;
} }
private async addIceCandidates(candidates: RTCIceCandidate[]) { private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
for (const candidate of candidates) { for (const candidate of candidates) {
if ( if (
(candidate.sdpMid === null || candidate.sdpMid === undefined) && (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); 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++) { for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled; tracks[i].enabled = enabled;
} }
} }
function getUserMediaContraints(type: ConstraintsType) { function getUserMediaContraints(type: ConstraintsType): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia; const isWebkit = !!navigator.webkitGetUserMedia;
switch (type) { 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) { if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources() // We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available..."); logger.debug("Electron getDesktopCapturerSources() is available...");
@@ -2020,14 +2016,14 @@ let videoInput: string;
* @param {string=} deviceId the identifier for the device * @param {string=} deviceId the identifier for the device
* undefined treated as unset * 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 * Set a video input device to use for MatrixCalls
* @function * @function
* @param {string=} deviceId the identifier for the device * @param {string=} deviceId the identifier for the device
* undefined treated as unset * undefined treated as unset
*/ */
export function setVideoInput(deviceId: string) { videoInput = deviceId; } export function setVideoInput(deviceId: string): void { videoInput = deviceId; }
/** /**
* DEPRECATED * DEPRECATED
@@ -2042,7 +2038,7 @@ export function setVideoInput(deviceId: string) { videoInput = deviceId; }
* since it's only possible to set this option on outbound calls. * 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. * @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 // typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') { 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 // 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 { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event'; import { EventType } from '../@types/event';
import { MatrixClient } from '../client'; 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 // Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button // time to press the 'accept' button
@@ -252,9 +253,9 @@ export class CallEventHandler {
} else { } else {
if (call.state !== CallState.Ended) { if (call.state !== CallState.Ended) {
if (type === EventType.CallHangup) { if (type === EventType.CallHangup) {
call.onHangupReceived(content); call.onHangupReceived(content as MCallHangupReject);
} else { } else {
call.onRejectReceived(content); call.onRejectReceived(content as MCallHangupReject);
} }
this.calls.delete(content.call_id); this.calls.delete(content.call_id);
} }
@@ -274,7 +275,7 @@ export class CallEventHandler {
case EventType.CallAnswer: case EventType.CallAnswer:
if (weSentTheEvent) { if (weSentTheEvent) {
if (call.state === CallState.Ringing) { if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content); call.onAnsweredElsewhere(content as MCallAnswer);
} }
} else { } else {
call.onAnswerReceived(event); call.onAnswerReceived(event);

View File

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