mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Add new WIP ConfCallManager
This commit is contained in:
parent
274b3336c9
commit
dff8a1acd3
@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
|
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
|
||||||
|
import { randomString } from "./randomstring";
|
||||||
|
|
||||||
const CONF_ROOM = "me.robertlong.conf";
|
const CONF_ROOM = "me.robertlong.conf";
|
||||||
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
||||||
@ -186,8 +187,17 @@ export class ConferenceCallManager extends EventEmitter {
|
|||||||
|
|
||||||
call.removeListener("hangup", onHangup);
|
call.removeListener("hangup", onHangup);
|
||||||
call.removeListener("replaced", onReplaced);
|
call.removeListener("replaced", onReplaced);
|
||||||
|
|
||||||
const userId = call.opponentMember.userId;
|
const userId = call.opponentMember.userId;
|
||||||
this._addCall(call, userId);
|
const existingParticipant = this.participants.find(
|
||||||
|
(p) => p.userId === userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingParticipant) {
|
||||||
|
existingParticipant.call = call;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addCall(call);
|
||||||
call.answer();
|
call.answer();
|
||||||
this.emit("call", call);
|
this.emit("call", call);
|
||||||
}
|
}
|
||||||
@ -270,7 +280,7 @@ export class ConferenceCallManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const call = this.client.createCall(this.roomId, userId);
|
const call = this.client.createCall(this.roomId, userId);
|
||||||
this._addCall(call, userId);
|
this._addCall(call);
|
||||||
call.placeVideoCall().then(() => {
|
call.placeVideoCall().then(() => {
|
||||||
this.emit("call", call);
|
this.emit("call", call);
|
||||||
});
|
});
|
||||||
@ -314,23 +324,21 @@ export class ConferenceCallManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId = call.opponentMember.userId;
|
const userId = call.opponentMember.userId;
|
||||||
this._addCall(call, userId);
|
const existingParticipant = this.participants.find(
|
||||||
|
(p) => p.userId === userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingParticipant) {
|
||||||
|
existingParticipant.call = call;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addCall(call);
|
||||||
call.answer();
|
call.answer();
|
||||||
this.emit("call", call);
|
this.emit("call", call);
|
||||||
};
|
};
|
||||||
|
|
||||||
_addCall(call, userId) {
|
_addCall(call) {
|
||||||
if (call.state === "ended") {
|
const userId = call.opponentMember.userId;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingCall = this.participants.find(
|
|
||||||
(p) => !p.local && p.call && p.call.callId === call.callId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingCall) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.participants.push({
|
this.participants.push({
|
||||||
userId,
|
userId,
|
||||||
@ -450,3 +458,221 @@ export class ConferenceCallManager extends EventEmitter {
|
|||||||
localStorage.removeItem("matrix-auth-store");
|
localStorage.removeItem("matrix-auth-store");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - incoming
|
||||||
|
* - you have not joined
|
||||||
|
* - you have joined
|
||||||
|
* - initial room members
|
||||||
|
* - new room members
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ConferenceCallManager2 extends EventEmitter {
|
||||||
|
constructor(client) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.client = client;
|
||||||
|
|
||||||
|
this.room = null;
|
||||||
|
|
||||||
|
// The session id is used to re-initiate calls if the user's participant
|
||||||
|
// session id has changed
|
||||||
|
this.sessionId = randomString(16);
|
||||||
|
|
||||||
|
this._memberParticipantStateTimeout = null;
|
||||||
|
|
||||||
|
// Whether or not we have entered the conference call.
|
||||||
|
this.entered = false;
|
||||||
|
|
||||||
|
// The MatrixCalls that were picked up by the Call.incoming listener,
|
||||||
|
// before the user entered the conference call.
|
||||||
|
this._incomingCallQueue = [];
|
||||||
|
|
||||||
|
// A representation of the conference call data for each room member
|
||||||
|
// that has entered the call.
|
||||||
|
this.participants = [];
|
||||||
|
|
||||||
|
this.localParticipant = null;
|
||||||
|
|
||||||
|
this.client.on("RoomState.members", this._onRoomStateMembers);
|
||||||
|
this.client.on("Call.incoming", this._onIncomingCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enter(roomId, timeout = 30000) {
|
||||||
|
// Ensure that we have joined the provided room.
|
||||||
|
await this.client.joinRoom(roomId);
|
||||||
|
|
||||||
|
// Get the room info, wait if it hasn't been fetched yet.
|
||||||
|
// Timeout after 30 seconds or the provided duration.
|
||||||
|
const room = await new Promise((resolve, reject) => {
|
||||||
|
const initialRoom = manager.client.getRoom(roomId);
|
||||||
|
|
||||||
|
if (initialRoom) {
|
||||||
|
resolve(initialRoom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomTimeout = setTimeout(() => {
|
||||||
|
reject(new Error("Room could not be found."));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
const roomCallback = (room) => {
|
||||||
|
if (room && room.roomId === roomId) {
|
||||||
|
this.client.removeListener("Room", roomCallback);
|
||||||
|
clearTimeout(roomTimeout);
|
||||||
|
resolve(room);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client.on("Room", roomCallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.room = room;
|
||||||
|
|
||||||
|
// Ensure that this room is marked as a conference room so clients can react appropriately
|
||||||
|
const activeConf = room.currentState
|
||||||
|
.getStateEvents(CONF_ROOM, "")
|
||||||
|
?.getContent()?.active;
|
||||||
|
|
||||||
|
if (!activeConf) {
|
||||||
|
this.client.sendStateEvent(room.roomId, CONF_ROOM, { active: true }, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permissions and get the user's webcam/mic stream if we haven't yet.
|
||||||
|
const userId = this.client.getUserId();
|
||||||
|
const stream = await this.client.getLocalVideoStream();
|
||||||
|
|
||||||
|
this.localParticipant = {
|
||||||
|
userId,
|
||||||
|
stream,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.participants.push(this.localParticipant);
|
||||||
|
this.emit("debugstate", userId, null, "you");
|
||||||
|
|
||||||
|
// Announce to the other room members that we have entered the room.
|
||||||
|
// Continue doing so every PARTICIPANT_TIMEOUT ms
|
||||||
|
this._updateMemberParticipantState();
|
||||||
|
|
||||||
|
// Set up participants for the members currently in the room.
|
||||||
|
// Other members will be picked up by the RoomState.members event.
|
||||||
|
const initialMembers = room.getMembers();
|
||||||
|
|
||||||
|
for (const member of initialMembers) {
|
||||||
|
this._onMemberChanged(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateMemberParticipantState = () => {
|
||||||
|
const userId = this.client.getUserId();
|
||||||
|
const currentMemberState = this.room.currentState.getStateEvents(
|
||||||
|
"m.room.member",
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
this.client.sendStateEvent(
|
||||||
|
this.room.roomId,
|
||||||
|
"m.room.member",
|
||||||
|
{
|
||||||
|
...currentMemberState.getContent(),
|
||||||
|
[CONF_PARTICIPANT]: {
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
expiresAt: new Date().getTime() + PARTICIPANT_TIMEOUT * 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
this._memberParticipantStateTimeout = setTimeout(
|
||||||
|
this._updateMemberParticipantState,
|
||||||
|
PARTICIPANT_TIMEOUT
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call Setup
|
||||||
|
*
|
||||||
|
* There are two different paths for calls to be created:
|
||||||
|
* 1. Incoming calls triggered by the Call.incoming event.
|
||||||
|
* 2. Outgoing calls to the initial members of a room or new members
|
||||||
|
* as they are observed by the RoomState.members event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
_onIncomingCall = (call) => {
|
||||||
|
// The incoming calls may be for another room, which we will ignore.
|
||||||
|
if (call.roomId !== this.room.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't entered yet, add the call to a queue which we'll use later.
|
||||||
|
if (!this.entered) {
|
||||||
|
this._incomingCallQueue.push(call);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user calling has an existing participant and use this call instead.
|
||||||
|
const userId = call.opponentMember.userId;
|
||||||
|
const existingParticipant = manager.participants.find(
|
||||||
|
(p) => p.userId === userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingParticipant) {
|
||||||
|
// This also fires the hangup event and triggers those side-effects
|
||||||
|
existingParticipant.call.hangup("user_hangup", false);
|
||||||
|
existingParticipant.call = call;
|
||||||
|
}
|
||||||
|
|
||||||
|
call.answer();
|
||||||
|
|
||||||
|
this.emit("call", call);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onRoomStateMembers = (_event, _state, member) => {
|
||||||
|
this._onMemberChanged(member);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMemberChanged = (member) => {
|
||||||
|
// Don't process new members until we've entered the conference call.
|
||||||
|
if (!this.entered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The member events may be received for another room, which we will ignore.
|
||||||
|
if (member.roomId !== this.room.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localUserId = this.client.getUserId();
|
||||||
|
|
||||||
|
if (member.userId === localUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberStateEvent = this.room.currentState.getStateEvents(
|
||||||
|
"m.room.member",
|
||||||
|
member.userId
|
||||||
|
);
|
||||||
|
const { expiresAt, sessionId } =
|
||||||
|
memberStateEvent.getContent()[CONF_PARTICIPANT];
|
||||||
|
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
if (expiresAt < now) {
|
||||||
|
this.emit("debugstate", member.userId, null, "inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the session id and expiration time of the existing participant to see if we should
|
||||||
|
// hang up the existing call and create a new one or ignore the changed member.
|
||||||
|
const participant = this.participants.find((p) => p.userId === userId);
|
||||||
|
|
||||||
|
if (participant && participant.sessionId !== sessionId) {
|
||||||
|
this.emit("debugstate", member.userId, null, "inactive");
|
||||||
|
participant.call.hangup("user_hangup", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("call", call);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
42
src/randomstring.js
Normal file
42
src/randomstring.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const DIGITS = "0123456789";
|
||||||
|
|
||||||
|
export function randomString(len) {
|
||||||
|
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomLowercaseString(len) {
|
||||||
|
return randomStringFrom(len, LOWERCASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomUppercaseString(len) {
|
||||||
|
return randomStringFrom(len, UPPERCASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomStringFrom(len, chars) {
|
||||||
|
let ret = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < len; ++i) {
|
||||||
|
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user