From 8947bace94038f9d951cb60e40d6afdac3c70fd0 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Thu, 3 Nov 2022 21:36:30 -0500 Subject: [PATCH] refactor: Room audio (#61) * Revert "Revert "refactor: move room audio controls to their own component"" This reverts commit 219e0670ca2c0c5e7bb1c25d4928cdc787934c09. * fix: prevent duplicate hook handlers * refactor: PeerRoom cleanup --- src/components/Room/Room.tsx | 112 +------------------- src/components/Room/RoomAudioControls.tsx | 121 ++++++++++++++++++++++ src/components/Room/useRoom.ts | 28 +---- src/components/Room/useRoomAudio.ts | 14 ++- src/services/PeerRoom/PeerRoom.ts | 65 ++++++++---- 5 files changed, 179 insertions(+), 161 deletions(-) create mode 100644 src/components/Room/RoomAudioControls.tsx diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index 4cf9b50..191bb0a 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -1,18 +1,9 @@ -import { useState } from 'react' import Accordion from '@mui/material/Accordion' import AccordionSummary from '@mui/material/AccordionSummary' import AccordionDetails from '@mui/material/AccordionDetails' import Box from '@mui/material/Box' import Divider from '@mui/material/Divider' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import RecordVoiceOver from '@mui/icons-material/RecordVoiceOver' -import VoiceOverOff from '@mui/icons-material/VoiceOverOff' -import List from '@mui/material/List' -import ListItem from '@mui/material/ListItem' -import ListItemText from '@mui/material/ListItemText' -import Menu from '@mui/material/Menu' -import MenuItem from '@mui/material/MenuItem' -import Fab from '@mui/material/Fab' import { v4 as uuid } from 'uuid' import { rtcConfig } from 'config/rtcConfig' @@ -21,6 +12,7 @@ import { MessageForm } from 'components/MessageForm' import { ChatTranscript } from 'components/ChatTranscript' import { useRoom } from './useRoom' +import { RoomAudioControls } from './RoomAudioControls' export interface RoomProps { appId?: string @@ -37,15 +29,7 @@ export function Room({ password, userId, }: RoomProps) { - const { - audioDevices, - messageLog, - sendMessage, - isMessageSending, - isSpeakingToRoom, - setIsSpeakingToRoom, - handleAudioDeviceSelect, - } = useRoom( + const { messageLog, peerRoom, sendMessage, isMessageSending } = useRoom( { appId, trackerUrls, @@ -59,37 +43,10 @@ export function Room({ } ) - const [audioAnchorEl, setAudioAnchorEl] = useState(null) - const isAudioDeviceSelectOpen = Boolean(audioAnchorEl) - const [selectedAudioDeviceIdx, setSelectedAudioDeviceIdx] = useState(0) - const handleMessageSubmit = async (message: string) => { await sendMessage(message) } - const handleVoiceCallClick = () => { - setIsSpeakingToRoom(!isSpeakingToRoom) - } - - const handleAudioDeviceListItemClick = ( - event: React.MouseEvent - ) => { - setAudioAnchorEl(event.currentTarget) - } - - const handleAudioDeviceMenuItemClick = ( - _event: React.MouseEvent, - idx: number - ) => { - setSelectedAudioDeviceIdx(idx) - handleAudioDeviceSelect(audioDevices[idx]) - setAudioAnchorEl(null) - } - - const handleAudioInputSelectMenuClose = () => { - setAudioAnchorEl(null) - } - return ( - - {isSpeakingToRoom ? ( - <> - - Stop speaking to room - - ) : ( - <> - - Start speaking to room - - )} - - {audioDevices.length > 0 && ( - - - - - - - - {audioDevices.map((audioDevice, idx) => ( - - handleAudioDeviceMenuItemClick(event, idx) - } - > - {audioDevice.label} - - ))} - - - )} + diff --git a/src/components/Room/RoomAudioControls.tsx b/src/components/Room/RoomAudioControls.tsx new file mode 100644 index 0000000..db810bb --- /dev/null +++ b/src/components/Room/RoomAudioControls.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import Box from '@mui/material/Box' +import RecordVoiceOver from '@mui/icons-material/RecordVoiceOver' +import VoiceOverOff from '@mui/icons-material/VoiceOverOff' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import Fab from '@mui/material/Fab' + +import { PeerRoom } from 'services/PeerRoom/PeerRoom' + +import { useRoomAudio } from './useRoomAudio' + +export interface RoomAudioControlsProps { + peerRoom: PeerRoom +} + +export function RoomAudioControls({ peerRoom }: RoomAudioControlsProps) { + const { + audioDevices, + isSpeakingToRoom, + setIsSpeakingToRoom, + handleAudioDeviceSelect, + } = useRoomAudio({ peerRoom }) + + const [audioAnchorEl, setAudioAnchorEl] = useState(null) + const isAudioDeviceSelectOpen = Boolean(audioAnchorEl) + const [selectedAudioDeviceIdx, setSelectedAudioDeviceIdx] = useState(0) + + const handleVoiceCallClick = () => { + setIsSpeakingToRoom(!isSpeakingToRoom) + } + + const handleAudioDeviceListItemClick = ( + event: React.MouseEvent + ) => { + setAudioAnchorEl(event.currentTarget) + } + + const handleAudioDeviceMenuItemClick = ( + _event: React.MouseEvent, + idx: number + ) => { + setSelectedAudioDeviceIdx(idx) + handleAudioDeviceSelect(audioDevices[idx]) + setAudioAnchorEl(null) + } + + const handleAudioInputSelectMenuClose = () => { + setAudioAnchorEl(null) + } + + return ( + <> + + {isSpeakingToRoom ? ( + <> + + Stop speaking to room + + ) : ( + <> + + Start speaking to room + + )} + + {audioDevices.length > 0 && ( + + + + + + + + {audioDevices.map((audioDevice, idx) => ( + handleAudioDeviceMenuItemClick(event, idx)} + > + {audioDevice.label} + + ))} + + + )} + + ) +} diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index 930aaea..f5298ab 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -17,12 +17,11 @@ import { funAnimalName } from 'fun-animal-names' import { getPeerName } from 'components/PeerNameDisplay' import { NotificationService } from 'services/Notification' import { Audio as AudioService } from 'services/Audio' -import { PeerRoom } from 'services/PeerRoom' +import { PeerRoom, PeerHookType } from 'services/PeerRoom' import { messageTranscriptSizeLimit } from 'config/messaging' import { usePeerRoomAction } from './usePeerRoomAction' -import { useRoomAudio } from './useRoomAudio' interface UseRoomConfig { roomId: string @@ -57,15 +56,6 @@ export function useRoom( _setMessageLog(messages.slice(-messageTranscriptSizeLimit)) } - const { - audioDevices, - isSpeakingToRoom, - setIsSpeakingToRoom, - handleAudioDeviceSelect, - handleAudioForNewPeer, - handleAudioForLeavingPeer, - } = useRoomAudio({ peerRoom }) - useEffect(() => { return () => { peerRoom.leaveRoom() @@ -153,7 +143,7 @@ export function useRoom( setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }]) }) - peerRoom.onPeerJoin((peerId: string) => { + peerRoom.onPeerJoin(PeerHookType.NEW_PEER, (peerId: string) => { shellContext.showAlert(`Someone has joined the room`, { severity: 'success', }) @@ -178,11 +168,7 @@ export function useRoom( })() }) - peerRoom.onPeerJoin((peerId: string) => { - handleAudioForNewPeer(peerId) - }) - - peerRoom.onPeerLeave((peerId: string) => { + peerRoom.onPeerLeave(PeerHookType.NEW_PEER, (peerId: string) => { const peerIndex = shellContext.peerList.findIndex( peer => peer.peerId === peerId ) @@ -209,18 +195,10 @@ export function useRoom( } }) - peerRoom.onPeerLeave((peerId: string) => { - handleAudioForLeavingPeer(peerId) - }) - return { - audioDevices, peerRoom, messageLog, sendMessage, isMessageSending, - isSpeakingToRoom, - setIsSpeakingToRoom, - handleAudioDeviceSelect, } } diff --git a/src/components/Room/useRoomAudio.ts b/src/components/Room/useRoomAudio.ts index 42e1186..9a657ec 100644 --- a/src/components/Room/useRoomAudio.ts +++ b/src/components/Room/useRoomAudio.ts @@ -3,7 +3,7 @@ import { useContext, useEffect, useCallback, useState } from 'react' import { ShellContext } from 'contexts/ShellContext' import { PeerActions } from 'models/network' import { AudioState, Peer } from 'models/chat' -import { PeerRoom } from 'services/PeerRoom' +import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom' import { usePeerRoomAction } from './usePeerRoomAction' @@ -57,7 +57,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { shellContext.setPeerList(newPeerList) }) - peerRoom.onPeerStream((stream, peerId) => { + peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId) => { const audio = new Audio() audio.srcObject = stream audio.autoplay = true @@ -161,12 +161,18 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { } } + peerRoom.onPeerJoin(PeerHookType.AUDIO, (peerId: string) => { + handleAudioForNewPeer(peerId) + }) + + peerRoom.onPeerLeave(PeerHookType.AUDIO, (peerId: string) => { + handleAudioForLeavingPeer(peerId) + }) + return { audioDevices, isSpeakingToRoom, setIsSpeakingToRoom, handleAudioDeviceSelect, - handleAudioForNewPeer, - handleAudioForLeavingPeer, } } diff --git a/src/services/PeerRoom/PeerRoom.ts b/src/services/PeerRoom/PeerRoom.ts index bd3734e..75c335d 100644 --- a/src/services/PeerRoom/PeerRoom.ts +++ b/src/services/PeerRoom/PeerRoom.ts @@ -1,37 +1,53 @@ import { joinRoom, Room, BaseRoomConfig } from 'trystero' import { TorrentRoomConfig } from 'trystero/torrent' +export enum PeerHookType { + NEW_PEER = 'NEW_PEER', + AUDIO = 'AUDIO', +} + +export enum PeerStreamType { + AUDIO = 'AUDIO', +} + export class PeerRoom { private room: Room private roomConfig: TorrentRoomConfig & BaseRoomConfig - private peerJoinHandlers: Set<(peerId: string) => void> = new Set() + private peerJoinHandlers: Map< + PeerHookType, + Parameters[0] + > = new Map() - private peerLeaveHandlers: Set<(peerId: string) => void> = new Set() + private peerLeaveHandlers: Map< + PeerHookType, + Parameters[0] + > = new Map() - private peerStreamHandlers: Set< - (stream: MediaStream, peerId: string) => void - > = new Set() + private peerStreamHandlers: Map< + PeerStreamType, + Parameters[0] + > = new Map() constructor(config: TorrentRoomConfig & BaseRoomConfig, roomId: string) { this.roomConfig = config this.room = joinRoom(this.roomConfig, roomId) this.room.onPeerJoin((...args) => { - for (const peerJoinHandler of this.peerJoinHandlers) { + for (const [, peerJoinHandler] of this.peerJoinHandlers) { peerJoinHandler(...args) } }) this.room.onPeerLeave((...args) => { - for (const peerLeaveHandler of this.peerLeaveHandlers) { + for (const [, peerLeaveHandler] of this.peerLeaveHandlers) { peerLeaveHandler(...args) } }) this.room.onPeerStream((...args) => { - for (const peerStreamHandler of this.peerStreamHandlers) { + for (const [, peerStreamHandler] of this.peerStreamHandlers) { peerStreamHandler(...args) } }) @@ -48,34 +64,37 @@ export class PeerRoom { this.flush() } - onPeerJoin: Room['onPeerJoin'] = fn => { - this.peerJoinHandlers.add(fn) + onPeerJoin = ( + peerHookType: PeerHookType, + fn: Parameters[0] + ) => { + this.peerJoinHandlers.set(peerHookType, fn) } onPeerJoinFlush = () => { - this.peerJoinHandlers.forEach(handler => - this.peerJoinHandlers.delete(handler) - ) + this.peerJoinHandlers = new Map() } - onPeerLeave: Room['onPeerLeave'] = fn => { - this.peerLeaveHandlers.add(fn) + onPeerLeave = ( + peerHookType: PeerHookType, + fn: Parameters[0] + ) => { + this.peerLeaveHandlers.set(peerHookType, fn) } onPeerLeaveFlush = () => { - this.peerLeaveHandlers.forEach(handler => - this.peerLeaveHandlers.delete(handler) - ) + this.peerLeaveHandlers = new Map() } - onPeerStream: Room['onPeerStream'] = fn => { - this.peerStreamHandlers.add(fn) + onPeerStream = ( + peerStreamType: PeerStreamType, + fn: Parameters[0] + ) => { + this.peerStreamHandlers.set(peerStreamType, fn) } onPeerStreamFlush = () => { - this.peerStreamHandlers.forEach(handler => - this.peerStreamHandlers.delete(handler) - ) + this.peerStreamHandlers = new Map() } getPeers: Room['getPeers'] = () => {