Merge pull request #27 from vector-im/feature/update-matrix-sdk
Migrate to matrix-js-sdk GroupCall
This commit is contained in:
		
				commit
				
					
						b50e3cb386
					
				
			
		
					 15 changed files with 821 additions and 63617 deletions
				
			
		|  | @ -8,7 +8,6 @@ | ||||||
|     <script> |     <script> | ||||||
|       window.global = window; |       window.global = window; | ||||||
|     </script> |     </script> | ||||||
|     <script src="/browser-matrix.js"></script> |  | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <div id="root"></div> |     <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", |     "color-hash": "^2.0.1", | ||||||
|     "events": "^3.3.0", |     "events": "^3.3.0", | ||||||
|     "lodash-move": "^1.1.1", |     "lodash-move": "^1.1.1", | ||||||
|     "matrix-js-sdk": "^12.0.1", |     "matrix-js-sdk": "file:../matrix-js-sdk", | ||||||
|     "re-resizable": "^6.9.0", |     "re-resizable": "^6.9.0", | ||||||
|     "react": "^17.0.0", |     "react": "^17.0.0", | ||||||
|     "react-dom": "^17.0.0", |     "react-dom": "^17.0.0", | ||||||
|  |  | ||||||
							
								
								
									
										60775
									
								
								public/browser-matrix.js
									
										
									
									
									
								
							
							
						
						
									
										60775
									
								
								public/browser-matrix.js
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										23
									
								
								src/App.jsx
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								src/App.jsx
									
										
									
									
									
								
							|  | @ -22,13 +22,14 @@ import { | ||||||
|   Redirect, |   Redirect, | ||||||
|   useLocation, |   useLocation, | ||||||
| } from "react-router-dom"; | } from "react-router-dom"; | ||||||
| import { useConferenceCallManager } from "./ConferenceCallManagerHooks"; | import { useClient } from "./ConferenceCallManagerHooks"; | ||||||
| import { Home } from "./Home"; | import { Home } from "./Home"; | ||||||
| import { Room, RoomAuth } from "./Room"; | import { Room } from "./Room"; | ||||||
| import { GridDemo } from "./GridDemo"; | import { GridDemo } from "./GridDemo"; | ||||||
| import { RegisterPage } from "./RegisterPage"; | import { RegisterPage } from "./RegisterPage"; | ||||||
| import { LoginPage } from "./LoginPage"; | import { LoginPage } from "./LoginPage"; | ||||||
| import { Center } from "./Layout"; | import { Center } from "./Layout"; | ||||||
|  | import { GuestAuthPage } from "./GuestAuthPage"; | ||||||
| 
 | 
 | ||||||
