Improved ConferenceCall logic
This commit is contained in:
parent
f9bc409a0e
commit
69810ea54c
2 changed files with 280 additions and 12 deletions
104
src/App.jsx
104
src/App.jsx
|
@ -26,6 +26,7 @@ import {
|
||||||
Link,
|
Link,
|
||||||
Redirect,
|
Redirect,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
import { ConferenceCall } from "./ConferenceCall";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { protocol, host } = window.location;
|
const { protocol, host } = window.location;
|
||||||
|
@ -357,21 +358,35 @@ function JoinOrCreateRoom({ client }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useVideoRoom(client, roomId, timeout = 5000) {
|
function useVideoRoom(client, roomId, timeout = 5000) {
|
||||||
const [{ loading, room, error }, setState] = useState({
|
const [{ loading, joined, room, participants, error }, setState] = useState({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
joined: false,
|
||||||
room: undefined,
|
room: undefined,
|
||||||
|
participants: [],
|
||||||
error: undefined,
|
error: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
let initialRoom = client.getRoom(roomId);
|
||||||
|
|
||||||
if (initialRoom) {
|
if (initialRoom) {
|
||||||
setState({ loading: false, room: initialRoom, error: undefined });
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
loading: false,
|
||||||
|
room: initialRoom,
|
||||||
|
error: undefined,
|
||||||
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,18 +396,24 @@ function useVideoRoom(client, roomId, timeout = 5000) {
|
||||||
if (room && room.roomId === roomId) {
|
if (room && room.roomId === roomId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
client.removeListener("Room", roomCallback);
|
client.removeListener("Room", roomCallback);
|
||||||
setState({ loading: false, room, error: undefined });
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
loading: false,
|
||||||
|
room,
|
||||||
|
error: undefined,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on("Room", roomCallback);
|
client.on("Room", roomCallback);
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
setState({
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
loading: false,
|
loading: false,
|
||||||
room: undefined,
|
room: undefined,
|
||||||
error: new Error("Room could not be found."),
|
error: new Error("Room could not be found."),
|
||||||
});
|
}));
|
||||||
client.removeListener("Room", roomCallback);
|
client.removeListener("Room", roomCallback);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
@ -403,15 +424,53 @@ function useVideoRoom(client, roomId, timeout = 5000) {
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
const joinCall = useCallback(() => {
|
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 }) {
|
function Room({ client }) {
|
||||||
const { roomId } = useParams();
|
const { roomId } = useParams();
|
||||||
const { loading, room, error, joinCall } = useVideoRoom(client, roomId);
|
const { loading, joined, room, participants, error, joinCall } = useVideoRoom(
|
||||||
|
client,
|
||||||
|
roomId
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -426,9 +485,32 @@ function Room({ client }) {
|
||||||
<li key={member.userId}>{member.name}</li>
|
<li key={member.userId}>{member.name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
{joined ? (
|
||||||
|
participants.map((participant) => (
|
||||||
|
<Participant key={participant.userId} participant={participant} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<button onClick={joinCall}>Join Call</button>
|
<button onClick={joinCall}>Join Call</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
186
src/ConferenceCall.js
Normal 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");
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue