@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.
Files changed (47) hide show
  1. package/lib/cjs/TwilioVideo/TwilioControls.d.ts +2 -0
  2. package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
  3. package/lib/cjs/TwilioVideo/TwilioControls.js +14 -3
  4. package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
  5. package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts +2 -0
  6. package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
  7. package/lib/cjs/TwilioVideo/TwilioParticipant.js +52 -21
  8. package/lib/cjs/TwilioVideo/TwilioParticipant.js.map +1 -1
  9. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +5 -0
  10. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  11. package/lib/cjs/TwilioVideo/TwilioVideoContext.js +107 -5
  12. package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
  13. package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts +2 -0
  14. package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -1
  15. package/lib/cjs/TwilioVideo/TwilioVideoRoom.js +49 -3
  16. package/lib/cjs/TwilioVideo/TwilioVideoRoom.js.map +1 -1
  17. package/lib/cjs/TwilioVideo/index.d.ts +1 -1
  18. package/lib/cjs/TwilioVideo/index.d.ts.map +1 -1
  19. package/lib/cjs/TwilioVideo/index.js +2 -1
  20. package/lib/cjs/TwilioVideo/index.js.map +1 -1
  21. package/lib/esm/TwilioVideo/TwilioControls.d.ts +2 -0
  22. package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
  23. package/lib/esm/TwilioVideo/TwilioControls.js +15 -4
  24. package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
  25. package/lib/esm/TwilioVideo/TwilioParticipant.d.ts +2 -0
  26. package/lib/esm/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
  27. package/lib/esm/TwilioVideo/TwilioParticipant.js +52 -21
  28. package/lib/esm/TwilioVideo/TwilioParticipant.js.map +1 -1
  29. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +5 -0
  30. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  31. package/lib/esm/TwilioVideo/TwilioVideoContext.js +83 -1
  32. package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
  33. package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts +2 -0
  34. package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -1
  35. package/lib/esm/TwilioVideo/TwilioVideoRoom.js +49 -3
  36. package/lib/esm/TwilioVideo/TwilioVideoRoom.js.map +1 -1
  37. package/lib/esm/TwilioVideo/index.d.ts +1 -1
  38. package/lib/esm/TwilioVideo/index.d.ts.map +1 -1
  39. package/lib/esm/TwilioVideo/index.js +1 -1
  40. package/lib/esm/TwilioVideo/index.js.map +1 -1
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +9 -9
  43. package/src/TwilioVideo/TwilioControls.tsx +30 -0
  44. package/src/TwilioVideo/TwilioParticipant.tsx +51 -17
  45. package/src/TwilioVideo/TwilioVideoContext.tsx +78 -0
  46. package/src/TwilioVideo/TwilioVideoRoom.tsx +92 -2
  47. 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.246.2",
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.246.2",
55
- "@tellescope/sdk": "1.246.2",
56
- "@tellescope/types-client": "1.246.2",
57
- "@tellescope/types-models": "1.246.2",
58
- "@tellescope/types-utilities": "1.246.2",
59
- "@tellescope/utilities": "1.246.2",
60
- "@tellescope/validation": "1.246.2",
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": "a9167f6d732de999cf012ffba3004f0d62ae8c14",
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 [videoTrack, setVideoTrack] = useState<VideoTrack | null>(null)
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
- setVideoTrack(track as VideoTrack)
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
- setVideoTrack(null)
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
- // For remote participants, listen for track subscriptions
56
- if ('on' in participant) {
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
- return () => {
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 (videoTrack && videoRef.current) {
72
- videoTrack.attach(videoRef.current)
101
+ if (activeVideoTrack && videoRef.current) {
102
+ const el = videoRef.current
103
+ activeVideoTrack.attach(el)
73
104
  return () => {
74
- videoTrack.detach()
105
+ activeVideoTrack.detach(el)
75
106
  }
76
107
  }
77
- }, [videoTrack])
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
- audioTrack.attach(audioRef.current)
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: isLocal ? 'scaleX(-1)' : 'none',
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
  }
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  TwilioVideoProvider,
3
3
  useTwilioVideo,
4
+ SCREEN_SHARE_TRACK_NAME,
4
5
  type TwilioVideoState,
5
6
  type TwilioVideoActions,
6
7
  type TwilioVideoContextType,