Add dev tools
This commit is contained in:
parent
fa60eb28e9
commit
0d7ad5c07a
8 changed files with 310 additions and 46 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
97
src/DevTools.jsx
Normal 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
70
src/DevTools.module.css
Normal 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;
|
||||
}
|
30
src/Room.jsx
30
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 (
|
||||
<div className={styles.room}>
|
||||
|
@ -70,6 +95,7 @@ export function Room({ manager }) {
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{debug && <DevTools manager={manager} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue