Group events by calls or users

This commit is contained in:
Robert Long 2021-07-30 16:55:25 -07:00
commit 661e3dd98e
6 changed files with 211 additions and 162 deletions

11
package-lock.json generated
View file

@ -7,6 +7,7 @@
"": { "": {
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"classnames": "^2.3.1",
"color-hash": "^2.0.1", "color-hash": "^2.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"matrix-js-sdk": "^12.0.1", "matrix-js-sdk": "^12.0.1",
@ -588,6 +589,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -2039,6 +2045,11 @@
"supports-color": "^5.3.0" "supports-color": "^5.3.0"
} }
}, },
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"color-convert": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",

View file

@ -6,6 +6,7 @@
"serve": "vite preview" "serve": "vite preview"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.3.1",
"color-hash": "^2.0.1", "color-hash": "^2.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"matrix-js-sdk": "^12.0.1", "matrix-js-sdk": "^12.0.1",

View file

@ -134,9 +134,10 @@ export class ConferenceCallManager extends EventEmitter {
this.client = client; this.client = client;
this.joined = false; this.joined = false;
this.room = null; this.room = null;
const localUserId = client.getUserId();
this.localParticipant = { this.localParticipant = {
local: true, local: true,
userId: client.getUserId(), userId: localUserId,
feed: null, feed: null,
call: null, call: null,
muted: true, muted: true,
@ -144,20 +145,24 @@ export class ConferenceCallManager extends EventEmitter {
this.participants = [this.localParticipant]; this.participants = [this.localParticipant];
this.pendingCalls = []; this.pendingCalls = [];
this.callUserMap = new Map(); this.callUserMap = new Map();
this.debugState = new Map(); this.debugState = {
this._setDebugState(client.getUserId(), "you"); users: new Map(),
calls: new Map(),
};
this.client.on("event", this._onEvent); this.client.on("event", this._onEvent);
this.client.on("RoomState.members", this._onMemberChanged); this.client.on("RoomState.members", this._onMemberChanged);
this.client.on("Call.incoming", this._onIncomingCall); this.client.on("Call.incoming", this._onIncomingCall);
} }
join(roomId) { setRoom(roomId) {
this.joined = true;
this._addDebugEvent(this.client.getUserId(), "joined call");
this.roomId = roomId; this.roomId = roomId;
this.room = this.client.getRoom(this.roomId); this.room = this.client.getRoom(this.roomId);
}
join() {
this.joined = true;
this._setDebugState(this.client.getUserId(), null, "you");
const activeConf = this.room.currentState const activeConf = this.room.currentState
.getStateEvents(CONF_ROOM, "") .getStateEvents(CONF_ROOM, "")
@ -169,18 +174,12 @@ export class ConferenceCallManager extends EventEmitter {
const roomMemberIds = this.room.getMembers().map(({ userId }) => userId); const roomMemberIds = this.room.getMembers().map(({ userId }) => userId);
for (const userId of this.debugState.keys()) {
if (roomMemberIds.indexOf(userId) === -1) {
this.debugState.delete(userId);
}
}
roomMemberIds.forEach((userId) => { roomMemberIds.forEach((userId) => {
this._processMember(userId); this._processMember(userId);
}); });
for (const { call, onHangup, onReplaced } of this.pendingCalls) { for (const { call, onHangup, onReplaced } of this.pendingCalls) {
if (call.roomId !== roomId) { if (call.roomId !== this.roomId) {
continue; continue;
} }
@ -200,31 +199,66 @@ export class ConferenceCallManager extends EventEmitter {
const roomId = event.getRoomId(); const roomId = event.getRoomId();
const type = event.getType(); const type = event.getType();
if (type.startsWith("m.call.") || type.startsWith("me.robertlong.conf")) { if (roomId === this.roomId && type.startsWith("m.call.")) {
const content = event.getContent(); const sender = event.getSender();
const details = { content: event.toJSON(), roomId }; const { call_id } = event.getContent();
if (content.invitee && content.call_id) { if (call_id) {
this.callUserMap.set(content.call_id, content.invitee); if (this.debugState.calls.has(call_id)) {
details.to = content.invitee; const callState = this.debugState.calls.get(call_id);
} else if (content.call_id) { callState.events.push(event);
details.to = this.callUserMap.get(content.call_id); } else {
this.debugState.calls.set(call_id, {
state: "unknown",
events: [event],
});
}
} }
switch (type) { if (this.debugState.users.has(sender)) {
case "m.call.invite": const userState = this.debugState.users.get(sender);
case "m.call.candidates": userState.events.push(event);
case "m.call.answer": } else {
case "m.call.hangup": this.debugState.users.set(sender, {
case "m.call.select_answer": state: "unknown",
details.callId = content.call_id; events: [event],
break; });
} }
this._addDebugEvent(event.getSender(), type, details); this.emit("debug");
} }
}; };
_setDebugState(userId, callId, state) {
if (userId) {
const userState = this.debugState.users.get(userId);
if (userState) {
userState.state = state;
} else {
this.debugState.users.set(userId, {
state,
events: [],
});
}
}
if (callId) {
const callState = this.debugState.calls.get(callId);
if (callState) {
callState.state = state;
} else {
this.debugState.calls.set(callId, {
state,
events: [],
});
}
}
this.emit("debug");
}
_updateParticipantState = () => { _updateParticipantState = () => {
const userId = this.client.getUserId(); const userId = this.client.getUserId();
const currentMemberState = this.room.currentState.getStateEvents( const currentMemberState = this.room.currentState.getStateEvents(
@ -286,20 +320,19 @@ export class ConferenceCallManager extends EventEmitter {
new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT
) { ) {
// Member is inactive so don't call them. // Member is inactive so don't call them.
this._setDebugState(userId, "inactive"); this._setDebugState(userId, null, "inactive");
return; return;
} }
// Only initiate a call with a user who has a userId that is lexicographically // Only initiate a call with a user who has a userId that is lexicographically
// less than your own. Otherwise, that user will call you. // less than your own. Otherwise, that user will call you.
if (userId < localUserId) { if (userId < localUserId) {
this._setDebugState(userId, "waiting for invite"); this._setDebugState(userId, null, "waiting for invite");
return; return;
} }
const call = this.client.createCall(this.roomId, userId); const call = this.client.createCall(this.roomId, userId);
this._addCall(call, userId); this._addCall(call, userId);
this._setDebugState(userId, "calling");
call.placeVideoCall(); call.placeVideoCall();
} }
@ -342,7 +375,6 @@ export class ConferenceCallManager extends EventEmitter {
const userId = call.opponentMember.userId; const userId = call.opponentMember.userId;
this._addCall(call, userId); this._addCall(call, userId);
this._setDebugState(userId, "answered");
call.answer(); call.answer();
}; };
@ -365,8 +397,9 @@ export class ConferenceCallManager extends EventEmitter {
call, call,
}); });
this._setDebugCallId(userId, call.callId); call.on("state", (state) =>
this._setDebugState(userId, call.callId, state)
);
call.on("feeds_changed", () => this._onCallFeedsChanged(call)); call.on("feeds_changed", () => this._onCallFeedsChanged(call));
call.on("hangup", () => this._onCallHangup(call)); call.on("hangup", () => this._onCallHangup(call));
@ -388,7 +421,6 @@ export class ConferenceCallManager extends EventEmitter {
if (!this.localParticipant.feed && localFeeds.length > 0) { if (!this.localParticipant.feed && localFeeds.length > 0) {
this.localParticipant.call = call; this.localParticipant.call = call;
this._setDebugCallId(this.localParticipant.userId, call.callId);
this.localParticipant.feed = localFeeds[0]; this.localParticipant.feed = localFeeds[0];
participantsChanged = true; participantsChanged = true;
} }
@ -400,7 +432,6 @@ export class ConferenceCallManager extends EventEmitter {
if (remoteFeeds.length > 0 && remoteParticipant.feed !== remoteFeeds[0]) { if (remoteFeeds.length > 0 && remoteParticipant.feed !== remoteFeeds[0]) {
remoteParticipant.feed = remoteFeeds[0]; remoteParticipant.feed = remoteFeeds[0];
this._setDebugState(call.opponentMember.userId, "streaming");
participantsChanged = true; participantsChanged = true;
} }
@ -410,16 +441,14 @@ export class ConferenceCallManager extends EventEmitter {
}; };
_onCallHangup = (call) => { _onCallHangup = (call) => {
if (call.hangupReason === "replaced") {
return;
}
this._setDebugState(call.opponentMember.userId, "hungup");
const participantIndex = this.participants.findIndex( const participantIndex = this.participants.findIndex(
(p) => !p.local && p.call === call (p) => !p.local && p.call === call
); );
if (call.hangupReason === "replaced") {
return;
}
if (participantIndex === -1) { if (participantIndex === -1) {
return; return;
} }
@ -436,16 +465,13 @@ export class ConferenceCallManager extends EventEmitter {
if (localFeeds.length > 0) { if (localFeeds.length > 0) {
this.localParticipant.call = call; this.localParticipant.call = call;
this._setDebugCallId(this.localParticipant.userId, call.callId);
this.localParticipant.feed = localFeeds[0]; this.localParticipant.feed = localFeeds[0];
} else { } else {
this.localParticipant.call = null; this.localParticipant.call = null;
this._setDebugCallId(this.localParticipant.userId, null);
this.localParticipant.feed = null; this.localParticipant.feed = null;
} }
} else { } else {
this.localParticipant.call = null; this.localParticipant.call = null;
this._setDebugCallId(this.localParticipant.userId, null);
this.localParticipant.feed = null; this.localParticipant.feed = null;
} }
} }
@ -454,17 +480,11 @@ export class ConferenceCallManager extends EventEmitter {
}; };
_onCallReplaced = (call, newCall) => { _onCallReplaced = (call, newCall) => {
this._addDebugEvent(call.opponentMember.userId, "replaced", {
callId: call.callId,
newCallId: newCall.callId,
});
const remoteParticipant = this.participants.find( const remoteParticipant = this.participants.find(
(p) => !p.local && p.call === call (p) => !p.local && p.call === call
); );
remoteParticipant.call = newCall; remoteParticipant.call = newCall;
this._setDebugCallId(remoteParticipant.userId, newCall.callId);
newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall)); newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall));
newCall.on("hangup", () => this._onCallHangup(newCall)); newCall.on("hangup", () => this._onCallHangup(newCall));
@ -507,48 +527,10 @@ export class ConferenceCallManager extends EventEmitter {
this.participants = [this.localParticipant]; this.participants = [this.localParticipant];
this.localParticipant.feed = null; this.localParticipant.feed = null;
this.localParticipant.call = null; this.localParticipant.call = null;
this._setDebugCallId(this.localParticipant.userId, null);
this.emit("participants_changed"); this.emit("participants_changed");
} }
_addDebugEvent(sender, type, content) {
if (!this.debugState.has(sender)) {
this.debugState.set(sender, {
callId: null,
state: "unknown",
events: [{ type, ...content }],
});
} else {
const { events } = this.debugState.get(sender);
events.push({ type, roomId: this.roomId, ...content });
}
this.emit("debug");
}
_setDebugState(userId, state) {
if (!this.debugState.has(userId)) {
this.debugState.set(userId, { state, callId: null, events: [] });
} else {
const userState = this.debugState.get(userId);
userState.state = state;
}
this.emit("debug");
}
_setDebugCallId(userId, callId) {
if (!this.debugState.has(userId)) {
this.debugState.set(userId, { state: "unknown", callId, events: [] });
} else {
const userState = this.debugState.get(userId);
userState.callId = callId;
}
this.emit("debug");
}
logout() { logout() {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
} }

