feat: [closes #67] Screen sharing (#68)

* feat: [#67] stand up useRoomScreenShare hook
* feat: [#67] stand up RoomScreenShareControls
* feat: [#67] display screen share streams
* fix: [#67] don't flip screen share preview
* feat: don't display screen share controls in unsupported environments
* fix: [#67] always remove media streams for exiting peers
This commit is contained in:
Jeremy Kahn 2022-11-13 17:11:09 -06:00 committed by GitHub
parent f6314501a2
commit 75a804abbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 377 additions and 44 deletions

13
package-lock.json generated
View File

@ -34,7 +34,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sass": "^1.54.3", "sass": "^1.54.3",
"trystero": "^0.11.4", "trystero": "github:jeremyckahn/trystero#bugfix/stream-metadata-type",
"typeface-public-sans": "^1.1.13", "typeface-public-sans": "^1.1.13",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.7.4", "typescript": "^4.7.4",
@ -22977,9 +22977,9 @@
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
}, },
"node_modules/trystero": { "node_modules/trystero": {
"version": "0.11.4", "version": "0.11.6",
"resolved": "https://registry.npmjs.org/trystero/-/trystero-0.11.4.tgz", "resolved": "git+ssh://git@github.com/jeremyckahn/trystero.git#b270de4b79b2fef1ca550187b6c0fbe72d4e8118",
"integrity": "sha512-x5SkPXlodoNhs1o2TLjarABfBLPtuR2iKovk3mtboCCr349eclpGrK3JPjs4Rp+coAMqC+l/ZcC39GCR9VCvlQ==", "license": "MIT",
"dependencies": { "dependencies": {
"firebase": "^9.6.5", "firebase": "^9.6.5",
"ipfs-core": "0.9.0", "ipfs-core": "0.9.0",
@ -41257,9 +41257,8 @@
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
}, },
"trystero": { "trystero": {
"version": "0.11.4", "version": "git+ssh://git@github.com/jeremyckahn/trystero.git#b270de4b79b2fef1ca550187b6c0fbe72d4e8118",
"resolved": "https://registry.npmjs.org/trystero/-/trystero-0.11.4.tgz", "from": "trystero@github:jeremyckahn/trystero#bugfix/stream-metadata-type",
"integrity": "sha512-x5SkPXlodoNhs1o2TLjarABfBLPtuR2iKovk3mtboCCr349eclpGrK3JPjs4Rp+coAMqC+l/ZcC39GCR9VCvlQ==",
"requires": { "requires": {
"firebase": "^9.6.5", "firebase": "^9.6.5",
"ipfs-core": "0.9.0", "ipfs-core": "0.9.0",

View File

@ -30,7 +30,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sass": "^1.54.3", "sass": "^1.54.3",
"trystero": "^0.11.4", "trystero": "github:jeremyckahn/trystero#bugfix/stream-metadata-type",
"typeface-public-sans": "^1.1.13", "typeface-public-sans": "^1.1.13",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.7.4", "typescript": "^4.7.4",

View File

@ -4,8 +4,8 @@ import Paper from '@mui/material/Paper'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
interface PeerVideoProps { interface PeerVideoProps {
isSelf?: boolean isSelfVideo?: boolean
numberOfPeers: number numberOfVideos: number
userId: string userId: string
videoStream: MediaStream videoStream: MediaStream
} }
@ -18,8 +18,8 @@ const nextPerfectSquare = (base: number) => {
} }
export const PeerVideo = ({ export const PeerVideo = ({
isSelf, isSelfVideo,
numberOfPeers, numberOfVideos,
userId, userId,
videoStream, videoStream,
}: PeerVideoProps) => { }: PeerVideoProps) => {
@ -33,7 +33,7 @@ export const PeerVideo = ({
video.srcObject = videoStream video.srcObject = videoStream
}, [videoRef, videoStream]) }, [videoRef, videoStream])
const sizePercent = 100 / Math.sqrt(nextPerfectSquare(numberOfPeers - 1)) const sizePercent = 100 / Math.sqrt(nextPerfectSquare(numberOfVideos - 1))
return ( return (
<Paper <Paper
@ -59,7 +59,7 @@ export const PeerVideo = ({
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
height: '100%', height: '100%',
...(isSelf && { ...(isSelfVideo && {
transform: 'rotateY(180deg)', transform: 'rotateY(180deg)',
}), }),
}} }}

View File

@ -15,6 +15,7 @@ import { ChatTranscript } from 'components/ChatTranscript'
import { useRoom } from './useRoom' import { useRoom } from './useRoom'
import { RoomAudioControls } from './RoomAudioControls' import { RoomAudioControls } from './RoomAudioControls'
import { RoomVideoControls } from './RoomVideoControls' import { RoomVideoControls } from './RoomVideoControls'
import { RoomScreenShareControls } from './RoomScreenShareControls'
import { RoomVideoDisplay } from './RoomVideoDisplay' import { RoomVideoDisplay } from './RoomVideoDisplay'
export interface RoomProps { export interface RoomProps {
@ -93,6 +94,7 @@ export function Room({
> >
<RoomAudioControls peerRoom={peerRoom} /> <RoomAudioControls peerRoom={peerRoom} />
<RoomVideoControls peerRoom={peerRoom} /> <RoomVideoControls peerRoom={peerRoom} />
<RoomScreenShareControls peerRoom={peerRoom} />
</Box> </Box>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>

View File

@ -0,0 +1,58 @@
import Box from '@mui/material/Box'
import ScreenShare from '@mui/icons-material/ScreenShare'
import StopScreenShare from '@mui/icons-material/StopScreenShare'
import Fab from '@mui/material/Fab'
import Tooltip from '@mui/material/Tooltip'
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { useRoomScreenShare } from './useRoomScreenShare'
export interface RoomVideoControlsProps {
peerRoom: PeerRoom
}
export function RoomScreenShareControls({ peerRoom }: RoomVideoControlsProps) {
const { isSharingScreen, handleScreenShareStart, handleScreenShareStop } =
useRoomScreenShare({
peerRoom,
})
const handleToggleScreenShareButtonClick = () => {
if (isSharingScreen) {
handleScreenShareStop()
} else {
handleScreenShareStart()
}
}
if (!window.navigator?.mediaDevices?.getDisplayMedia) {
return <></>
}
return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 1,
}}
>
<Tooltip
title={
isSharingScreen ? 'Stop sharing screen' : 'Share screen with room'
}
>
<Fab
color={isSharingScreen ? 'error' : 'success'}
aria-label="share screen"
onClick={handleToggleScreenShareButtonClick}
>
{isSharingScreen ? <StopScreenShare /> : <ScreenShare />}
</Fab>
</Tooltip>
</Box>
)
}

View File

@ -66,7 +66,7 @@ export function RoomVideoControls({ peerRoom }: RoomVideoControlsProps) {
<Tooltip title={isCameraEnabled ? 'Turn off camera' : 'Turn on camera'}> <Tooltip title={isCameraEnabled ? 'Turn off camera' : 'Turn on camera'}>
<Fab <Fab
color={isCameraEnabled ? 'error' : 'success'} color={isCameraEnabled ? 'error' : 'success'}
aria-label="call" aria-label="toggle camera"
onClick={handleEnableCameraClick} onClick={handleEnableCameraClick}
> >
{isCameraEnabled ? <VideocamOff /> : <Videocam />} {isCameraEnabled ? <VideocamOff /> : <Videocam />}

View File

@ -1,4 +1,4 @@
import { useContext } from 'react' import { Fragment, useContext } from 'react'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
@ -7,7 +7,11 @@ import { Peer } from 'models/chat'
import { PeerVideo } from './PeerVideo' import { PeerVideo } from './PeerVideo'
type PeerWithVideo = { peer: Peer; videoStream: MediaStream } type PeerWithVideo = {
peer: Peer
videoStream?: MediaStream
screenStream?: MediaStream
}
export interface RoomVideoDisplayProps { export interface RoomVideoDisplayProps {
userId: string userId: string
@ -18,15 +22,23 @@ export const RoomVideoDisplay = ({ userId }: RoomVideoDisplayProps) => {
const roomContext = useContext(RoomContext) const roomContext = useContext(RoomContext)
const { peerList } = shellContext const { peerList } = shellContext
const { peerVideoStreams, selfVideoStream } = roomContext const {
peerVideoStreams,
selfVideoStream,
peerScreenStreams,
selfScreenStream,
} = roomContext
const peersWithVideo: PeerWithVideo[] = peerList.reduce( const peersWithVideo: PeerWithVideo[] = peerList.reduce(
(acc: PeerWithVideo[], peer: Peer) => { (acc: PeerWithVideo[], peer: Peer) => {
const videoStream = peerVideoStreams[peer.peerId] const videoStream = peerVideoStreams[peer.peerId]
if (videoStream) { const screenStream = peerScreenStreams[peer.peerId]
if (videoStream || screenStream) {
acc.push({ acc.push({
peer, peer,
videoStream, videoStream,
screenStream,
}) })
} }
@ -35,7 +47,15 @@ export const RoomVideoDisplay = ({ userId }: RoomVideoDisplayProps) => {
[] []
) )
const numberOfPeers = (selfVideoStream ? 1 : 0) + peersWithVideo.length const numberOfVideos =
(selfVideoStream ? 1 : 0) +
(selfScreenStream ? 1 : 0) +
peersWithVideo.reduce((sum, peerWithVideo) => {
if (peerWithVideo.videoStream) sum++
if (peerWithVideo.screenStream) sum++
return sum
}, 0)
return ( return (
<Paper <Paper
@ -46,7 +66,7 @@ export const RoomVideoDisplay = ({ userId }: RoomVideoDisplayProps) => {
alignContent: 'center', alignContent: 'center',
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: numberOfPeers === 1 ? 'column' : 'row', flexDirection: numberOfVideos === 1 ? 'column' : 'row',
flexGrow: 1, flexGrow: 1,
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'center', justifyContent: 'center',
@ -56,19 +76,36 @@ export const RoomVideoDisplay = ({ userId }: RoomVideoDisplayProps) => {
> >
{selfVideoStream && ( {selfVideoStream && (
<PeerVideo <PeerVideo
isSelf isSelfVideo
numberOfPeers={numberOfPeers} numberOfVideos={numberOfVideos}
userId={userId} userId={userId}
videoStream={selfVideoStream} videoStream={selfVideoStream}
/> />
)} )}
{peersWithVideo.map(peerWithVideo => ( {selfScreenStream && (
<PeerVideo <PeerVideo
key={peerWithVideo.peer.peerId} numberOfVideos={numberOfVideos}
numberOfPeers={numberOfPeers} userId={userId}
userId={peerWithVideo.peer.userId} videoStream={selfScreenStream}
videoStream={peerWithVideo.videoStream}
/> />
)}
{peersWithVideo.map(peerWithVideo => (
<Fragment key={peerWithVideo.peer.peerId}>
{peerWithVideo.videoStream && (
<PeerVideo
numberOfVideos={numberOfVideos}
userId={peerWithVideo.peer.userId}
videoStream={peerWithVideo.videoStream}
/>
)}
{peerWithVideo.screenStream && (
<PeerVideo
numberOfVideos={numberOfVideos}
userId={peerWithVideo.peer.userId}
videoStream={peerWithVideo.screenStream}
/>
)}
</Fragment>
))} ))}
</Paper> </Paper>
) )

View File

@ -13,6 +13,7 @@ import {
ReceivedMessage, ReceivedMessage,
UnsentMessage, UnsentMessage,
VideoState, VideoState,
ScreenShareState,
isMessageReceived, isMessageReceived,
} from 'models/chat' } from 'models/chat'
import { getPeerName } from 'components/PeerNameDisplay' import { getPeerName } from 'components/PeerNameDisplay'
@ -62,14 +63,34 @@ export function useRoom(
Record<string, MediaStream> Record<string, MediaStream>
>({}) >({})
const [selfScreenStream, setSelfScreenStream] = useState<MediaStream | null>(
null
)
const [peerScreenStreams, setPeerScreenStreams] = useState<
Record<string, MediaStream>
>({})
const roomContextValue = useMemo( const roomContextValue = useMemo(
() => ({ () => ({
selfVideoStream, selfVideoStream,
setSelfVideoStream, setSelfVideoStream,
peerVideoStreams, peerVideoStreams,
setPeerVideoStreams, setPeerVideoStreams,
selfScreenStream,
setSelfScreenStream,
peerScreenStreams,
setPeerScreenStreams,
}), }),
[selfVideoStream, setSelfVideoStream, peerVideoStreams, setPeerVideoStreams] [
selfVideoStream,
setSelfVideoStream,
peerVideoStreams,
setPeerVideoStreams,
selfScreenStream,
setSelfScreenStream,
peerScreenStreams,
setPeerScreenStreams,
]
) )
useEffect(() => { useEffect(() => {
@ -131,6 +152,7 @@ export function useRoom(
userId, userId,
audioState: AudioState.STOPPED, audioState: AudioState.STOPPED,
videoState: VideoState.STOPPED, videoState: VideoState.STOPPED,
screenShareState: ScreenShareState.NOT_SHARING,
}, },
]) ])
} else { } else {
@ -216,8 +238,11 @@ export function useRoom(
} }
}) })
const showVideoDisplay = const showVideoDisplay = Boolean(
selfVideoStream || Object.values(peerVideoStreams).length > 0 selfVideoStream ||
selfScreenStream ||
Object.values({ ...peerVideoStreams, ...peerScreenStreams }).length > 0
)
return { return {
isMessageSending, isMessageSending,

View File

@ -0,0 +1,159 @@
import { useContext, useEffect, useCallback, useState } from 'react'
import { isRecord } from 'utils'
import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network'
import { ScreenShareState, Peer, VideoStreamType } from 'models/chat'
import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
import { usePeerRoomAction } from './usePeerRoomAction'
interface UseRoomScreenShareConfig {
peerRoom: PeerRoom
}
export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
const shellContext = useContext(ShellContext)
const roomContext = useContext(RoomContext)
const [isSharingScreen, setIsSharingScreen] = useState(false)
const { peerList, setPeerList, setScreenState } = shellContext
const {
peerScreenStreams,
selfScreenStream,
setPeerScreenStreams,
setSelfScreenStream,
} = roomContext
const [sendScreenShare, receiveScreenShare] =
usePeerRoomAction<ScreenShareState>(peerRoom, PeerActions.SCREEN_SHARE)
receiveScreenShare((screenState, peerId) => {
const newPeerList = peerList.map(peer => {
const newPeer: Peer = { ...peer }
if (peer.peerId === peerId) {
newPeer.screenShareState = screenState
if (screenState === ScreenShareState.NOT_SHARING) {
deletePeerScreen(peerId)
}
}
return newPeer
})
setPeerList(newPeerList)
})
peerRoom.onPeerStream(PeerStreamType.SCREEN, (stream, peerId, metadata) => {
const isScreenShareStream =
isRecord(metadata) &&
'type' in metadata &&
metadata.type === VideoStreamType.SCREEN_SHARE
if (!isScreenShareStream) return
setPeerScreenStreams({
...peerScreenStreams,
[peerId]: stream,
})
})
const cleanupScreenStream = useCallback(() => {
if (!selfScreenStream) return
for (const screenStreamTrack of selfScreenStream.getTracks()) {
screenStreamTrack.stop()
selfScreenStream.removeTrack(screenStreamTrack)
}
}, [selfScreenStream])
const handleScreenShareStart = async () => {
if (selfScreenStream) return
const displayMedia = await window.navigator.mediaDevices.getDisplayMedia({
audio: true,
video: true,
})
peerRoom.addStream(displayMedia, null, {
type: VideoStreamType.SCREEN_SHARE,
})
setSelfScreenStream(displayMedia)
sendScreenShare(ScreenShareState.SHARING)
setScreenState(ScreenShareState.SHARING)
setIsSharingScreen(true)
}
const handleScreenShareStop = () => {
if (!selfScreenStream) return
cleanupScreenStream()
peerRoom.removeStream(selfScreenStream, peerRoom.getPeers())
sendScreenShare(ScreenShareState.NOT_SHARING)
setScreenState(ScreenShareState.NOT_SHARING)
setSelfScreenStream(null)
setIsSharingScreen(false)
}
useEffect(() => {
return () => {
cleanupScreenStream()
}
}, [cleanupScreenStream])
useEffect(() => {
return () => {
if (selfScreenStream) {
setSelfScreenStream(null)
setScreenState(ScreenShareState.NOT_SHARING)
}
}
}, [selfScreenStream, setSelfScreenStream, setScreenState])
useEffect(() => {
return () => {
setPeerScreenStreams({})
}
}, [setPeerScreenStreams])
const deletePeerScreen = (peerId: string) => {
const newPeerScreens = { ...peerScreenStreams }
delete newPeerScreens[peerId]
setPeerScreenStreams(newPeerScreens)
}
const handleScreenForNewPeer = (peerId: string) => {
if (selfScreenStream) {
peerRoom.addStream(selfScreenStream, peerId, {
type: VideoStreamType.SCREEN_SHARE,
})
}
}
const handleScreenForLeavingPeer = (peerId: string) => {
if (selfScreenStream) {
peerRoom.removeStream(selfScreenStream, peerId)
}
deletePeerScreen(peerId)
}
peerRoom.onPeerJoin(PeerHookType.SCREEN, (peerId: string) => {
handleScreenForNewPeer(peerId)
})
peerRoom.onPeerLeave(PeerHookType.SCREEN, (peerId: string) => {
handleScreenForLeavingPeer(peerId)
})
return {
handleScreenShareStart,
handleScreenShareStop,
isSharingScreen,
setIsSharingScreen,
}
}

View File

@ -3,9 +3,11 @@ import { useContext, useEffect, useCallback, useState } from 'react'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { VideoState, Peer } from 'models/chat' import { VideoState, Peer, VideoStreamType } from 'models/chat'
import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom' import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
import { isRecord } from 'utils'
import { usePeerRoomAction } from './usePeerRoomAction' import { usePeerRoomAction } from './usePeerRoomAction'
interface UseRoomVideoConfig { interface UseRoomVideoConfig {
@ -60,7 +62,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
}, },
}) })
peerRoom.addStream(newSelfStream) peerRoom.addStream(newSelfStream, null, {
type: VideoStreamType.WEBCAM,
})
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
@ -91,10 +95,13 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
setPeerList(newPeerList) setPeerList(newPeerList)
}) })
peerRoom.onPeerStream(PeerStreamType.VIDEO, (stream, peerId) => { peerRoom.onPeerStream(PeerStreamType.VIDEO, (stream, peerId, metadata) => {
const videoTracks = stream.getVideoTracks() const isWebcamStream =
isRecord(metadata) &&
'type' in metadata &&
metadata.type === VideoStreamType.WEBCAM
if (videoTracks.length === 0) return if (!isWebcamStream) return
setPeerVideoStreams({ setPeerVideoStreams({
...peerVideoStreams, ...peerVideoStreams,
@ -122,7 +129,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
: true, : true,
}) })
peerRoom.addStream(newSelfStream) peerRoom.addStream(newSelfStream, null, {
type: VideoStreamType.WEBCAM,
})
sendVideoChange(VideoState.PLAYING) sendVideoChange(VideoState.PLAYING)
setVideoState(VideoState.PLAYING) setVideoState(VideoState.PLAYING)
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
@ -191,7 +200,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
}, },
}) })
peerRoom.addStream(newSelfStream) peerRoom.addStream(newSelfStream, null, { type: VideoStreamType.WEBCAM })
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
} }
@ -203,15 +212,18 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
const handleVideoForNewPeer = (peerId: string) => { const handleVideoForNewPeer = (peerId: string) => {
if (selfVideoStream) { if (selfVideoStream) {
peerRoom.addStream(selfVideoStream, peerId) peerRoom.addStream(selfVideoStream, peerId, {
type: VideoStreamType.WEBCAM,
})
} }
} }
const handleVideoForLeavingPeer = (peerId: string) => { const handleVideoForLeavingPeer = (peerId: string) => {
if (selfVideoStream) { if (selfVideoStream) {
peerRoom.removeStream(selfVideoStream, peerId) peerRoom.removeStream(selfVideoStream, peerId)
deletePeerVideo(peerId)
} }
deletePeerVideo(peerId)
} }
peerRoom.onPeerJoin(PeerHookType.VIDEO, (peerId: string) => { peerRoom.onPeerJoin(PeerHookType.VIDEO, (peerId: string) => {

View File

@ -15,7 +15,7 @@ import { AlertColor } from '@mui/material/Alert'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { AudioState, VideoState, Peer } from 'models/chat' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { Drawer } from './Drawer' import { Drawer } from './Drawer'
@ -46,6 +46,9 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [tabHasFocus, setTabHasFocus] = useState(true) const [tabHasFocus, setTabHasFocus] = useState(true)
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED) const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED) const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
const [screenState, setScreenState] = useState<ScreenShareState>(
ScreenShareState.NOT_SHARING
)
const showAlert = useCallback< const showAlert = useCallback<
(message: string, options?: AlertOptions) => void (message: string, options?: AlertOptions) => void
@ -72,6 +75,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setAudioState, setAudioState,
videoState, videoState,
setVideoState, setVideoState,
screenState,
setScreenState,
}), }),
[ [
isPeerListOpen, isPeerListOpen,
@ -87,6 +92,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setAudioState, setAudioState,
videoState, videoState,
setVideoState, setVideoState,
screenState,
setScreenState,
] ]
) )

