From 69810ea54c9e332ccf2b924b43608d5a8cb76369 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 21 Jul 2021 23:28:01 -0700 Subject: [PATCH] Improved ConferenceCall logic --- src/App.jsx | 106 +++++++++++++++++++++--- src/ConferenceCall.js | 186 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 src/ConferenceCall.js diff --git a/src/App.jsx b/src/App.jsx index fb29008..fc49b6c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,6 +26,7 @@ import { Link, Redirect, } from "react-router-dom"; +import { ConferenceCall } from "./ConferenceCall"; export default function App() { const { protocol, host } = window.location; @@ -357,21 +358,35 @@ function JoinOrCreateRoom({ client }) { } function useVideoRoom(client, roomId, timeout = 5000) { - const [{ loading, room, error }, setState] = useState({ + const [{ loading, joined, room, participants, error }, setState] = useState({ loading: true, + joined: false, room: undefined, + participants: [], error: undefined, }); useEffect(() => { - setState({ loading: true, room: undefined, error: undefined }); + setState((prevState) => ({ + ...prevState, + loading: true, + room: undefined, + error: undefined, + })); - client.joinRoom(roomId).catch(console.error); + client.joinRoom(roomId).catch((err) => { + setState((prevState) => ({ ...prevState, loading: false, error: err })); + }); let initialRoom = client.getRoom(roomId); if (initialRoom) { - setState({ loading: false, room: initialRoom, error: undefined }); + setState((prevState) => ({ + ...prevState, + loading: false, + room: initialRoom, + error: undefined, + })); return; } @@ -381,18 +396,24 @@ function useVideoRoom(client, roomId, timeout = 5000) { if (room && room.roomId === roomId) { clearTimeout(timeoutId); client.removeListener("Room", roomCallback); - setState({ loading: false, room, error: undefined }); + setState((prevState) => ({ + ...prevState, + loading: false, + room, + error: undefined, + })); } } client.on("Room", roomCallback); timeoutId = setTimeout(() => { - setState({ + setState((prevState) => ({ + ...prevState, loading: false, room: undefined, error: new Error("Room could not be found."), - }); + })); client.removeListener("Room", roomCallback); }, timeout); @@ -403,15 +424,53 @@ function useVideoRoom(client, roomId, timeout = 5000) { }, [roomId]); const joinCall = useCallback(() => { - console.log("join call"); - }); + const conferenceCall = new ConferenceCall(client, roomId); - return { loading, room, error, joinCall }; + const onJoined = () => { + setState((prevState) => ({ + ...prevState, + joined: true, + })); + }; + + conferenceCall.on("joined", onJoined); + + const onParticipantsChanged = () => { + setState((prevState) => ({ + ...prevState, + participants: conferenceCall.participants, + })); + }; + + conferenceCall.on("participants_changed", onParticipantsChanged); + + conferenceCall.join(); + + return () => { + conferenceCall.removeListener("joined", onJoined); + conferenceCall.removeListener( + "participants_changed", + onParticipantsChanged + ); + conferenceCall.leave(); + + setState((prevState) => ({ + ...prevState, + joined: false, + participants: [], + })); + }; + }, [client, roomId]); + + return { loading, joined, room, participants, error, joinCall }; } function Room({ client }) { const { roomId } = useParams(); - const { loading, room, error, joinCall } = useVideoRoom(client, roomId); + const { loading, joined, room, participants, error, joinCall } = useVideoRoom( + client, + roomId + ); return (
@@ -426,9 +485,32 @@ function Room({ client }) {
  • {member.name}
  • ))} - + {joined ? ( + participants.map((participant) => ( + + )) + ) : ( + + )} )}
    ); } + +function Participant({ participant }) { + const videoRef = useRef(); + + useEffect(() => { + if (participant.feed) { + if (participant.muted) { + videoRef.current.muted = true; + } + + videoRef.current.srcObject = participant.feed.stream; + videoRef.current.play(); + } + }, [participant.feed]); + + return ; +} diff --git a/src/ConferenceCall.js b/src/ConferenceCall.js new file mode 100644 index 0000000..37fba5f --- /dev/null +++ b/src/ConferenceCall.js @@ -0,0 +1,186 @@ +import EventEmitter from "events"; + +const CONF_ROOM = "me.robertlong.conf"; +const CONF_PARTICIPANT = "me.robertlong.conf.participant"; +const PARTICIPANT_TIMEOUT = 1000 * 30; + +export class ConferenceCall extends EventEmitter { + constructor(client, roomId) { + super(); + this.client = client; + this.roomId = roomId; + this.confId = null; + this.room = client.getRoom(roomId); + this.localParticipant = { + userId: client.getUserId(), + feed: null, + call: null, + muted: true, + }; + this.participants = [this.localParticipant]; + } + + join() { + const activeConf = this.room.currentState + .getStateEvents(CONF_ROOM, "") + ?.getContent()?.active; + + if (!activeConf) { + this.client.sendStateEvent(this.roomId, CONF_ROOM, { active: true }, ""); + } + + this._updateParticipantState(); + + this.client.on("RoomState.members", this._onMemberChanged); + this.client.on("Call.incoming", this._onIncomingCall); + this.room + .getMembers() + .forEach((member) => this._processMember(member.userId)); + + this.emit("joined"); + } + + _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) { + if (userId === this.client.getUserId()) { + return; + } + + const participant = this.participants.find((p) => p.userId === userId); + + 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 * 1.5 + ) { + if (participant && participant.call) { + participant.call.hangup("user_hangup"); + } + + return; + } + + if (!participant) { + this._callUser(userId); + } + } + + _onIncomingCall = (call) => { + console.debug("onIncomingCall", call); + this._addCall(call); + call.answer(); + }; + + _callUser = (userId) => { + const call = this.client.createCall(this.roomId, userId); + console.debug("_callUser", call, userId); + // TODO: Handle errors + this._addCall(call, userId); + call.placeVideoCall(); + }; + + _addCall(call, userId) { + this.participants.push({ + userId: userId || call.getOpponentMember().userId, + feed: null, + call, + }); + + call.on("feeds_changed", () => this._onCallFeedsChanged(call)); + call.on("hangup", () => this._onCallHangup(call)); + call.on("replaced", (newCall) => this._onCallReplaced(call, newCall)); + this._onCallFeedsChanged(call); + + this.emit("participants_changed"); + } + + _onCallFeedsChanged = (call) => { + console.debug("_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) => { + 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, newCall); + + const remoteParticipant = this.participants.find((p) => p.call === call); + + remoteParticipant.call = newCall; + + 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"); + }; +}