@tellescope/react-components 1.240.0 → 1.242.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 (76) hide show
  1. package/lib/cjs/Forms/inputs.d.ts +1 -1
  2. package/lib/cjs/TwilioVideo/TwilioControls.d.ts +8 -0
  3. package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -0
  4. package/lib/cjs/TwilioVideo/TwilioControls.js +57 -0
  5. package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -0
  6. package/lib/cjs/TwilioVideo/TwilioLocalPreview.d.ts +6 -0
  7. package/lib/cjs/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -0
  8. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js +173 -0
  9. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js.map +1 -0
  10. package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts +11 -0
  11. package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts.map +1 -0
  12. package/lib/cjs/TwilioVideo/TwilioParticipant.js +98 -0
  13. package/lib/cjs/TwilioVideo/TwilioParticipant.js.map +1 -0
  14. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +28 -0
  15. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -0
  16. package/lib/cjs/TwilioVideo/TwilioVideoContext.js +222 -0
  17. package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -0
  18. package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts +10 -0
  19. package/lib/cjs/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -0
  20. package/lib/cjs/TwilioVideo/TwilioVideoRoom.js +49 -0
  21. package/lib/cjs/TwilioVideo/TwilioVideoRoom.js.map +1 -0
  22. package/lib/cjs/TwilioVideo/hooks.d.ts +44 -0
  23. package/lib/cjs/TwilioVideo/hooks.d.ts.map +1 -0
  24. package/lib/cjs/TwilioVideo/hooks.js +232 -0
  25. package/lib/cjs/TwilioVideo/hooks.js.map +1 -0
  26. package/lib/cjs/TwilioVideo/index.d.ts +7 -0
  27. package/lib/cjs/TwilioVideo/index.d.ts.map +1 -0
  28. package/lib/cjs/TwilioVideo/index.js +19 -0
  29. package/lib/cjs/TwilioVideo/index.js.map +1 -0
  30. package/lib/cjs/index.d.ts +1 -0
  31. package/lib/cjs/index.d.ts.map +1 -1
  32. package/lib/cjs/index.js +1 -0
  33. package/lib/cjs/index.js.map +1 -1
  34. package/lib/esm/Forms/inputs.d.ts +1 -1
  35. package/lib/esm/TwilioVideo/TwilioControls.d.ts +8 -0
  36. package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -0
  37. package/lib/esm/TwilioVideo/TwilioControls.js +53 -0
  38. package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -0
  39. package/lib/esm/TwilioVideo/TwilioLocalPreview.d.ts +6 -0
  40. package/lib/esm/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -0
  41. package/lib/esm/TwilioVideo/TwilioLocalPreview.js +166 -0
  42. package/lib/esm/TwilioVideo/TwilioLocalPreview.js.map +1 -0
  43. package/lib/esm/TwilioVideo/TwilioParticipant.d.ts +11 -0
  44. package/lib/esm/TwilioVideo/TwilioParticipant.d.ts.map +1 -0
  45. package/lib/esm/TwilioVideo/TwilioParticipant.js +94 -0
  46. package/lib/esm/TwilioVideo/TwilioParticipant.js.map +1 -0
  47. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +28 -0
  48. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -0
  49. package/lib/esm/TwilioVideo/TwilioVideoContext.js +214 -0
  50. package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -0
  51. package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts +10 -0
  52. package/lib/esm/TwilioVideo/TwilioVideoRoom.d.ts.map +1 -0
  53. package/lib/esm/TwilioVideo/TwilioVideoRoom.js +45 -0
  54. package/lib/esm/TwilioVideo/TwilioVideoRoom.js.map +1 -0
  55. package/lib/esm/TwilioVideo/hooks.d.ts +44 -0
  56. package/lib/esm/TwilioVideo/hooks.d.ts.map +1 -0
  57. package/lib/esm/TwilioVideo/hooks.js +226 -0
  58. package/lib/esm/TwilioVideo/hooks.js.map +1 -0
  59. package/lib/esm/TwilioVideo/index.d.ts +7 -0
  60. package/lib/esm/TwilioVideo/index.d.ts.map +1 -0
  61. package/lib/esm/TwilioVideo/index.js +7 -0
  62. package/lib/esm/TwilioVideo/index.js.map +1 -0
  63. package/lib/esm/index.d.ts +1 -0
  64. package/lib/esm/index.d.ts.map +1 -1
  65. package/lib/esm/index.js +1 -0
  66. package/lib/esm/index.js.map +1 -1
  67. package/lib/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +12 -11
  69. package/src/TwilioVideo/TwilioControls.tsx +110 -0
  70. package/src/TwilioVideo/TwilioLocalPreview.tsx +151 -0
  71. package/src/TwilioVideo/TwilioParticipant.tsx +136 -0
  72. package/src/TwilioVideo/TwilioVideoContext.tsx +198 -0
  73. package/src/TwilioVideo/TwilioVideoRoom.tsx +98 -0
  74. package/src/TwilioVideo/hooks.ts +159 -0
  75. package/src/TwilioVideo/index.ts +19 -0
  76. package/src/index.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.240.0",
