Add dev tools

This commit is contained in:
Robert Long 2021-07-28 16:14:38 -07:00
parent fa60eb28e9
commit 0d7ad5c07a
8 changed files with 310 additions and 46 deletions

11
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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");
}
}

View file

@ -28,7 +28,6 @@ export function useConferenceCallManager(homeserverUrl) {
useEffect(() => {
ConferenceCallManager.restore(homeserverUrl)
.then((manager) => {
console.log(manager);
setState({
manager,
loading: false,

97
src/DevTools.jsx Normal file
View file

@ -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 (
<span style={{ color }} {...rest}>
{shortId}
</span>
);
}
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 (
<div className={styles.devTools}>
{Array.from(debugState.entries()).map(([userId, props]) => (
<UserState key={userId} userId={userId} {...props} />
))}
</div>
);
}
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 (
<div className={styles.user}>
<div className={styles.userId}>
<span>{userId}</span>
{callId && <CallId callId={callId} />}
<span>{`(${state})`}</span>
</div>
<div ref={eventsRef} className={styles.events} onScroll={onScroll}>
{events.map((event, idx) => (
<div className={styles.event} key={idx}>
<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 && (
<span className={styles.eventDetails}>{event.to}</span>
)}
{event.reason && (
<span className={styles.eventDetails}>{event.reason}</span>
)}
</div>
))}
</div>
</div>
);
}

70
src/DevTools.module.css Normal file
View file

@ -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;
}

View file

@ -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 (
<div className={styles.room}>
@ -70,6 +95,7 @@ export function Room({ manager }) {
</button>
</div>
)}
{debug && <DevTools manager={manager} />}
</div>
);
}

View file

@ -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;
}