This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.
hydrogen-web/src/matrix/calls/group/GroupCall.ts

330 lines
13 KiB
TypeScript
Raw Normal View History

2022-02-17 21:28:44 +05:30
/*
Copyright 2022 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 {ObservableMap} from "../../../observable/map/ObservableMap";
2022-03-10 19:23:31 +05:30
import {Member} from "./Member";
2022-03-09 15:59:39 +05:30
import {LocalMedia} from "../LocalMedia";
2022-03-10 19:23:31 +05:30
import {RoomMember} from "../../room/members/RoomMember";
import {makeId} from "../../common";
2022-03-24 18:22:19 +05:30
import {EventEmitter} from "../../../utils/EventEmitter";
2022-03-10 19:23:31 +05:30
import type {Options as MemberOptions} from "./Member";
import type {BaseObservableMap} from "../../../observable/map/BaseObservableMap";
2022-03-09 15:59:39 +05:30
import type {Track} from "../../../platform/types/MediaDevices";
import type {SignallingMessage, MGroupCallBase, CallMembership} from "../callEventTypes";
2022-03-09 23:23:51 +05:30
import type {Room} from "../../room/Room";
import type {StateEvent} from "../../storage/types";
import type {Platform} from "../../../platform/web/Platform";
2022-03-10 19:23:31 +05:30
import type {EncryptedMessage} from "../../e2ee/olm/Encryption";
import type {ILogItem} from "../../../logging/types";
2022-03-10 22:09:29 +05:30
import type {Storage} from "../../storage/idb/Storage";
2022-03-24 18:22:19 +05:30
const CALL_TYPE = "m.call";
const CALL_MEMBER_TYPE = "m.call.member";
2022-03-10 22:09:29 +05:30
export enum GroupCallState {
Fledgling = "fledgling",
Creating = "creating",
Created = "created",
Joining = "joining",
Joined = "joined",
2022-03-10 22:09:29 +05:30
}
2022-03-10 19:23:31 +05:30
function getMemberKey(userId: string, deviceId: string) {
return JSON.stringify(userId)+`,`+JSON.stringify(deviceId);
}
function memberKeyIsForUser(key: string, userId: string) {
return key.startsWith(JSON.stringify(userId)+`,`);
}
function getDeviceFromMemberKey(key: string): string {
return JSON.parse(`[${key}]`)[1];
}
2022-03-10 19:23:31 +05:30
export type Options = Omit<MemberOptions, "emitUpdate" | "confId" | "encryptDeviceMessage"> & {
emitUpdate: (call: GroupCall, params?: any) => void;
2022-03-23 16:53:10 +05:30
encryptDeviceMessage: (roomId: string, userId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
2022-03-10 22:09:29 +05:30
storage: Storage,
2022-03-10 19:23:31 +05:30
};
2022-02-17 21:28:44 +05:30
2022-03-24 18:22:19 +05:30
export class GroupCall extends EventEmitter<{change: never}> {
public readonly id: string;
2022-03-10 19:23:31 +05:30
private readonly _members: ObservableMap<string, Member> = new ObservableMap();
2022-03-23 00:59:31 +05:30
private _localMedia?: LocalMedia = undefined;
2022-03-10 19:23:31 +05:30
private _memberOptions: MemberOptions;
private _state: GroupCallState;
2022-03-09 23:23:51 +05:30
constructor(
id: string | undefined,
private callContent: Record<string, any> | undefined,
2022-03-17 17:34:14 +05:30
public readonly roomId: string,
2022-03-25 19:13:02 +05:30
private readonly options: Options,
private readonly logItem: ILogItem,
2022-03-09 23:23:51 +05:30
) {
2022-03-24 18:22:19 +05:30
super();
this.id = id ?? makeId("conf-");
2022-03-25 19:13:02 +05:30
logItem.set("id", this.id);
this._state = id ? GroupCallState.Created : GroupCallState.Fledgling;
2022-03-23 16:53:10 +05:30
this._memberOptions = Object.assign({}, options, {
2022-03-10 19:23:31 +05:30
confId: this.id,
emitUpdate: member => this._members.update(getMemberKey(member.userId, member.deviceId), member),
2022-03-23 16:53:10 +05:30
encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log) => {
return this.options.encryptDeviceMessage(this.roomId, userId, message, log);
2022-03-10 19:23:31 +05:30
}
2022-03-23 16:53:10 +05:30
});
2022-02-17 21:28:44 +05:30
}
get localMedia(): LocalMedia | undefined { return this._localMedia; }
2022-03-10 19:23:31 +05:30
get members(): BaseObservableMap<string, Member> { return this._members; }
get isTerminated(): boolean {
return this.callContent?.["m.terminated"] === true;
2022-03-10 19:23:31 +05:30
}
2022-03-17 17:34:14 +05:30
get name(): string {
return this.callContent?.["m.name"];
}
2022-03-25 19:13:02 +05:30
join(localMedia: LocalMedia): Promise<void> {
return this.logItem.wrap("join", async log => {
if (this._state !== GroupCallState.Created) {
return;
}
this._state = GroupCallState.Joining;
this._localMedia = localMedia;
this.emitChange();
const memberContent = await this._createJoinPayload();
// send m.call.member state event
const request = this.options.hsApi.sendState(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId, memberContent, {log});
await request.response();
this.emitChange();
// send invite to all members that are < my userId
for (const [,member] of this._members) {
member.connect(this._localMedia);
}
});
}
2022-03-23 16:53:10 +05:30
get hasJoined() {
return this._state === GroupCallState.Joining || this._state === GroupCallState.Joined;
}
2022-03-25 19:13:02 +05:30
leave(): Promise<void> {
return this.logItem.wrap("leave", async log => {
const memberContent = await this._leaveCallMemberContent();
// send m.call.member state event
if (memberContent) {
const request = this.options.hsApi.sendState(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId, memberContent, {log});
await request.response();
// our own user isn't included in members, so not in the count
if (this._members.size === 0) {
await this.terminate();
}
2022-03-24 18:22:19 +05:30
}
2022-03-25 19:13:02 +05:30
});
}
2022-03-25 19:13:02 +05:30
terminate(): Promise<void> {
return this.logItem.wrap("terminate", async log => {
if (this._state === GroupCallState.Fledgling) {
return;
}
const request = this.options.hsApi.sendState(this.roomId, CALL_TYPE, this.id, Object.assign({}, this.callContent, {
"m.terminated": true
}), {log});
await request.response();
});
2022-03-24 18:22:19 +05:30
}
/** @internal */
2022-03-25 19:13:02 +05:30
create(localMedia: LocalMedia, name: string): Promise<void> {
return this.logItem.wrap("create", async log => {
if (this._state !== GroupCallState.Fledgling) {
return;
}
this._state = GroupCallState.Creating;
this.emitChange();
this.callContent = {
"m.type": localMedia.cameraTrack ? "m.video" : "m.voice",
"m.name": name,
"m.intent": "m.ring"
};
const request = this.options.hsApi.sendState(this.roomId, CALL_TYPE, this.id, this.callContent, {log});
await request.response();
this._state = GroupCallState.Created;
this.emitChange();
});
2022-02-17 21:28:44 +05:30
}
2022-03-10 19:23:31 +05:30
/** @internal */
2022-03-25 19:13:02 +05:30
updateCallEvent(callContent: Record<string, any>, syncLog: ILogItem) {
this.logItem.wrap("updateCallEvent", log => {
syncLog.refDetached(log);
this.callContent = callContent;
if (this._state === GroupCallState.Creating) {
this._state = GroupCallState.Created;
}
log.set("status", this._state);
this.emitChange();
});
2022-02-17 21:28:44 +05:30
}
2022-03-10 19:23:31 +05:30
/** @internal */
updateMember(userId: string, callMembership: CallMembership, syncLog: ILogItem) {
this.logItem.wrap({l: "updateMember", id: userId}, log => {
2022-03-25 19:13:02 +05:30
syncLog.refDetached(log);
const devices = callMembership["m.devices"];
const previousDeviceIds = this.getDeviceIdsForUserId(userId);
for (const device of devices) {
const deviceId = device.device_id;
const memberKey = getMemberKey(userId, deviceId);
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) {
if (this._state === GroupCallState.Joining) {
this._state = GroupCallState.Joined;
this.emitChange();
}
return;
}
let member = this._members.get(memberKey);
if (member) {
member.updateCallInfo(device);
} else {
const logItem = this.logItem.child("member");
member = new Member(
RoomMember.fromUserId(this.roomId, userId, "join"),
device, this._memberOptions, logItem
);
this._members.add(memberKey, member);
if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) {
member.connect(this._localMedia!.clone());
}
2022-03-25 19:13:02 +05:30
}
}
const newDeviceIds = new Set<string>(devices.map(call => call.device_id));
// remove user as member of any calls not present anymore
for (const previousDeviceId of previousDeviceIds) {
if (!newDeviceIds.has(previousDeviceId)) {
this.removeMemberDevice(userId, previousDeviceId, syncLog);
2022-03-25 19:13:02 +05:30
}
}
2022-03-25 19:13:02 +05:30
});
2022-02-17 21:28:44 +05:30
}
2022-03-10 19:23:31 +05:30
/** @internal */
2022-03-25 19:13:02 +05:30
removeMember(userId: string, syncLog: ILogItem) {
const deviceIds = this.getDeviceIdsForUserId(userId);
for (const deviceId of deviceIds) {
this.removeMemberDevice(userId, deviceId, syncLog);
}
}
private getDeviceIdsForUserId(userId: string): string[] {
return Array.from(this._members.keys())
.filter(key => memberKeyIsForUser(key, userId))
.map(key => getDeviceFromMemberKey(key));
}
/** @internal */
private removeMemberDevice(userId: string, deviceId: string, syncLog: ILogItem) {
const memberKey = getMemberKey(userId, deviceId);
this.logItem.wrap({l: "removeMemberDevice", id: memberKey}, log => {
2022-03-25 19:13:02 +05:30
syncLog.refDetached(log);
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) {
2022-03-25 19:13:02 +05:30
if (this._state === GroupCallState.Joined) {
this._localMedia?.dispose();
this._localMedia = undefined;
for (const [,member] of this._members) {
member.disconnect();
}
this._state = GroupCallState.Created;
}
} else {
const member = this._members.get(memberKey);
2022-03-25 19:13:02 +05:30
if (member) {
this._members.remove(memberKey);
2022-03-24 18:22:19 +05:30
member.disconnect();
}
}
2022-03-25 19:13:02 +05:30
this.emitChange();
});
2022-03-09 23:23:51 +05:30
}
2022-03-10 19:23:31 +05:30
/** @internal */
2022-03-25 19:13:02 +05:30
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, userId: string, deviceId: string, syncLog: ILogItem) {
2022-03-10 19:23:31 +05:30
// TODO: return if we are not membering to the call
let member = this._members.get(getMemberKey(userId, deviceId));
2022-03-10 19:23:31 +05:30
if (member) {
2022-03-25 19:13:02 +05:30
member.handleDeviceMessage(message, deviceId, syncLog);
2022-03-10 19:23:31 +05:30
} else {
2022-03-25 19:13:02 +05:30
const item = this.logItem.log({l: "could not find member for signalling message", userId, deviceId});
syncLog.refDetached(item);
2022-03-10 19:23:31 +05:30
// we haven't received the m.call.member yet for this caller. buffer the device messages or create the member/call anyway?
2022-02-17 21:28:44 +05:30
}
}
2022-03-10 22:09:29 +05:30
2022-03-25 19:13:02 +05:30
/** @internal */
dispose() {
this.logItem.finish();
}
2022-03-24 18:22:19 +05:30
private async _createJoinPayload() {
2022-03-10 22:09:29 +05:30
const {storage} = this.options;
const txn = await storage.readTxn([storage.storeNames.roomState]);
2022-03-24 18:22:19 +05:30
const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId);
2022-03-10 22:09:29 +05:30
const stateContent = stateEvent?.event?.content ?? {
["m.calls"]: []
};
const callsInfo = stateContent["m.calls"];
let callInfo = callsInfo.find(c => c["m.call_id"] === this.id);
if (!callInfo) {
callInfo = {
["m.call_id"]: this.id,
["m.devices"]: []
};
callsInfo.push(callInfo);
}
const devicesInfo = callInfo["m.devices"];
let deviceInfo = devicesInfo.find(d => d["device_id"] === this.options.ownDeviceId);
if (!deviceInfo) {
deviceInfo = {
["device_id"]: this.options.ownDeviceId
};
devicesInfo.push(deviceInfo);
}
return stateContent;
}
private async _leaveCallMemberContent(): Promise<Record<string, any> | undefined> {
const {storage} = this.options;
const txn = await storage.readTxn([storage.storeNames.roomState]);
2022-03-24 18:22:19 +05:30
const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId);
if (stateEvent) {
const content = stateEvent.event.content;
const callsInfo = content["m.calls"];
content["m.calls"] = callsInfo?.filter(c => c["m.call_id"] !== this.id);
return content;
}
}
protected emitChange() {
this.emit("change");
this.options.emitUpdate(this);
}
2022-02-17 21:28:44 +05:30
}