* 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:
parent
f6314501a2
commit
75a804abbd
13
package-lock.json
generated
13
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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)',
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
|
58
src/components/Room/RoomScreenShareControls.tsx
Normal file
58
src/components/Room/RoomScreenShareControls.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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 />}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
159
src/components/Room/useRoomScreenShare.ts
Normal file
159
src/components/Room/useRoomScreenShare.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -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) => {
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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: () => {},
|
||||||
})
|
})
|
||||||
|
@ -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: () => {},
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user