From 0d7ad5c07a5b61370336b7304729102a30f679eb Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 28 Jul 2021 16:14:38 -0700 Subject: [PATCH] Add dev tools --- package-lock.json | 11 +++ package.json | 1 + src/ConferenceCallManager.js | 144 +++++++++++++++++++++--------- src/ConferenceCallManagerHooks.js | 1 - src/DevTools.jsx | 97 ++++++++++++++++++++ src/DevTools.module.css | 70 +++++++++++++++ src/Room.jsx | 30 ++++++- src/Room.module.css | 2 + 8 files changed, 310 insertions(+), 46 deletions(-) create mode 100644 src/DevTools.jsx create mode 100644 src/DevTools.module.css diff --git a/package-lock.json b/package-lock.json index 0442c6c..eaf8dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "version": "0.0.0", "dependencies": { + "color-hash": "^2.0.1", "events": "^3.3.0", "matrix-js-sdk": "^12.0.1", "react": "^17.0.0", @@ -596,6 +597,11 @@ "color-name": "1.1.3" } }, + "node_modules/color-hash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-hash/-/color-hash-2.0.1.tgz", + "integrity": "sha512-/wIYAQ3xL9ruURLmDbxAsXEsivaOfwWDUVy+zbWJZL3bnNQIDNSmmqbkNzeTOQvDdiz11Kb010UlJN7hUXLg/w==" + }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", @@ -2042,6 +2048,11 @@ "color-name": "1.1.3" } }, + "color-hash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-hash/-/color-hash-2.0.1.tgz", + "integrity": "sha512-/wIYAQ3xL9ruURLmDbxAsXEsivaOfwWDUVy+zbWJZL3bnNQIDNSmmqbkNzeTOQvDdiz11Kb010UlJN7hUXLg/w==" + }, "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", diff --git a/package.json b/package.json index d69922a..b084fbc 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "serve": "vite preview" }, "dependencies": { + "color-hash": "^2.0.1", "events": "^3.3.0", "matrix-js-sdk": "^12.0.1", "react": "^17.0.0", diff --git a/src/ConferenceCallManager.js b/src/ConferenceCallManager.js index 74a05dc..bc97f97 100644 --- a/src/ConferenceCallManager.js +++ b/src/ConferenceCallManager.js @@ -142,21 +142,19 @@ export class ConferenceCallManager extends EventEmitter { muted: true, }; this.participants = [this.localParticipant]; - this.pendingCalls = []; - + this.debugState = new Map(); + this._setDebugState(client.getUserId(), "you"); + this.client.on("event", this._onEvent); this.client.on("RoomState.members", this._onMemberChanged); this.client.on("Call.incoming", this._onIncomingCall); } join(roomId) { - console.debug( - "join", - `Local user ${this.client.getUserId()} joining room ${this.roomId}` - ); - this.joined = true; + this._addDebugEvent(this.client.getUserId(), "joined call"); + this.roomId = roomId; this.room = this.client.getRoom(this.roomId); @@ -185,6 +183,27 @@ export class ConferenceCallManager extends EventEmitter { this._updateParticipantState(); } + _onEvent = (event) => { + const type = event.getType(); + + if (type.startsWith("m.call.") || type.startsWith("me.robertlong.conf")) { + const content = event.getContent(); + const details = {}; + + switch (type) { + case "m.call.invite": + case "m.call.candidates": + case "m.call.answer": + case "m.call.hangup": + case "m.call.select_answer": + details.callId = content.call_id; + break; + } + + this._addDebugEvent(event.getSender(), type, details); + } + }; + _updateParticipantState = () => { const userId = this.client.getUserId(); const currentMemberState = this.room.currentState.getStateEvents( @@ -221,20 +240,6 @@ export class ConferenceCallManager extends EventEmitter { // 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.` - ); return; } @@ -256,23 +261,32 @@ export class ConferenceCallManager extends EventEmitter { new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT ) { // Member is inactive so don't call them. - console.debug( - "_processMember", - `Ignored ${userId}. User is not active in conference.` - ); + this._setDebugState(userId, "inactive"); + 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) { + this._setDebugState(userId, "waiting for invite"); return; } const call = this.client.createCall(this.roomId, userId); this._addCall(call, userId); - console.debug( - "_processMember", - `Placing video call ${call.callId} to ${userId}.` - ); + this._setDebugState(userId, "calling"); + this._addDebugEvent(this.client.getUserId(), "placeVideoCall", { + callId: call.callId, + to: userId, + }); call.placeVideoCall(); } _onIncomingCall = (call) => { + this._addDebugEvent(call.opponentMember.userId, "incoming call", { + callId: call.callId, + }); + if (!this.joined) { const onHangup = (call) => { const index = this.pendingCalls.findIndex((p) => p.call === call); @@ -308,10 +322,8 @@ export class ConferenceCallManager extends EventEmitter { if (call.opponentMember) { const userId = call.opponentMember.userId; this._addCall(call, userId); - console.debug( - "_onIncomingCall", - `Answering incoming call ${call.callId} from ${userId}` - ); + this._setDebugState(userId, "answered"); + this._addDebugEvent(userId, "answer", { callId: call.callId }); call.answer(); return; } @@ -327,10 +339,6 @@ export class ConferenceCallManager extends EventEmitter { ); if (existingCall) { - console.debug( - "_addCall", - `Found existing call ${call.callId}. Ignoring.` - ); return; } @@ -340,7 +348,8 @@ export class ConferenceCallManager extends EventEmitter { call, }); - console.debug("_addCall", `Added new participant ${userId}`); + this._setDebugCallId(userId, call.callId); + this._addDebugEvent(userId, "add participant"); call.on("feeds_changed", () => this._onCallFeedsChanged(call)); call.on("hangup", () => this._onCallHangup(call)); @@ -363,6 +372,7 @@ export class ConferenceCallManager extends EventEmitter { if (!this.localParticipant.feed && localFeeds.length > 0) { this.localParticipant.call = call; + this._setDebugCallId(this.localParticipant.userId, call.callId); this.localParticipant.feed = localFeeds[0]; participantsChanged = true; } @@ -374,6 +384,7 @@ export class ConferenceCallManager extends EventEmitter { if (remoteFeeds.length > 0 && remoteParticipant.feed !== remoteFeeds[0]) { remoteParticipant.feed = remoteFeeds[0]; + this._setDebugState(call.opponentMember.userId, "streaming"); participantsChanged = true; } @@ -383,12 +394,17 @@ export class ConferenceCallManager extends EventEmitter { }; _onCallHangup = (call) => { - console.debug("_onCallHangup", `Hangup reason ${call.hangupReason}`); + this._addDebugEvent(call.opponentMember.userId, "hangup", { + callId: call.callId, + reason: call.hangupReason, + }); if (call.hangupReason === "replaced") { return; } + this._setDebugState(call.opponentMember.userId, "hungup"); + const participantIndex = this.participants.findIndex( (p) => !p.local && p.call === call ); @@ -409,13 +425,16 @@ export class ConferenceCallManager extends EventEmitter { if (localFeeds.length > 0) { this.localParticipant.call = call; + this._setDebugCallId(this.localParticipant.userId, call.callId); this.localParticipant.feed = localFeeds[0]; } else { this.localParticipant.call = null; + this._setDebugCallId(this.localParticipant.userId, null); this.localParticipant.feed = null; } } else { this.localParticipant.call = null; + this._setDebugCallId(this.localParticipant.userId, null); this.localParticipant.feed = null; } } @@ -424,16 +443,17 @@ export class ConferenceCallManager extends EventEmitter { }; _onCallReplaced = (call, newCall) => { - console.debug( - "_onCallReplaced", - `Call ${call.callId} replaced with ${newCall.callId}` - ); + this._addDebugEvent(call.opponentMember.userId, "replaced", { + callId: call.callId, + newCallId: newCall.callId, + }); const remoteParticipant = this.participants.find( (p) => !p.local && p.call === call ); remoteParticipant.call = newCall; + this._setDebugCallId(remoteParticipant.userId, newCall.callId); newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall)); newCall.on("hangup", () => this._onCallHangup(newCall)); @@ -472,7 +492,45 @@ export class ConferenceCallManager extends EventEmitter { this.participants = [this.localParticipant]; this.localParticipant.feed = null; this.localParticipant.call = null; + this._setDebugCallId(this.localParticipant.userId, null); 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, ...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"); + } } diff --git a/src/ConferenceCallManagerHooks.js b/src/ConferenceCallManagerHooks.js index 8d89093..31121f9 100644 --- a/src/ConferenceCallManagerHooks.js +++ b/src/ConferenceCallManagerHooks.js @@ -28,7 +28,6 @@ export function useConferenceCallManager(homeserverUrl) { useEffect(() => { ConferenceCallManager.restore(homeserverUrl) .then((manager) => { - console.log(manager); setState({ manager, loading: false, diff --git a/src/DevTools.jsx b/src/DevTools.jsx new file mode 100644 index 0000000..49775bf --- /dev/null +++ b/src/DevTools.jsx @@ -0,0 +1,97 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import ColorHash from "color-hash"; +import styles from "./DevTools.module.css"; + +const colorHash = new ColorHash({ lightness: 0.8 }); + +function CallId({ callId, ...rest }) { + const shortId = callId.substr(callId.length - 16); + const color = colorHash.hex(shortId); + + return ( + + {shortId} + + ); +} + +export function DevTools({ manager }) { + const [debugState, setDebugState] = useState(manager.debugState); + + useEffect(() => { + function onRoomDebug() { + setDebugState(manager.debugState); + } + + manager.on("debug", onRoomDebug); + + return () => { + manager.removeListener("debug", onRoomDebug); + }; + }, [manager]); + + return ( +
+ {Array.from(debugState.entries()).map(([userId, props]) => ( + + ))} +
+ ); +} + +function UserState({ userId, state, callId, events }) { + const eventsRef = useRef(); + const [autoScroll, setAutoScroll] = useState(true); + + useEffect(() => { + if (autoScroll) { + const el = eventsRef.current; + el.scrollTop = el.scrollHeight - el.clientHeight; + } + }); + + const onScroll = useCallback((event) => { + const el = eventsRef.current; + + if (el.scrollHeight - el.scrollTop === el.clientHeight) { + setAutoScroll(true); + } else { + setAutoScroll(false); + } + }, []); + + return ( +
+
+ {userId} + {callId && } + {`(${state})`} +
+
+ {events.map((event, idx) => ( +
+ {event.type} + {event.callId && ( + + )} + {event.newCallId && ( + <> + {"->"} + + + )} + {event.to && ( + {event.to} + )} + {event.reason && ( + {event.reason} + )} +
+ ))} +
+
+ ); +} diff --git a/src/DevTools.module.css b/src/DevTools.module.css new file mode 100644 index 0000000..2a18ef3 --- /dev/null +++ b/src/DevTools.module.css @@ -0,0 +1,70 @@ +.devTools { + display: flex; + height: 250px; + border-top: 2px solid #111; + gap: 2px; + background-color: #111; + overflow-x: auto; +} + +.user { + display: flex; + flex-direction: column; + flex: 1; + background-color: #555; + min-width: 320px; +} + +.userId { + font-size: 14px; + font-weight: bold; + padding: 12px; +} + +.userId > * { + margin-right: 4px; +} + +.userId > :last-child { + margin-right: 0; +} + +.events { + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; + background-color: #222; +} + +.event { + display: flex; + font-family: monospace; + padding: 4px 12px; + background-color: #333; +} + +.event:nth-child(even) { + background-color: #444; +} + +.event > * { + margin-right: 4px; +} + +.event > :last-child { + margin-right: 0; +} + +.eventType, .eventDetails { + font-size: 12px; +} + +.eventType { + font-weight: bold; +} + +.eventDetails { + font-weight: 200; + word-break: break-all; +} \ No newline at end of file diff --git a/src/Room.jsx b/src/Room.jsx index d5a2d34..668123a 100644 --- a/src/Room.jsx +++ b/src/Room.jsx @@ -14,15 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import styles from "./Room.module.css"; -import { useParams } from "react-router-dom"; +import { useParams, useLocation } from "react-router-dom"; import { useVideoRoom } from "./ConferenceCallManagerHooks"; +import { DevTools } from "./DevTools"; + +function useQuery() { + const location = useLocation(); + return useMemo(() => new URLSearchParams(location.search), [location.search]); +} export function Room({ manager }) { const { roomId } = useParams(); + const query = useQuery(); const { loading, joined, room, participants, error, joinCall, leaveCall } = useVideoRoom(manager, roomId); + const [debug, setDebug] = useState(!!query.get("debug")); + + useEffect(() => { + function onKeyDown(event) { + if ( + document.activeElement.tagName !== "input" && + event.code === "Backquote" + ) { + setDebug((prevDebug) => !prevDebug); + } + } + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, []); return (
@@ -70,6 +95,7 @@ export function Room({ manager }) {
)} + {debug && } ); } diff --git a/src/Room.module.css b/src/Room.module.css index d39c873..42894ef 100644 --- a/src/Room.module.css +++ b/src/Room.module.css @@ -42,6 +42,7 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; + flex: 1; } .joinRoom ul { @@ -66,6 +67,7 @@ limitations under the License. .centerMessage { display: flex; + flex: 1; justify-content: center; align-items: center; }