Improved ConferenceCall logic

This commit is contained in:
Robert Long 2021-07-21 23:28:01 -07:00
parent f9bc409a0e
commit 69810ea54c
2 changed files with 280 additions and 12 deletions

View file

@ -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 (
<div>
@ -426,9 +485,32 @@ function Room({ client }) {
<li key={member.userId}>{member.name}</li>
))}
</ul>
<button onClick={joinCall}>Join Call</button>
{joined ? (
participants.map((participant) => (
<Participant key={participant.userId} participant={participant} />
))
) : (
<button onClick={joinCall}>Join Call</button>
)}
</>
)}
</div>
);
}
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 <video ref={videoRef}></video>;
}

186
src/ConferenceCall.js Normal file
View file

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