3
+ "version": "1.242.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -42,18 +42,18 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@emotion/css": "11.10.5",
45
- "@mui/icons-material": "5.10.15",
46
- "@mui/material": "5.10.15",
45
+ "@mui/icons-material": "5.15.0",
46
+ "@mui/material": "5.15.0",
47
47
  "@reduxjs/toolkit": "1.9.0",
48
48
  "@stripe/react-stripe-js": "2.9.0",
49
49
  "@stripe/stripe-js": "1.52.1",
50
- "@tellescope/constants": "1.240.0",
51
- "@tellescope/sdk": "1.240.0",
52
- "@tellescope/types-client": "1.240.0",
53
- "@tellescope/types-models": "1.240.0",
54
- "@tellescope/types-utilities": "1.240.0",
55
- "@tellescope/utilities": "1.240.0",
56
- "@tellescope/validation": "1.240.0",
50
+ "@tellescope/constants": "1.242.0",
51
+ "@tellescope/sdk": "1.242.0",
52
+ "@tellescope/types-client": "1.242.0",
53
+ "@tellescope/types-models": "1.242.0",
54
+ "@tellescope/types-utilities": "1.242.0",
55
+ "@tellescope/utilities": "1.242.0",
56
+ "@tellescope/validation": "1.242.0",
57
57
  "@typescript-eslint/eslint-plugin": "4.33.0",
58
58
  "@typescript-eslint/parser": "4.33.0",
59
59
  "css-to-react-native": "3.0.0",
@@ -76,6 +76,7 @@
76
76
  "react-native-video": "5.2.1",
77
77
  "react-redux": "7.2.9",
78
78
  "react-window": "1.8.9",
79
+ "twilio-video": "^2.28.1",
79
80
  "yup": "0.32.11"
80
81
  },
81
82
  "peerDependencies": {
@@ -83,7 +84,7 @@
83
84
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
84
85
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
85
86
  },
86
- "gitHead": "16b2b3499eb36e6b1715c6ede6d07832b7376917",
87
+ "gitHead": "f2880f8663e523e8c99ded28e67c071f94921265",
87
88
  "publishConfig": {
88
89
  "access": "public"
89
90
  }
@@ -0,0 +1,110 @@
1
+ import React from 'react'
2
+ import { Box, IconButton, Button } from '@mui/material'
3
+ import {
4
+ Mic as MicIcon,
5
+ MicOff as MicOffIcon,
6
+ Videocam as VideocamIcon,
7
+ VideocamOff as VideocamOffIcon,
8
+ CallEnd as CallEndIcon,
9
+ } from '@mui/icons-material'
10
+ import { useTwilioVideo } from './TwilioVideoContext'
11
+
12
+ export interface TwilioControlBarProps {
13
+ onLeave?: () => void
14
+ onEndForAll?: () => void
15
+ style?: React.CSSProperties
16
+ }
17
+
18
+ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
19
+ onLeave,
20
+ onEndForAll,
21
+ style,
22
+ }) => {
23
+ const {
24
+ isVideoEnabled,
25
+ isAudioEnabled,
26
+ toggleVideo,
27
+ toggleAudio,
28
+ disconnect,
29
+ isHost,
30
+ } = useTwilioVideo()
31
+
32
+ const handleLeave = () => {
33
+ disconnect()
34
+ onLeave?.()
35
+ }
36
+
37
+ const handleEndForAll = () => {
38
+ onEndForAll?.()
39
+ disconnect()
40
+ }
41
+
42
+ return (
43
+ <Box
44
+ sx={{
45
+ display: 'flex',
46
+ justifyContent: 'center',
47
+ alignItems: 'center',
48
+ gap: 2,
49
+ backgroundColor: '#1a1a1a',
50
+ padding: '12px 24px',
51
+ ...style,
52
+ }}
53
+ >
54
+ <IconButton
55
+ onClick={toggleAudio}
56
+ sx={{
57
+ color: isAudioEnabled ? 'white' : '#ff4444',
58
+ '&:hover': {
59
+ backgroundColor: 'rgba(255,255,255,0.1)',
60
+ },
61
+ }}
62
+ >
63
+ {isAudioEnabled ? <MicIcon /> : <MicOffIcon />}
64
+ </IconButton>
65
+
66
+ <IconButton
67
+ onClick={toggleVideo}
68
+ sx={{
69
+ color: isVideoEnabled ? 'white' : '#ff4444',
70
+ '&:hover': {
71
+ backgroundColor: 'rgba(255,255,255,0.1)',
72
+ },
73
+ }}
74
+ >
75
+ {isVideoEnabled ? <VideocamIcon /> : <VideocamOffIcon />}
76
+ </IconButton>
77
+
78
+ <IconButton
79
+ onClick={handleLeave}
80
+ sx={{
81
+ backgroundColor: '#ff4444',
82
+ color: 'white',
83
+ '&:hover': {
84
+ backgroundColor: '#cc3333',
85
+ },
86
+ }}
87
+ >
88
+ <CallEndIcon />
89
+ </IconButton>
90
+
91
+ {isHost && onEndForAll && (
92
+ <Button
93
+ variant="outlined"
94
+ onClick={handleEndForAll}
95
+ sx={{
96
+ color: 'white',
97
+ borderColor: 'white',
98
+ marginLeft: 2,
99
+ '&:hover': {
100
+ borderColor: '#ff4444',
101
+ color: '#ff4444',
102
+ },
103
+ }}
104
+ >
105
+ End For All
106
+ </Button>
107
+ )}
108
+ </Box>
109
+ )
110
+ }
@@ -0,0 +1,151 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import Video, { LocalVideoTrack } from 'twilio-video'
3
+ import { Box, Typography, CircularProgress, FormControl, InputLabel, Select, MenuItem } from '@mui/material'
4
+
5
+ export interface TwilioLocalPreviewProps {
6
+ style?: React.CSSProperties
7
+ }
8
+
9
+ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style }) => {
10
+ const containerRef = useRef<HTMLDivElement>(null)
11
+ const trackRef = useRef<LocalVideoTrack | null>(null)
12
+ const [error, setError] = useState<string | null>(null)
13
+ const [loading, setLoading] = useState(true)
14
+ const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
15
+ const [selectedDeviceId, setSelectedDeviceId] = useState<string>('')
16
+
17
+ // Enumerate video devices
18
+ useEffect(() => {
19
+ const getDevices = async () => {
20
+ try {
21
+ // Request permission first to get device labels, then immediately stop
22
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true })
23
+ stream.getTracks().forEach(track => track.stop())
24
+
25
+ const allDevices = await navigator.mediaDevices.enumerateDevices()
26
+ const videoDevices = allDevices.filter(d => d.kind === 'videoinput')
27
+ setDevices(videoDevices)
28
+ if (videoDevices.length > 0 && !selectedDeviceId) {
29
+ setSelectedDeviceId(videoDevices[0].deviceId)
30
+ }
31
+ } catch (err: any) {
32
+ setError(err?.message || 'Failed to enumerate devices')
33
+ setLoading(false)
34
+ }
35
+ }
36
+ getDevices()
37
+ }, [])
38
+
39
+ // Create video track when device is selected
40
+ useEffect(() => {
41
+ if (!selectedDeviceId) return
42
+
43
+ let mounted = true
44
+
45
+ const getVideoTrack = async () => {
46
+ // Stop existing track
47
+ if (trackRef.current) {
48
+ trackRef.current.stop()
49
+ trackRef.current = null
50
+ }
51
+ // Clear container
52
+ if (containerRef.current) {
53
+ containerRef.current.innerHTML = ''
54
+ }
55
+
56
+ setLoading(true)
57
+ setError(null)
58
+
59
+ try {
60
+ const track = await Video.createLocalVideoTrack({
61
+ deviceId: { exact: selectedDeviceId },
62
+ width: 640,
63
+ height: 480,
64
+ })
65
+ if (mounted && containerRef.current) {
66
+ trackRef.current = track
67
+ const videoElement = track.attach()
68
+ videoElement.style.width = '100%'
69
+ videoElement.style.height = '100%'
70
+ videoElement.style.objectFit = 'cover'
71
+ videoElement.style.transform = 'scaleX(-1)'
72
+ containerRef.current.appendChild(videoElement)
73
+ setLoading(false)
74
+ } else if (!mounted) {
75
+ track.stop()
76
+ }
77
+ } catch (err: any) {
78
+ if (mounted) {
79
+ setError(err?.message || 'Failed to access camera')
80
+ setLoading(false)
81
+ }
82
+ }
83
+ }
84
+
85
+ getVideoTrack()
86
+
87
+ return () => {
88
+ mounted = false
89
+ if (trackRef.current) {
90
+ trackRef.current.stop()
91
+ trackRef.current = null
92
+ }
93
+ if (containerRef.current) {
94
+ containerRef.current.innerHTML = ''
95
+ }
96
+ }
97
+ }, [selectedDeviceId])
98
+
99
+ return (
100
+ <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
101
+ {/* Camera selector */}
102
+ {devices.length > 0 && (
103
+ <FormControl size="small" sx={{ minWidth: 320 }}>
104
+ <InputLabel id="camera-select-label">Camera</InputLabel>
105
+ <Select
106
+ labelId="camera-select-label"
107
+ value={selectedDeviceId}
108
+ label="Camera"
109
+ onChange={(e) => setSelectedDeviceId(e.target.value)}
110
+ >
111
+ {devices.map((device) => (
112
+ <MenuItem key={device.deviceId} value={device.deviceId}>
113
+ {device.label || `Camera ${devices.indexOf(device) + 1}`}
114
+ </MenuItem>
115
+ ))}
116
+ </Select>
117
+ </FormControl>
118
+ )}
119
+
120
+ {/* Video preview */}
121
+ <Box
122
+ sx={{
123
+ width: 320,
124
+ height: 240,
125
+ backgroundColor: '#1a1a1a',
126
+ borderRadius: 1,
127
+ overflow: 'hidden',
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ justifyContent: 'center',
131
+ ...style,
132
+ }}
133
+ >
134
+ {loading && <CircularProgress size={24} sx={{ color: 'white' }} />}
135
+ {error && (
136
+ <Typography color="error" variant="body2" textAlign="center" sx={{ p: 2 }}>
137
+ {error}
138
+ </Typography>
139
+ )}
140
+ <Box
141
+ ref={containerRef}
142
+ sx={{
143
+ width: '100%',
144
+ height: '100%',
145
+ display: loading || error ? 'none' : 'block',
146
+ }}
147
+ />
148
+ </Box>
149
+ </Box>
150
+ )
151
+ }
@@ -0,0 +1,136 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import {
3
+ RemoteParticipant,
4
+ LocalParticipant,
5
+ VideoTrack,
6
+ AudioTrack,
7
+ RemoteTrackPublication,
8
+ LocalTrackPublication,
9
+ } from 'twilio-video'
10
+ import { Box, Typography } from '@mui/material'
11
+
12
+ export interface TwilioParticipantProps {
13
+ participant: RemoteParticipant | LocalParticipant
14
+ isLocal?: boolean
15
+ style?: React.CSSProperties
16
+ /** Resolve participant identity to a display label. Defaults to empty string. */
17
+ resolveIdentity?: (identity: string) => string
18
+ }
19
+
20
+ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
21
+ participant,
22
+ isLocal = false,
23
+ style,
24
+ resolveIdentity = () => '',
25
+ }) => {
26
+ const videoRef = useRef<HTMLVideoElement>(null)
27
+ const audioRef = useRef<HTMLAudioElement>(null)
28
+ const [videoTrack, setVideoTrack] = useState<VideoTrack | null>(null)
29
+ const [audioTrack, setAudioTrack] = useState<AudioTrack | null>(null)
30
+
31
+ useEffect(() => {
32
+ const handleTrackSubscribed = (track: VideoTrack | AudioTrack) => {
33
+ if (track.kind === 'video') {
34
+ setVideoTrack(track as VideoTrack)
35
+ } else if (track.kind === 'audio') {
36
+ setAudioTrack(track as AudioTrack)
37
+ }
38
+ }
39
+
40
+ const handleTrackUnsubscribed = (track: VideoTrack | AudioTrack) => {
41
+ if (track.kind === 'video') {
42
+ setVideoTrack(null)
43
+ } else if (track.kind === 'audio') {
44
+ setAudioTrack(null)
45
+ }
46
+ }
47
+
48
+ // Get existing tracks
49
+ participant.tracks.forEach((publication: RemoteTrackPublication | LocalTrackPublication) => {
50
+ if (publication.track) {
51
+ handleTrackSubscribed(publication.track as VideoTrack | AudioTrack)
52
+ }
53
+ })
54
+
55
+ // For remote participants, listen for track subscriptions
56
+ if ('on' in participant) {
57
+ participant.on('trackSubscribed', handleTrackSubscribed)
58
+ participant.on('trackUnsubscribed', handleTrackUnsubscribed)
59
+ }
60
+
61
+ return () => {
62
+ if ('off' in participant) {
63
+ participant.off('trackSubscribed', handleTrackSubscribed)
64
+ participant.off('trackUnsubscribed', handleTrackUnsubscribed)
65
+ }
66
+ }
67
+ }, [participant])
68
+
69
+ // Attach video track
70
+ useEffect(() => {
71
+ if (videoTrack && videoRef.current) {
72
+ videoTrack.attach(videoRef.current)
73
+ return () => {
74
+ videoTrack.detach()
75
+ }
76
+ }
77
+ }, [videoTrack])
78
+
79
+ // Attach audio track (not for local participant to avoid echo)
80
+ useEffect(() => {
81
+ if (audioTrack && audioRef.current && !isLocal) {
82
+ audioTrack.attach(audioRef.current)
83
+ return () => {
84
+ audioTrack.detach()
85
+ }
86
+ }
87
+ }, [audioTrack, isLocal])
88
+
89
+ return (
90
+ <Box
91
+ sx={{
92
+ position: 'relative',
93
+ width: '100%',
94
+ height: '100%',
95
+ backgroundColor: '#1a1a1a',
96
+ borderRadius: 1,
97
+ overflow: 'hidden',
98
+ ...style,
99
+ }}
100
+ >
101
+ <video
102
+ ref={videoRef}
103
+ autoPlay
104
+ muted={isLocal}
105
+ playsInline
106
+ style={{
107
+ width: '100%',
108
+ height: '100%',
109
+ objectFit: 'cover',
110
+ transform: isLocal ? 'scaleX(-1)' : 'none',
111
+ }}
112
+ />
113
+ {!isLocal && <audio ref={audioRef} autoPlay />}
114
+ {(() => {
115
+ const label = resolveIdentity(participant.identity)
116
+ if (!label && !isLocal) return null
117
+ return (
118
+ <Typography
119
+ variant="caption"
120
+ sx={{
121
+ position: 'absolute',
122
+ bottom: 8,
123
+ left: 8,
124
+ color: 'white',
125
+ backgroundColor: 'rgba(0,0,0,0.5)',
126
+ padding: '2px 8px',
127
+ borderRadius: 1,
128
+ }}
129
+ >
130
+ {label}{isLocal && (label ? ' (You)' : '(You)')}
131
+ </Typography>
132
+ )
133
+ })()}
134
+ </Box>
135
+ )
136
+ }
@@ -0,0 +1,198 @@
1
+ import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
2
+ import Video, {
3
+ Room,
4
+ LocalVideoTrack,
5
+ LocalAudioTrack,
6
+ RemoteParticipant,
7
+ LocalTrack,
8
+ RemoteTrack,
9
+ LocalParticipant,
10
+ } from 'twilio-video'
11
+
12
+ export interface TwilioVideoState {
13
+ room: Room | null
14
+ isConnecting: boolean
15
+ isConnected: boolean
16
+ localVideoTrack: LocalVideoTrack | null
17
+ localAudioTrack: LocalAudioTrack | null
18
+ participants: RemoteParticipant[]
19
+ error: Error | null
20
+ isHost: boolean
21
+ isVideoEnabled: boolean
22
+ isAudioEnabled: boolean
23
+ }
24
+
25
+ export interface TwilioVideoActions {
26
+ connect: (token: string, roomName: string) => Promise<void>
27
+ disconnect: () => void
28
+ toggleVideo: () => Promise<void>
29
+ toggleAudio: () => void
30
+ setIsHost: (isHost: boolean) => void
31
+ }
32
+
33
+ export type TwilioVideoContextType = TwilioVideoState & TwilioVideoActions
34
+
35
+ const TwilioVideoContext = createContext<TwilioVideoContextType | null>(null)
36
+
37
+ export const useTwilioVideo = (): TwilioVideoContextType => {
38
+ const context = useContext(TwilioVideoContext)
39
+ if (!context) {
40
+ throw new Error('useTwilioVideo must be used within TwilioVideoProvider')
41
+ }
42
+ return context
43
+ }
44
+
45
+ export interface TwilioVideoProviderProps {
46
+ children: React.ReactNode
47
+ }
48
+
49
+ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ children }) => {
50
+ const [room, setRoom] = useState<Room | null>(null)
51
+ const [isConnecting, setIsConnecting] = useState(false)
52
+ const [localVideoTrack, setLocalVideoTrack] = useState<LocalVideoTrack | null>(null)
53
+ const [localAudioTrack, setLocalAudioTrack] = useState<LocalAudioTrack | null>(null)
54
+ const [participants, setParticipants] = useState<RemoteParticipant[]>([])
55
+ const [error, setError] = useState<Error | null>(null)
56
+ const [isHost, setIsHost] = useState(false)
57
+ const [isVideoEnabled, setIsVideoEnabled] = useState(true)
58
+ const [isAudioEnabled, setIsAudioEnabled] = useState(true)
59
+
60
+ const localTracksRef = useRef<(LocalVideoTrack | LocalAudioTrack)[]>([])
61
+
62
+ const connect = useCallback(async (token: string, roomName: string) => {
63
+ setIsConnecting(true)
64
+ setError(null)
65
+
66
+ try {
67
+ // Create local tracks
68
+ const tracks = await Video.createLocalTracks({
69
+ audio: true,
70
+ video: { width: 640 },
71
+ })
72
+
73
+ localTracksRef.current = tracks as (LocalVideoTrack | LocalAudioTrack)[]
74
+
75
+ const videoTrack = tracks.find((track: LocalTrack) => track.kind === 'video') as LocalVideoTrack | undefined
76
+ const audioTrack = tracks.find((track: LocalTrack) => track.kind === 'audio') as LocalAudioTrack | undefined
77
+
78
+ if (videoTrack) setLocalVideoTrack(videoTrack)
79
+ if (audioTrack) setLocalAudioTrack(audioTrack)
80
+
81
+ // Connect to room
82
+ const newRoom = await Video.connect(token, {
83
+ name: roomName,
84
+ tracks,
85
+ dominantSpeaker: true,
86
+ networkQuality: { local: 1, remote: 1 },
87
+ })
88
+
89
+ setRoom(newRoom)
90
+
91
+ // Handle existing participants
92
+ const existingParticipants = Array.from(newRoom.participants.values())
93
+ setParticipants(existingParticipants)
94
+
95
+ // Listen for new participants
96
+ newRoom.on('participantConnected', (participant: RemoteParticipant) => {
97
+ setParticipants(prev => [...prev, participant])
98
+ })
99
+
100
+ newRoom.on('participantDisconnected', (participant: RemoteParticipant) => {
101
+ setParticipants(prev => prev.filter(p => p.sid !== participant.sid))
102
+ })
103
+
104
+ newRoom.on('disconnected', () => {
105
+ // Stop all local tracks when disconnected
106
+ localTracksRef.current.forEach(track => {
107
+ track.stop()
108
+ })
109
+ localTracksRef.current = []
110
+ setRoom(null)
111
+ setLocalVideoTrack(null)
112
+ setLocalAudioTrack(null)
113
+ setParticipants([])
114
+ })
115
+
116
+ } catch (err) {
117
+ setError(err as Error)
118
+ console.error('Failed to connect to Twilio Video:', err)
119
+ } finally {
120
+ setIsConnecting(false)
121
+ }
122
+ }, [])
123
+
124
+ const disconnect = useCallback(() => {
125
+ if (room) {
126
+ room.disconnect()
127
+ }
128
+
129
+ // Stop local tracks
130
+ localTracksRef.current.forEach(track => {
131
+ track.stop()
132
+ })
133
+ localTracksRef.current = []
134
+
135
+ setRoom(null)
136
+ setLocalVideoTrack(null)
137
+ setLocalAudioTrack(null)
138
+ setParticipants([])
139
+ }, [room])
140
+
141
+ const toggleVideo = useCallback(async () => {
142
+ if (localVideoTrack) {
143
+ if (isVideoEnabled) {
144
+ localVideoTrack.disable()
145
+ } else {
146
+ localVideoTrack.enable()
147
+ }
148
+ setIsVideoEnabled(!isVideoEnabled)
149
+ }
150
+ }, [localVideoTrack, isVideoEnabled])
151
+
152
+ const toggleAudio = useCallback(() => {
153
+ if (localAudioTrack) {
154
+ if (isAudioEnabled) {
155
+ localAudioTrack.disable()
156
+ } else {
157
+ localAudioTrack.enable()
158
+ }
159
+ setIsAudioEnabled(!isAudioEnabled)
160
+ }
161
+ }, [localAudioTrack, isAudioEnabled])
162
+
163
+ // Cleanup on unmount
164
+ useEffect(() => {
165
+ return () => {
166
+ if (room) {
167
+ room.disconnect()
168
+ }
169
+ localTracksRef.current.forEach(track => {
170
+ track.stop()
171
+ })
172
+ }
173
+ }, [])
174
+
175
+ const value: TwilioVideoContextType = {
176
+ room,
177
+ isConnecting,
178
+ isConnected: !!room,
179
+ localVideoTrack,
180
+ localAudioTrack,
181
+ participants,
182
+ error,
183
+ isHost,
184
+ isVideoEnabled,
185
+ isAudioEnabled,
186
+ connect,
187
+ disconnect,
188
+ toggleVideo,
189
+ toggleAudio,
190
+ setIsHost,
191
+ }
192
+
193
+ return (
194
+ <TwilioVideoContext.Provider value={value}>
195
+ {children}
196
+ </TwilioVideoContext.Provider>
197
+ )
198
+ }