View file

@ -136,6 +136,8 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
error: undefined, error: undefined,
})); }));
manager.setRoom(roomId);
manager.client.joinRoom(roomId).catch((err) => { manager.client.joinRoom(roomId).catch((err) => {
setState((prevState) => ({ ...prevState, loading: false, error: err })); setState((prevState) => ({ ...prevState, loading: false, error: err }));
}); });
@ -196,7 +198,7 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
manager.on("participants_changed", onParticipantsChanged); manager.on("participants_changed", onParticipantsChanged);
manager.join(roomId); manager.join();
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,

View file

@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import ColorHash from "color-hash"; import ColorHash from "color-hash";
import classNames from "classnames";
import styles from "./DevTools.module.css"; import styles from "./DevTools.module.css";
const colorHash = new ColorHash({ lightness: 0.8 }); const colorHash = new ColorHash({ lightness: 0.8 });
@ -25,13 +26,27 @@ function CallId({ callId, ...rest }) {
); );
} }
function sortEntries(a, b) {
const aInactive = a[1].state === "inactive";
const bInactive = b[1].state === "inactive";
if (aInactive && !bInactive) {
return 1;
} else if (bInactive && !aInactive) {
return -1;
} else {
return a[0] < b[0] ? -1 : 1;
}
}
export function DevTools({ manager }) { export function DevTools({ manager }) {
const [debugState, setDebugState] = useState(manager.debugState); const [debugState, setDebugState] = useState(manager.debugState);
const [selectedEvent, setSelectedEvent] = useState(); const [selectedEvent, setSelectedEvent] = useState();
const [activeTab, setActiveTab] = useState("users");
useEffect(() => { useEffect(() => {
function onRoomDebug() { function onRoomDebug() {
setDebugState(manager.debugState); setDebugState({ ...manager.debugState });
} }
manager.on("debug", onRoomDebug); manager.on("debug", onRoomDebug);
@ -47,15 +62,50 @@ export function DevTools({ manager }) {
return ( return (
<div className={styles.devTools}> <div className={styles.devTools}>
{Array.from(debugState.entries()).map(([userId, props]) => ( <div className={styles.toolbar}>
<UserState <div
key={userId} className={classNames(styles.tab, {
roomId={manager.roomId} [styles.activeTab]: activeTab === "users",
onSelectEvent={setSelectedEvent} })}
userId={userId} onClick={() => setActiveTab("users")}
{...props} >
/> Users
))} </div>
<div
className={classNames(styles.tab, {
[styles.activeTab]: activeTab === "calls",
})}
onClick={() => setActiveTab("calls")}
>
Calls
</div>
</div>
<div className={styles.devToolsContainer}>
{activeTab === "users" &&
Array.from(debugState.users.entries())
.sort(sortEntries)
.map(([userId, props]) => (
<EventContainer
key={userId}
showCallId
title={<UserId userId={userId} />}
{...props}
onSelect={setSelectedEvent}
/>
))}
{activeTab === "calls" &&
Array.from(debugState.calls.entries())
.sort(sortEntries)
.map(([callId, props]) => (
<EventContainer
key={callId}
showSender
title={<CallId callId={callId} />}
{...props}
onSelect={setSelectedEvent}
/>
))}
</div>
{selectedEvent && ( {selectedEvent && (
<EventViewer <EventViewer
event={selectedEvent} event={selectedEvent}
@ -66,7 +116,7 @@ export function DevTools({ manager }) {
); );
} }
function UserState({ roomId, userId, state, callId, events, onSelectEvent }) { function EventContainer({ title, state, events, ...rest }) {
const eventsRef = useRef(); const eventsRef = useRef();
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
@ -90,78 +140,59 @@ function UserState({ roomId, userId, state, callId, events, onSelectEvent }) {
return ( return (
<div className={styles.user}> <div className={styles.user}>
<div className={styles.userId}> <div className={styles.userId}>
<UserId userId={userId} /> <span>{title}</span>
<span>{`(${state})`}</span> <span>{`(${state})`}</span>
{callId && <CallId callId={callId} />}
</div> </div>
<div ref={eventsRef} className={styles.events} onScroll={onScroll}> <div ref={eventsRef} className={styles.events} onScroll={onScroll}>
{events {events.map((event, idx) => (
.filter((e) => e.roomId === roomId) <EventItem key={idx} event={event} {...rest} />
.map((event, idx) => ( ))}
<div
className={styles.event}
key={idx}
onClick={() => onSelectEvent(event)}
>
<span className={styles.eventType}>{event.type}</span>
{event.callId && (
<CallId className={styles.eventDetails} callId={event.callId} />
)}
{event.newCallId && (
<>
<span className={styles.eventDetails}>{"->"}</span>
<CallId
className={styles.eventDetails}
callId={event.newCallId}
/>
</>
)}
{event.to && (
<UserId className={styles.eventDetails} userId={event.to} />
)}
{event.reason && (
<span className={styles.eventDetails}>{event.reason}</span>
)}
</div>
))}
</div> </div>
</div> </div>
); );
} }
function EventItem({ event, showCallId, showSender, onSelect }) {
const type = event.getType();
const sender = event.getSender();
const { call_id, invitee } = event.getContent();
return (
<div className={styles.event} onClick={() => onSelect(event)}>
{showSender && sender && (
<UserId className={styles.eventDetails} userId={sender} />
)}
<span className={styles.eventType}>{type}</span>
{showCallId && call_id && (
<CallId className={styles.eventDetails} callId={call_id} />
)}
{invitee && <UserId className={styles.eventDetails} userId={invitee} />}
</div>
);
}
function EventViewer({ event, onClose }) { function EventViewer({ event, onClose }) {
const type = event.getType();
const sender = event.getSender();
const { call_id, invitee } = event.getContent();
const json = event.toJSON();
return ( return (
<div className={styles.eventViewer}> <div className={styles.eventViewer}>
<p>Event Type: {event.type}</p> <p>Event Type: {type}</p>
{event.callId && ( <p>Sender: {sender}</p>
{call_id && (
<p> <p>
Call Id: <CallId callId={event.callId} /> Call Id: <CallId callId={call_id} />
</p> </p>
)} )}
{event.newCallId && ( {invitee && (
<p> <p>
New Call Id: Invitee: <UserId userId={invitee} />
<CallId callId={event.newCallId} />
</p> </p>
)} )}
{event.to && ( <p>Raw Event:</p>
<p> <pre className={styles.content}>{JSON.stringify(json, undefined, 2)}</pre>
To: <UserId userId={event.to} />
</p>
)}
{event.reason && (
<p>
Reason: <span>{event.reason}</span>
</p>
)}
{event.content && (
<>
<p>Content:</p>
<pre className={styles.content}>
{JSON.stringify(event.content, undefined, 2)}
</pre>
</>
)}
<button onClick={onClose}>Close</button> <button onClick={onClose}>Close</button>
</div> </div>
); );

View file

@ -1,9 +1,31 @@
.devTools { .devTools {
display: flex; display: flex;
height: 250px; flex-direction: column;
border-top: 2px solid #111; border-top: 2px solid #111;
gap: 2px;
background-color: #111; background-color: #111;
}
.toolbar {
display: flex;
background-color: #222;
}
.tab {
vertical-align: middle;
padding: 4px 8px;
background-color: #444;
margin: 0 2px;
cursor: pointer;
}
.activeTab {
background-color: #666;
}
.devToolsContainer {
display: flex;
height: 250px;
gap: 2px;
overflow-x: auto; overflow-x: auto;
} }