| export default function App() { | export default function App() { | ||||||
|   const { protocol, host } = window.location; |   const { protocol, host } = window.location; | ||||||
|  | @ -37,12 +38,12 @@ export default function App() { | ||||||
|   const { |   const { | ||||||
|     loading, |     loading, | ||||||
|     authenticated, |     authenticated, | ||||||
|     error, |     client, | ||||||
|     manager, |  | ||||||
|     login, |     login, | ||||||
|     loginAsGuest, |     logout, | ||||||
|  |     registerGuest, | ||||||
|     register, |     register, | ||||||
|   } = useConferenceCallManager(homeserverUrl); |   } = useClient(homeserverUrl); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Router> |     <Router> | ||||||
|  | @ -54,19 +55,19 @@ export default function App() { | ||||||
|         ) : ( |         ) : ( | ||||||
|           <Switch> |           <Switch> | ||||||
|             <AuthenticatedRoute authenticated={authenticated} exact path="/"> |             <AuthenticatedRoute authenticated={authenticated} exact path="/"> | ||||||
|               <Home manager={manager} error={error} /> |               <Home client={client} onLogout={logout} /> | ||||||
|             </AuthenticatedRoute> |             </AuthenticatedRoute> | ||||||
|             <Route exact path="/login"> |             <Route exact path="/login"> | ||||||
|               <LoginPage onLogin={login} error={error} /> |               <LoginPage onLogin={login} /> | ||||||
|             </Route> |             </Route> | ||||||
|             <Route exact path="/register"> |             <Route exact path="/register"> | ||||||
|               <RegisterPage onRegister={register} error={error} /> |               <RegisterPage onRegister={register} /> | ||||||
|             </Route> |             </Route> | ||||||
|             <Route path="/room/:roomId"> |             <Route path="/room/:roomId"> | ||||||
|               {authenticated ? ( |               {authenticated ? ( | ||||||
|                 <Room manager={manager} error={error} /> |                 <Room client={client} /> | ||||||
|               ) : ( |               ) : ( | ||||||
|                 <RoomAuth error={error} onLoginAsGuest={loginAsGuest} /> |                 <GuestAuthPage onRegisterGuest={registerGuest} /> | ||||||
|               )} |               )} | ||||||
|             </Route> |             </Route> | ||||||
|             <Route exact path="/grid"> |             <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. | limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| import { useCallback, useEffect, useState } from "react"; | import { useCallback, useEffect, useRef, useState } from "react"; | ||||||
| import { ConferenceCallManager } from "./ConferenceCallManager"; | import matrix from "matrix-js-sdk"; | ||||||
| 
 | 
 | ||||||
| // https://stackoverflow.com/a/9039885
 | // https://stackoverflow.com/a/9039885
 | ||||||
| function isIOS() { | function isIOS() { | ||||||
|  | @ -33,289 +33,230 @@ function isIOS() { | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useConferenceCallManager(homeserverUrl) { | function waitForSync(client) { | ||||||
|   const [{ loading, authenticated, manager, error }, setState] = useState({ |   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, |     loading: true, | ||||||
|     authenticated: false, |     authenticated: false, | ||||||
|     manager: undefined, |     client: undefined, | ||||||
|     error: undefined, |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     ConferenceCallManager.restore(homeserverUrl) |     async function restore() { | ||||||
|       .then((manager) => { |       try { | ||||||
|         setState({ |         const authStore = localStorage.getItem("matrix-auth-store"); | ||||||
|           manager, |  | ||||||
|           loading: false, |  | ||||||
|           authenticated: !!manager, |  | ||||||
|           error: undefined, |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|       .catch((err) => { |  | ||||||
|         console.error(err); |  | ||||||
| 
 | 
 | ||||||
|         setState({ |         if (authStore) { | ||||||
|           manager: undefined, |           const { user_id, device_id, access_token } = JSON.parse(authStore); | ||||||
|           loading: false, |  | ||||||
|           authenticated: false, |  | ||||||
|           error: err, |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|   }, []); |  | ||||||
| 
 | 
 | ||||||
|   const login = useCallback(async (username, password, cb) => { |           const client = await initClient({ | ||||||
|     setState((prevState) => ({ |             baseUrl: homeserverUrl, | ||||||
|       ...prevState, |             accessToken: access_token, | ||||||
|       authenticated: false, |             userId: user_id, | ||||||
|       error: undefined, |             deviceId: device_id, | ||||||
|     })); |           }); | ||||||
| 
 | 
 | ||||||
|     ConferenceCallManager.login(homeserverUrl, username, password) |           localStorage.setItem( | ||||||
|       .then((manager) => { |             "matrix-auth-store", | ||||||
|         setState({ |             JSON.stringify({ user_id, device_id, access_token }) | ||||||
|           manager, |           ); | ||||||
|           loading: false, |  | ||||||
|           authenticated: true, |  | ||||||
|           error: undefined, |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         if (cb) { |           return client; | ||||||
|           cb(); |         } | ||||||
|  |       } 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) => { |       .catch(() => { | ||||||
|         console.error(err); |         setState({ client: undefined, loading: false, authenticated: false }); | ||||||
| 
 |  | ||||||
|         setState({ |  | ||||||
|           manager: undefined, |  | ||||||
|           loading: false, |  | ||||||
|           authenticated: false, |  | ||||||
|           error: err, |  | ||||||
|         }); |  | ||||||
|       }); |       }); | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const loginAsGuest = useCallback(async (displayName) => { |   const login = useCallback(async (username, password) => { | ||||||
|     setState((prevState) => ({ |     try { | ||||||
|       ...prevState, |       const registrationClient = matrix.createClient(homeserverUrl); | ||||||
|       authenticated: false, |  | ||||||
|       error: undefined, |  | ||||||
|     })); |  | ||||||
| 
 | 
 | ||||||
|     ConferenceCallManager.loginAsGuest(homeserverUrl, displayName) |       const { user_id, device_id, access_token } = | ||||||
|       .then((manager) => { |         await registrationClient.loginWithPassword(username, password); | ||||||
|         setState({ |  | ||||||
|           manager, |  | ||||||
|           loading: false, |  | ||||||
|           authenticated: true, |  | ||||||
|           error: undefined, |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|       .catch((err) => { |  | ||||||
|         console.error(err); |  | ||||||
| 
 | 
 | ||||||
|         setState({ |       const client = await initClient({ | ||||||
|           manager: undefined, |         baseUrl: homeserverUrl, | ||||||
|           loading: false, |         accessToken: access_token, | ||||||
|           authenticated: false, |         userId: user_id, | ||||||
|           error: err, |         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) => { |   const registerGuest = useCallback(async (displayName) => { | ||||||
|     setState((prevState) => ({ |     try { | ||||||
|       ...prevState, |       const registrationClient = matrix.createClient(homeserverUrl); | ||||||
|       authenticated: false, |  | ||||||
|       error: undefined, |  | ||||||
|     })); |  | ||||||
| 
 | 
 | ||||||
|     ConferenceCallManager.register(homeserverUrl, username, password) |       const { user_id, device_id, access_token } = | ||||||
|       .then((manager) => { |         await registrationClient.registerGuest({}); | ||||||
|         setState({ |  | ||||||
|           manager, |  | ||||||
|           loading: false, |  | ||||||
|           authenticated: true, |  | ||||||
|           error: undefined, |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         if (cb) { |       const client = await initClient( | ||||||
|           cb(); |         { | ||||||
|         } |           baseUrl: homeserverUrl, | ||||||
|       }) |           accessToken: access_token, | ||||||
|       .catch((err) => { |           userId: user_id, | ||||||
|         console.error(err); |           deviceId: device_id, | ||||||
|  |         }, | ||||||
|  |         true | ||||||
|  |       ); | ||||||
| 
 | 
 | ||||||
|         setState({ |       localStorage.setItem( | ||||||
|           manager: undefined, |         "matrix-auth-store", | ||||||
|           loading: false, |         JSON.stringify({ user_id, device_id, access_token }) | ||||||
|           authenticated: false, |       ); | ||||||
|           error: err, | 
 | ||||||
|         }); |       setState({ client, loading: false, authenticated: true }); | ||||||
|       }); |     } catch (err) { | ||||||
|  |       localStorage.removeItem("matrix-auth-store"); | ||||||
|  |       setState({ client: undefined, loading: false, authenticated: false }); | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   const register = useCallback(async (username, password) => { | ||||||
|     window.confManager = manager; |     try { | ||||||
|  |       const registrationClient = matrix.createClient(homeserverUrl); | ||||||
| 
 | 
 | ||||||
|     return () => { |       const { user_id, device_id, access_token } = | ||||||
|       window.confManager = undefined; |         await registrationClient.register(username, password, null, { | ||||||
|     }; |           type: "m.login.dummy", | ||||||
|   }, [manager]); |         }); | ||||||
|  | 
 | ||||||
|  |       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 { |   return { | ||||||
|     loading, |     loading, | ||||||
|     authenticated, |     authenticated, | ||||||
|     manager, |     client, | ||||||
|     error, |  | ||||||
|     login, |     login, | ||||||
|     loginAsGuest, |     registerGuest, | ||||||
|     register, |     register, | ||||||
|  |     logout, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useVideoRoom(manager, roomId, timeout = 5000) { | function usePageUnload(callback) { | ||||||
|   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]); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let pageVisibilityTimeout; |     let pageVisibilityTimeout; | ||||||
| 
 | 
 | ||||||
|  | @ -327,25 +268,11 @@ export function useVideoRoom(manager, roomId, timeout = 5000) { | ||||||
|           // Wait 5 seconds before closing the page to avoid accidentally leaving
 |           // Wait 5 seconds before closing the page to avoid accidentally leaving
 | ||||||
|           // TODO: Make this configurable?
 |           // TODO: Make this configurable?
 | ||||||
|           pageVisibilityTimeout = setTimeout(() => { |           pageVisibilityTimeout = setTimeout(() => { | ||||||
|             manager.leaveCall(); |             callback(); | ||||||
| 
 |  | ||||||
|             setState((prevState) => ({ |  | ||||||
|               ...prevState, |  | ||||||
|               participants: [...manager.participants], |  | ||||||
|               joined: false, |  | ||||||
|               joining: false, |  | ||||||
|             })); |  | ||||||
|           }, 5000); |           }, 5000); | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         manager.leaveCall(); |         callback(); | ||||||
| 
 |  | ||||||
|         setState((prevState) => ({ |  | ||||||
|           ...prevState, |  | ||||||
|           participants: [...manager.participants], |  | ||||||
|           joined: false, |  | ||||||
|           joining: false, |  | ||||||
|         })); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -363,31 +290,167 @@ export function useVideoRoom(manager, roomId, timeout = 5000) { | ||||||
|       window.removeEventListener("beforeunload", onBeforeUnload); |       window.removeEventListener("beforeunload", onBeforeUnload); | ||||||
|       clearTimeout(pageVisibilityTimeout); |       clearTimeout(pageVisibilityTimeout); | ||||||
|     }; |     }; | ||||||
|   }, [manager]); |   }, []); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|   const toggleMuteAudio = useCallback(() => { | function getParticipants(groupCall) { | ||||||
|     manager.setAudioMuted(!manager.audioMuted); |   return [...groupCall.participants]; | ||||||
|     setState((prevState) => ({ ...prevState, audioMuted: manager.audioMuted })); | } | ||||||
|   }, [manager]); |  | ||||||
| 
 | 
 | ||||||
|   const toggleMuteVideo = useCallback(() => { | export function useGroupCall(client, roomId) { | ||||||
|     manager.setVideoMuted(!manager.videoMuted); |   const groupCallRef = useRef(null); | ||||||
|     setState((prevState) => ({ ...prevState, videoMuted: manager.videoMuted })); | 
 | ||||||
|   }, [manager]); |   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) { | ||||||
|  |       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("call_replaced", onParticipantsChanged); | ||||||
|  |       groupCall.on("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("call_replaced", onParticipantsChanged); | ||||||
|  |         groupCall.removeListener("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 { |   return { | ||||||
|     loading, |     loading, | ||||||
|     joined, |     entered, | ||||||
|     joining, |     entering, | ||||||
|     room, |     roomName: room ? room.name : null, | ||||||
|     participants, |     participants, | ||||||
|  |     groupCall: groupCallRef.current, | ||||||
|  |     microphoneMuted, | ||||||
|  |     localVideoMuted, | ||||||
|     error, |     error, | ||||||
|     joinCall, |     initLocalParticipant, | ||||||
|     leaveCall, |     enter, | ||||||
|     toggleMuteVideo, |     leave, | ||||||
|     toggleMuteAudio, |     toggleLocalVideoMuted, | ||||||
|     videoMuted, |     toggleMicrophoneMuted, | ||||||
|     audioMuted, |  | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -440,22 +503,22 @@ function sortRooms(client, rooms) { | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useRooms(manager) { | export function useRooms(client) { | ||||||
|   const [rooms, setRooms] = useState([]); |   const [rooms, setRooms] = useState([]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     function updateRooms() { |     function updateRooms() { | ||||||
|       const visibleRooms = manager.client.getVisibleRooms(); |       const visibleRooms = client.getVisibleRooms(); | ||||||
|       const sortedRooms = sortRooms(manager.client, visibleRooms); |       const sortedRooms = sortRooms(client, visibleRooms); | ||||||
|       setRooms(sortedRooms); |       setRooms(sortedRooms); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     updateRooms(); |     updateRooms(); | ||||||
| 
 | 
 | ||||||
|     manager.client.on("Room", updateRooms); |     client.on("Room", updateRooms); | ||||||
| 
 | 
 | ||||||
|     return () => { |     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 }); | const colorHash = new ColorHash({ lightness: 0.3 }); | ||||||
| 
 | 
 | ||||||
| export function Home({ manager }) { | export function Home({ client, onLogout }) { | ||||||
|   const history = useHistory(); |   const history = useHistory(); | ||||||
|   const roomNameRef = useRef(); |   const roomNameRef = useRef(); | ||||||
|   const guestAccessRef = useRef(); |   const guestAccessRef = useRef(); | ||||||
|   const [createRoomError, setCreateRoomError] = useState(); |   const [createRoomError, setCreateRoomError] = useState(); | ||||||
|   const rooms = useRooms(manager); |   const rooms = useRooms(client); | ||||||
| 
 | 
 | ||||||
|   const onCreateRoom = useCallback( |   const onCreateRoom = useCallback( | ||||||
|     (e) => { |     (e) => { | ||||||
|  | @ -38,7 +38,7 @@ export function Home({ manager }) { | ||||||
|       setCreateRoomError(undefined); |       setCreateRoomError(undefined); | ||||||
| 
 | 
 | ||||||
|       async function createRoom(name, guestAccess) { |       async function createRoom(name, guestAccess) { | ||||||
|         const { room_id } = await manager.client.createRoom({ |         const { room_id } = await client.createRoom({ | ||||||
|           visibility: "private", |           visibility: "private", | ||||||
|           preset: "public_chat", |           preset: "public_chat", | ||||||
|           name, |           name, | ||||||
|  | @ -62,14 +62,14 @@ export function Home({ manager }) { | ||||||
|                   "m.sticker": 50, |                   "m.sticker": 50, | ||||||
|                 }, |                 }, | ||||||
|                 users: { |                 users: { | ||||||
|                   [manager.client.getUserId()]: 100, |                   [client.getUserId()]: 100, | ||||||
|                 }, |                 }, | ||||||
|               } |               } | ||||||
|             : undefined, |             : undefined, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (guestAccess) { |         if (guestAccess) { | ||||||
|           await manager.client.setGuestAccess(room_id, { |           await client.setGuestAccess(room_id, { | ||||||
|             allowJoin: true, |             allowJoin: true, | ||||||
|             allowRead: true, |             allowRead: true, | ||||||
|           }); |           }); | ||||||
|  | @ -83,27 +83,14 @@ export function Home({ manager }) { | ||||||
|         guestAccessRef.current.checked |         guestAccessRef.current.checked | ||||||
|       ).catch(setCreateRoomError); |       ).catch(setCreateRoomError); | ||||||
|     }, |     }, | ||||||
|     [manager] |     [client] | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const onLogout = useCallback( |  | ||||||
|     (e) => { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       manager.logout(); |  | ||||||
|       location.reload(); |  | ||||||
|     }, |  | ||||||
|     [manager] |  | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Header> |       <Header> | ||||||
|         <LeftNav /> |         <LeftNav /> | ||||||
|         <UserNav |         <UserNav signedIn userName={client.getUserId()} onLogout={onLogout} /> | ||||||
|           signedIn={manager.client} |  | ||||||
|           userName={manager.client.getUserId()} |  | ||||||
|           onLogout={onLogout} |  | ||||||
|         /> |  | ||||||
|       </Header> |       </Header> | ||||||
|       <Content> |       <Content> | ||||||
|         <Center> |         <Center> | ||||||
|  |  | ||||||
|  | @ -20,26 +20,25 @@ import { Header, LeftNav } from "./Header"; | ||||||
| import { FieldRow, InputField, Button, ErrorMessage } from "./Input"; | import { FieldRow, InputField, Button, ErrorMessage } from "./Input"; | ||||||
| import { Center, Content, Info, Modal } from "./Layout"; | import { Center, Content, Info, Modal } from "./Layout"; | ||||||
| 
 | 
 | ||||||
| export function LoginPage({ onLogin, error }) { | export function LoginPage({ onLogin }) { | ||||||
|   const loginUsernameRef = useRef(); |   const loginUsernameRef = useRef(); | ||||||
|   const loginPasswordRef = useRef(); |   const loginPasswordRef = useRef(); | ||||||
|   const history = useHistory(); |   const history = useHistory(); | ||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
|  |   const [error, setError] = useState(); | ||||||
| 
 | 
 | ||||||
|   const onSubmitLoginForm = useCallback( |   const onSubmitLoginForm = useCallback( | ||||||
|     (e) => { |     (e) => { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       onLogin( |       onLogin(loginUsernameRef.current.value, loginPasswordRef.current.value) | ||||||
|         loginUsernameRef.current.value, |         .then(() => { | ||||||
|         loginPasswordRef.current.value, |  | ||||||
|         () => { |  | ||||||
|           if (location.state && location.state.from) { |           if (location.state && location.state.from) { | ||||||
|             history.replace(location.state.from); |             history.replace(location.state.from); | ||||||
|           } else { |           } else { | ||||||
|             history.replace("/"); |             history.replace("/"); | ||||||
|           } |           } | ||||||
|         } |         }) | ||||||
|       ); |         .catch(setError); | ||||||
|     }, |     }, | ||||||
|     [onLogin, location, history] |     [onLogin, location, history] | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -14,32 +14,34 @@ See the License for the specific language governing permissions and | ||||||
| limitations under the License. | 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 { useHistory, useLocation, Link } from "react-router-dom"; | ||||||
| import { Header, LeftNav } from "./Header"; | import { Header, LeftNav } from "./Header"; | ||||||
| import { FieldRow, InputField, Button, ErrorMessage } from "./Input"; | import { FieldRow, InputField, Button, ErrorMessage } from "./Input"; | ||||||
| import { Center, Content, Info, Modal } from "./Layout"; | import { Center, Content, Info, Modal } from "./Layout"; | ||||||
| 
 | 
 | ||||||
| export function RegisterPage({ onRegister, error }) { | export function RegisterPage({ onRegister }) { | ||||||
|   const registerUsernameRef = useRef(); |   const registerUsernameRef = useRef(); | ||||||
|   const registerPasswordRef = useRef(); |   const registerPasswordRef = useRef(); | ||||||
|   const history = useHistory(); |   const history = useHistory(); | ||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
|  |   const [error, setError] = useState(); | ||||||
| 
 | 
 | ||||||
|   const onSubmitRegisterForm = useCallback( |   const onSubmitRegisterForm = useCallback( | ||||||
|     (e) => { |     (e) => { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       onRegister( |       onRegister( | ||||||
|         registerUsernameRef.current.value, |         registerUsernameRef.current.value, | ||||||
|         registerPasswordRef.current.value, |         registerPasswordRef.current.value | ||||||
|         () => { |       ) | ||||||
|  |         .then(() => { | ||||||
|           if (location.state && location.state.from) { |           if (location.state && location.state.from) { | ||||||
|             history.replace(location.state.from); |             history.replace(location.state.from); | ||||||
|           } else { |           } else { | ||||||
|             history.replace("/"); |             history.replace("/"); | ||||||
|           } |           } | ||||||
|         } |         }) | ||||||
|       ); |         .catch(setError); | ||||||
|     }, |     }, | ||||||
|     [onRegister, location, history] |     [onRegister, location, history] | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
							
								
								
									
										399
									
								
								src/Room.jsx
									
										
									
									
									
								
							
							
						
						
									
										399
									
								
								src/Room.jsx
									
										
									
									
									
								
							|  | @ -23,7 +23,7 @@ import React, { | ||||||
| } from "react"; | } from "react"; | ||||||
| import styles from "./Room.module.css"; | import styles from "./Room.module.css"; | ||||||
| import { useParams, useLocation, useHistory, Link } from "react-router-dom"; | import { useParams, useLocation, useHistory, Link } from "react-router-dom"; | ||||||
| import { useVideoRoom } from "./ConferenceCallManagerHooks"; | import { useGroupCall } from "./ConferenceCallManagerHooks"; | ||||||
| import { DevTools } from "./DevTools"; | import { DevTools } from "./DevTools"; | ||||||
| import { VideoGrid } from "./VideoGrid"; | import { VideoGrid } from "./VideoGrid"; | ||||||
| import { | import { | ||||||
|  | @ -42,25 +42,16 @@ function useQuery() { | ||||||
|   return useMemo(() => new URLSearchParams(location.search), [location.search]); |   return useMemo(() => new URLSearchParams(location.search), [location.search]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function Room({ manager }) { | function useDebugMode() { | ||||||
|   const { roomId } = useParams(); |  | ||||||
|   const query = useQuery(); |   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 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(() => { |   useEffect(() => { | ||||||
|     function onKeyDown(event) { |     function onKeyDown(event) { | ||||||
|  | @ -68,7 +59,7 @@ export function Room({ manager }) { | ||||||
|         document.activeElement.tagName !== "input" && |         document.activeElement.tagName !== "input" && | ||||||
|         event.code === "Backquote" |         event.code === "Backquote" | ||||||
|       ) { |       ) { | ||||||
|         setDebug((prevDebug) => !prevDebug); |         toggleDebugMode(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -79,190 +70,242 @@ export function Room({ manager }) { | ||||||
|     }; |     }; | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|  |   return [debugMode, toggleDebugMode]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function useRoomLayout() { | ||||||
|   const [layout, setLayout] = useState("gallery"); |   const [layout, setLayout] = useState("gallery"); | ||||||
| 
 | 
 | ||||||
|   const toggleLayout = useCallback(() => { |   const toggleLayout = useCallback(() => { | ||||||
|     setLayout(layout === "spotlight" ? "gallery" : "spotlight"); |     setLayout(layout === "spotlight" ? "gallery" : "spotlight"); | ||||||
|   }, [layout]); |   }, [layout]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return [layout, toggleLayout]; | ||||||
|     <div className={styles.room}> | } | ||||||
|       {!loading && room && ( | 
 | ||||||
|         <Header> | export function Room({ client }) { | ||||||
|           <LeftNav /> |   const { roomId } = useParams(); | ||||||
|           <CenterNav> |   const { | ||||||
|             <h3>{room.name}</h3> |     loading, | ||||||
|           </CenterNav> |     entered, | ||||||
|           <RightNav> |     entering, | ||||||
|             {!loading && room && joined && ( |     roomName, | ||||||
|               <LayoutToggleButton |     participants, | ||||||
|                 title={layout === "spotlight" ? "Spotlight" : "Gallery"} |     groupCall, | ||||||
|                 layout={layout} |     microphoneMuted, | ||||||
|                 onClick={toggleLayout} |     localVideoMuted, | ||||||
|               /> |     error, | ||||||
|             )} |     initLocalParticipant, | ||||||
|             <SettingsButton |     enter, | ||||||
|               title={debug ? "Disable DevTools" : "Enable DevTools"} |     leave, | ||||||
|               on={debug} |     toggleLocalVideoMuted, | ||||||
|               onClick={() => setDebug((debug) => !debug)} |     toggleMicrophoneMuted, | ||||||
|             /> |   } = useGroupCall(client, roomId); | ||||||
|           </RightNav> | 
 | ||||||
|         </Header> |   const content = () => { | ||||||
|       )} |     if (error) { | ||||||
|       {loading && ( |       return <LoadingErrorView error={error} />; | ||||||
|         <div className={styles.centerMessage}> |     } | ||||||
|           <p>Loading room...</p> | 
 | ||||||
|         </div> |     if (loading) { | ||||||
|       )} |       return <LoadingRoomView />; | ||||||
|       {error && <div className={styles.centerMessage}>{error.message}</div>} |     } | ||||||
|       {!loading && room && !joined && ( | 
 | ||||||
|         <JoinRoom |     if (entering) { | ||||||
|           manager={manager} |       return <EnteringRoomView />; | ||||||
|           joining={joining} |     } | ||||||
|           joinCall={joinCall} | 
 | ||||||
|           toggleMuteVideo={toggleMuteVideo} |     if (!entered) { | ||||||
|           toggleMuteAudio={toggleMuteAudio} |       return ( | ||||||
|           videoMuted={videoMuted} |         <RoomSetupView | ||||||
|           audioMuted={audioMuted} |           roomName={roomName} | ||||||
|  |           onInitLocalParticipant={initLocalParticipant} | ||||||
|  |           onEnter={enter} | ||||||
|  |           microphoneMuted={microphoneMuted} | ||||||
|  |           localVideoMuted={localVideoMuted} | ||||||
|  |           toggleLocalVideoMuted={toggleLocalVideoMuted} | ||||||
|  |           toggleMicrophoneMuted={toggleMicrophoneMuted} | ||||||
|         /> |         /> | ||||||
|       )} |       ); | ||||||
|       {!loading && room && joined && participants.length === 0 && ( |     } else { | ||||||
|         <div className={styles.centerMessage}> |       return ( | ||||||
|           <p>Waiting for other participants...</p> |         <InRoomView | ||||||
|         </div> |           roomName={roomName} | ||||||
|       )} |           microphoneMuted={microphoneMuted} | ||||||
|       {!loading && room && joined && participants.length > 0 && ( |           localVideoMuted={localVideoMuted} | ||||||
|         <VideoGrid participants={participants} layout={layout} /> |           toggleLocalVideoMuted={toggleLocalVideoMuted} | ||||||
|       )} |           toggleMicrophoneMuted={toggleMicrophoneMuted} | ||||||
|       {!loading && room && joined && ( |           participants={participants} | ||||||
|         <div className={styles.footer}> |           onLeave={leave} | ||||||
|           <MicButton muted={audioMuted} onClick={toggleMuteAudio} /> |           groupCall={groupCall} | ||||||
|           <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} /> |         /> | ||||||
|           <HangupButton onClick={leaveCall} /> |       ); | ||||||
|         </div> |     } | ||||||
|       )} |   }; | ||||||
|       {debug && <DevTools manager={manager} />} | 
 | ||||||
|     </div> |   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 }) { | export function EnteringRoomView() { | ||||||
|   const displayNameRef = useRef(); |   return ( | ||||||
|   const history = useHistory(); |     <> | ||||||
|   const location = useLocation(); |       <div className={styles.centerMessage}> | ||||||
| 
 |         <p>Entering room...</p> | ||||||
|   const onSubmitLoginForm = useCallback( |       </div> | ||||||
|     (e) => { |     </> | ||||||
|       e.preventDefault(); |  | ||||||
|       onLoginAsGuest(displayNameRef.current.value); |  | ||||||
|     }, |  | ||||||
|     [onLoginAsGuest, location, history] |  | ||||||
|   ); |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function LoadingErrorView({ error }) { | ||||||
|  |   useEffect(() => { | ||||||
|  |     console.error(error); | ||||||
|  |   }, [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, | ||||||
|  | }) { | ||||||
|  |   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((error) => { | ||||||
|  |         console.error(error); | ||||||
|  | 
 | ||||||
|  |         if (videoRef.current) { | ||||||
|  |           setPermissionState(PermissionState.Denied); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |   }, [onInitLocalParticipant]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Header> |       <Header> | ||||||
|         <LeftNav /> |         <LeftNav /> | ||||||
|  |         <CenterNav> | ||||||
|  |           <h3>{roomName}</h3> | ||||||
|  |         </CenterNav> | ||||||
|       </Header> |       </Header> | ||||||
|       <Content> |       <div className={styles.joinRoom}> | ||||||
|         <Center> |         <div className={styles.preview}> | ||||||
|           <Modal> |           {permissionState === PermissionState.Denied && ( | ||||||
|             <h2>Login As Guest</h2> |             <p className={styles.webcamPermissions}> | ||||||
|             <form onSubmit={onSubmitLoginForm}> |               Webcam permissions needed to join the call. | ||||||
|               <FieldRow> |             </p> | ||||||
|                 <InputField |           )} | ||||||
|                   type="text" |           <video ref={videoRef} muted playsInline disablePictureInPicture /> | ||||||
|                   ref={displayNameRef} |         </div> | ||||||
|                   placeholder="Display Name" |         {permissionState === PermissionState.Granted && ( | ||||||
|                   label="Display Name" |           <div className={styles.previewButtons}> | ||||||
|                   autoCorrect="off" |             <MicButton | ||||||
|                   autoCapitalize="none" |               muted={microphoneMuted} | ||||||
|                 /> |               onClick={toggleMicrophoneMuted} | ||||||
|               </FieldRow> |             /> | ||||||
|               {error && ( |             <VideoButton | ||||||
|                 <FieldRow> |               enabled={localVideoMuted} | ||||||
|                   <ErrorMessage>{error.message}</ErrorMessage> |               onClick={toggleLocalVideoMuted} | ||||||
|                 </FieldRow> |             /> | ||||||
|               )} |           </div> | ||||||
|               <FieldRow rightAlign> |         )} | ||||||
|                 <Button type="submit">Login as guest</Button> |         <Button | ||||||
|               </FieldRow> |           disabled={permissionState !== PermissionState.Granted} | ||||||
|             </form> |           onClick={onEnter} | ||||||
|             <Info> |         > | ||||||
|               <Link |           Enter Call | ||||||
|                 to={{ |         </Button> | ||||||
|                   pathname: "/login", |       </div> | ||||||
|                   state: location.state, |  | ||||||
|                 }} |  | ||||||
|               > |  | ||||||
|                 Sign in |  | ||||||
|               </Link> |  | ||||||
|               {" or "} |  | ||||||
|               <Link |  | ||||||
|                 to={{ |  | ||||||
|                   pathname: "/register", |  | ||||||
|                   state: location.state, |  | ||||||
|                 }} |  | ||||||
|               > |  | ||||||
|                 Create account |  | ||||||
|               </Link> |  | ||||||
|             </Info> |  | ||||||
|           </Modal> |  | ||||||
|         </Center> |  | ||||||
|       </Content> |  | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function JoinRoom({ | function InRoomView({ | ||||||
|   joining, |   roomName, | ||||||
|   joinCall, |   microphoneMuted, | ||||||
|   manager, |   localVideoMuted, | ||||||
|   toggleMuteVideo, |   toggleLocalVideoMuted, | ||||||
|   toggleMuteAudio, |   toggleMicrophoneMuted, | ||||||
|   videoMuted, |   participants, | ||||||
|   audioMuted, |   onLeave, | ||||||
|  |   groupCall, | ||||||
| }) { | }) { | ||||||
|   const videoRef = useRef(); |   const [debugMode, toggleDebugMode] = useDebugMode(); | ||||||
|   const [hasPermissions, setHasPermissions] = useState(false); |   const [roomLayout, toggleRoomLayout] = useRoomLayout(); | ||||||
|   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]); |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={styles.joinRoom}> |     <> | ||||||
|       <div className={styles.preview}> |       <Header> | ||||||
|         {needsPermissions && ( |         <LeftNav /> | ||||||
|           <p className={styles.webcamPermissions}> |         <CenterNav> | ||||||
|             Webcam permissions needed to join the call. |           <h3>{roomName}</h3> | ||||||
|           </p> |         </CenterNav> | ||||||
|         )} |         <RightNav> | ||||||
|         <video ref={videoRef} muted playsInline disablePictureInPicture /> |           <LayoutToggleButton | ||||||
|       </div> |             title={roomLayout === "spotlight" ? "Spotlight" : "Gallery"} | ||||||
|       {hasPermissions && ( |             layout={roomLayout} | ||||||
|         <div className={styles.previewButtons}> |             onClick={toggleRoomLayout} | ||||||
|           <MicButton muted={audioMuted} onClick={toggleMuteAudio} /> |           /> | ||||||
|           <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} /> |           <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> |         </div> | ||||||
|  |       ) : ( | ||||||
|  |         <VideoGrid participants={participants} layout={roomLayout} /> | ||||||
|       )} |       )} | ||||||
|       <Button disabled={!hasPermissions || joining} onClick={joinCall}> |       <div className={styles.footer}> | ||||||
|         Join Call |         <MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} /> | ||||||
|       </Button> |         <VideoButton | ||||||
|     </div> |           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) { |       for (const tile of tiles) { | ||||||
|         let participant = participants.find( |         let participant = participants.find( | ||||||
|           (participant) => participant.userId === tile.key |           (participant) => participant.member.userId === tile.key | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         let remove = false; |         let remove = false; | ||||||
|  | @ -436,13 +436,13 @@ export function VideoGrid({ participants, layout }) { | ||||||
|         let presenter; |         let presenter; | ||||||
| 
 | 
 | ||||||
|         if (layout === "spotlight") { |         if (layout === "spotlight") { | ||||||
|           presenter = participant.activeSpeaker; |           presenter = participant.isActiveSpeaker(); | ||||||
|         } else { |         } else { | ||||||
|           presenter = layout === lastLayoutRef.current ? tile.presenter : false; |           presenter = layout === lastLayoutRef.current ? tile.presenter : false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         newTiles.push({ |         newTiles.push({ | ||||||
|           key: participant.userId, |           key: participant.member.userId, | ||||||
|           participant, |           participant, | ||||||
|           remove, |           remove, | ||||||
|           presenter, |           presenter, | ||||||
|  | @ -450,16 +450,16 @@ export function VideoGrid({ participants, layout }) { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       for (const participant of participants) { |       for (const participant of participants) { | ||||||
|         if (newTiles.some(({ key }) => participant.userId === key)) { |         if (newTiles.some(({ key }) => participant.member.userId === key)) { | ||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Added tiles |         // Added tiles | ||||||
|         newTiles.push({ |         newTiles.push({ | ||||||
|           key: participant.userId, |           key: participant.member.userId, | ||||||
|           participant, |           participant, | ||||||
|           remove: false, |           remove: false, | ||||||
|           presenter: layout === "spotlight" && participant.activeSpeaker, |           presenter: layout === "spotlight" && participant.isActiveSpeaker(), | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | @ -719,17 +719,19 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) { | ||||||
|   const videoRef = useRef(); |   const videoRef = useRef(); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (participant.stream) { |     if (participant.usermediaStream) { | ||||||
|       if (participant.local) { |       // Mute the local video | ||||||
|  |       // TODO: Should GroupCallParticipant have a local field? | ||||||
|  |       if (!participant.call) { | ||||||
|         videoRef.current.muted = true; |         videoRef.current.muted = true; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       videoRef.current.srcObject = participant.stream; |       videoRef.current.srcObject = participant.usermediaStream; | ||||||
|       videoRef.current.play(); |       videoRef.current.play(); | ||||||
|     } else { |     } else { | ||||||
|       videoRef.current.srcObject = null; |       videoRef.current.srcObject = null; | ||||||
|     } |     } | ||||||
|   }, [participant.stream]); |   }, [participant.usermediaStream]); | ||||||
| 
 | 
 | ||||||
|   // Firefox doesn't respect the disablePictureInPicture attribute |   // Firefox doesn't respect the disablePictureInPicture attribute | ||||||
|   // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 |   // 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}> |     <animated.div className={styles.participantTile} style={style} {...rest}> | ||||||
|       <div |       <div | ||||||
|         className={classNames(styles.participantName, { |         className={classNames(styles.participantName, { | ||||||
|           [styles.speaking]: participant.speaking, |           [styles.speaking]: participant.usermediaFeed?.isSpeaking(), | ||||||
|         })} |         })} | ||||||
|       > |       > | ||||||
|         {participant.speaking ? ( |         {participant.usermediaFeed?.isSpeaking() ? ( | ||||||
|           <MicIcon /> |           <MicIcon /> | ||||||
|         ) : participant.audioMuted ? ( |         ) : participant.isAudioMuted() ? ( | ||||||
|           <MuteMicIcon className={styles.muteMicIcon} /> |           <MuteMicIcon className={styles.muteMicIcon} /> | ||||||
|         ) : null} |         ) : null} | ||||||
|         <span>{participant.displayName}</span> |         <span>{participant.member.rawDisplayName}</span> | ||||||
|       </div> |       </div> | ||||||
|       {participant.videoMuted && ( |       {participant.videoMuted && ( | ||||||
|         <DisableVideoIcon |         <DisableVideoIcon | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue