From f219218d0260f570d241a52433ca390a4bb3b05a Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sun, 30 Oct 2022 16:07:32 -0500 Subject: [PATCH] feat: (closes #19) Audio calls (#60) * feat: [#19] set up voice call UI * feat: [#19] get audio calls * feat: [#19] implement audio device selection --- src/components/Room/Room.tsx | 132 +++++++++++++++++++++++++++++- src/components/Room/useRoom.ts | 89 +++++++++++++++++++- src/services/PeerRoom/PeerRoom.ts | 16 ++++ 3 files changed, 234 insertions(+), 3 deletions(-) diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index 264424e..4cf9b50 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -1,5 +1,18 @@ +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' @@ -24,7 +37,15 @@ export function Room({ password, userId, }: RoomProps) { - const { messageLog, sendMessage, isMessageSending } = useRoom( + const { + audioDevices, + messageLog, + sendMessage, + isMessageSending, + isSpeakingToRoom, + setIsSpeakingToRoom, + handleAudioDeviceSelect, + } = useRoom( { appId, trackerUrls, @@ -38,10 +59,37 @@ 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 ( + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + + + + {isSpeakingToRoom ? ( + <> + + Stop speaking to room + + ) : ( + <> + + Start speaking to room + + )} + + {audioDevices.length > 0 && ( + + + + + + + + {audioDevices.map((audioDevice, idx) => ( + + handleAudioDeviceMenuItemClick(event, idx) + } + > + {audioDevice.label} + + ))} + + + )} + + + >([]) const [newMessageAudio] = useState( - () => new Audio(process.env.PUBLIC_URL + '/sounds/new-message.aac') + () => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac') ) + const [isSpeakingToRoom, setIsSpeakingToRoom] = useState(false) + const [peerAudios, setPeerAudios] = useState< + Record + >({}) + const [audioStream, setAudioStream] = useState() const setMessageLog = (messages: Message[]) => { _setMessageLog(messages.slice(-messageTranscriptSizeLimit)) } + const [audioDevices, setAudioDevices] = useState([]) useEffect(() => { return () => { @@ -66,6 +72,16 @@ export function useRoom( } }, [shellContext]) + useEffect(() => { + ;(async () => { + if (!audioStream) return + + const devices = await window.navigator.mediaDevices.enumerateDevices() + const audioDevices = devices.filter(({ kind }) => kind === 'audioinput') + setAudioDevices(audioDevices) + })() + }, [audioStream]) + const [sendPeerId, receivePeerId] = usePeerRoomAction( peerRoom, PeerActions.PEER_NAME @@ -147,6 +163,11 @@ export function useRoom( const newNumberOfPeers = numberOfPeers + 1 setNumberOfPeers(newNumberOfPeers) shellContext.setNumberOfPeers(newNumberOfPeers) + + if (audioStream) { + peerRoom.addStream(audioStream, peerId) + } + ;(async () => { try { const promises: Promise[] = [sendPeerId(userId, peerId)] @@ -184,6 +205,10 @@ export function useRoom( setNumberOfPeers(newNumberOfPeers) shellContext.setNumberOfPeers(newNumberOfPeers) + if (audioStream) { + peerRoom.removeStream(audioStream, peerId) + } + if (peerExist) { const peerListClone = [...shellContext.peerList] peerListClone.splice(peerIndex, 1) @@ -191,10 +216,70 @@ export function useRoom( } }) + peerRoom.onPeerStream((stream, peerId) => { + const audio = new Audio() + audio.srcObject = stream + audio.autoplay = true + + setPeerAudios({ ...peerAudios, [peerId]: audio }) + }) + + useEffect(() => { + ;(async () => { + if (isSpeakingToRoom) { + if (!audioStream) { + const newSelfStream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false, + }) + + peerRoom.addStream(newSelfStream) + + setAudioStream(newSelfStream) + } + } else { + if (audioStream) { + for (const audioTrack of audioStream.getTracks()) { + audioTrack.stop() + audioStream.removeTrack(audioTrack) + } + + peerRoom.removeStream(audioStream, peerRoom.getPeers()) + setAudioStream(null) + } + } + })() + }, [isSpeakingToRoom, peerAudios, peerRoom, audioStream]) + + const handleAudioDeviceSelect = async (audioDevice: MediaDeviceInfo) => { + if (!audioStream) return + + for (const audioTrack of audioStream.getTracks()) { + audioTrack.stop() + audioStream.removeTrack(audioTrack) + } + + peerRoom.removeStream(audioStream, peerRoom.getPeers()) + + const newSelfStream = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: audioDevice.deviceId, + }, + video: false, + }) + + peerRoom.addStream(newSelfStream) + setAudioStream(newSelfStream) + } + return { + audioDevices, peerRoom, messageLog, sendMessage, isMessageSending, + isSpeakingToRoom, + setIsSpeakingToRoom, + handleAudioDeviceSelect, } } diff --git a/src/services/PeerRoom/PeerRoom.ts b/src/services/PeerRoom/PeerRoom.ts index affbfd1..f17c394 100644 --- a/src/services/PeerRoom/PeerRoom.ts +++ b/src/services/PeerRoom/PeerRoom.ts @@ -26,7 +26,23 @@ export class PeerRoom { this.room.onPeerLeave((...args) => fn(...args)) } + onPeerStream: Room['onPeerStream'] = fn => { + this.room.onPeerStream((...args) => fn(...args)) + } + + getPeers: Room['getPeers'] = () => { + return this.room.getPeers() + } + makeAction = (namespace: string) => { return this.room.makeAction(namespace) } + + addStream: Room['addStream'] = stream => { + return this.room.addStream(stream) + } + + removeStream: Room['removeStream'] = (stream, targetPeers) => { + return this.room.removeStream(stream, targetPeers) + } }