* feat(ui): present mic volume icon * feat(ui): improve mic volume display * refactor(ui): nest mic audio as a channel * fix(ui): prevent volume control from reappearing for returning peers * refactor(audio): update specific audio channel states * refactor(audio): use enum for audio channel name * refactor(types): improve audio type names * feat(audio): wire up screen share audio * refactor(networking): always provide stream metadata * fix(audio): remove screen audio when stream ends * fix(audio): stop audio when removing it * feat(audio): show appropriate icon for channel * fix(audio): clean up audio for leaving peers consistently * fix(audio): use up-to-date peerAudios reference * refactor(audio): simplify audio state updating * refactor(audio): use functional setState to update peer list * refactor(variables): rename peerAudios to peerAudioChannels * refactor(types): consolidate stream types * refactor(types): require stream type metadata
This commit is contained in:
		
							parent
							
								
									89abe718db
								
							
						
					
					
						commit
						05b4615af9
					
				| @ -1,16 +1,25 @@ | ||||
| import { useState, useEffect } from 'react' | ||||
| import Slider from '@mui/material/Slider' | ||||
| import Box from '@mui/material/Box' | ||||
| import Paper from '@mui/material/Paper' | ||||
| import ListItemIcon from '@mui/material/ListItemIcon' | ||||
| import VolumeUp from '@mui/icons-material/VolumeUp' | ||||
| import VolumeDown from '@mui/icons-material/VolumeDown' | ||||
| import VolumeMute from '@mui/icons-material/VolumeMute' | ||||
| import VolumeUpIcon from '@mui/icons-material/VolumeUp' | ||||
| import VolumeDownIcon from '@mui/icons-material/VolumeDown' | ||||
| import VolumeMuteIcon from '@mui/icons-material/VolumeMute' | ||||
| import MicIcon from '@mui/icons-material/Mic' | ||||
| import LaptopWindowsIcon from '@mui/icons-material/LaptopWindows' | ||||
| import Tooltip from '@mui/material/Tooltip' | ||||
| import { AudioChannelName } from 'models/chat' | ||||
| 
 | ||||
| interface AudioVolumeProps { | ||||
|   audioEl: HTMLAudioElement | ||||
|   audioChannelName: AudioChannelName | ||||
| } | ||||
| 
 | ||||
