element-call/src/ConferenceCallManager.js

369 lines
8.9 KiB
JavaScript
Raw Normal View History

2021-07-21 23:28:01 -07:00
import EventEmitter from "events";
const CONF_ROOM = "me.robertlong.conf";
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
const PARTICIPANT_TIMEOUT = 1000 * 5;
2021-07-21 23:28:01 -07:00
function waitForSync(client) {
return new Promise((resolve, reject) => {
const onSync = (state) => {
if (state === "PREPARED") {
resolve();
client.removeListener("sync", onSync);
}
};
client.on("sync", onSync);
});
}
export class ConferenceCallManager extends EventEmitter {
static async restore(homeserverUrl) {
try {
const authStore = localStorage.getItem("matrix-auth-store");
if (authStore) {
const { user_id, device_id, access_token } = JSON.parse(authStore);
const client = matrixcs.createClient({
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
const manager = new ConferenceCallManager(client);
await client.startClient();
await waitForSync(client);
return manager;
}
} catch (err) {
localStorage.removeItem("matrix-auth-store");
throw err;
}
}
static async login(homeserverUrl, username, password) {
try {
const registrationClient = matrixcs.createClient(homeserverUrl);
const { user_id, device_id, access_token } =
await registrationClient.loginWithPassword(username, password);
const client = matrixcs.createClient({
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token })
);
const manager = new ConferenceCallManager(client);
await client.startClient();
await waitForSync(client);
return manager;
} catch (err) {
localStorage.removeItem("matrix-auth-store");
throw err;
}
}
static async register(homeserverUrl, username, password) {
try {
const registrationClient = matrixcs.createClient(homeserverUrl);
const { user_id, device_id, access_token } =
await registrationClient.register(username, password, null, {
type: "m.login.dummy",
});
const client = matrixcs.createClient({
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token })
);
const manager = new ConferenceCallManager(client);
await client.startClient();
await waitForSync(client);
return manager;
} catch (err) {
localStorage.removeItem("matrix-auth-store");
throw err;
}
}
constructor(client) {
2021-07-21 23:28:01 -07:00
super();
this.client = client;
this.joined = false;
2021-07-26 16:01:13 -07:00
this.room = null;
2021-07-21 23:28:01 -07:00
this.localParticipant = {
local: true,
2021-07-21 23:28:01 -07:00
userId: client.getUserId(),
feed: null,
call: null,
muted: true,
calls: [],
2021-07-21 23:28:01 -07:00
};
this.participants = [this.localParticipant];
2021-07-22 16:41:57 -07:00
this.client.on("RoomState.members", this._onMemberChanged);
this.client.on("Call.incoming", this._onIncomingCall);
}
2021-07-22 16:41:57 -07:00
join(roomId) {
console.debug(
"join",
`Local user ${this.client.getUserId()} joining room ${this.roomId}`
);
this.joined = true;
2021-07-22 16:41:57 -07:00
this.roomId = roomId;
2021-07-26 16:01:13 -07:00
this.room = this.client.getRoom(this.roomId);
2021-07-21 23:28:01 -07:00
const activeConf = this.room.currentState
.getStateEvents(CONF_ROOM, "")
?.getContent()?.active;
if (!activeConf) {
this.client.sendStateEvent(this.roomId, CONF_ROOM, { active: true }, "");
}
2021-07-23 14:52:42 -07:00
this.room
.getMembers()
.forEach((member) => this._processMember(member.userId));
2021-07-21 23:28:01 -07:00
this._updateParticipantState();
}
_updateParticipantState = () => {
const userId = this.client.getUserId();
const currentMemberState = this.room.currentState.getStateEvents(
"m.room.member",
userId
);
this.client.sendStateEvent(
this.roomId,
"m.room.member",
{
...currentMemberState.getContent(),
[CONF_PARTICIPANT]: new Date().getTime(),
},
userId
);
this._participantStateTimeout = setTimeout(
this._updateParticipantState,
PARTICIPANT_TIMEOUT
);
};
_onMemberChanged = (_event, _state, member) => {
this._processMember(member.userId);
};
_processMember(userId) {
const localUserId = this.client.getUserId();
if (userId === localUserId) {
return;
}
// Don't process members until we've joined
if (!this.joined) {
console.debug(
"_processMember",
`Ignored ${userId}. Local user has not joined conference yet.`
);
return;
}
// Only initiate a call with a user who has a userId that is lexicographically
// less than your own. Otherwise, that user will call you.
if (userId < localUserId) {
console.debug(
"_processMember",
`Ignored ${userId}. Local user will answer call instead.`
);
2021-07-21 23:28:01 -07:00
return;
}
const participant = this.participants.find((p) => p.userId === userId);
if (participant) {
// Member has already been processed
return;
}
2021-07-21 23:28:01 -07:00
const memberStateEvent = this.room.currentState.getStateEvents(
"m.room.member",
userId
);
const participantTimeout = memberStateEvent.getContent()[CONF_PARTICIPANT];
if (
typeof participantTimeout === "number" &&
new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT
2021-07-21 23:28:01 -07:00
) {
// Member is inactive so don't call them.
console.debug(
"_processMember",
`Ignored ${userId}. User is not active in conference.`
);
2021-07-21 23:28:01 -07:00
return;
}
const call = this.client.createCall(this.roomId, userId);
this._addCall(call, userId);
console.debug(
"_processMember",
`Placing video call ${call.callId} to ${userId}.`
);
call.placeVideoCall();
2021-07-21 23:28:01 -07:00
}
_onIncomingCall = (call) => {
if (!this.joined) {
console.debug(
"_onIncomingCall",
"Local user hasn't joined yet. Not answering."
);
return;
}
2021-07-26 11:44:43 -07:00
if (call.opponentMember) {
const userId = call.opponentMember.userId;
this._addCall(call, userId);
console.debug(
"_onIncomingCall",
`Answering incoming call ${call.callId} from ${userId}`
);
call.answer();
return;
}
2021-07-21 23:28:01 -07:00
};
_addCall(call, userId) {
2021-07-22 16:41:57 -07:00
const existingCall = this.participants.find(
(p) => p.call && p.call.callId === call.callId
);
if (existingCall) {
console.debug(
"_addCall",
`Found existing call ${call.callId}. Ignoring.`
);
2021-07-22 16:41:57 -07:00
return;
}
2021-07-21 23:28:01 -07:00
this.participants.push({
2021-07-22 16:41:57 -07:00
userId,
2021-07-21 23:28:01 -07:00
feed: null,
call,
calls: [call],
2021-07-21 23:28:01 -07:00
});
console.debug("_addCall", `Added new participant ${userId}`);
2021-07-21 23:28:01 -07:00
call.on("feeds_changed", () => this._onCallFeedsChanged(call));
call.on("hangup", () => this._onCallHangup(call));
2021-07-22 16:41:57 -07:00
const onReplaced = (newCall) => {
this._onCallReplaced(call, newCall);
call.removeListener("replaced", onReplaced);
};
call.on("replaced", onReplaced);
2021-07-21 23:28:01 -07:00
this._onCallFeedsChanged(call);
this.emit("participants_changed");
}
_onCallFeedsChanged = (call) => {
const localFeeds = call.getLocalFeeds();
let participantsChanged = false;
if (!this.localParticipant.feed && localFeeds.length > 0) {
this.localParticipant.feed = localFeeds[0];
participantsChanged = true;
}
const remoteFeeds = call.getRemoteFeeds();
const remoteParticipant = this.participants.find((p) => p.call === call);
if (remoteFeeds.length > 0 && remoteParticipant.feed !== remoteFeeds[0]) {
remoteParticipant.feed = remoteFeeds[0];
participantsChanged = true;
}
if (participantsChanged) {
this.emit("participants_changed");
}
};
_onCallHangup = (call) => {
console.debug("_onCallHangup", `Hangup reason ${call.hangupReason}`);
2021-07-22 16:41:57 -07:00
2021-07-21 23:28:01 -07:00
if (call.hangupReason === "replaced") {
return;
}
const participantIndex = this.participants.findIndex(
(p) => p.call === call
);
if (participantIndex === -1) {
return;
}
this.participants.splice(participantIndex, 1);
this.emit("participants_changed");
};
_onCallReplaced = (call, newCall) => {
console.debug(
"_onCallReplaced",
`Call ${call.callId} replaced with ${newCall.callId}`
);
2021-07-21 23:28:01 -07:00
const remoteParticipant = this.participants.find((p) => p.call === call);
remoteParticipant.call = newCall;
remoteParticipant.calls.push(newCall);
2021-07-21 23:28:01 -07:00
newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall));
newCall.on("hangup", () => this._onCallHangup(newCall));
newCall.on("replaced", (nextCall) =>
this._onCallReplaced(newCall, nextCall)
);
this._onCallFeedsChanged(newCall);
this.emit("participants_changed");
};
}