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