| export const AudioVolume = ({ audioEl }: AudioVolumeProps) => { | ||||
| export const AudioVolume = ({ | ||||
|   audioEl, | ||||
|   audioChannelName, | ||||
| }: AudioVolumeProps) => { | ||||
|   const [audioVolume, setAudioVolume] = useState(audioEl.volume) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -32,27 +41,48 @@ export const AudioVolume = ({ audioEl }: AudioVolumeProps) => { | ||||
| 
 | ||||
|   const formatLabelValue = () => `${Math.round(audioVolume * 100)}%` | ||||
| 
 | ||||
|   let VolumeIcon = VolumeUp | ||||
|   let VolumeIcon = VolumeUpIcon | ||||
| 
 | ||||
|   if (audioVolume === 0) { | ||||
|     VolumeIcon = VolumeMute | ||||
|     VolumeIcon = VolumeMuteIcon | ||||
|   } else if (audioVolume < 0.5) { | ||||
|     VolumeIcon = VolumeDown | ||||
|     VolumeIcon = VolumeDownIcon | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Box sx={{ display: 'flex', pt: 1, pr: 3, alignItems: 'center' }}> | ||||
|       <ListItemIcon> | ||||
|         <VolumeIcon sx={{ cursor: 'pointer' }} onClick={handleIconClick} /> | ||||
|     <Paper | ||||
|       sx={{ | ||||
|         alignItems: 'center', | ||||
|         display: 'flex', | ||||
|         mt: 1.5, | ||||
|         pl: 2, | ||||
|         pr: 3, | ||||
|         py: 1, | ||||
|       }} | ||||
|     > | ||||
|       <ListItemIcon sx={{ cursor: 'pointer' }} onClick={handleIconClick}> | ||||
|         <VolumeIcon fontSize="small" /> | ||||
|         {audioChannelName === AudioChannelName.MICROPHONE && ( | ||||
|           <Tooltip title="Their microphone volume"> | ||||
|             <MicIcon fontSize="small" sx={{ ml: 1, mr: 2 }} /> | ||||
|           </Tooltip> | ||||
|         )} | ||||
|         {audioChannelName === AudioChannelName.SCREEN_SHARE && ( | ||||
|           <Tooltip title="Their screen's volume"> | ||||
|             <LaptopWindowsIcon fontSize="small" sx={{ ml: 1, mr: 2 }} /> | ||||
|           </Tooltip> | ||||
|         )} | ||||
|       </ListItemIcon> | ||||
|       <Slider | ||||
|         aria-label="Volume" | ||||
|         getAriaValueText={formatLabelValue} | ||||
|         valueLabelFormat={formatLabelValue} | ||||
|         valueLabelDisplay="auto" | ||||
|         onChange={handleSliderChange} | ||||
|         value={audioVolume * 100} | ||||
|       ></Slider> | ||||
|     </Box> | ||||
|       <Box display="flex" width={1}> | ||||
|         <Slider | ||||
|           aria-label="Volume" | ||||
|           getAriaValueText={formatLabelValue} | ||||
|           valueLabelFormat={formatLabelValue} | ||||
|           valueLabelDisplay="auto" | ||||
|           onChange={handleSliderChange} | ||||
|           value={audioVolume * 100} | ||||
|         ></Slider> | ||||
|       </Box> | ||||
|     </Paper> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,7 @@ import Paper from '@mui/material/Paper' | ||||
| import Tooltip from '@mui/material/Tooltip' | ||||
| 
 | ||||
| import { PeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { VideoStreamType } from 'models/chat' | ||||
| import { StreamType } from 'models/chat' | ||||
| 
 | ||||
| import { SelectedPeerStream } from './RoomVideoDisplay' | ||||
| 
 | ||||
| @ -13,13 +13,13 @@ interface PeerVideoProps { | ||||
|   numberOfVideos: number | ||||
|   onVideoClick?: ( | ||||
|     userId: string, | ||||
|     videoStreamType: VideoStreamType, | ||||
|     streamType: StreamType, | ||||
|     videoStream: MediaStream | ||||
|   ) => void | ||||
|   selectedPeerStream: SelectedPeerStream | null | ||||
|   userId: string | ||||
|   videoStream: MediaStream | ||||
|   videoStreamType: VideoStreamType | ||||
|   streamType: StreamType | ||||
| } | ||||
| 
 | ||||
| // Adapted from https://www.geeksforgeeks.org/find-the-next-perfect-square-greater-than-a-given-number/
 | ||||
| @ -37,7 +37,7 @@ export const PeerVideo = ({ | ||||
|   userId, | ||||
|   selectedPeerStream, | ||||
|   videoStream, | ||||
|   videoStreamType, | ||||
|   streamType, | ||||
| }: PeerVideoProps) => { | ||||
|   const videoRef = useRef<HTMLVideoElement>(null) | ||||
| 
 | ||||
| @ -47,13 +47,14 @@ export const PeerVideo = ({ | ||||
| 
 | ||||
|     video.autoplay = true | ||||
|     video.srcObject = videoStream | ||||
|     video.muted = true | ||||
|   }, [videoRef, videoStream]) | ||||
| 
 | ||||
|   const cols = Math.sqrt(nextPerfectSquare(numberOfVideos - 1)) | ||||
|   const rows = Math.ceil(numberOfVideos / cols) | ||||
| 
 | ||||
|   const handleVideoClick = () => { | ||||
|     onVideoClick?.(userId, videoStreamType, videoStream) | ||||
|     onVideoClick?.(userId, streamType, videoStream) | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -4,7 +4,7 @@ import Paper from '@mui/material/Paper' | ||||
| 
 | ||||
| import { RoomContext } from 'contexts/RoomContext' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { Peer, VideoStreamType } from 'models/chat' | ||||
| import { Peer, StreamType } from 'models/chat' | ||||
| 
 | ||||
| import { PeerVideo } from './PeerVideo' | ||||
| 
 | ||||
| @ -16,7 +16,7 @@ interface PeerWithVideo { | ||||
| 
 | ||||
| export interface SelectedPeerStream { | ||||
|   peerId: string | ||||
|   videoStreamType: VideoStreamType | ||||
|   streamType: StreamType | ||||
|   videoStream: MediaStream | ||||
| } | ||||
| 
 | ||||
| @ -105,13 +105,13 @@ export const RoomVideoDisplay = ({ | ||||
| 
 | ||||
|   const handleVideoClick = ( | ||||
|     peerId: string, | ||||
|     videoStreamType: VideoStreamType, | ||||
|     streamType: StreamType, | ||||
|     videoStream: MediaStream | ||||
|   ) => { | ||||
|     if (selectedPeerStream?.videoStream === videoStream) { | ||||
|       setSelectedPeerStream(null) | ||||
|     } else if (numberOfVideos > 1) { | ||||
|       setSelectedPeerStream({ peerId, videoStreamType, videoStream }) | ||||
|       setSelectedPeerStream({ peerId, streamType, videoStream }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -139,7 +139,7 @@ export const RoomVideoDisplay = ({ | ||||
|             userId={selectedPeerStream.peerId} | ||||
|             selectedPeerStream={selectedPeerStream} | ||||
|             videoStream={selectedPeerStream.videoStream} | ||||
|             videoStreamType={selectedPeerStream.videoStreamType} | ||||
|             streamType={selectedPeerStream.streamType} | ||||
|           /> | ||||
|         </Box> | ||||
|       )} | ||||
| @ -168,7 +168,7 @@ export const RoomVideoDisplay = ({ | ||||
|             userId={userId} | ||||
|             selectedPeerStream={selectedPeerStream} | ||||
|             videoStream={selfVideoStream} | ||||
|             videoStreamType={VideoStreamType.WEBCAM} | ||||
|             streamType={StreamType.WEBCAM} | ||||
|           /> | ||||
|         )} | ||||
|         {selfScreenStream && ( | ||||
| @ -179,7 +179,7 @@ export const RoomVideoDisplay = ({ | ||||
|             userId={userId} | ||||
|             selectedPeerStream={selectedPeerStream} | ||||
|             videoStream={selfScreenStream} | ||||
|             videoStreamType={VideoStreamType.SCREEN_SHARE} | ||||
|             streamType={StreamType.SCREEN_SHARE} | ||||
|           /> | ||||
|         )} | ||||
|         {peersWithVideo.map(peerWithVideo => ( | ||||
| @ -191,7 +191,7 @@ export const RoomVideoDisplay = ({ | ||||
|                 userId={peerWithVideo.peer.userId} | ||||
|                 selectedPeerStream={selectedPeerStream} | ||||
|                 videoStream={peerWithVideo.videoStream} | ||||
|                 videoStreamType={VideoStreamType.WEBCAM} | ||||
|                 streamType={StreamType.WEBCAM} | ||||
|               /> | ||||
|             )} | ||||
|             {peerWithVideo.screenStream && ( | ||||
| @ -201,7 +201,7 @@ export const RoomVideoDisplay = ({ | ||||
|                 userId={peerWithVideo.peer.userId} | ||||
|                 selectedPeerStream={selectedPeerStream} | ||||
|                 videoStream={peerWithVideo.screenStream} | ||||
|                 videoStreamType={VideoStreamType.SCREEN_SHARE} | ||||
|                 streamType={StreamType.SCREEN_SHARE} | ||||
|               /> | ||||
|             )} | ||||
|           </Fragment> | ||||
|  | ||||
| @ -23,6 +23,7 @@ import { | ||||
|   TypingStatus, | ||||
|   Peer, | ||||
|   PeerVerificationState, | ||||
|   AudioChannelName, | ||||
| } from 'models/chat' | ||||
| import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { Audio } from 'lib/Audio' | ||||
| @ -269,7 +270,10 @@ export function useRoom( | ||||
|           userId, | ||||
|           publicKey, | ||||
|           customUsername, | ||||
|           audioState: AudioState.STOPPED, | ||||
|           audioChannelState: { | ||||
|             [AudioChannelName.MICROPHONE]: AudioState.STOPPED, | ||||
|             [AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED, | ||||
|           }, | ||||
|           videoState: VideoState.STOPPED, | ||||
|           screenShareState: ScreenShareState.NOT_SHARING, | ||||
|           offeredFileId: null, | ||||
|  | ||||
| @ -2,7 +2,13 @@ import { useContext, useEffect, useCallback, useState } from 'react' | ||||
| 
 | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { PeerActions } from 'models/network' | ||||
| import { AudioState, Peer } from 'models/chat' | ||||
| import { | ||||
|   AudioState, | ||||
|   Peer, | ||||
|   AudioChannelName, | ||||
|   PeerAudioChannelState, | ||||
|   StreamType, | ||||
| } from 'models/chat' | ||||
| import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' | ||||
| 
 | ||||
| interface UseRoomAudioConfig { | ||||
| @ -19,7 +25,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|     string | null | ||||
|   >(null) | ||||
| 
 | ||||
|   const { peerList, setPeerList, setAudioState, peerAudios, setPeerAudios } = | ||||
|   const { setPeerList, setAudioChannelState, setPeerAudioChannels } = | ||||
|     shellContext | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -32,29 +38,46 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|     })() | ||||
|   }, [audioStream]) | ||||
| 
 | ||||
|   const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction<AudioState>( | ||||
|     PeerActions.AUDIO_CHANGE | ||||
|   ) | ||||
|   const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction< | ||||
|     Partial<PeerAudioChannelState> | ||||
|   >(PeerActions.AUDIO_CHANGE) | ||||
| 
 | ||||
|   receiveAudioChange((audioState, peerId) => { | ||||
|     const newPeerList = peerList.map(peer => { | ||||
|       const newPeer: Peer = { ...peer } | ||||
|   receiveAudioChange((peerAudioChannelState, peerId) => { | ||||
|     setPeerList(peerList => { | ||||
|       return peerList.map(peer => { | ||||
|         const newPeer: Peer = { ...peer } | ||||
| 
 | ||||
|       if (peer.peerId === peerId) { | ||||
|         newPeer.audioState = audioState | ||||
|         const microphoneAudioChannel = | ||||
|           peerAudioChannelState[AudioChannelName.MICROPHONE] | ||||
| 
 | ||||
|         if (audioState === AudioState.STOPPED) { | ||||
|           deletePeerAudio(peerId) | ||||
|         if (microphoneAudioChannel) { | ||||
|           if (peer.peerId === peerId) { | ||||
|             newPeer.audioChannelState = { | ||||
|               ...newPeer.audioChannelState, | ||||
|               ...peerAudioChannelState, | ||||
|             } | ||||
| 
 | ||||
|             if (microphoneAudioChannel === AudioState.STOPPED) { | ||||
|               deletePeerAudio(peerId) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return newPeer | ||||
|         return newPeer | ||||
|       }) | ||||
|     }) | ||||
| 
 | ||||
|     setPeerList(newPeerList) | ||||
|   }) | ||||
| 
 | ||||
|   peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId) => { | ||||
|   peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId, metadata) => { | ||||
|     if ( | ||||
|       typeof metadata === 'object' && | ||||
|       metadata !== null && | ||||
|       'type' in metadata && | ||||
|       metadata.type !== StreamType.MICROPHONE | ||||
|     ) { | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     const audioTracks = stream.getAudioTracks() | ||||
| 
 | ||||
|     if (audioTracks.length === 0) return | ||||
| @ -63,7 +86,13 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|     audio.srcObject = stream | ||||
|     audio.autoplay = true | ||||
| 
 | ||||
|     setPeerAudios({ ...peerAudios, [peerId]: audio }) | ||||
|     setPeerAudioChannels(peerAudioChannels => ({ | ||||
|       ...peerAudioChannels, | ||||
|       [peerId]: { | ||||
|         ...peerAudioChannels[peerId], | ||||
|         [AudioChannelName.MICROPHONE]: audio, | ||||
|       }, | ||||
|     })) | ||||
|   }) | ||||
| 
 | ||||
|   const cleanupAudio = useCallback(() => { | ||||
| @ -86,9 +115,19 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|             video: false, | ||||
|           }) | ||||
| 
 | ||||
|           peerRoom.addStream(newSelfStream) | ||||
|           sendAudioChange(AudioState.PLAYING) | ||||
|           setAudioState(AudioState.PLAYING) | ||||
|           peerRoom.addStream(newSelfStream, null, { | ||||
|             type: StreamType.MICROPHONE, | ||||
|           }) | ||||
| 
 | ||||
|           sendAudioChange({ | ||||
|             [AudioChannelName.MICROPHONE]: AudioState.PLAYING, | ||||
|           }) | ||||
| 
 | ||||
|           setAudioChannelState(prevState => ({ | ||||
|             ...prevState, | ||||
|             [AudioChannelName.MICROPHONE]: AudioState.PLAYING, | ||||
|           })) | ||||
| 
 | ||||
|           setAudioStream(newSelfStream) | ||||
|         } | ||||
|       } else { | ||||
| @ -96,8 +135,16 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|           cleanupAudio() | ||||
| 
 | ||||
|           peerRoom.removeStream(audioStream, peerRoom.getPeers()) | ||||
|           sendAudioChange(AudioState.STOPPED) | ||||
|           setAudioState(AudioState.STOPPED) | ||||
| 
 | ||||
|           sendAudioChange({ | ||||
|             [AudioChannelName.MICROPHONE]: AudioState.STOPPED, | ||||
|           }) | ||||
| 
 | ||||
|           setAudioChannelState(prevState => ({ | ||||
|             ...prevState, | ||||
|             [AudioChannelName.MICROPHONE]: AudioState.STOPPED, | ||||
|           })) | ||||
| 
 | ||||
|           setAudioStream(null) | ||||
|         } | ||||
|       } | ||||
| @ -106,11 +153,10 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|     audioStream, | ||||
|     cleanupAudio, | ||||
|     isSpeakingToRoom, | ||||
|     peerAudios, | ||||
|     peerRoom, | ||||
|     selectedAudioDeviceId, | ||||
|     sendAudioChange, | ||||
|     setAudioState, | ||||
|     setAudioChannelState, | ||||
|   ]) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -139,27 +185,45 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { | ||||
|       video: false, | ||||
|     }) | ||||
| 
 | ||||
|     peerRoom.addStream(newSelfStream) | ||||
|     peerRoom.addStream(newSelfStream, null, { | ||||
|       type: StreamType.MICROPHONE, | ||||
|     }) | ||||
| 
 | ||||
|     setAudioStream(newSelfStream) | ||||
|   } | ||||
| 
 | ||||
|   const deletePeerAudio = (peerId: string) => { | ||||
|     const newPeerAudios = { ...peerAudios } | ||||
|     delete newPeerAudios[peerId] | ||||
|     setPeerAudios(newPeerAudios) | ||||
|     setPeerAudioChannels(({ ...newPeerAudios }) => { | ||||
|       if (!newPeerAudios[peerId]) { | ||||
|         return newPeerAudios | ||||
|       } | ||||
| 
 | ||||
|       const microphoneAudio = newPeerAudios[peerId][AudioChannelName.MICROPHONE] | ||||
|       microphoneAudio?.pause() | ||||
| 
 | ||||
|       const { [AudioChannelName.MICROPHONE]: _, ...newPeerAudioChannels } = | ||||
|         newPeerAudios[peerId] | ||||
| 
 | ||||
|       newPeerAudios[peerId] = newPeerAudioChannels | ||||
| 
 | ||||
|       return newPeerAudios | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   const handleAudioForNewPeer = (peerId: string) => { | ||||
|     if (audioStream) { | ||||
|       peerRoom.addStream(audioStream, peerId) | ||||
|       peerRoom.addStream(audioStream, peerId, { | ||||
|         type: StreamType.MICROPHONE, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const handleAudioForLeavingPeer = (peerId: string) => { | ||||
|     if (audioStream) { | ||||
|       peerRoom.removeStream(audioStream, peerId) | ||||
|       deletePeerAudio(peerId) | ||||
|     } | ||||
| 
 | ||||
|     deletePeerAudio(peerId) | ||||
|   } | ||||
| 
 | ||||
|   peerRoom.onPeerJoin(PeerHookType.AUDIO, (peerId: string) => { | ||||
|  | ||||
| @ -4,7 +4,13 @@ import { isRecord } from 'lib/type-guards' | ||||
| import { RoomContext } from 'contexts/RoomContext' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { PeerActions } from 'models/network' | ||||
| import { ScreenShareState, Peer, VideoStreamType } from 'models/chat' | ||||
| import { | ||||
|   ScreenShareState, | ||||
|   Peer, | ||||
|   StreamType, | ||||
|   AudioChannelName, | ||||
|   AudioState, | ||||
| } from 'models/chat' | ||||
| import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' | ||||
| 
 | ||||
| interface UseRoomScreenShareConfig { | ||||
| @ -16,7 +22,13 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { | ||||
|   const roomContext = useContext(RoomContext) | ||||
|   const [isSharingScreen, setIsSharingScreen] = useState(false) | ||||
| 
 | ||||
|   const { peerList, setPeerList, setScreenState } = shellContext | ||||
|   const { | ||||
|     peerList, | ||||
|     setPeerList, | ||||
|     setScreenState, | ||||
|     setAudioChannelState, | ||||
|     setPeerAudioChannels, | ||||
|   } = shellContext | ||||
| 
 | ||||
|   const { | ||||
|     peerScreenStreams, | ||||
| @ -50,7 +62,7 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { | ||||
|     const isScreenShareStream = | ||||
|       isRecord(metadata) && | ||||
|       'type' in metadata && | ||||
|       metadata.type === VideoStreamType.SCREEN_SHARE | ||||
|       metadata.type === StreamType.SCREEN_SHARE | ||||
| 
 | ||||
|     if (!isScreenShareStream) return | ||||
| 
 | ||||
| @ -58,6 +70,33 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { | ||||
|       ...peerScreenStreams, | ||||
|       [peerId]: stream, | ||||
|     }) | ||||
| 
 | ||||
|     const [audioStream] = stream.getAudioTracks() | ||||
| 
 | ||||
|     if (audioStream) { | ||||
|       setAudioChannelState(prevState => ({ | ||||
|         ...prevState, | ||||
|         [AudioChannelName.SCREEN_SHARE]: AudioState.PLAYING, | ||||
|       })) | ||||
| 
 | ||||
|       const audioTracks = stream.getAudioTracks() | ||||
| 
 | ||||
|       if (audioTracks.length > 0) { | ||||
|         const audio = new Audio() | ||||
|         audio.srcObject = stream | ||||
|         audio.autoplay = true | ||||
| 
 | ||||
|         setPeerAudioChannels(peerAudioChannels => { | ||||
|           return { | ||||
|             ...peerAudioChannels, | ||||
|             [peerId]: { | ||||
|               ...peerAudioChannels[peerId], | ||||
|               [AudioChannelName.SCREEN_SHARE]: audio, | ||||
|             }, | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   const cleanupScreenStream = useCallback(() => { | ||||
| @ -78,8 +117,9 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { | ||||
|     }) | ||||
| 
 | ||||
|     peerRoom.addStream(displayMedia, null, { | ||||
|       type: VideoStreamType.SCREEN_SHARE, | ||||
|       type: StreamType.SCREEN_SHARE, | ||||
|     }) | ||||
| 
 | ||||
|     setSelfScreenStream(displayMedia) | ||||
|     sendScreenShare(ScreenShareState.SHARING) | ||||
|     setScreenState(ScreenShareState.SHARING) | ||||
| @ -119,15 +159,33 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { | ||||
|   }, [setPeerScreenStreams]) | ||||
| 
 | ||||
|   const deletePeerScreen = (peerId: string) => { | ||||
|     const newPeerScreens = { ...peerScreenStreams } | ||||
|     delete newPeerScreens[peerId] | ||||
|     setPeerScreenStreams(newPeerScreens) | ||||
|     setPeerScreenStreams(({ [peerId]: _, ...newPeerScreens }) => { | ||||
|       return newPeerScreens | ||||
|     }) | ||||
| 
 | ||||
|     setPeerAudioChannels(({ ...newPeerAudios }) => { | ||||
|       if (!newPeerAudios[peerId]) { | ||||
|         return newPeerAudios | ||||
|       } | ||||
| 
 | ||||
|       const screenShareAudio = | ||||
|         newPeerAudios[peerId][AudioChannelName.SCREEN_SHARE] | ||||
| 
 | ||||
|       screenShareAudio?.pause() | ||||
| 
 | ||||
|       const { [AudioChannelName.SCREEN_SHARE]: _, ...newPeerAudioChannels } = | ||||
|         newPeerAudios[peerId] | ||||
| 
 | ||||
|       newPeerAudios[peerId] = newPeerAudioChannels | ||||
| 
 | ||||
|       return newPeerAudios | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   const handleScreenForNewPeer = (peerId: string) => { | ||||
|     if (selfScreenStream) { | ||||
|       peerRoom.addStream(selfScreenStream, peerId, { | ||||
|         type: VideoStreamType.SCREEN_SHARE, | ||||
|         type: StreamType.SCREEN_SHARE, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { useContext, useEffect, useCallback, useState } from 'react' | ||||
| import { RoomContext } from 'contexts/RoomContext' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { PeerActions } from 'models/network' | ||||
| import { VideoState, Peer, VideoStreamType } from 'models/chat' | ||||
| import { VideoState, Peer, StreamType } from 'models/chat' | ||||
| import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' | ||||
| import { isRecord } from 'lib/type-guards' | ||||
| 
 | ||||
| @ -60,8 +60,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { | ||||
|         }) | ||||
| 
 | ||||
|         peerRoom.addStream(newSelfStream, null, { | ||||
|           type: VideoStreamType.WEBCAM, | ||||
|           type: StreamType.WEBCAM, | ||||
|         }) | ||||
| 
 | ||||
|         setSelfVideoStream(newSelfStream) | ||||
|       } | ||||
|     })() | ||||
| @ -93,7 +94,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { | ||||
|     const isWebcamStream = | ||||
|       isRecord(metadata) && | ||||
|       'type' in metadata && | ||||
|       metadata.type === VideoStreamType.WEBCAM | ||||
|       metadata.type === StreamType.WEBCAM | ||||
| 
 | ||||
|     if (!isWebcamStream) return | ||||
| 
 | ||||
| @ -124,8 +125,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { | ||||
|           }) | ||||
| 
 | ||||
|           peerRoom.addStream(newSelfStream, null, { | ||||
|             type: VideoStreamType.WEBCAM, | ||||
|             type: StreamType.WEBCAM, | ||||
|           }) | ||||
| 
 | ||||
|           sendVideoChange(VideoState.PLAYING) | ||||
|           setVideoState(VideoState.PLAYING) | ||||
|           setSelfVideoStream(newSelfStream) | ||||
| @ -193,7 +195,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { | ||||
|       }, | ||||
|     }) | ||||
| 
 | ||||
|     peerRoom.addStream(newSelfStream, null, { type: VideoStreamType.WEBCAM }) | ||||
|     peerRoom.addStream(newSelfStream, null, { type: StreamType.WEBCAM }) | ||||
|     setSelfVideoStream(newSelfStream) | ||||
|   } | ||||
| 
 | ||||
| @ -206,7 +208,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { | ||||
|   const handleVideoForNewPeer = (peerId: string) => { | ||||
|     if (selfVideoStream) { | ||||
|       peerRoom.addStream(selfVideoStream, peerId, { | ||||
|         type: VideoStreamType.WEBCAM, | ||||
|         type: StreamType.WEBCAM, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -9,7 +9,13 @@ import Box from '@mui/material/Box' | ||||
| import CircularProgress from '@mui/material/CircularProgress' | ||||
| 
 | ||||
| import { UserInfo } from 'components/UserInfo' | ||||
| import { AudioState, Peer } from 'models/chat' | ||||
| import { | ||||
|   AudioState, | ||||
|   Peer, | ||||
|   AudioChannel, | ||||
|   AudioChannelName, | ||||
|   PeerAudioChannelState, | ||||
| } from 'models/chat' | ||||
| import { PeerConnectionType } from 'lib/PeerRoom' | ||||
| import { TrackerConnection } from 'lib/ConnectionTest' | ||||
| 
 | ||||
| @ -25,8 +31,8 @@ export interface PeerListProps extends PropsWithChildren { | ||||
|   onPeerListClose: () => void | ||||
|   peerList: Peer[] | ||||
|   peerConnectionTypes: Record<string, PeerConnectionType> | ||||
|   audioState: AudioState | ||||
|   peerAudios: Record<string, HTMLAudioElement> | ||||
|   peerAudioChannelState: PeerAudioChannelState | ||||
|   peerAudioChannels: Record<string, AudioChannel> | ||||
|   connectionTestResults: IConnectionTestResults | ||||
| } | ||||
| 
 | ||||
| @ -36,8 +42,8 @@ export const PeerList = ({ | ||||
|   onPeerListClose, | ||||
|   peerList, | ||||
|   peerConnectionTypes, | ||||
|   audioState, | ||||
|   peerAudios, | ||||
|   peerAudioChannelState, | ||||
|   peerAudioChannels, | ||||
|   connectionTestResults, | ||||
| }: PeerListProps) => { | ||||
|   return ( | ||||
| @ -49,7 +55,8 @@ export const PeerList = ({ | ||||
|       <Divider /> | ||||
|       <List> | ||||
|         <ListItem divider={true}> | ||||
|           {audioState === AudioState.PLAYING && ( | ||||
|           {peerAudioChannelState[AudioChannelName.MICROPHONE] === | ||||
|             AudioState.PLAYING && ( | ||||
|             <ListItemIcon> | ||||
|               <VolumeUp /> | ||||
|             </ListItemIcon> | ||||
| @ -63,7 +70,7 @@ export const PeerList = ({ | ||||
|             key={peer.peerId} | ||||
|             peer={peer} | ||||
|             peerConnectionTypes={peerConnectionTypes} | ||||
|             peerAudios={peerAudios} | ||||
|             peerAudioChannels={peerAudioChannels} | ||||
|           /> | ||||
|         ))} | ||||
|         {peerList.length === 0 && | ||||
|  | ||||
| @ -18,7 +18,12 @@ import EnhancedEncryptionIcon from '@mui/icons-material/EnhancedEncryption' | ||||
| import { AudioVolume } from 'components/AudioVolume' | ||||
| import { PeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { PublicKey } from 'components/PublicKey' | ||||
| import { Peer, PeerVerificationState } from 'models/chat' | ||||
| import { | ||||
|   Peer, | ||||
|   AudioChannel, | ||||
|   AudioChannelName, | ||||
|   PeerVerificationState, | ||||
| } from 'models/chat' | ||||
| import { PeerConnectionType } from 'lib/PeerRoom' | ||||
| 
 | ||||
| import { PeerDownloadFileButton } from './PeerDownloadFileButton' | ||||
| @ -26,7 +31,7 @@ import { PeerDownloadFileButton } from './PeerDownloadFileButton' | ||||
| interface PeerListItemProps { | ||||
|   peer: Peer | ||||
|   peerConnectionTypes: Record<string, PeerConnectionType> | ||||
|   peerAudios: Record<string, HTMLAudioElement> | ||||
|   peerAudioChannels: Record<string, AudioChannel> | ||||
| } | ||||
| 
 | ||||
| const verificationStateDisplayMap = { | ||||
| @ -52,8 +57,8 @@ const iconRightPadding = 1 | ||||
| export const PeerListItem = ({ | ||||
|   peer, | ||||
|   peerConnectionTypes, | ||||
|   peerAudios, | ||||
| }: PeerListItemProps): JSX.Element => { | ||||
|   peerAudioChannels, | ||||
| }: PeerListItemProps) => { | ||||
|   const [showPeerDialog, setShowPeerDialog] = useState(false) | ||||
| 
 | ||||
|   const hasPeerConnection = peer.peerId in peerConnectionTypes | ||||
| @ -69,6 +74,11 @@ export const PeerListItem = ({ | ||||
|     setShowPeerDialog(false) | ||||
|   } | ||||
| 
 | ||||
|   const microphoneAudio = | ||||
|     peerAudioChannels[peer.peerId]?.[AudioChannelName.MICROPHONE] | ||||
|   const screenShareAudio = | ||||
|     peerAudioChannels[peer.peerId]?.[AudioChannelName.SCREEN_SHARE] | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ListItem key={peer.peerId} divider={true}> | ||||
| @ -124,8 +134,17 @@ export const PeerListItem = ({ | ||||
|             </Box> | ||||
|             <PeerNameDisplay>{peer.userId}</PeerNameDisplay> | ||||
|           </Box> | ||||
|           {peer.peerId in peerAudios && ( | ||||
|             <AudioVolume audioEl={peerAudios[peer.peerId]} /> | ||||
|           {microphoneAudio && ( | ||||
|             <AudioVolume | ||||
|               audioEl={microphoneAudio} | ||||
|               audioChannelName={AudioChannelName.MICROPHONE} | ||||
|             /> | ||||
|           )} | ||||
|           {screenShareAudio && ( | ||||
|             <AudioVolume | ||||
|               audioEl={screenShareAudio} | ||||
|               audioChannelName={AudioChannelName.SCREEN_SHARE} | ||||
|             /> | ||||
|           )} | ||||
|         </ListItemText> | ||||
|       </ListItem> | ||||
|  | ||||
| @ -19,7 +19,15 @@ import { useWindowSize } from '@react-hook/window-size' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| import { AlertOptions, QueryParamKeys } from 'models/shell' | ||||
| import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' | ||||
| import { | ||||
|   AudioState, | ||||
|   ScreenShareState, | ||||
|   VideoState, | ||||
|   Peer, | ||||
|   AudioChannel, | ||||
|   PeerAudioChannelState, | ||||
|   AudioChannelName, | ||||
| } from 'models/chat' | ||||
| import { ErrorBoundary } from 'components/ErrorBoundary' | ||||
| import { PeerConnectionType } from 'lib/PeerRoom' | ||||
| 
 | ||||
| @ -86,7 +94,11 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|     Record<string, PeerConnectionType> | ||||
|   >({}) | ||||
|   const [tabHasFocus, setTabHasFocus] = useState(true) | ||||
|   const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED) | ||||
|   const [audioChannelState, setAudioChannelState] = | ||||
|     useState<PeerAudioChannelState>({ | ||||
|       [AudioChannelName.MICROPHONE]: AudioState.STOPPED, | ||||
|       [AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED, | ||||
|     }) | ||||
|   const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED) | ||||
|   const [screenState, setScreenState] = useState<ScreenShareState>( | ||||
|     ScreenShareState.NOT_SHARING | ||||
| @ -94,8 +106,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|   const [customUsername, setCustomUsername] = useState( | ||||
|     getUserSettings().customUsername | ||||
|   ) | ||||
|   const [peerAudios, setPeerAudios] = useState< | ||||
|     Record<string, HTMLAudioElement> | ||||
|   const [peerAudioChannels, setPeerAudioChannels] = useState< | ||||
|     Record<string, AudioChannel> | ||||
|   >({}) | ||||
| 
 | ||||
|   const showAlert = useCallback((message: string, options?: AlertOptions) => { | ||||
| @ -144,14 +156,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|       setIsServerConnectionFailureDialogOpen, | ||||
|       peerConnectionTypes, | ||||
|       setPeerConnectionTypes, | ||||
|       audioState, | ||||
|       setAudioState, | ||||
|       audioChannelState, | ||||
|       setAudioChannelState, | ||||
|       videoState, | ||||
|       setVideoState, | ||||
|       screenState, | ||||
|       setScreenState, | ||||
|       peerAudios, | ||||
|       setPeerAudios, | ||||
|       peerAudioChannels, | ||||
|       setPeerAudioChannels, | ||||
|       customUsername, | ||||
|       setCustomUsername, | ||||
|       connectionTestResults, | ||||
| @ -174,14 +186,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|       setShowRoomControls, | ||||
|       setTitle, | ||||
|       showAlert, | ||||
|       audioState, | ||||
|       setAudioState, | ||||
|       audioChannelState, | ||||
|       setAudioChannelState, | ||||
|       videoState, | ||||
|       setVideoState, | ||||
|       screenState, | ||||
|       setScreenState, | ||||
|       peerAudios, | ||||
|       setPeerAudios, | ||||
|       peerAudioChannels, | ||||
|       setPeerAudioChannels, | ||||
|       customUsername, | ||||
|       setCustomUsername, | ||||
|       connectionTestResults, | ||||
| @ -393,8 +405,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|                   onPeerListClose={handlePeerListClick} | ||||
|                   peerList={peerList} | ||||
|                   peerConnectionTypes={peerConnectionTypes} | ||||
|                   audioState={audioState} | ||||
|                   peerAudios={peerAudios} | ||||
|                   peerAudioChannelState={audioChannelState} | ||||
|                   peerAudioChannels={peerAudioChannels} | ||||
|                   connectionTestResults={connectionTestResults} | ||||
|                 /> | ||||
|                 {isEmbedded ? ( | ||||
|  | ||||
| @ -1,7 +1,15 @@ | ||||
| import { createContext, Dispatch, SetStateAction } from 'react' | ||||
| 
 | ||||
| import { AlertOptions } from 'models/shell' | ||||
| import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' | ||||
| import { | ||||
|   AudioState, | ||||
|   ScreenShareState, | ||||
|   VideoState, | ||||
|   Peer, | ||||
|   AudioChannel, | ||||
|   PeerAudioChannelState, | ||||
|   AudioChannelName, | ||||
| } from 'models/chat' | ||||
| import { PeerConnectionType } from 'lib/PeerRoom' | ||||
| import { ConnectionTestResults } from 'components/Shell/useConnectionTest' | ||||
| import { TrackerConnection } from 'lib/ConnectionTest' | ||||
| @ -27,14 +35,14 @@ interface ShellContextProps { | ||||
|   setPeerConnectionTypes: Dispatch< | ||||
|     SetStateAction<Record<string, PeerConnectionType>> | ||||
|   > | ||||
|   audioState: AudioState | ||||
|   setAudioState: Dispatch<SetStateAction<AudioState>> | ||||
|   audioChannelState: PeerAudioChannelState | ||||
|   setAudioChannelState: Dispatch<SetStateAction<PeerAudioChannelState>> | ||||
|   videoState: VideoState | ||||
|   setVideoState: Dispatch<SetStateAction<VideoState>> | ||||
|   screenState: ScreenShareState | ||||
|   setScreenState: Dispatch<SetStateAction<ScreenShareState>> | ||||
|   peerAudios: Record<string, HTMLAudioElement> | ||||
|   setPeerAudios: Dispatch<SetStateAction<Record<string, HTMLAudioElement>>> | ||||
|   peerAudioChannels: Record<string, AudioChannel> | ||||
|   setPeerAudioChannels: Dispatch<SetStateAction<Record<string, AudioChannel>>> | ||||
|   customUsername: string | ||||
|   setCustomUsername: Dispatch<SetStateAction<string>> | ||||
|   connectionTestResults: ConnectionTestResults | ||||
| @ -60,14 +68,17 @@ export const ShellContext = createContext<ShellContextProps>({ | ||||
|   setIsServerConnectionFailureDialogOpen: () => {}, | ||||
|   peerConnectionTypes: {}, | ||||
|   setPeerConnectionTypes: () => {}, | ||||
|   audioState: AudioState.STOPPED, | ||||
|   setAudioState: () => {}, | ||||
|   audioChannelState: { | ||||
|     [AudioChannelName.MICROPHONE]: AudioState.STOPPED, | ||||
|     [AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED, | ||||
|   }, | ||||
|   setAudioChannelState: () => {}, | ||||
|   videoState: VideoState.STOPPED, | ||||
|   setVideoState: () => {}, | ||||
|   screenState: ScreenShareState.NOT_SHARING, | ||||
|   setScreenState: () => {}, | ||||
|   peerAudios: {}, | ||||
|   setPeerAudios: () => {}, | ||||
|   peerAudioChannels: {}, | ||||
|   setPeerAudioChannels: () => {}, | ||||
|   customUsername: '', | ||||
|   setCustomUsername: () => {}, | ||||
|   connectionTestResults: { | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { joinRoom, Room, BaseRoomConfig, DataPayload } from 'trystero' | ||||
| import { RelayConfig } from 'trystero/torrent' | ||||
| 
 | ||||
| import { sleep } from 'lib/sleep' | ||||
| import { StreamType } from 'models/chat' | ||||
| 
 | ||||
| export enum PeerHookType { | ||||
|   NEW_PEER = 'NEW_PEER', | ||||
| @ -171,12 +172,16 @@ export class PeerRoom { | ||||
|     return this.room.makeAction<T>(namespace) | ||||
|   } | ||||
| 
 | ||||
|   addStream = (...args: Parameters<Room['addStream']>) => { | ||||
|   addStream = ( | ||||
|     stream: Parameters<Room['addStream']>[0], | ||||
|     targetPeers: Parameters<Room['addStream']>[1], | ||||
|     metadata: { type: StreamType } | ||||
|   ) => { | ||||
|     // New streams need to be added as a delayed queue to prevent race
 | ||||
|     // conditions on the receiver's end where streams and their metadata get
 | ||||
|     // mixed up.
 | ||||
|     this.streamQueue.push( | ||||
|       () => Promise.all(this.room.addStream(...args)), | ||||
|       () => Promise.all(this.room.addStream(stream, targetPeers, metadata)), | ||||
|       () => sleep(streamQueueAddDelay) | ||||
|     ) | ||||
| 
 | ||||
|  | ||||
| @ -31,9 +31,10 @@ export enum VideoState { | ||||
|   STOPPED = 'STOPPED', | ||||
| } | ||||
| 
 | ||||
| export enum VideoStreamType { | ||||
| export enum StreamType { | ||||
|   WEBCAM = 'WEBCAM', | ||||
|   SCREEN_SHARE = 'SCREEN_SHARE', | ||||
|   MICROPHONE = 'MICROPHONE', | ||||
| } | ||||
| 
 | ||||
| export enum ScreenShareState { | ||||
| @ -47,12 +48,21 @@ export enum PeerVerificationState { | ||||
|   VERIFIED, | ||||
| } | ||||
| 
 | ||||
| export enum AudioChannelName { | ||||
|   MICROPHONE = 'microphone', | ||||
|   SCREEN_SHARE = 'screen-share', | ||||
| } | ||||
| 
 | ||||
| export type AudioChannel = Partial<Record<AudioChannelName, HTMLAudioElement>> | ||||
| 
 | ||||
| export type PeerAudioChannelState = Record<AudioChannelName, AudioState> | ||||
| 
 | ||||
| export interface Peer { | ||||
|   peerId: string | ||||
|   userId: string | ||||
|   publicKey: CryptoKey | ||||
|   customUsername: string | ||||
|   audioState: AudioState | ||||
|   audioChannelState: PeerAudioChannelState | ||||
|   videoState: VideoState | ||||
|   screenShareState: ScreenShareState | ||||
|   offeredFileId: string | null | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user