forked from Shiloh/remnantchat
* feat: [#19] set up voice call UI * feat: [#19] get audio calls * feat: [#19] implement audio device selection
This commit is contained in:
parent
02a2e53b64
commit
f219218d02
@ -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 Box from '@mui/material/Box'
|
||||||
import Divider from '@mui/material/Divider'
|
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 { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
import { rtcConfig } from 'config/rtcConfig'
|
import { rtcConfig } from 'config/rtcConfig'
|
||||||
@ -24,7 +37,15 @@ export function Room({
|
|||||||
password,
|
password,
|
||||||
userId,
|
userId,
|
||||||
}: RoomProps) {
|
}: RoomProps) {
|
||||||
const { messageLog, sendMessage, isMessageSending } = useRoom(
|
const {
|
||||||
|
audioDevices,
|
||||||
|
messageLog,
|
||||||
|
sendMessage,
|
||||||
|
isMessageSending,
|
||||||
|
isSpeakingToRoom,
|
||||||
|
setIsSpeakingToRoom,
|
||||||
|
handleAudioDeviceSelect,
|
||||||
|
} = useRoom(
|
||||||
{
|
{
|
||||||
appId,
|
appId,
|
||||||
trackerUrls,
|
trackerUrls,
|
||||||
@ -38,10 +59,37 @@ export function Room({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [audioAnchorEl, setAudioAnchorEl] = useState<null | HTMLElement>(null)
|
||||||
|
const isAudioDeviceSelectOpen = Boolean(audioAnchorEl)
|
||||||
|
const [selectedAudioDeviceIdx, setSelectedAudioDeviceIdx] = useState(0)
|
||||||
|
|
||||||
const handleMessageSubmit = async (message: string) => {
|
const handleMessageSubmit = async (message: string) => {
|
||||||
await sendMessage(message)
|
await sendMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleVoiceCallClick = () => {
|
||||||
|
setIsSpeakingToRoom(!isSpeakingToRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAudioDeviceListItemClick = (
|
||||||
|
event: React.MouseEvent<HTMLElement>
|
||||||
|
) => {
|
||||||
|
setAudioAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAudioDeviceMenuItemClick = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
idx: number
|
||||||
|
) => {
|
||||||
|
setSelectedAudioDeviceIdx(idx)
|
||||||
|
handleAudioDeviceSelect(audioDevices[idx])
|
||||||
|
setAudioAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAudioInputSelectMenuClose = () => {
|
||||||
|
setAudioAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="Room"
|
className="Room"
|
||||||
@ -51,6 +99,88 @@ export function Room({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
aria-controls="panel1a-content"
|
||||||
|
id="panel1a-header"
|
||||||
|
></AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fab
|
||||||
|
variant="extended"
|
||||||
|
color={isSpeakingToRoom ? 'error' : 'success'}
|
||||||
|
aria-label="call"
|
||||||
|
onClick={handleVoiceCallClick}
|
||||||
|
>
|
||||||
|
{isSpeakingToRoom ? (
|
||||||
|
<>
|
||||||
|
<VoiceOverOff sx={{ mr: 1 }} />
|
||||||
|
Stop speaking to room
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RecordVoiceOver sx={{ mr: 1 }} />
|
||||||
|
Start speaking to room
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Fab>
|
||||||
|
{audioDevices.length > 0 && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
<List
|
||||||
|
component="nav"
|
||||||
|
aria-label="Audio device selection"
|
||||||
|
sx={{ bgcolor: 'background.paper' }}
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
id="audio-input-select-button"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-controls="audio-input-select-menu"
|
||||||
|
aria-label="Audio input device to use"
|
||||||
|
aria-expanded={isAudioDeviceSelectOpen ? 'true' : undefined}
|
||||||
|
onClick={handleAudioDeviceListItemClick}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary="Selected audio input device"
|
||||||
|
secondary={audioDevices[selectedAudioDeviceIdx]?.label}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Menu
|
||||||
|
id="audio-input-select-menu"
|
||||||
|
anchorEl={audioAnchorEl}
|
||||||
|
open={isAudioDeviceSelectOpen}
|
||||||
|
onClose={handleAudioInputSelectMenuClose}
|
||||||
|
MenuListProps={{
|
||||||
|
'aria-labelledby': 'audio-input-select-button',
|
||||||
|
role: 'listbox',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{audioDevices.map((audioDevice, idx) => (
|
||||||
|
<MenuItem
|
||||||
|
key={audioDevice.deviceId}
|
||||||
|
selected={idx === selectedAudioDeviceIdx}
|
||||||
|
onClick={event =>
|
||||||
|
handleAudioDeviceMenuItemClick(event, idx)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{audioDevice.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
<ChatTranscript
|
<ChatTranscript
|
||||||
messageLog={messageLog}
|
messageLog={messageLog}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { funAnimalName } from 'fun-animal-names'
|
import { funAnimalName } from 'fun-animal-names'
|
||||||
import { getPeerName } from 'components/PeerNameDisplay'
|
import { getPeerName } from 'components/PeerNameDisplay'
|
||||||
import { NotificationService } from 'services/Notification'
|
import { NotificationService } from 'services/Notification'
|
||||||
import { Audio } from 'services/Audio'
|
import { Audio as AudioService } from 'services/Audio'
|
||||||
import { PeerRoom } from 'services/PeerRoom'
|
import { PeerRoom } from 'services/PeerRoom'
|
||||||
|
|
||||||
import { messageTranscriptSizeLimit } from 'config/messaging'
|
import { messageTranscriptSizeLimit } from 'config/messaging'
|
||||||
@ -45,12 +45,18 @@ export function useRoom(
|
|||||||
Array<ReceivedMessage | UnsentMessage>
|
Array<ReceivedMessage | UnsentMessage>
|
||||||
>([])
|
>([])
|
||||||
const [newMessageAudio] = useState(
|
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<string, HTMLAudioElement>
|
||||||
|
>({})
|
||||||
|
const [audioStream, setAudioStream] = useState<MediaStream | null>()
|
||||||
|
|
||||||
const setMessageLog = (messages: Message[]) => {
|
const setMessageLog = (messages: Message[]) => {
|
||||||
_setMessageLog(messages.slice(-messageTranscriptSizeLimit))
|
_setMessageLog(messages.slice(-messageTranscriptSizeLimit))
|
||||||
}
|
}
|
||||||
|
const [audioDevices, setAudioDevices] = useState<MediaDeviceInfo[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -66,6 +72,16 @@ export function useRoom(
|
|||||||
}
|
}
|
||||||
}, [shellContext])
|
}, [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<string>(
|
const [sendPeerId, receivePeerId] = usePeerRoomAction<string>(
|
||||||
peerRoom,
|
peerRoom,
|
||||||
PeerActions.PEER_NAME
|
PeerActions.PEER_NAME
|
||||||
@ -147,6 +163,11 @@ export function useRoom(
|
|||||||
const newNumberOfPeers = numberOfPeers + 1
|
const newNumberOfPeers = numberOfPeers + 1
|
||||||
setNumberOfPeers(newNumberOfPeers)
|
setNumberOfPeers(newNumberOfPeers)
|
||||||
shellContext.setNumberOfPeers(newNumberOfPeers)
|
shellContext.setNumberOfPeers(newNumberOfPeers)
|
||||||
|
|
||||||
|
if (audioStream) {
|
||||||
|
peerRoom.addStream(audioStream, peerId)
|
||||||
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const promises: Promise<any>[] = [sendPeerId(userId, peerId)]
|
const promises: Promise<any>[] = [sendPeerId(userId, peerId)]
|
||||||
@ -184,6 +205,10 @@ export function useRoom(
|
|||||||
setNumberOfPeers(newNumberOfPeers)
|
setNumberOfPeers(newNumberOfPeers)
|
||||||
shellContext.setNumberOfPeers(newNumberOfPeers)
|
shellContext.setNumberOfPeers(newNumberOfPeers)
|
||||||
|
|
||||||
|
if (audioStream) {
|
||||||
|
peerRoom.removeStream(audioStream, peerId)
|
||||||
|
}
|
||||||
|
|
||||||
if (peerExist) {
|
if (peerExist) {
|
||||||
const peerListClone = [...shellContext.peerList]
|
const peerListClone = [...shellContext.peerList]
|
||||||
peerListClone.splice(peerIndex, 1)
|
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 {
|
return {
|
||||||
|
audioDevices,
|
||||||
peerRoom,
|
peerRoom,
|
||||||
messageLog,
|
messageLog,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
isMessageSending,
|
isMessageSending,
|
||||||
|
isSpeakingToRoom,
|
||||||
|
setIsSpeakingToRoom,
|
||||||
|
handleAudioDeviceSelect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,23 @@ export class PeerRoom {
|
|||||||
this.room.onPeerLeave((...args) => fn(...args))
|
this.room.onPeerLeave((...args) => fn(...args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPeerStream: Room['onPeerStream'] = fn => {
|
||||||
|
this.room.onPeerStream((...args) => fn(...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeers: Room['getPeers'] = () => {
|
||||||
|
return this.room.getPeers()
|
||||||
|
}
|
||||||
|
|
||||||
makeAction = <T>(namespace: string) => {
|
makeAction = <T>(namespace: string) => {
|
||||||
return this.room.makeAction<T>(namespace)
|
return this.room.makeAction<T>(namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addStream: Room['addStream'] = stream => {
|
||||||
|
return this.room.addStream(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeStream: Room['removeStream'] = (stream, targetPeers) => {
|
||||||
|
return this.room.removeStream(stream, targetPeers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user