View File

@ -5,6 +5,10 @@ interface RoomContextProps {
setSelfVideoStream: Dispatch<SetStateAction<MediaStream | null>> setSelfVideoStream: Dispatch<SetStateAction<MediaStream | null>>
peerVideoStreams: Record<string, MediaStream> peerVideoStreams: Record<string, MediaStream>
setPeerVideoStreams: Dispatch<SetStateAction<Record<string, MediaStream>>> setPeerVideoStreams: Dispatch<SetStateAction<Record<string, MediaStream>>>
selfScreenStream: MediaStream | null
setSelfScreenStream: Dispatch<SetStateAction<MediaStream | null>>
peerScreenStreams: Record<string, MediaStream>
setPeerScreenStreams: Dispatch<SetStateAction<Record<string, MediaStream>>>
} }
export const RoomContext = createContext<RoomContextProps>({ export const RoomContext = createContext<RoomContextProps>({
@ -12,4 +16,8 @@ export const RoomContext = createContext<RoomContextProps>({
setSelfVideoStream: () => {}, setSelfVideoStream: () => {},
peerVideoStreams: {}, peerVideoStreams: {},
setPeerVideoStreams: () => {}, setPeerVideoStreams: () => {},
selfScreenStream: null,
setSelfScreenStream: () => {},
peerScreenStreams: {},
setPeerScreenStreams: () => {},
}) })

View File

@ -1,7 +1,7 @@
import { createContext, Dispatch, SetStateAction } from 'react' import { createContext, Dispatch, SetStateAction } from 'react'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { AudioState, VideoState, Peer } from 'models/chat' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
interface ShellContextProps { interface ShellContextProps {
numberOfPeers: number numberOfPeers: number
@ -18,6 +18,8 @@ interface ShellContextProps {
setAudioState: Dispatch<SetStateAction<AudioState>> setAudioState: Dispatch<SetStateAction<AudioState>>
videoState: VideoState videoState: VideoState
setVideoState: Dispatch<SetStateAction<VideoState>> setVideoState: Dispatch<SetStateAction<VideoState>>
screenState: ScreenShareState
setScreenState: Dispatch<SetStateAction<ScreenShareState>>
} }
export const ShellContext = createContext<ShellContextProps>({ export const ShellContext = createContext<ShellContextProps>({
@ -35,4 +37,6 @@ export const ShellContext = createContext<ShellContextProps>({
setAudioState: () => {}, setAudioState: () => {},
videoState: VideoState.STOPPED, videoState: VideoState.STOPPED,
setVideoState: () => {}, setVideoState: () => {},
screenState: ScreenShareState.NOT_SHARING,
setScreenState: () => {},
}) })

View File

@ -15,11 +15,22 @@ export enum VideoState {
STOPPED = 'STOPPED', STOPPED = 'STOPPED',
} }
export enum VideoStreamType {
WEBCAM = 'WEBCAM',
SCREEN_SHARE = 'SCREEN_SHARE',
}
export enum ScreenShareState {
SHARING = 'SHARING',
NOT_SHARING = 'NOT_SHARING',
}
export interface Peer { export interface Peer {
peerId: string peerId: string
userId: string userId: string
audioState: AudioState audioState: AudioState
videoState: VideoState videoState: VideoState
screenShareState: ScreenShareState
} }
export interface ReceivedMessage extends UnsentMessage { export interface ReceivedMessage extends UnsentMessage {

View File

@ -5,4 +5,5 @@ export enum PeerActions {
PEER_NAME = 'PEER_NAME', PEER_NAME = 'PEER_NAME',
AUDIO_CHANGE = 'AUDIO_CHANGE', AUDIO_CHANGE = 'AUDIO_CHANGE',
VIDEO_CHANGE = 'VIDEO_CHANGE', VIDEO_CHANGE = 'VIDEO_CHANGE',
SCREEN_SHARE = 'SCREEN_SHARE',
} }

View File

@ -5,11 +5,13 @@ export enum PeerHookType {
NEW_PEER = 'NEW_PEER', NEW_PEER = 'NEW_PEER',
AUDIO = 'AUDIO', AUDIO = 'AUDIO',
VIDEO = 'VIDEO', VIDEO = 'VIDEO',
SCREEN = 'SCREEN',
} }
export enum PeerStreamType { export enum PeerStreamType {
AUDIO = 'AUDIO', AUDIO = 'AUDIO',
VIDEO = 'VIDEO', VIDEO = 'VIDEO',
SCREEN = 'SCREEN',
} }
export class PeerRoom { export class PeerRoom {
@ -107,8 +109,8 @@ export class PeerRoom {
return this.room.makeAction<T>(namespace) return this.room.makeAction<T>(namespace)
} }
addStream: Room['addStream'] = stream => { addStream: Room['addStream'] = (...args) => {
return this.room.addStream(stream) return this.room.addStream(...args)
} }
removeStream: Room['removeStream'] = (stream, targetPeers) => { removeStream: Room['removeStream'] = (stream, targetPeers) => {

View File

@ -2,3 +2,11 @@ export const sleep = (milliseconds: number): Promise<void> =>
new Promise<void>(res => { new Promise<void>(res => {
setTimeout(res, milliseconds) setTimeout(res, milliseconds)
}) })
export const isRecord = (variable: any): variable is Record<string, any> => {
return (
typeof variable === 'object' &&
!Array.isArray(variable) &&
variable !== null
)
}