diff --git a/src/App.jsx b/src/App.jsx
index fb290086..fc49b6c5 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 00000000..37fba5f9
--- /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");
+ };
+}