mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Moving to matrix-js-sdk
This commit is contained in:
parent
4d7e5583eb
commit
d813509541
@ -8,7 +8,6 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
<script src="/browser-matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
1110
package-lock.json
generated
1110
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"lodash-move": "^1.1.1",
|
||||
"matrix-js-sdk": "^12.0.1",
|
||||
"matrix-js-sdk": "file:../matrix-js-sdk",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
|
23
src/App.jsx
23
src/App.jsx
@ -22,13 +22,14 @@ import {
|
||||
Redirect,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { useConferenceCallManager } from "./ConferenceCallManagerHooks";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
import { Home } from "./Home";
|
||||
import { Room, RoomAuth } from "./Room";
|
||||
import { Room } from "./Room";
|
||||
import { GridDemo } from "./GridDemo";
|
||||
import { RegisterPage } from "./RegisterPage";
|
||||
import { LoginPage } from "./LoginPage";
|
||||
import { Center } from "./Layout";
|
||||
import { GuestAuthPage } from "./GuestAuthPage";
|
||||
|
||||
export default function App() {
|
||||
const { protocol, host } = window.location;
|
||||
@ -37,12 +38,12 @@ export default function App() {
|
||||
const {
|
||||
loading,
|
||||
authenticated,
|
||||
error,
|
||||
manager,
|
||||
client,
|
||||
login,
|
||||
loginAsGuest,
|
||||
logout,
|
||||
registerGuest,
|
||||
register,
|
||||
} = useConferenceCallManager(homeserverUrl);
|
||||
} = useClient(homeserverUrl);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
@ -54,19 +55,19 @@ export default function App() {
|
||||
) : (
|
||||
<Switch>
|
||||
<AuthenticatedRoute authenticated={authenticated} exact path="/">
|
||||
<Home manager={manager} error={error} />
|
||||
<Home client={client} onLogout={logout} />
|
||||
</AuthenticatedRoute>
|
||||
<Route exact path="/login">
|
||||
<LoginPage onLogin={login} error={error} />
|
||||
<LoginPage onLogin={login} />
|
||||
</Route>
|
||||
<Route exact path="/register">
|
||||
<RegisterPage onRegister={register} error={error} />
|
||||
<RegisterPage onRegister={register} />
|
||||
</Route>
|
||||
<Route path="/room/:roomId">
|
||||
{authenticated ? (
|
||||
<Room manager={manager} error={error} />
|
||||
<Room client={client} />
|
||||
) : (
|
||||
<RoomAuth error={error} onLoginAsGuest={loginAsGuest} />
|
||||
<GuestAuthPage onRegisterGuest={registerGuest} />
|
||||
)}
|
||||
</Route>
|
||||
<Route exact path="/grid">
|
||||
|
@ -1,981 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
|
||||
import { randomString } from "./randomstring";
|
||||
|
||||
const CONF_ROOM = "me.robertlong.conf";
|
||||
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
||||
const PARTICIPANT_TIMEOUT = 1000 * 15;
|
||||
const SPEAKING_THRESHOLD = -80;
|
||||
const ACTIVE_SPEAKER_INTERVAL = 1000;
|
||||
const ACTIVE_SPEAKER_SAMPLES = 8;
|
||||
|
||||
function waitForSync(client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onSync = (state) => {
|
||||
if (state === "PREPARED") {
|
||||
resolve();
|
||||
client.removeListener("sync", onSync);
|
||||
}
|
||||
};
|
||||
client.on("sync", onSync);
|
||||
});
|
||||
}
|
||||
|
||||
export class ConferenceCallManager extends EventEmitter {
|
||||
static async restore(homeserverUrl) {
|
||||
try {
|
||||
const authStore = localStorage.getItem("matrix-auth-store");
|
||||
|
||||
if (authStore) {
|
||||
const { user_id, device_id, access_token } = JSON.parse(authStore);
|
||||
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
const manager = new ConferenceCallManager(client);
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return manager;
|
||||
}
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async login(homeserverUrl, username, password) {
|
||||
try {
|
||||
const registrationClient = matrixcs.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.loginWithPassword(username, password);
|
||||
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
const manager = new ConferenceCallManager(client);
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return manager;
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async loginAsGuest(homeserverUrl, displayName) {
|
||||
const registrationClient = matrixcs.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.registerGuest();
|
||||
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
await client.setDisplayName(displayName);
|
||||
|
||||
client.setGuest(true);
|
||||
|
||||
const manager = new ConferenceCallManager(client);
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
static async register(homeserverUrl, username, password) {
|
||||
try {
|
||||
const registrationClient = matrixcs.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.register(username, password, null, {
|
||||
type: "m.login.dummy",
|
||||
});
|
||||
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
const manager = new ConferenceCallManager(client);
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return manager;
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(client) {
|
||||
super();
|
||||
|
||||
this.client = client;
|
||||
|
||||
this.room = null;
|
||||
|
||||
// The session id is used to re-initiate calls if the user's participant
|
||||
// session id has changed
|
||||
this.sessionId = randomString(16);
|
||||
|
||||
this._memberParticipantStateTimeout = null;
|
||||
|
||||
// Whether or not we have entered the conference call.
|
||||
this.entered = false;
|
||||
|
||||
this._left = false;
|
||||
|
||||
// The MatrixCalls that were picked up by the Call.incoming listener,
|
||||
// before the user entered the conference call.
|
||||
this._incomingCallQueue = [];
|
||||
|
||||
// A representation of the conference call data for each room member
|
||||
// that has entered the call.
|
||||
this.participants = [];
|
||||
|
||||
this.localVideoStream = null;
|
||||
this.localParticipant = null;
|
||||
this.localCallFeed = null;
|
||||
|
||||
this.audioMuted = false;
|
||||
this.videoMuted = false;
|
||||
|
||||
this.activeSpeaker = null;
|
||||
this._speakerMap = new Map();
|
||||
this._activeSpeakerLoopTimeout = null;
|
||||
|
||||
this.client.on("RoomState.members", this._onRoomStateMembers);
|
||||
this.client.on("Call.incoming", this._onIncomingCall);
|
||||
this.callDebugger = new ConferenceCallDebugger(this);
|
||||
}
|
||||
|
||||
async enter(roomId, timeout = 30000) {
|
||||
this._left = false;
|
||||
|
||||
// Ensure that we have joined the provided room.
|
||||
await this.client.joinRoom(roomId);
|
||||
|
||||
// Get the room info, wait if it hasn't been fetched yet.
|
||||
// Timeout after 30 seconds or the provided duration.
|
||||
const room = await new Promise((resolve, reject) => {
|
||||
const initialRoom = this.client.getRoom(roomId);
|
||||
|
||||
if (initialRoom) {
|
||||
resolve(initialRoom);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomTimeout = setTimeout(() => {
|
||||
reject(new Error("Room could not be found."));
|
||||
}, timeout);
|
||||
|
||||
const roomCallback = (room) => {
|
||||
if (room && room.roomId === roomId) {
|
||||
this.client.removeListener("Room", roomCallback);
|
||||
clearTimeout(roomTimeout);
|
||||
resolve(room);
|
||||
}
|
||||
};
|
||||
|
||||
this.client.on("Room", roomCallback);
|
||||
});
|
||||
|
||||
// Ensure that this room is marked as a conference room so clients can react appropriately
|
||||
const activeConf = room.currentState
|
||||
.getStateEvents(CONF_ROOM, "")
|
||||
?.getContent()?.active;
|
||||
|
||||
if (!activeConf) {
|
||||
this._sendStateEventWithRetry(
|
||||
room.roomId,
|
||||
CONF_ROOM,
|
||||
{ active: true },
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
// Request permissions and get the user's webcam/mic stream if we haven't yet.
|
||||
const userId = this.client.getUserId();
|
||||
const stream = await this.getLocalVideoStream();
|
||||
|
||||
// It's possible to navigate to another page while the microphone permission prompt is
|
||||
// open, so check to see if we've left the call.
|
||||
// Only set class variables below this check so that leaveRoom properly handles
|
||||
// state cleanup.
|
||||
if (this._left) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.room = room;
|
||||
|
||||
this.localParticipant = {
|
||||
local: true,
|
||||
userId,
|
||||
displayName: this.client.getUser(this.client.getUserId()).rawDisplayName,
|
||||
sessionId: this.sessionId,
|
||||
call: null,
|
||||
stream,
|
||||
audioMuted: this.audioMuted,
|
||||
videoMuted: this.videoMuted,
|
||||
speaking: false,
|
||||
activeSpeaker: true,
|
||||
};
|
||||
|
||||
this.activeSpeaker = this.localParticipant;
|
||||
|
||||
this.participants.push(this.localParticipant);
|
||||
this.emit("debugstate", userId, null, "you");
|
||||
|
||||
this.localCallFeed = new matrixcs.CallFeed(
|
||||
stream,
|
||||
this.localParticipant.userId,
|
||||
"m.usermedia",
|
||||
this.client,
|
||||
this.room.roomId,
|
||||
this.audioMuted,
|
||||
this.videoMuted
|
||||
);
|
||||
this.localCallFeed.on("mute_state_changed", () =>
|
||||
this._onCallFeedMuteStateChanged(
|
||||
this.localParticipant,
|
||||
this.localCallFeed
|
||||
)
|
||||
);
|
||||
this.localCallFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
|
||||
this.localCallFeed.measureVolumeActivity(true);
|
||||
this.localCallFeed.on("speaking", (speaking) => {
|
||||
this._onCallFeedSpeaking(this.localParticipant, speaking);
|
||||
});
|
||||
this.localCallFeed.on("volume_changed", (maxVolume) =>
|
||||
this._onCallFeedVolumeChange(this.localParticipant, maxVolume)
|
||||
);
|
||||
|
||||
// Announce to the other room members that we have entered the room.
|
||||
// Continue doing so every PARTICIPANT_TIMEOUT ms
|
||||
this._updateMemberParticipantState();
|
||||
|
||||
this.entered = true;
|
||||
|
||||
// Answer any pending incoming calls.
|
||||
const incomingCallCount = this._incomingCallQueue.length;
|
||||
|
||||
for (let i = 0; i < incomingCallCount; i++) {
|
||||
const call = this._incomingCallQueue.pop();
|
||||
this._onIncomingCall(call);
|
||||
}
|
||||
|
||||
// Set up participants for the members currently in the room.
|
||||
// Other members will be picked up by the RoomState.members event.
|
||||
const initialMembers = room.getMembers();
|
||||
|
||||
for (const member of initialMembers) {
|
||||
this._onMemberChanged(member);
|
||||
}
|
||||
|
||||
this.emit("entered");
|
||||
this.emit("participants_changed");
|
||||
this._onActiveSpeakerLoop();
|
||||
}
|
||||
|
||||
leaveCall() {
|
||||
if (!this.entered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = this.client.getUserId();
|
||||
const currentMemberState = this.room.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
userId
|
||||
);
|
||||
|
||||
this._sendStateEventWithRetry(
|
||||
this.room.roomId,
|
||||
"m.room.member",
|
||||
{
|
||||
...currentMemberState.getContent(),
|
||||
[CONF_PARTICIPANT]: null,
|
||||
},
|
||||
userId
|
||||
);
|
||||
|
||||
for (const participant of this.participants) {
|
||||
if (!participant.local && participant.call) {
|
||||
participant.call.hangup("user_hangup", false);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.stopLocalMediaStream();
|
||||
this.localVideoStream = null;
|
||||
this.localCallFeed.dispose();
|
||||
this.localCallFeed = null;
|
||||
|
||||
this.room = null;
|
||||
this.entered = false;
|
||||
this._left = true;
|
||||
this.participants = [];
|
||||
this.localParticipant = null;
|
||||
this.activeSpeaker = null;
|
||||
this.audioMuted = false;
|
||||
this.videoMuted = false;
|
||||
clearTimeout(this._memberParticipantStateTimeout);
|
||||
clearTimeout(this._activeSpeakerLoopTimeout);
|
||||
this._speakerMap.clear();
|
||||
|
||||
this.emit("participants_changed");
|
||||
this.emit("left");
|
||||
}
|
||||
|
||||
async getLocalVideoStream() {
|
||||
if (this.localVideoStream) {
|
||||
return this.localVideoStream;
|
||||
}
|
||||
|
||||
const stream = await this.client.getLocalVideoStream();
|
||||
|
||||
this.localVideoStream = stream;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
setAudioMuted(muted) {
|
||||
this.audioMuted = muted;
|
||||
|
||||
if (this.localCallFeed) {
|
||||
this.localCallFeed.setAudioMuted(muted);
|
||||
}
|
||||
|
||||
const localStream = this.localVideoStream;
|
||||
|
||||
if (localStream) {
|
||||
for (const track of localStream.getTracks()) {
|
||||
if (track.kind === "audio") {
|
||||
track.enabled = !this.audioMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let participant of this.participants) {
|
||||
const call = participant.call;
|
||||
|
||||
if (
|
||||
call &&
|
||||
call.localUsermediaStream &&
|
||||
call.isMicrophoneMuted() !== this.audioMuted
|
||||
) {
|
||||
call.setMicrophoneMuted(this.audioMuted);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("participants_changed");
|
||||
}
|
||||
|
||||
setVideoMuted(muted) {
|
||||
this.videoMuted = muted;
|
||||
|
||||
if (this.localCallFeed) {
|
||||
this.localCallFeed.setVideoMuted(muted);
|
||||
}
|
||||
|
||||
const localStream = this.localVideoStream;
|
||||
|
||||
if (localStream) {
|
||||
for (const track of localStream.getTracks()) {
|
||||
if (track.kind === "video") {
|
||||
track.enabled = !this.videoMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let participant of this.participants) {
|
||||
const call = participant.call;
|
||||
|
||||
if (
|
||||
call &&
|
||||
call.localUsermediaStream &&
|
||||
call.isLocalVideoMuted() !== this.videoMuted
|
||||
) {
|
||||
call.setLocalVideoMuted(this.videoMuted);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("participants_changed");
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Call presence
|
||||
*/
|
||||
|
||||
_updateMemberParticipantState = () => {
|
||||
const userId = this.client.getUserId();
|
||||
const currentMemberState = this.room.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
userId
|
||||
);
|
||||
|
||||
this._sendStateEventWithRetry(
|
||||
this.room.roomId,
|
||||
"m.room.member",
|
||||
{
|
||||
...currentMemberState.getContent(),
|
||||
[CONF_PARTICIPANT]: {
|
||||
sessionId: this.sessionId,
|
||||
expiresAt: new Date().getTime() + PARTICIPANT_TIMEOUT * 2,
|
||||
},
|
||||
},
|
||||
userId
|
||||
);
|
||||
|
||||
const now = new Date().getTime();
|
||||
|
||||
for (const participant of this.participants) {
|
||||
if (participant.local) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const memberStateEvent = this.room.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
participant.userId
|
||||
);
|
||||
|
||||
const memberStateContent = memberStateEvent.getContent();
|
||||
|
||||
if (
|
||||
!memberStateContent ||
|
||||
!memberStateContent[CONF_PARTICIPANT] ||
|
||||
typeof memberStateContent[CONF_PARTICIPANT] !== "object" ||
|
||||
(memberStateContent[CONF_PARTICIPANT].expiresAt &&
|
||||
memberStateContent[CONF_PARTICIPANT].expiresAt < now)
|
||||
) {
|
||||
this.emit("debugstate", participant.userId, null, "inactive");
|
||||
|
||||
if (participant.call) {
|
||||
// NOTE: This should remove the participant on the next tick
|
||||
// since matrix-js-sdk awaits a promise before firing user_hangup
|
||||
participant.call.hangup("user_hangup", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._memberParticipantStateTimeout = setTimeout(
|
||||
this._updateMemberParticipantState,
|
||||
PARTICIPANT_TIMEOUT
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Call Setup
|
||||
*
|
||||
* There are two different paths for calls to be created:
|
||||
* 1. Incoming calls triggered by the Call.incoming event.
|
||||
* 2. Outgoing calls to the initial members of a room or new members
|
||||
* as they are observed by the RoomState.members event.
|
||||
*/
|
||||
|
||||
_onIncomingCall = (call) => {
|
||||
// If we haven't entered yet, add the call to a queue which we'll use later.
|
||||
if (!this.entered) {
|
||||
this._incomingCallQueue.push(call);
|
||||
return;
|
||||
}
|
||||
|
||||
// The incoming calls may be for another room, which we will ignore.
|
||||
if (call.roomId !== this.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (call.state !== "ringing") {
|
||||
console.warn("Incoming call no longer in ringing state. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the remote video stream if it exists.
|
||||
const remoteFeed = call.getRemoteFeeds()[0];
|
||||
const stream = remoteFeed && remoteFeed.stream;
|
||||
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
|
||||
const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
|
||||
|
||||
const userId = call.opponentMember.userId;
|
||||
|
||||
const memberStateEvent = this.room.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
userId
|
||||
);
|
||||
|
||||
const memberStateContent = memberStateEvent.getContent();
|
||||
|
||||
if (!memberStateContent || !memberStateContent[CONF_PARTICIPANT]) {
|
||||
call.reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const { sessionId } = memberStateContent[CONF_PARTICIPANT];
|
||||
|
||||
// Check if the user calling has an existing participant and use this call instead.
|
||||
const existingParticipant = this.participants.find(
|
||||
(p) => p.userId === userId
|
||||
);
|
||||
|
||||
let participant;
|
||||
|
||||
console.log(call.opponentMember);
|
||||
|
||||
if (existingParticipant) {
|
||||
participant = existingParticipant;
|
||||
// This also fires the hangup event and triggers those side-effects
|
||||
existingParticipant.call.hangup("replaced", false);
|
||||
existingParticipant.call = call;
|
||||
existingParticipant.stream = stream;
|
||||
existingParticipant.audioMuted = audioMuted;
|
||||
existingParticipant.videoMuted = videoMuted;
|
||||
existingParticipant.speaking = false;
|
||||
existingParticipant.activeSpeaker = false;
|
||||
existingParticipant.sessionId = sessionId;
|
||||
} else {
|
||||
participant = {
|
||||
local: false,
|
||||
userId,
|
||||
displayName: call.opponentMember.rawDisplayName,
|
||||
sessionId,
|
||||
call,
|
||||
stream,
|
||||
audioMuted,
|
||||
videoMuted,
|
||||
speaking: false,
|
||||
activeSpeaker: false,
|
||||
};
|
||||
this.participants.push(participant);
|
||||
}
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteFeed.on("mute_state_changed", () =>
|
||||
this._onCallFeedMuteStateChanged(participant, remoteFeed)
|
||||
);
|
||||
remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
|
||||
remoteFeed.measureVolumeActivity(true);
|
||||
remoteFeed.on("speaking", (speaking) => {
|
||||
this._onCallFeedSpeaking(participant, speaking);
|
||||
});
|
||||
remoteFeed.on("volume_changed", (maxVolume) =>
|
||||
this._onCallFeedVolumeChange(participant, maxVolume)
|
||||
);
|
||||
}
|
||||
|
||||
call.on("state", (state) =>
|
||||
this._onCallStateChanged(participant, call, state)
|
||||
);
|
||||
call.on("feeds_changed", () => this._onCallFeedsChanged(participant, call));
|
||||
call.on("replaced", (newCall) =>
|
||||
this._onCallReplaced(participant, call, newCall)
|
||||
);
|
||||
call.on("hangup", () => this._onCallHangup(participant, call));
|
||||
call.answer();
|
||||
|
||||
this.emit("call", call);
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
_onRoomStateMembers = (_event, _state, member) => {
|
||||
this._onMemberChanged(member);
|
||||
};
|
||||
|
||||
_onMemberChanged = (member) => {
|
||||
// Don't process new members until we've entered the conference call.
|
||||
if (!this.entered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The member events may be received for another room, which we will ignore.
|
||||
if (member.roomId !== this.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't process your own member.
|
||||
const localUserId = this.client.getUserId();
|
||||
|
||||
if (member.userId === localUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the latest member participant state event.
|
||||
const memberStateEvent = this.room.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
member.userId
|
||||
);
|
||||
const memberStateContent = memberStateEvent.getContent();
|
||||
|
||||
if (!memberStateContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantInfo = memberStateContent[CONF_PARTICIPANT];
|
||||
|
||||
if (!participantInfo || typeof participantInfo !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { expiresAt, sessionId } = participantInfo;
|
||||
|
||||
// If the participant state has expired, ignore this user.
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (expiresAt < now) {
|
||||
this.emit("debugstate", member.userId, null, "inactive");
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is an existing participant for this member check the session id.
|
||||
// If the session id changed then we can hang up the old call and start a new one.
|
||||
// Otherwise, ignore the member change event because we already have an active participant.
|
||||
let participant = this.participants.find((p) => p.userId === member.userId);
|
||||
|
||||
if (participant) {
|
||||
if (participant.sessionId !== sessionId) {
|
||||
this.emit("debugstate", member.userId, null, "inactive");
|
||||
participant.call.hangup("replaced", false);
|
||||
} else {
|
||||
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 (member.userId < localUserId) {
|
||||
this.emit("debugstate", member.userId, null, "waiting for invite");
|
||||
return;
|
||||
}
|
||||
|
||||
const call = this.client.createCall(this.room.roomId, member.userId);
|
||||
|
||||
if (participant) {
|
||||
participant.sessionId = sessionId;
|
||||
participant.call = call;
|
||||
participant.stream = null;
|
||||
participant.audioMuted = false;
|
||||
participant.videoMuted = false;
|
||||
participant.speaking = false;
|
||||
participant.activeSpeaker = false;
|
||||
} else {
|
||||
participant = {
|
||||
local: false,
|
||||
userId: member.userId,
|
||||
displayName: member.rawDisplayName,
|
||||
sessionId,
|
||||
call,
|
||||
stream: null,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
speaking: false,
|
||||
activeSpeaker: false,
|
||||
};
|
||||
// TODO: Should we wait until the call has been answered to push the participant?
|
||||
// Or do we hide the participant until their stream is live?
|
||||
// Does hiding a participant without a stream present a privacy problem because
|
||||
// a participant without a stream can still listen in on other user's streams?
|
||||
this.participants.push(participant);
|
||||
}
|
||||
|
||||
call.on("state", (state) =>
|
||||
this._onCallStateChanged(participant, call, state)
|
||||
);
|
||||
call.on("feeds_changed", () => this._onCallFeedsChanged(participant, call));
|
||||
call.on("replaced", (newCall) =>
|
||||
this._onCallReplaced(participant, call, newCall)
|
||||
);
|
||||
call.on("hangup", () => this._onCallHangup(participant, call));
|
||||
|
||||
call.placeVideoCall().then(() => {
|
||||
this.emit("call", call);
|
||||
});
|
||||
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
/**
|
||||
* Call Event Handlers
|
||||
*/
|
||||
|
||||
_onCallStateChanged = (participant, call, state) => {
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isMicrophoneMuted() !== this.audioMuted
|
||||
) {
|
||||
call.setMicrophoneMuted(this.audioMuted);
|
||||
}
|
||||
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isLocalVideoMuted() !== this.videoMuted
|
||||
) {
|
||||
call.setLocalVideoMuted(this.videoMuted);
|
||||
}
|
||||
|
||||
this.emit("debugstate", participant.userId, call.callId, state);
|
||||
};
|
||||
|
||||
_onCallFeedsChanged = (participant, call) => {
|
||||
const remoteFeed = call.getRemoteFeeds()[0];
|
||||
const stream = remoteFeed && remoteFeed.stream;
|
||||
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
|
||||
const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
|
||||
|
||||
if (remoteFeed && participant.stream !== stream) {
|
||||
participant.stream = stream;
|
||||
participant.audioMuted = audioMuted;
|
||||
participant.videoMuted = videoMuted;
|
||||
remoteFeed.on("mute_state_changed", () =>
|
||||
this._onCallFeedMuteStateChanged(participant, remoteFeed)
|
||||
);
|
||||
remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
|
||||
remoteFeed.measureVolumeActivity(true);
|
||||
remoteFeed.on("speaking", (speaking) => {
|
||||
this._onCallFeedSpeaking(participant, speaking);
|
||||
});
|
||||
remoteFeed.on("volume_changed", (maxVolume) =>
|
||||
this._onCallFeedVolumeChange(participant, maxVolume)
|
||||
);
|
||||
this._onCallFeedMuteStateChanged(participant, remoteFeed);
|
||||
}
|
||||
};
|
||||
|
||||
_onCallFeedMuteStateChanged = (participant, feed) => {
|
||||
participant.audioMuted = feed.isAudioMuted();
|
||||
participant.videoMuted = feed.isVideoMuted();
|
||||
|
||||
if (participant.audioMuted) {
|
||||
this._speakerMap.set(
|
||||
participant.userId,
|
||||
Array(ACTIVE_SPEAKER_SAMPLES).fill(-Infinity)
|
||||
);
|
||||
}
|
||||
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
_onCallFeedSpeaking = (participant, speaking) => {
|
||||
participant.speaking = speaking;
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
_onCallFeedVolumeChange = (participant, maxVolume) => {
|
||||
if (!this._speakerMap.has(participant.userId)) {
|
||||
this._speakerMap.set(
|
||||
participant.userId,
|
||||
Array(ACTIVE_SPEAKER_SAMPLES).fill(-Infinity)
|
||||
);
|
||||
}
|
||||
|
||||
const volumeArr = this._speakerMap.get(participant.userId);
|
||||
volumeArr.shift();
|
||||
volumeArr.push(maxVolume);
|
||||
};
|
||||
|
||||
_onActiveSpeakerLoop = () => {
|
||||
let topAvg;
|
||||
let activeSpeakerId;
|
||||
|
||||
for (const [userId, volumeArr] of this._speakerMap) {
|
||||
let total = 0;
|
||||
|
||||
for (let i = 0; i < volumeArr.length; i++) {
|
||||
const volume = volumeArr[i];
|
||||
total += Math.max(volume, SPEAKING_THRESHOLD);
|
||||
}
|
||||
|
||||
const avg = total / ACTIVE_SPEAKER_SAMPLES;
|
||||
|
||||
if (!topAvg) {
|
||||
topAvg = avg;
|
||||
activeSpeakerId = userId;
|
||||
} else if (avg > topAvg) {
|
||||
topAvg = avg;
|
||||
activeSpeakerId = userId;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeSpeakerId && topAvg > SPEAKING_THRESHOLD) {
|
||||
const nextActiveSpeaker = this.participants.find(
|
||||
(p) => p.userId === activeSpeakerId
|
||||
);
|
||||
|
||||
if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker) {
|
||||
this.activeSpeaker.activeSpeaker = false;
|
||||
nextActiveSpeaker.activeSpeaker = true;
|
||||
this.activeSpeaker = nextActiveSpeaker;
|
||||
this.emit("participants_changed");
|
||||
}
|
||||
}
|
||||
|
||||
this._activeSpeakerLoopTimeout = setTimeout(
|
||||
this._onActiveSpeakerLoop,
|
||||
ACTIVE_SPEAKER_INTERVAL
|
||||
);
|
||||
};
|
||||
|
||||
_onCallReplaced = (participant, call, newCall) => {
|
||||
participant.call = newCall;
|
||||
|
||||
newCall.on("state", (state) =>
|
||||
this._onCallStateChanged(participant, newCall, state)
|
||||
);
|
||||
newCall.on("feeds_changed", () =>
|
||||
this._onCallFeedsChanged(participant, newCall)
|
||||
);
|
||||
newCall.on("replaced", (nextCall) =>
|
||||
this._onCallReplaced(participant, newCall, nextCall)
|
||||
);
|
||||
newCall.on("hangup", () => this._onCallHangup(participant, newCall));
|
||||
|
||||
const remoteFeed = newCall.getRemoteFeeds()[0];
|
||||
participant.stream = remoteFeed ? remoteFeed.stream : null;
|
||||
participant.audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
|
||||
participant.videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteFeed.on("mute_state_changed", () =>
|
||||
this._onCallFeedMuteStateChanged(participant, remoteFeed)
|
||||
);
|
||||
remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
|
||||
remoteFeed.measureVolumeActivity(true);
|
||||
remoteFeed.on("speaking", (speaking) => {
|
||||
this._onCallFeedSpeaking(participant, speaking);
|
||||
});
|
||||
remoteFeed.on("volume_changed", (maxVolume) =>
|
||||
this._onCallFeedVolumeChange(participant, maxVolume)
|
||||
);
|
||||
}
|
||||
|
||||
this.emit("call", newCall);
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
_onCallHangup = (participant, call) => {
|
||||
if (call.hangupReason === "replaced") {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantIndex = this.participants.indexOf(participant);
|
||||
|
||||
if (participantIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.participants.splice(participantIndex, 1);
|
||||
|
||||
if (this.activeSpeaker === participant && this.participants.length > 0) {
|
||||
this.activeSpeaker = this.participants[0];
|
||||
this.activeSpeaker.activeSpeaker = true;
|
||||
}
|
||||
|
||||
this._speakerMap.delete(participant.userId);
|
||||
|
||||
this.emit("participants_changed");
|
||||
};
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
_sendStateEventWithRetry(
|
||||
roomId,
|
||||
eventType,
|
||||
content,
|
||||
stateKey,
|
||||
callback,
|
||||
maxAttempts = 5
|
||||
) {
|
||||
const sendStateEventWithRetry = async (attempt = 0) => {
|
||||
try {
|
||||
return await this.client.sendStateEvent(
|
||||
roomId,
|
||||
eventType,
|
||||
content,
|
||||
stateKey,
|
||||
callback
|
||||
);
|
||||
} catch (error) {
|
||||
if (attempt >= maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve(), 5));
|
||||
|
||||
return sendStateEventWithRetry(attempt + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return sendStateEventWithRetry();
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ConferenceCallManager } from "./ConferenceCallManager";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import matrix from "matrix-js-sdk";
|
||||
|
||||
// https://stackoverflow.com/a/9039885
|
||||
function isIOS() {
|
||||
@ -33,289 +33,230 @@ function isIOS() {
|
||||
);
|
||||
}
|
||||
|
||||
export function useConferenceCallManager(homeserverUrl) {
|
||||
const [{ loading, authenticated, manager, error }, setState] = useState({
|
||||
function waitForSync(client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onSync = (state) => {
|
||||
if (state === "PREPARED") {
|
||||
resolve();
|
||||
client.removeListener("sync", onSync);
|
||||
}
|
||||
};
|
||||
client.on("sync", onSync);
|
||||
});
|
||||
}
|
||||
|
||||
async function initClient(clientOptions, guest) {
|
||||
const client = matrix.createClient(clientOptions);
|
||||
|
||||
if (guest) {
|
||||
client.setGuest(true);
|
||||
}
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function fetchRoom(client, roomId, join, timeout = 5000) {
|
||||
let room = client.getRoom(roomId);
|
||||
|
||||
if (room) {
|
||||
return room;
|
||||
}
|
||||
|
||||
if (join) {
|
||||
room = await client.joinRoom(roomId);
|
||||
|
||||
if (room) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId;
|
||||
|
||||
function onRoom(room) {
|
||||
if (room && room.roomId === roomId) {
|
||||
clearTimeout(timeoutId);
|
||||
client.removeListener("Room", onRoom);
|
||||
resolve(room);
|
||||
}
|
||||
}
|
||||
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
if (room) {
|
||||
resolve(room);
|
||||
}
|
||||
|
||||
client.on("Room", onRoom);
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
client.removeListener("Room", onRoom);
|
||||
reject(new Error("Fetching room timed out."));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useClient(homeserverUrl) {
|
||||
const [{ loading, authenticated, client }, setState] = useState({
|
||||
loading: true,
|
||||
authenticated: false,
|
||||
manager: undefined,
|
||||
error: undefined,
|
||||
client: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
ConferenceCallManager.restore(homeserverUrl)
|
||||
.then((manager) => {
|
||||
setState({
|
||||
manager,
|
||||
loading: false,
|
||||
authenticated: !!manager,
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
async function restore() {
|
||||
try {
|
||||
const authStore = localStorage.getItem("matrix-auth-store");
|
||||
|
||||
setState({
|
||||
manager: undefined,
|
||||
loading: false,
|
||||
authenticated: false,
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
if (authStore) {
|
||||
const { user_id, device_id, access_token } = JSON.parse(authStore);
|
||||
|
||||
const login = useCallback(async (username, password, cb) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
authenticated: false,
|
||||
error: undefined,
|
||||
}));
|
||||
const client = await initClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
ConferenceCallManager.login(homeserverUrl, username, password)
|
||||
.then((manager) => {
|
||||
setState({
|
||||
manager,
|
||||
loading: false,
|
||||
authenticated: true,
|
||||
error: undefined,
|
||||
});
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
return client;
|
||||
}
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
restore()
|
||||
.then((client) => {
|
||||
if (client) {
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} else {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
setState({
|
||||
manager: undefined,
|
||||
loading: false,
|
||||
authenticated: false,
|
||||
error: err,
|
||||
});
|
||||
.catch(() => {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loginAsGuest = useCallback(async (displayName) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
authenticated: false,
|
||||
error: undefined,
|
||||
}));
|
||||
const login = useCallback(async (username, password) => {
|
||||
try {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
ConferenceCallManager.loginAsGuest(homeserverUrl, displayName)
|
||||
.then((manager) => {
|
||||
setState({
|
||||
manager,
|
||||
loading: false,
|
||||
authenticated: true,
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.loginWithPassword(username, password);
|
||||
|
||||
setState({
|
||||
manager: undefined,
|
||||
loading: false,
|
||||
authenticated: false,
|
||||
error: err,
|
||||
});
|
||||
const client = await initClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (username, password, cb) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
authenticated: false,
|
||||
error: undefined,
|
||||
}));
|
||||
const registerGuest = useCallback(async (displayName) => {
|
||||
try {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
ConferenceCallManager.register(homeserverUrl, username, password)
|
||||
.then((manager) => {
|
||||
setState({
|
||||
manager,
|
||||
loading: false,
|
||||
authenticated: true,
|
||||
error: undefined,
|
||||
});
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.registerGuest({});
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
setState({
|
||||
manager: undefined,
|
||||
loading: false,
|
||||
authenticated: false,
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.confManager = manager;
|
||||
const register = useCallback(async (username, password) => {
|
||||
try {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
return () => {
|
||||
window.confManager = undefined;
|
||||
};
|
||||
}, [manager]);
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.register(username, password, null, {
|
||||
type: "m.login.dummy",
|
||||
});
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
authenticated,
|
||||
manager,
|
||||
error,
|
||||
client,
|
||||
login,
|
||||
loginAsGuest,
|
||||
registerGuest,
|
||||
register,
|
||||
logout,
|
||||
};
|
||||
}
|
||||
|
||||
export function useVideoRoom(manager, roomId, timeout = 5000) {
|
||||
const [
|
||||
{
|
||||
loading,
|
||||
joined,
|
||||
joining,
|
||||
room,
|
||||
participants,
|
||||
error,
|
||||
videoMuted,
|
||||
audioMuted,
|
||||
},
|
||||
setState,
|
||||
] = useState({
|
||||
loading: true,
|
||||
joining: false,
|
||||
joined: false,
|
||||
room: undefined,
|
||||
participants: [],
|
||||
error: undefined,
|
||||
videoMuted: false,
|
||||
audioMuted: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
loading: true,
|
||||
room: undefined,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
const onParticipantsChanged = () => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
participants: [...manager.participants],
|
||||
}));
|
||||
};
|
||||
|
||||
manager.on("participants_changed", onParticipantsChanged);
|
||||
|
||||
manager.client.joinRoom(roomId).catch((err) => {
|
||||
setState((prevState) => ({ ...prevState, loading: false, error: err }));
|
||||
});
|
||||
|
||||
let timeoutId;
|
||||
|
||||
function roomCallback(room) {
|
||||
if (room && room.roomId === roomId) {
|
||||
clearTimeout(timeoutId);
|
||||
manager.client.removeListener("Room", roomCallback);
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
loading: false,
|
||||
room,
|
||||
error: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let initialRoom = manager.client.getRoom(roomId);
|
||||
|
||||
if (initialRoom) {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
loading: false,
|
||||
room: initialRoom,
|
||||
error: undefined,
|
||||
}));
|
||||
} else {
|
||||
manager.client.on("Room", roomCallback);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
loading: false,
|
||||
room: undefined,
|
||||
error: new Error("Room could not be found."),
|
||||
}));
|
||||
manager.client.removeListener("Room", roomCallback);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function onLeaveCall() {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
videoMuted: manager.videoMuted,
|
||||
audioMuted: manager.audioMuted,
|
||||
}));
|
||||
}
|
||||
|
||||
manager.on("left", onLeaveCall);
|
||||
|
||||
return () => {
|
||||
manager.client.removeListener("Room", roomCallback);
|
||||
manager.removeListener("participants_changed", onParticipantsChanged);
|
||||
clearTimeout(timeoutId);
|
||||
manager.leaveCall();
|
||||
manager.removeListener("left", onLeaveCall);
|
||||
};
|
||||
}, [manager, roomId]);
|
||||
|
||||
const joinCall = useCallback(() => {
|
||||
if (joining || joined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
joining: true,
|
||||
}));
|
||||
|
||||
manager
|
||||
.enter(roomId)
|
||||
.then(() => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
joining: false,
|
||||
joined: true,
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
joining: false,
|
||||
joined: false,
|
||||
error,
|
||||
}));
|
||||
});
|
||||
}, [manager, roomId, joining, joined]);
|
||||
|
||||
const leaveCall = useCallback(() => {
|
||||
manager.leaveCall();
|
||||
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
participants: [...manager.participants],
|
||||
joined: false,
|
||||
joining: false,
|
||||
}));
|
||||
}, [manager]);
|
||||
|
||||
function usePageUnload(callback) {
|
||||
useEffect(() => {
|
||||
let pageVisibilityTimeout;
|
||||
|
||||
@ -327,25 +268,11 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
|
||||
// Wait 5 seconds before closing the page to avoid accidentally leaving
|
||||
// TODO: Make this configurable?
|
||||
pageVisibilityTimeout = setTimeout(() => {
|
||||
manager.leaveCall();
|
||||
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
participants: [...manager.participants],
|
||||
joined: false,
|
||||
joining: false,
|
||||
}));
|
||||
callback();
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
manager.leaveCall();
|
||||
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
participants: [...manager.participants],
|
||||
joined: false,
|
||||
joining: false,
|
||||
}));
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,31 +290,174 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
|
||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||
clearTimeout(pageVisibilityTimeout);
|
||||
};
|
||||
}, [manager]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
const toggleMuteAudio = useCallback(() => {
|
||||
manager.setAudioMuted(!manager.audioMuted);
|
||||
setState((prevState) => ({ ...prevState, audioMuted: manager.audioMuted }));
|
||||
}, [manager]);
|
||||
function getParticipants(groupCall) {
|
||||
return [...groupCall.participants];
|
||||
}
|
||||
|
||||
const toggleMuteVideo = useCallback(() => {
|
||||
manager.setVideoMuted(!manager.videoMuted);
|
||||
setState((prevState) => ({ ...prevState, videoMuted: manager.videoMuted }));
|
||||
}, [manager]);
|
||||
export function useGroupCall(client, roomId) {
|
||||
const groupCallRef = useRef(null);
|
||||
|
||||
const [
|
||||
{
|
||||
loading,
|
||||
entered,
|
||||
entering,
|
||||
room,
|
||||
participants,
|
||||
error,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
},
|
||||
setState,
|
||||
] = useState({
|
||||
loading: true,
|
||||
entered: false,
|
||||
entering: false,
|
||||
room: null,
|
||||
participants: [],
|
||||
error: null,
|
||||
microphoneMuted: false,
|
||||
localVideoMuted: false,
|
||||
});
|
||||
|
||||
const updateState = (state) =>
|
||||
setState((prevState) => ({ ...prevState, ...state }));
|
||||
|
||||
useEffect(() => {
|
||||
function onParticipantsChanged(...args) {
|
||||
console.log(...args);
|
||||
updateState({ participants: getParticipants(groupCallRef.current) });
|
||||
}
|
||||
|
||||
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
|
||||
updateState({
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const room = await fetchRoom(client, roomId, true);
|
||||
|
||||
const groupCall = client.createGroupCall(roomId, "video");
|
||||
groupCallRef.current = groupCall;
|
||||
groupCall.on("active_speaker_changed", onParticipantsChanged);
|
||||
groupCall.on("participants_changed", onParticipantsChanged);
|
||||
groupCall.on("speaking", onParticipantsChanged);
|
||||
groupCall.on("mute_state_changed", onParticipantsChanged);
|
||||
groupCall.on("participant_call_replaced", onParticipantsChanged);
|
||||
groupCall.on("participant_call_feeds_changed", onParticipantsChanged);
|
||||
groupCall.on("local_mute_state_changed", onLocalMuteStateChanged);
|
||||
|
||||
updateState({
|
||||
room,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
init().catch((error) => {
|
||||
if (groupCallRef.current) {
|
||||
const groupCall = groupCallRef.current;
|
||||
groupCall.removeListener(
|
||||
"active_speaker_changed",
|
||||
onParticipantsChanged
|
||||
);
|
||||
groupCall.removeListener("participants_changed", onParticipantsChanged);
|
||||
groupCall.removeListener("speaking", onParticipantsChanged);
|
||||
groupCall.removeListener("mute_state_changed", onParticipantsChanged);
|
||||
groupCall.removeListener(
|
||||
"participant_call_replaced",
|
||||
onParticipantsChanged
|
||||
);
|
||||
groupCall.removeListener(
|
||||
"participant_call_feeds_changed",
|
||||
onParticipantsChanged
|
||||
);
|
||||
groupCall.removeListener(
|
||||
"local_mute_state_changed",
|
||||
onLocalMuteStateChanged
|
||||
);
|
||||
groupCall.leave();
|
||||
}
|
||||
|
||||
updateState({ error, loading: false });
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (groupCallRef.current) {
|
||||
groupCallRef.current.leave();
|
||||
}
|
||||
};
|
||||
}, [client, roomId]);
|
||||
|
||||
usePageUnload(() => {
|
||||
if (groupCallRef.current) {
|
||||
groupCallRef.current.leave();
|
||||
}
|
||||
});
|
||||
|
||||
const initLocalParticipant = useCallback(
|
||||
() => groupCallRef.current.initLocalParticipant(),
|
||||
[]
|
||||
);
|
||||
|
||||
const enter = useCallback(() => {
|
||||
updateState({ entering: true });
|
||||
|
||||
groupCallRef.current
|
||||
.enter()
|
||||
.then(() => {
|
||||
updateState({
|
||||
entered: true,
|
||||
entering: false,
|
||||
participants: getParticipants(groupCallRef.current),
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
updateState({ error, entering: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const leave = useCallback(() => {
|
||||
groupCallRef.current.leave();
|
||||
updateState({
|
||||
entered: false,
|
||||
participants: [],
|
||||
microphoneMuted: false,
|
||||
localVideoMuted: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleLocalVideoMuted = useCallback(() => {
|
||||
groupCallRef.current.setLocalVideoMuted(
|
||||
!groupCallRef.current.isLocalVideoMuted()
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleMicrophoneMuted = useCallback(() => {
|
||||
groupCallRef.current.setMicrophoneMuted(
|
||||
!groupCallRef.current.isMicrophoneMuted()
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
joined,
|
||||
joining,
|
||||
room,
|
||||
entered,
|
||||
entering,
|
||||
roomName: room ? room.name : null,
|
||||
participants,
|
||||
groupCall: groupCallRef.current,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
error,
|
||||
joinCall,
|
||||
leaveCall,
|
||||
toggleMuteVideo,
|
||||
toggleMuteAudio,
|
||||
videoMuted,
|
||||
audioMuted,
|
||||
initLocalParticipant,
|
||||
enter,
|
||||
leave,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
};
|
||||
}
|
||||
|
||||
@ -440,22 +510,22 @@ function sortRooms(client, rooms) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useRooms(manager) {
|
||||
export function useRooms(client) {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateRooms() {
|
||||
const visibleRooms = manager.client.getVisibleRooms();
|
||||
const sortedRooms = sortRooms(manager.client, visibleRooms);
|
||||
const visibleRooms = client.getVisibleRooms();
|
||||
const sortedRooms = sortRooms(client, visibleRooms);
|
||||
setRooms(sortedRooms);
|
||||
}
|
||||
|
||||
updateRooms();
|
||||
|
||||
manager.client.on("Room", updateRooms);
|
||||
client.on("Room", updateRooms);
|
||||
|
||||
return () => {
|
||||
manager.client.removeListener("Room", updateRooms);
|
||||
client.removeListener("Room", updateRooms);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
91
src/GuestAuthPage.jsx
Normal file
91
src/GuestAuthPage.jsx
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import styles from "./GuestAuthPage.module.css";
|
||||
import { useLocation, useHistory, Link } from "react-router-dom";
|
||||
import { Header, LeftNav } from "./Header";
|
||||
import { Button, FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
|
||||
export function GuestAuthPage({ onLoginAsGuest }) {
|
||||
const displayNameRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onLoginAsGuest(displayNameRef.current.value).catch(setError);
|
||||
},
|
||||
[onLoginAsGuest, location, history]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.guestAuthPage}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<h2>Login As Guest</h2>
|
||||
<form onSubmit={onSubmitLoginForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="text"
|
||||
ref={displayNameRef}
|
||||
placeholder="Display Name"
|
||||
label="Display Name"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="submit">Login as guest</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<Info>
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
{" or "}
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/register",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
</Info>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
}
|
8
src/GuestAuthPage.module.css
Normal file
8
src/GuestAuthPage.module.css
Normal file
@ -0,0 +1,8 @@
|
||||
.guestAuthPage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
27
src/Home.jsx
27
src/Home.jsx
@ -25,12 +25,12 @@ import { Center, Content, Modal } from "./Layout";
|
||||
|
||||
const colorHash = new ColorHash({ lightness: 0.3 });
|
||||
|
||||
export function Home({ manager }) {
|
||||
export function Home({ client, onLogout }) {
|
||||
const history = useHistory();
|
||||
const roomNameRef = useRef();
|
||||
const guestAccessRef = useRef();
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
const rooms = useRooms(manager);
|
||||
const rooms = useRooms(client);
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(e) => {
|
||||
@ -38,7 +38,7 @@ export function Home({ manager }) {
|
||||
setCreateRoomError(undefined);
|
||||
|
||||
async function createRoom(name, guestAccess) {
|
||||
const { room_id } = await manager.client.createRoom({
|
||||
const { room_id } = await client.createRoom({
|
||||
visibility: "private",
|
||||
preset: "public_chat",
|
||||
name,
|
||||
@ -62,14 +62,14 @@ export function Home({ manager }) {
|
||||
"m.sticker": 50,
|
||||
},
|
||||
users: {
|
||||
[manager.client.getUserId()]: 100,
|
||||
[client.getUserId()]: 100,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (guestAccess) {
|
||||
await manager.client.setGuestAccess(room_id, {
|
||||
await client.setGuestAccess(room_id, {
|
||||
allowJoin: true,
|
||||
allowRead: true,
|
||||
});
|
||||
@ -83,27 +83,14 @@ export function Home({ manager }) {
|
||||
guestAccessRef.current.checked
|
||||
).catch(setCreateRoomError);
|
||||
},
|
||||
[manager]
|
||||
);
|
||||
|
||||
const onLogout = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
manager.logout();
|
||||
location.reload();
|
||||
},
|
||||
[manager]
|
||||
[client]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<UserNav
|
||||
signedIn={manager.client}
|
||||
userName={manager.client.getUserId()}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
<UserNav signedIn userName={client.getUserId()} onLogout={onLogout} />
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
|
@ -20,26 +20,25 @@ import { Header, LeftNav } from "./Header";
|
||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
|
||||
export function LoginPage({ onLogin, error }) {
|
||||
export function LoginPage({ onLogin }) {
|
||||
const loginUsernameRef = useRef();
|
||||
const loginPasswordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onLogin(
|
||||
loginUsernameRef.current.value,
|
||||
loginPasswordRef.current.value,
|
||||
() => {
|
||||
onLogin(loginUsernameRef.current.value, loginPasswordRef.current.value)
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.replace(location.state.from);
|
||||
} else {
|
||||
history.replace("/");
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(setError);
|
||||
},
|
||||
[onLogin, location, history]
|
||||
);
|
||||
|
@ -14,32 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { Header, LeftNav } from "./Header";
|
||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
|
||||
export function RegisterPage({ onRegister, error }) {
|
||||
export function RegisterPage({ onRegister }) {
|
||||
const registerUsernameRef = useRef();
|
||||
const registerPasswordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onSubmitRegisterForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onRegister(
|
||||
registerUsernameRef.current.value,
|
||||
registerPasswordRef.current.value,
|
||||
() => {
|
||||
registerPasswordRef.current.value
|
||||
)
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.replace(location.state.from);
|
||||
} else {
|
||||
history.replace("/");
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(setError);
|
||||
},
|
||||
[onRegister, location, history]
|
||||
);
|
||||
|
394
src/Room.jsx
394
src/Room.jsx
@ -23,7 +23,7 @@ import React, {
|
||||
} from "react";
|
||||
import styles from "./Room.module.css";
|
||||
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
||||
import { useVideoRoom } from "./ConferenceCallManagerHooks";
|
||||
import { useGroupCall } from "./ConferenceCallManagerHooks";
|
||||
import { DevTools } from "./DevTools";
|
||||
import { VideoGrid } from "./VideoGrid";
|
||||
import {
|
||||
@ -42,25 +42,16 @@ function useQuery() {
|
||||
return useMemo(() => new URLSearchParams(location.search), [location.search]);
|
||||
}
|
||||
|
||||
export function Room({ manager }) {
|
||||
const { roomId } = useParams();
|
||||
function useDebugMode() {
|
||||
const query = useQuery();
|
||||
const {
|
||||
loading,
|
||||
joined,
|
||||
joining,
|
||||
room,
|
||||
participants,
|
||||
error,
|
||||
joinCall,
|
||||
leaveCall,
|
||||
toggleMuteVideo,
|
||||
toggleMuteAudio,
|
||||
videoMuted,
|
||||
audioMuted,
|
||||
} = useVideoRoom(manager, roomId);
|
||||
const debugStr = query.get("debug");
|
||||
const [debug, setDebug] = useState(debugStr === "" || debugStr === "true");
|
||||
const [debugMode, setDebugMode] = useState(
|
||||
debugStr === "" || debugStr === "true"
|
||||
);
|
||||
|
||||
const toggleDebugMode = useCallback(() => {
|
||||
setDebugMode((prevDebugMode) => !prevDebugMode);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
@ -68,7 +59,7 @@ export function Room({ manager }) {
|
||||
document.activeElement.tagName !== "input" &&
|
||||
event.code === "Backquote"
|
||||
) {
|
||||
setDebug((prevDebug) => !prevDebug);
|
||||
toggleDebugMode();
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,190 +70,237 @@ export function Room({ manager }) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [debugMode, toggleDebugMode];
|
||||
}
|
||||
|
||||
function useRoomLayout() {
|
||||
const [layout, setLayout] = useState("gallery");
|
||||
|
||||
const toggleLayout = useCallback(() => {
|
||||
setLayout(layout === "spotlight" ? "gallery" : "spotlight");
|
||||
}, [layout]);
|
||||
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
{!loading && room && (
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<CenterNav>
|
||||
<h3>{room.name}</h3>
|
||||
</CenterNav>
|
||||
<RightNav>
|
||||
{!loading && room && joined && (
|
||||
<LayoutToggleButton
|
||||
title={layout === "spotlight" ? "Spotlight" : "Gallery"}
|
||||
layout={layout}
|
||||
onClick={toggleLayout}
|
||||
/>
|
||||
)}
|
||||
<SettingsButton
|
||||
title={debug ? "Disable DevTools" : "Enable DevTools"}
|
||||
on={debug}
|
||||
onClick={() => setDebug((debug) => !debug)}
|
||||
/>
|
||||
</RightNav>
|
||||
</Header>
|
||||
)}
|
||||
{loading && (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Loading room...</p>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className={styles.centerMessage}>{error.message}</div>}
|
||||
{!loading && room && !joined && (
|
||||
<JoinRoom
|
||||
manager={manager}
|
||||
joining={joining}
|
||||
joinCall={joinCall}
|
||||
toggleMuteVideo={toggleMuteVideo}
|
||||
toggleMuteAudio={toggleMuteAudio}
|
||||
videoMuted={videoMuted}
|
||||
audioMuted={audioMuted}
|
||||
return [layout, toggleLayout];
|
||||
}
|
||||
|
||||
export function Room({ client }) {
|
||||
const { roomId } = useParams();
|
||||
const {
|
||||
loading,
|
||||
entered,
|
||||
entering,
|
||||
roomName,
|
||||
participants,
|
||||
groupCall,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
error,
|
||||
initLocalParticipant,
|
||||
enter,
|
||||
leave,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
} = useGroupCall(client, roomId);
|
||||
|
||||
const content = () => {
|
||||
if (error) {
|
||||
return <LoadingErrorView error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingRoomView />;
|
||||
}
|
||||
|
||||
if (entering) {
|
||||
return <EnteringRoomView />;
|
||||
}
|
||||
|
||||
if (!entered) {
|
||||
return (
|
||||
<RoomSetupView
|
||||
roomName={roomName}
|
||||
onInitLocalParticipant={initLocalParticipant}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
/>
|
||||
)}
|
||||
{!loading && room && joined && participants.length === 0 && (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Waiting for other participants...</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && room && joined && participants.length > 0 && (
|
||||
<VideoGrid participants={participants} layout={layout} />
|
||||
)}
|
||||
{!loading && room && joined && (
|
||||
<div className={styles.footer}>
|
||||
<MicButton muted={audioMuted} onClick={toggleMuteAudio} />
|
||||
<VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
|
||||
<HangupButton onClick={leaveCall} />
|
||||
</div>
|
||||
)}
|
||||
{debug && <DevTools manager={manager} />}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<InRoomView
|
||||
roomName={roomName}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
participants={participants}
|
||||
onLeave={leave}
|
||||
groupCall={groupCall}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={styles.room}>{content()}</div>;
|
||||
}
|
||||
|
||||
export function LoadingRoomView() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Loading room...</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomAuth({ onLoginAsGuest, error }) {
|
||||
const displayNameRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onLoginAsGuest(displayNameRef.current.value);
|
||||
},
|
||||
[onLoginAsGuest, location, history]
|
||||
export function EnteringRoomView() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Entering room...</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingErrorView({ error }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.centerMessage}>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PermissionState = {
|
||||
Waiting: "waiting",
|
||||
Granted: "granted",
|
||||
Denied: "denied",
|
||||
};
|
||||
|
||||
function RoomSetupView({
|
||||
roomName,
|
||||
onInitLocalParticipant,
|
||||
onEnter,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
groupCall,
|
||||
}) {
|
||||
const videoRef = useRef();
|
||||
const [permissionState, setPermissionState] = useState(
|
||||
PermissionState.Waiting
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onInitLocalParticipant()
|
||||
.then((localParticipant) => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = localParticipant.usermediaStream;
|
||||
videoRef.current.play();
|
||||
setPermissionState(PermissionState.Granted);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (videoRef.current) {
|
||||
setPermissionState(PermissionState.Denied);
|
||||
}
|
||||
});
|
||||
}, [onInitLocalParticipant]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<CenterNav>
|
||||
<h3>{roomName}</h3>
|
||||
</CenterNav>
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<h2>Login As Guest</h2>
|
||||
<form onSubmit={onSubmitLoginForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="text"
|
||||
ref={displayNameRef}
|
||||
placeholder="Display Name"
|
||||
label="Display Name"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="submit">Login as guest</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<Info>
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
{" or "}
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/register",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
</Info>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.preview}>
|
||||
{permissionState === PermissionState.Denied && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Webcam permissions needed to join the call.
|
||||
</p>
|
||||
)}
|
||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||
</div>
|
||||
{permissionState === PermissionState.Granted && (
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={microphoneMuted}
|
||||
onClick={toggleMicrophoneMuted}
|
||||
/>
|
||||
<VideoButton
|
||||
enabled={localVideoMuted}
|
||||
onClick={toggleLocalVideoMuted}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
disabled={permissionState !== PermissionState.Granted}
|
||||
onClick={onEnter}
|
||||
>
|
||||
Enter Call
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function JoinRoom({
|
||||
joining,
|
||||
joinCall,
|
||||
manager,
|
||||
toggleMuteVideo,
|
||||
toggleMuteAudio,
|
||||
videoMuted,
|
||||
audioMuted,
|
||||
function InRoomView({
|
||||
roomName,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
participants,
|
||||
onLeave,
|
||||
groupCall,
|
||||
}) {
|
||||
const videoRef = useRef();
|
||||
const [hasPermissions, setHasPermissions] = useState(false);
|
||||
const [needsPermissions, setNeedsPermissions] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
manager
|
||||
.getLocalVideoStream()
|
||||
.then((stream) => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
videoRef.current.play();
|
||||
setHasPermissions(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (videoRef.current) {
|
||||
setNeedsPermissions(true);
|
||||
}
|
||||
});
|
||||
}, [manager]);
|
||||
const [debugMode, toggleDebugMode] = useDebugMode();
|
||||
const [roomLayout, toggleRoomLayout] = useRoomLayout();
|
||||
|
||||
return (
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.preview}>
|
||||
{needsPermissions && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Webcam permissions needed to join the call.
|
||||
</p>
|
||||
)}
|
||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||
</div>
|
||||
{hasPermissions && (
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton muted={audioMuted} onClick={toggleMuteAudio} />
|
||||
<VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<CenterNav>
|
||||
<h3>{roomName}</h3>
|
||||
</CenterNav>
|
||||
<RightNav>
|
||||
<LayoutToggleButton
|
||||
title={roomLayout === "spotlight" ? "Spotlight" : "Gallery"}
|
||||
layout={roomLayout}
|
||||
onClick={toggleRoomLayout}
|
||||
/>
|
||||
<SettingsButton
|
||||
title={debugMode ? "Disable DevTools" : "Enable DevTools"}
|
||||
onClick={toggleDebugMode}
|
||||
/>
|
||||
</RightNav>
|
||||
</Header>
|
||||
{participants.length === 0 ? (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Waiting for other participants...</p>
|
||||
</div>
|
||||
) : (
|
||||
<VideoGrid participants={participants} layout={roomLayout} />
|
||||
)}
|
||||
<Button disabled={!hasPermissions || joining} onClick={joinCall}>
|
||||
Join Call
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} />
|
||||
<VideoButton
|
||||
enabled={localVideoMuted}
|
||||
onClick={toggleLocalVideoMuted}
|
||||
/>
|
||||
<HangupButton onClick={onLeave} />
|
||||
</div>
|
||||
{debugMode && <DevTools groupCall={groupCall} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -422,7 +422,7 @@ export function VideoGrid({ participants, layout }) {
|
||||
|
||||
for (const tile of tiles) {
|
||||
let participant = participants.find(
|
||||
(participant) => participant.userId === tile.key
|
||||
(participant) => participant.member.userId === tile.key
|
||||
);
|
||||
|
||||
let remove = false;
|
||||
@ -442,7 +442,7 @@ export function VideoGrid({ participants, layout }) {
|
||||
}
|
||||
|
||||
newTiles.push({
|
||||
key: participant.userId,
|
||||
key: participant.member.userId,
|
||||
participant,
|
||||
remove,
|
||||
presenter,
|
||||
@ -450,13 +450,13 @@ export function VideoGrid({ participants, layout }) {
|
||||
}
|
||||
|
||||
for (const participant of participants) {
|
||||
if (newTiles.some(({ key }) => participant.userId === key)) {
|
||||
if (newTiles.some(({ key }) => participant.member.userId === key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Added tiles
|
||||
newTiles.push({
|
||||
key: participant.userId,
|
||||
key: participant.member.userId,
|
||||
participant,
|
||||
remove: false,
|
||||
presenter: layout === "spotlight" && participant.activeSpeaker,
|
||||
@ -719,17 +719,19 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
|
||||
const videoRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (participant.stream) {
|
||||
if (participant.local) {
|
||||
if (participant.usermediaStream) {
|
||||
// Mute the local video
|
||||
// TODO: Should GroupCallParticipant have a local field?
|
||||
if (!participant.call) {
|
||||
videoRef.current.muted = true;
|
||||
}
|
||||
|
||||
videoRef.current.srcObject = participant.stream;
|
||||
videoRef.current.srcObject = participant.usermediaStream;
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.srcObject = null;
|
||||
}
|
||||
}, [participant.stream]);
|
||||
}, [participant.usermediaStream]);
|
||||
|
||||
// Firefox doesn't respect the disablePictureInPicture attribute
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
||||
@ -738,15 +740,15 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
|
||||
<animated.div className={styles.participantTile} style={style} {...rest}>
|
||||
<div
|
||||
className={classNames(styles.participantName, {
|
||||
[styles.speaking]: participant.speaking,
|
||||
[styles.speaking]: participant.usermediaStream?.speaking,
|
||||
})}
|
||||
>
|
||||
{participant.speaking ? (
|
||||
{participant.usermediaStream?.speaking ? (
|
||||
<MicIcon />
|
||||
) : participant.audioMuted ? (
|
||||
) : participant.isAudioMuted() ? (
|
||||
<MuteMicIcon className={styles.muteMicIcon} />
|
||||
) : null}
|
||||
<span>{participant.displayName}</span>
|
||||
<span>{participant.member.rawDisplayName}</span>
|
||||
</div>
|
||||
{participant.videoMuted && (
|
||||
<DisableVideoIcon
|
||||
|
Loading…
Reference in New Issue
Block a user