@tellescope/react-components 1.246.2 → 1.247.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/TwilioVideo/TwilioControls.d.ts +2 -0
- package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioControls.js +14 -3
- package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts +2 -0
- package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioParticipant.js +52 -21
- package/lib/cjs/TwilioVideo/TwilioParticipant.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +5 -0
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js +107 -5
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts +2 -0
- package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoRoom.js +49 -3
- package/lib/cjs/TwilioVideo/TwilioVideoRoom.js.map +1 -1
- package/lib/cjs/TwilioVideo/index.d.ts +1 -1
- package/lib/cjs/TwilioVideo/index.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/index.js +2 -1
- package/lib/cjs/TwilioVideo/index.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.d.ts +2 -0
- package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.js +15 -4
- package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioParticipant.d.ts +2 -0
- package/lib/esm/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioParticipant.js +52 -21
- package/lib/esm/TwilioVideo/TwilioParticipant.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +5 -0
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.js +83 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts +2 -0
- package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoRoom.js +49 -3
- package/lib/esm/TwilioVideo/TwilioVideoRoom.js.map +1 -1
- package/lib/esm/TwilioVideo/index.d.ts +1 -1
- package/lib/esm/TwilioVideo/index.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/index.js +1 -1
- package/lib/esm/TwilioVideo/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/TwilioVideo/TwilioControls.tsx +30 -0
- package/src/TwilioVideo/TwilioParticipant.tsx +51 -17
- package/src/TwilioVideo/TwilioVideoContext.tsx +78 -0
- package/src/TwilioVideo/TwilioVideoRoom.tsx +92 -2
- package/src/TwilioVideo/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.247.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/cjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"@reduxjs/toolkit": "1.9.0",
|
|
52
52
|
"@stripe/react-stripe-js": "2.9.0",
|
|
53
53
|
"@stripe/stripe-js": "1.52.1",
|
|
54
|
-
"@tellescope/constants": "1.
|
|
55
|
-
"@tellescope/sdk": "1.
|
|
56
|
-
"@tellescope/types-client": "1.
|
|
57
|
-
"@tellescope/types-models": "1.
|
|
58
|
-
"@tellescope/types-utilities": "1.
|
|
59
|
-
"@tellescope/utilities": "1.
|
|
60
|
-
"@tellescope/validation": "1.
|
|
54
|
+
"@tellescope/constants": "1.247.0",
|
|
55
|
+
"@tellescope/sdk": "1.247.0",
|
|
56
|
+
"@tellescope/types-client": "1.247.0",
|
|
57
|
+
"@tellescope/types-models": "1.247.0",
|
|
58
|
+
"@tellescope/types-utilities": "1.247.0",
|
|
59
|
+
"@tellescope/utilities": "1.247.0",
|
|
60
|
+
"@tellescope/validation": "1.247.0",
|
|
61
61
|
"css-to-react-native": "3.0.0",
|
|
62
62
|
"draft-js": "0.11.7",
|
|
63
63
|
"draftjs-to-html": "0.9.1",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
85
85
|
"react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "408421d578d9c63493ff94464da6e4317aa3af20",
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
}
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
Videocam as VideocamIcon,
|
|
7
7
|
VideocamOff as VideocamOffIcon,
|
|
8
8
|
CallEnd as CallEndIcon,
|
|
9
|
+
ScreenShare as ScreenShareIcon,
|
|
10
|
+
StopScreenShare as StopScreenShareIcon,
|
|
9
11
|
} from '@mui/icons-material'
|
|
10
12
|
import { useTwilioVideo } from './TwilioVideoContext'
|
|
11
13
|
|
|
@@ -13,20 +15,26 @@ export interface TwilioControlBarProps {
|
|
|
13
15
|
onLeave?: () => void
|
|
14
16
|
onEndForAll?: () => void
|
|
15
17
|
style?: React.CSSProperties
|
|
18
|
+
/** Whether to show the screen share button. Defaults to true. */
|
|
19
|
+
showScreenShare?: boolean
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
19
23
|
onLeave,
|
|
20
24
|
onEndForAll,
|
|
21
25
|
style,
|
|
26
|
+
showScreenShare: showScreenShareProp = true,
|
|
22
27
|
}) => {
|
|
23
28
|
const {
|
|
24
29
|
isVideoEnabled,
|
|
25
30
|
isAudioEnabled,
|
|
31
|
+
isScreenSharing,
|
|
26
32
|
toggleVideo,
|
|
27
33
|
toggleAudio,
|
|
34
|
+
toggleScreenShare,
|
|
28
35
|
disconnect,
|
|
29
36
|
isHost,
|
|
37
|
+
room,
|
|
30
38
|
} = useTwilioVideo()
|
|
31
39
|
|
|
32
40
|
const handleLeave = () => {
|
|
@@ -39,6 +47,10 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
39
47
|
disconnect()
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
const supportsScreenShare = typeof navigator !== 'undefined'
|
|
51
|
+
&& navigator.mediaDevices
|
|
52
|
+
&& typeof navigator.mediaDevices.getDisplayMedia === 'function'
|
|
53
|
+
|
|
42
54
|
return (
|
|
43
55
|
<Box
|
|
44
56
|
sx={{
|
|
@@ -75,6 +87,24 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
75
87
|
{isVideoEnabled ? <VideocamIcon /> : <VideocamOffIcon />}
|
|
76
88
|
</IconButton>
|
|
77
89
|
|
|
90
|
+
{showScreenShareProp && supportsScreenShare && (
|
|
91
|
+
<IconButton
|
|
92
|
+
onClick={toggleScreenShare}
|
|
93
|
+
disabled={!room}
|
|
94
|
+
sx={{
|
|
95
|
+
color: isScreenSharing ? '#4caf50' : 'white',
|
|
96
|
+
'&:hover': {
|
|
97
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
98
|
+
},
|
|
99
|
+
'&.Mui-disabled': {
|
|
100
|
+
color: 'rgba(255,255,255,0.3)',
|
|
101
|
+
},
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{isScreenSharing ? <StopScreenShareIcon /> : <ScreenShareIcon />}
|
|
105
|
+
</IconButton>
|
|
106
|
+
)}
|
|
107
|
+
|
|
78
108
|
<IconButton
|
|
79
109
|
onClick={handleLeave}
|
|
80
110
|
sx={{
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
LocalTrackPublication,
|
|
9
9
|
} from 'twilio-video'
|
|
10
10
|
import { Box, Typography } from '@mui/material'
|
|
11
|
+
import { SCREEN_SHARE_TRACK_NAME } from './TwilioVideoContext'
|
|
11
12
|
|
|
12
13
|
export interface TwilioParticipantProps {
|
|
13
14
|
participant: RemoteParticipant | LocalParticipant
|
|
@@ -15,6 +16,8 @@ export interface TwilioParticipantProps {
|
|
|
15
16
|
style?: React.CSSProperties
|
|
16
17
|
/** Resolve participant identity to a display label. Defaults to empty string. */
|
|
17
18
|
resolveIdentity?: (identity: string) => string
|
|
19
|
+
/** When true, render the screen share track instead of the camera track */
|
|
20
|
+
showScreenShare?: boolean
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
@@ -22,16 +25,22 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
22
25
|
isLocal = false,
|
|
23
26
|
style,
|
|
24
27
|
resolveIdentity = () => '',
|
|
28
|
+
showScreenShare = false,
|
|
25
29
|
}) => {
|
|
26
30
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
27
31
|
const audioRef = useRef<HTMLAudioElement>(null)
|
|
28
|
-
const [
|
|
32
|
+
const [cameraTrack, setCameraTrack] = useState<VideoTrack | null>(null)
|
|
33
|
+
const [screenTrack, setScreenTrack] = useState<VideoTrack | null>(null)
|
|
29
34
|
const [audioTrack, setAudioTrack] = useState<AudioTrack | null>(null)
|
|
30
35
|
|
|
31
36
|
useEffect(() => {
|
|
32
37
|
const handleTrackSubscribed = (track: VideoTrack | AudioTrack) => {
|
|
33
38
|
if (track.kind === 'video') {
|
|
34
|
-
|
|
39
|
+
if (track.name === SCREEN_SHARE_TRACK_NAME) {
|
|
40
|
+
setScreenTrack(track as VideoTrack)
|
|
41
|
+
} else {
|
|
42
|
+
setCameraTrack(track as VideoTrack)
|
|
43
|
+
}
|
|
35
44
|
} else if (track.kind === 'audio') {
|
|
36
45
|
setAudioTrack(track as AudioTrack)
|
|
37
46
|
}
|
|
@@ -39,7 +48,11 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
39
48
|
|
|
40
49
|
const handleTrackUnsubscribed = (track: VideoTrack | AudioTrack) => {
|
|
41
50
|
if (track.kind === 'video') {
|
|
42
|
-
|
|
51
|
+
if (track.name === SCREEN_SHARE_TRACK_NAME) {
|
|
52
|
+
setScreenTrack(null)
|
|
53
|
+
} else {
|
|
54
|
+
setCameraTrack(null)
|
|
55
|
+
}
|
|
43
56
|
} else if (track.kind === 'audio') {
|
|
44
57
|
setAudioTrack(null)
|
|
45
58
|
}
|
|
@@ -52,40 +65,61 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
52
65
|
}
|
|
53
66
|
})
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
if (isLocal) {
|
|
69
|
+
// For local participants, listen for track publications (trackSubscribed doesn't fire for local)
|
|
70
|
+
const handleTrackPublished = (publication: LocalTrackPublication) => {
|
|
71
|
+
if (publication.track) {
|
|
72
|
+
handleTrackSubscribed(publication.track as VideoTrack | AudioTrack)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const handleTrackStopped = (track: VideoTrack | AudioTrack) => {
|
|
76
|
+
handleTrackUnsubscribed(track)
|
|
77
|
+
}
|
|
78
|
+
participant.on('trackPublished', handleTrackPublished)
|
|
79
|
+
participant.on('trackStopped', handleTrackStopped)
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
participant.off('trackPublished', handleTrackPublished)
|
|
83
|
+
participant.off('trackStopped', handleTrackStopped)
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// For remote participants, listen for track subscriptions
|
|
57
87
|
participant.on('trackSubscribed', handleTrackSubscribed)
|
|
58
88
|
participant.on('trackUnsubscribed', handleTrackUnsubscribed)
|
|
59
|
-
}
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
if ('off' in participant) {
|
|
90
|
+
return () => {
|
|
63
91
|
participant.off('trackSubscribed', handleTrackSubscribed)
|
|
64
92
|
participant.off('trackUnsubscribed', handleTrackUnsubscribed)
|
|
65
93
|
}
|
|
66
94
|
}
|
|
67
|
-
}, [participant])
|
|
95
|
+
}, [participant, isLocal])
|
|
96
|
+
|
|
97
|
+
const activeVideoTrack = showScreenShare ? screenTrack : cameraTrack
|
|
68
98
|
|
|
69
99
|
// Attach video track
|
|
70
100
|
useEffect(() => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
101
|
+
if (activeVideoTrack && videoRef.current) {
|
|
102
|
+
const el = videoRef.current
|
|
103
|
+
activeVideoTrack.attach(el)
|
|
73
104
|
return () => {
|
|
74
|
-
|
|
105
|
+
activeVideoTrack.detach(el)
|
|
75
106
|
}
|
|
76
107
|
}
|
|
77
|
-
}, [
|
|
108
|
+
}, [activeVideoTrack])
|
|
78
109
|
|
|
79
110
|
// Attach audio track (not for local participant to avoid echo)
|
|
80
111
|
useEffect(() => {
|
|
81
112
|
if (audioTrack && audioRef.current && !isLocal) {
|
|
82
|
-
|
|
113
|
+
const el = audioRef.current
|
|
114
|
+
audioTrack.attach(el)
|
|
83
115
|
return () => {
|
|
84
|
-
audioTrack.detach()
|
|
116
|
+
audioTrack.detach(el)
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
}, [audioTrack, isLocal])
|
|
88
120
|
|
|
121
|
+
const shouldMirror = isLocal && !showScreenShare
|
|
122
|
+
|
|
89
123
|
return (
|
|
90
124
|
<Box
|
|
91
125
|
sx={{
|
|
@@ -106,8 +140,8 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
106
140
|
style={{
|
|
107
141
|
width: '100%',
|
|
108
142
|
height: '100%',
|
|
109
|
-
objectFit: 'cover',
|
|
110
|
-
transform:
|
|
143
|
+
objectFit: showScreenShare ? 'contain' : 'cover',
|
|
144
|
+
transform: shouldMirror ? 'scaleX(-1)' : 'none',
|
|
111
145
|
}}
|
|
112
146
|
/>
|
|
113
147
|
{!isLocal && <audio ref={audioRef} autoPlay />}
|
|
@@ -9,12 +9,17 @@ import Video, {
|
|
|
9
9
|
LocalParticipant,
|
|
10
10
|
} from 'twilio-video'
|
|
11
11
|
|
|
12
|
+
export const SCREEN_SHARE_TRACK_NAME = 'screen-share'
|
|
13
|
+
|
|
12
14
|
export interface TwilioVideoState {
|
|
13
15
|
room: Room | null
|
|
14
16
|
isConnecting: boolean
|
|
15
17
|
isConnected: boolean
|
|
16
18
|
localVideoTrack: LocalVideoTrack | null
|
|
17
19
|
localAudioTrack: LocalAudioTrack | null
|
|
20
|
+
localScreenTrack: LocalVideoTrack | null
|
|
21
|
+
isScreenSharing: boolean
|
|
22
|
+
screenSharingParticipantSid: string | null
|
|
18
23
|
participants: RemoteParticipant[]
|
|
19
24
|
error: Error | null
|
|
20
25
|
isHost: boolean
|
|
@@ -27,6 +32,7 @@ export interface TwilioVideoActions {
|
|
|
27
32
|
disconnect: () => void
|
|
28
33
|
toggleVideo: () => Promise<void>
|
|
29
34
|
toggleAudio: () => void
|
|
35
|
+
toggleScreenShare: () => Promise<void>
|
|
30
36
|
setIsHost: (isHost: boolean) => void
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -56,6 +62,9 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
56
62
|
const [isHost, setIsHost] = useState(false)
|
|
57
63
|
const [isVideoEnabled, setIsVideoEnabled] = useState(true)
|
|
58
64
|
const [isAudioEnabled, setIsAudioEnabled] = useState(true)
|
|
65
|
+
const [localScreenTrack, setLocalScreenTrack] = useState<LocalVideoTrack | null>(null)
|
|
66
|
+
const [isScreenSharing, setIsScreenSharing] = useState(false)
|
|
67
|
+
const [screenSharingParticipantSid, setScreenSharingParticipantSid] = useState<string | null>(null)
|
|
59
68
|
|
|
60
69
|
const localTracksRef = useRef<(LocalVideoTrack | LocalAudioTrack)[]>([])
|
|
61
70
|
|
|
@@ -101,6 +110,18 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
101
110
|
setParticipants(prev => prev.filter(p => p.sid !== participant.sid))
|
|
102
111
|
})
|
|
103
112
|
|
|
113
|
+
// Track remote screen sharing for React re-renders
|
|
114
|
+
newRoom.on('trackSubscribed', (track: RemoteTrack, publication, participant: RemoteParticipant) => {
|
|
115
|
+
if (track.kind === 'video' && track.name === SCREEN_SHARE_TRACK_NAME) {
|
|
116
|
+
setScreenSharingParticipantSid(participant.sid)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
newRoom.on('trackUnsubscribed', (track: RemoteTrack, publication, participant: RemoteParticipant) => {
|
|
120
|
+
if (track.kind === 'video' && track.name === SCREEN_SHARE_TRACK_NAME) {
|
|
121
|
+
setScreenSharingParticipantSid(null)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
104
125
|
newRoom.on('disconnected', () => {
|
|
105
126
|
// Stop all local tracks when disconnected
|
|
106
127
|
localTracksRef.current.forEach(track => {
|
|
@@ -110,6 +131,9 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
110
131
|
setRoom(null)
|
|
111
132
|
setLocalVideoTrack(null)
|
|
112
133
|
setLocalAudioTrack(null)
|
|
134
|
+
setLocalScreenTrack(null)
|
|
135
|
+
setIsScreenSharing(false)
|
|
136
|
+
setScreenSharingParticipantSid(null)
|
|
113
137
|
setParticipants([])
|
|
114
138
|
})
|
|
115
139
|
|
|
@@ -135,6 +159,9 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
135
159
|
setRoom(null)
|
|
136
160
|
setLocalVideoTrack(null)
|
|
137
161
|
setLocalAudioTrack(null)
|
|
162
|
+
setLocalScreenTrack(null)
|
|
163
|
+
setIsScreenSharing(false)
|
|
164
|
+
setScreenSharingParticipantSid(null)
|
|
138
165
|
setParticipants([])
|
|
139
166
|
}, [room])
|
|
140
167
|
|
|
@@ -160,6 +187,53 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
160
187
|
}
|
|
161
188
|
}, [localAudioTrack, isAudioEnabled])
|
|
162
189
|
|
|
190
|
+
const stopScreenShare = useCallback(() => {
|
|
191
|
+
if (localScreenTrack) {
|
|
192
|
+
if (room) {
|
|
193
|
+
room.localParticipant.unpublishTrack(localScreenTrack)
|
|
194
|
+
}
|
|
195
|
+
localScreenTrack.stop()
|
|
196
|
+
localTracksRef.current = localTracksRef.current.filter(t => t !== localScreenTrack)
|
|
197
|
+
setLocalScreenTrack(null)
|
|
198
|
+
setIsScreenSharing(false)
|
|
199
|
+
}
|
|
200
|
+
}, [localScreenTrack, room])
|
|
201
|
+
|
|
202
|
+
const toggleScreenShare = useCallback(async () => {
|
|
203
|
+
if (isScreenSharing) {
|
|
204
|
+
stopScreenShare()
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })
|
|
210
|
+
const mediaStreamTrack = stream.getVideoTracks()[0]
|
|
211
|
+
const screenTrack = new LocalVideoTrack(mediaStreamTrack, { name: SCREEN_SHARE_TRACK_NAME })
|
|
212
|
+
|
|
213
|
+
if (room) {
|
|
214
|
+
await room.localParticipant.publishTrack(screenTrack)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
localTracksRef.current.push(screenTrack)
|
|
218
|
+
setLocalScreenTrack(screenTrack)
|
|
219
|
+
setIsScreenSharing(true)
|
|
220
|
+
|
|
221
|
+
// Handle browser "Stop sharing" button
|
|
222
|
+
mediaStreamTrack.onended = () => {
|
|
223
|
+
if (room) {
|
|
224
|
+
room.localParticipant.unpublishTrack(screenTrack)
|
|
225
|
+
}
|
|
226
|
+
screenTrack.stop()
|
|
227
|
+
localTracksRef.current = localTracksRef.current.filter(t => t !== screenTrack)
|
|
228
|
+
setLocalScreenTrack(null)
|
|
229
|
+
setIsScreenSharing(false)
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// User cancelled the screen share picker — not an error
|
|
233
|
+
console.log('Screen share cancelled or failed:', err)
|
|
234
|
+
}
|
|
235
|
+
}, [isScreenSharing, stopScreenShare, room])
|
|
236
|
+
|
|
163
237
|
// Cleanup on unmount
|
|
164
238
|
useEffect(() => {
|
|
165
239
|
return () => {
|
|
@@ -178,6 +252,9 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
178
252
|
isConnected: !!room,
|
|
179
253
|
localVideoTrack,
|
|
180
254
|
localAudioTrack,
|
|
255
|
+
localScreenTrack,
|
|
256
|
+
isScreenSharing,
|
|
257
|
+
screenSharingParticipantSid,
|
|
181
258
|
participants,
|
|
182
259
|
error,
|
|
183
260
|
isHost,
|
|
@@ -187,6 +264,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
187
264
|
disconnect,
|
|
188
265
|
toggleVideo,
|
|
189
266
|
toggleAudio,
|
|
267
|
+
toggleScreenShare,
|
|
190
268
|
setIsHost,
|
|
191
269
|
}
|
|
192
270
|
|
|
@@ -3,6 +3,7 @@ import { Box, Grid } from '@mui/material'
|
|
|
3
3
|
import { useTwilioVideo } from './TwilioVideoContext'
|
|
4
4
|
import { TwilioParticipant } from './TwilioParticipant'
|
|
5
5
|
import { TwilioControlBar } from './TwilioControls'
|
|
6
|
+
import { RemoteParticipant, LocalParticipant } from 'twilio-video'
|
|
6
7
|
|
|
7
8
|
export interface TwilioVideoRoomProps {
|
|
8
9
|
onLeave?: () => void
|
|
@@ -10,6 +11,8 @@ export interface TwilioVideoRoomProps {
|
|
|
10
11
|
style?: React.CSSProperties
|
|
11
12
|
/** Resolve participant identity to a display label. Defaults to empty string. */
|
|
12
13
|
resolveIdentity?: (identity: string) => string
|
|
14
|
+
/** Whether to show the screen share button. Defaults to true. */
|
|
15
|
+
showScreenShare?: boolean
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export const TwilioVideoRoom: React.FC<TwilioVideoRoomProps> = ({
|
|
@@ -17,14 +20,101 @@ export const TwilioVideoRoom: React.FC<TwilioVideoRoomProps> = ({
|
|
|
17
20
|
onEndForAll,
|
|
18
21
|
style,
|
|
19
22
|
resolveIdentity,
|
|
23
|
+
showScreenShare: showScreenShareProp = true,
|
|
20
24
|
}) => {
|
|
21
|
-
const { room, participants } = useTwilioVideo()
|
|
25
|
+
const { room, participants, isScreenSharing, screenSharingParticipantSid } = useTwilioVideo()
|
|
22
26
|
|
|
23
27
|
if (!room) return null
|
|
24
28
|
|
|
25
29
|
const localParticipant = room.localParticipant
|
|
26
30
|
const hasRemoteParticipants = participants.length > 0
|
|
27
31
|
|
|
32
|
+
// Find who is sharing their screen (context-driven so React re-renders properly)
|
|
33
|
+
const screenShareParticipant: RemoteParticipant | LocalParticipant | null = (() => {
|
|
34
|
+
if (isScreenSharing) return localParticipant
|
|
35
|
+
if (screenSharingParticipantSid) {
|
|
36
|
+
return participants.find(p => p.sid === screenSharingParticipantSid) || null
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
})()
|
|
40
|
+
|
|
41
|
+
const isScreenShareActive = screenShareParticipant !== null
|
|
42
|
+
|
|
43
|
+
// All participants for the camera strip (local + remote)
|
|
44
|
+
const allParticipants: (RemoteParticipant | LocalParticipant)[] = [
|
|
45
|
+
localParticipant,
|
|
46
|
+
...participants,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
if (isScreenShareActive) {
|
|
50
|
+
// Presentation layout: screen share large on top, camera strip on bottom
|
|
51
|
+
return (
|
|
52
|
+
<Box
|
|
53
|
+
sx={{
|
|
54
|
+
display: 'flex',
|
|
55
|
+
flexDirection: 'column',
|
|
56
|
+
height: '100%',
|
|
57
|
+
width: '100%',
|
|
58
|
+
backgroundColor: '#1a1a1a',
|
|
59
|
+
...style,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{/* Screen share - main area */}
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
flex: 1,
|
|
66
|
+
overflow: 'hidden',
|
|
67
|
+
minHeight: 0,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<TwilioParticipant
|
|
71
|
+
participant={screenShareParticipant}
|
|
72
|
+
isLocal={screenShareParticipant === localParticipant}
|
|
73
|
+
showScreenShare
|
|
74
|
+
resolveIdentity={resolveIdentity}
|
|
75
|
+
/>
|
|
76
|
+
</Box>
|
|
77
|
+
|
|
78
|
+
{/* Camera strip */}
|
|
79
|
+
<Box
|
|
80
|
+
sx={{
|
|
81
|
+
height: 120,
|
|
82
|
+
display: 'flex',
|
|
83
|
+
flexDirection: 'row',
|
|
84
|
+
gap: 1,
|
|
85
|
+
padding: 1,
|
|
86
|
+
overflowX: 'auto',
|
|
87
|
+
flexShrink: 0,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{allParticipants.map((p) => (
|
|
91
|
+
<Box
|
|
92
|
+
key={p.sid}
|
|
93
|
+
sx={{
|
|
94
|
+
height: '100%',
|
|
95
|
+
aspectRatio: '4/3',
|
|
96
|
+
flexShrink: 0,
|
|
97
|
+
borderRadius: 1,
|
|
98
|
+
overflow: 'hidden',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<TwilioParticipant
|
|
102
|
+
participant={p}
|
|
103
|
+
isLocal={p === localParticipant}
|
|
104
|
+
showScreenShare={false}
|
|
105
|
+
resolveIdentity={resolveIdentity}
|
|
106
|
+
/>
|
|
107
|
+
</Box>
|
|
108
|
+
))}
|
|
109
|
+
</Box>
|
|
110
|
+
|
|
111
|
+
{/* Control bar */}
|
|
112
|
+
<TwilioControlBar onLeave={onLeave} onEndForAll={onEndForAll} showScreenShare={showScreenShareProp} />
|
|
113
|
+
</Box>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Normal layout (no screen share active)
|
|
28
118
|
return (
|
|
29
119
|
<Box
|
|
30
120
|
sx={{
|
|
@@ -92,7 +182,7 @@ export const TwilioVideoRoom: React.FC<TwilioVideoRoomProps> = ({
|
|
|
92
182
|
</Box>
|
|
93
183
|
|
|
94
184
|
{/* Control bar */}
|
|
95
|
-
<TwilioControlBar onLeave={onLeave} onEndForAll={onEndForAll} />
|
|
185
|
+
<TwilioControlBar onLeave={onLeave} onEndForAll={onEndForAll} showScreenShare={showScreenShareProp} />
|
|
96
186
|
</Box>
|
|
97
187
|
)
|
|
98
188
|
}
|