@tellescope/react-components 1.249.1 → 1.249.2
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/Forms/forms.d.ts.map +1 -1
- package/lib/cjs/Forms/forms.js +13 -5
- package/lib/cjs/Forms/forms.js.map +1 -1
- package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
- package/lib/cjs/Forms/forms.v2.js +13 -5
- package/lib/cjs/Forms/forms.v2.js.map +1 -1
- package/lib/cjs/Forms/hooks.d.ts +2 -1
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +49 -26
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/inputs.d.ts +19 -4
- package/lib/cjs/Forms/inputs.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.js +224 -173
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/cjs/Forms/inputs.v2.d.ts +7 -3
- package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.v2.js +42 -32
- package/lib/cjs/Forms/inputs.v2.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioControls.js +12 -2
- package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioLocalPreview.js +154 -2
- package/lib/cjs/TwilioVideo/TwilioLocalPreview.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +7 -0
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js +148 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/esm/Forms/forms.d.ts.map +1 -1
- package/lib/esm/Forms/forms.js +13 -5
- package/lib/esm/Forms/forms.js.map +1 -1
- package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
- package/lib/esm/Forms/forms.v2.js +13 -5
- package/lib/esm/Forms/forms.v2.js.map +1 -1
- package/lib/esm/Forms/hooks.d.ts +2 -1
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +49 -26
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +19 -4
- package/lib/esm/Forms/inputs.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.js +69 -20
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/esm/Forms/inputs.v2.d.ts +7 -3
- package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.v2.js +27 -17
- package/lib/esm/Forms/inputs.v2.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.js +14 -4
- package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioLocalPreview.js +155 -3
- package/lib/esm/TwilioVideo/TwilioLocalPreview.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +7 -0
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.js +146 -0
- package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -10
- package/src/Forms/forms.tsx +18 -2
- package/src/Forms/forms.v2.tsx +18 -2
- package/src/Forms/hooks.tsx +67 -30
- package/src/Forms/inputs.tsx +143 -18
- package/src/Forms/inputs.v2.tsx +58 -8
- package/src/TwilioVideo/TwilioControls.tsx +27 -1
- package/src/TwilioVideo/TwilioLocalPreview.tsx +136 -1
- package/src/TwilioVideo/TwilioVideoContext.tsx +126 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Box, IconButton, Button } from '@mui/material'
|
|
2
|
+
import { Box, IconButton, Button, CircularProgress } from '@mui/material'
|
|
3
3
|
import {
|
|
4
4
|
Mic as MicIcon,
|
|
5
5
|
MicOff as MicOffIcon,
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
CallEnd as CallEndIcon,
|
|
9
9
|
ScreenShare as ScreenShareIcon,
|
|
10
10
|
StopScreenShare as StopScreenShareIcon,
|
|
11
|
+
BlurOn as BlurOnIcon,
|
|
12
|
+
BlurOff as BlurOffIcon,
|
|
11
13
|
} from '@mui/icons-material'
|
|
12
14
|
import { useTwilioVideo } from './TwilioVideoContext'
|
|
13
15
|
|
|
@@ -29,9 +31,13 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
29
31
|
isVideoEnabled,
|
|
30
32
|
isAudioEnabled,
|
|
31
33
|
isScreenSharing,
|
|
34
|
+
isBlurSupported,
|
|
35
|
+
isBlurEnabled,
|
|
36
|
+
isBlurLoading,
|
|
32
37
|
toggleVideo,
|
|
33
38
|
toggleAudio,
|
|
34
39
|
toggleScreenShare,
|
|
40
|
+
toggleBlur,
|
|
35
41
|
disconnect,
|
|
36
42
|
isHost,
|
|
37
43
|
room,
|
|
@@ -87,6 +93,26 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
87
93
|
{isVideoEnabled ? <VideocamIcon /> : <VideocamOffIcon />}
|
|
88
94
|
</IconButton>
|
|
89
95
|
|
|
96
|
+
{isBlurSupported && (
|
|
97
|
+
<IconButton
|
|
98
|
+
onClick={toggleBlur}
|
|
99
|
+
disabled={isBlurLoading}
|
|
100
|
+
sx={{
|
|
101
|
+
color: isBlurEnabled ? '#4caf50' : 'white',
|
|
102
|
+
'&:hover': {
|
|
103
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
104
|
+
},
|
|
105
|
+
'&.Mui-disabled': {
|
|
106
|
+
color: 'rgba(255,255,255,0.5)',
|
|
107
|
+
},
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{isBlurLoading
|
|
111
|
+
? <CircularProgress size={20} sx={{ color: 'white' }} />
|
|
112
|
+
: isBlurEnabled ? <BlurOnIcon /> : <BlurOffIcon />}
|
|
113
|
+
</IconButton>
|
|
114
|
+
)}
|
|
115
|
+
|
|
90
116
|
{showScreenShareProp && supportsScreenShare && (
|
|
91
117
|
<IconButton
|
|
92
118
|
onClick={toggleScreenShare}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import Video, { LocalVideoTrack } from 'twilio-video'
|
|
3
|
-
import {
|
|
3
|
+
import type { GaussianBlurBackgroundProcessor as GaussianBlurBackgroundProcessorType } from '@twilio/video-processors'
|
|
4
|
+
import { Box, Typography, CircularProgress, FormControl, InputLabel, Select, MenuItem, IconButton } from '@mui/material'
|
|
5
|
+
import { BlurOn as BlurOnIcon, BlurOff as BlurOffIcon } from '@mui/icons-material'
|
|
6
|
+
import {
|
|
7
|
+
loadTwilioVideoProcessorsModule,
|
|
8
|
+
BLUR_BACKGROUND_ASSETS_PATH,
|
|
9
|
+
BLUR_BACKGROUND_STORAGE_KEY,
|
|
10
|
+
} from './TwilioVideoContext'
|
|
11
|
+
|
|
12
|
+
const readBlurPreference = (): boolean => {
|
|
13
|
+
try {
|
|
14
|
+
return typeof localStorage !== 'undefined'
|
|
15
|
+
&& localStorage.getItem(BLUR_BACKGROUND_STORAGE_KEY) === 'true'
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const writeBlurPreference = (enabled: boolean) => {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof localStorage !== 'undefined') {
|
|
24
|
+
localStorage.setItem(BLUR_BACKGROUND_STORAGE_KEY, enabled ? 'true' : 'false')
|
|
25
|
+
}
|
|
26
|
+
} catch { /* ignore */ }
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
export interface TwilioLocalPreviewProps {
|
|
6
30
|
style?: React.CSSProperties
|
|
@@ -9,10 +33,33 @@ export interface TwilioLocalPreviewProps {
|
|
|
9
33
|
export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style }) => {
|
|
10
34
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
11
35
|
const trackRef = useRef<LocalVideoTrack | null>(null)
|
|
36
|
+
const blurProcessorRef = useRef<GaussianBlurBackgroundProcessorType | null>(null)
|
|
37
|
+
const blurAttachedTrackRef = useRef<LocalVideoTrack | null>(null)
|
|
12
38
|
const [error, setError] = useState<string | null>(null)
|
|
13
39
|
const [loading, setLoading] = useState(true)
|
|
14
40
|
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
|
15
41
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('')
|
|
42
|
+
const [isBlurSupported, setIsBlurSupported] = useState(false)
|
|
43
|
+
const [isBlurEnabled, setIsBlurEnabled] = useState<boolean>(readBlurPreference)
|
|
44
|
+
const [isBlurLoading, setIsBlurLoading] = useState(false)
|
|
45
|
+
const [trackVersion, setTrackVersion] = useState(0)
|
|
46
|
+
|
|
47
|
+
// Probe video-processors support once on mount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let mounted = true
|
|
50
|
+
loadTwilioVideoProcessorsModule()
|
|
51
|
+
.then(({ isSupported }) => { if (mounted) setIsBlurSupported(!!isSupported) })
|
|
52
|
+
.catch(() => { /* leave unsupported */ })
|
|
53
|
+
return () => { mounted = false }
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const toggleBlur = () => {
|
|
57
|
+
setIsBlurEnabled(prev => {
|
|
58
|
+
const next = !prev
|
|
59
|
+
writeBlurPreference(next)
|
|
60
|
+
return next
|
|
61
|
+
})
|
|
62
|
+
}
|
|
16
63
|
|
|
17
64
|
// Enumerate video devices
|
|
18
65
|
useEffect(() => {
|
|
@@ -43,6 +90,11 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
43
90
|
let mounted = true
|
|
44
91
|
|
|
45
92
|
const getVideoTrack = async () => {
|
|
93
|
+
// Detach blur from previous track, if attached
|
|
94
|
+
if (blurAttachedTrackRef.current && blurProcessorRef.current) {
|
|
95
|
+
try { blurAttachedTrackRef.current.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
96
|
+
blurAttachedTrackRef.current = null
|
|
97
|
+
}
|
|
46
98
|
// Stop existing track
|
|
47
99
|
if (trackRef.current) {
|
|
48
100
|
trackRef.current.stop()
|
|
@@ -71,6 +123,7 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
71
123
|
videoElement.style.transform = 'scaleX(-1)'
|
|
72
124
|
containerRef.current.appendChild(videoElement)
|
|
73
125
|
setLoading(false)
|
|
126
|
+
setTrackVersion(v => v + 1)
|
|
74
127
|
} else if (!mounted) {
|
|
75
128
|
track.stop()
|
|
76
129
|
}
|
|
@@ -86,6 +139,10 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
86
139
|
|
|
87
140
|
return () => {
|
|
88
141
|
mounted = false
|
|
142
|
+
if (blurAttachedTrackRef.current && blurProcessorRef.current) {
|
|
143
|
+
try { blurAttachedTrackRef.current.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
144
|
+
blurAttachedTrackRef.current = null
|
|
145
|
+
}
|
|
89
146
|
if (trackRef.current) {
|
|
90
147
|
trackRef.current.stop()
|
|
91
148
|
trackRef.current = null
|
|
@@ -96,6 +153,63 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
96
153
|
}
|
|
97
154
|
}, [selectedDeviceId])
|
|
98
155
|
|
|
156
|
+
// Sync blur processor with the current preview track + enabled state
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!isBlurSupported) return
|
|
159
|
+
const track = trackRef.current
|
|
160
|
+
if (!track) return
|
|
161
|
+
|
|
162
|
+
let cancelled = false
|
|
163
|
+
const apply = async () => {
|
|
164
|
+
const previouslyAttached = blurAttachedTrackRef.current
|
|
165
|
+
if (previouslyAttached && previouslyAttached !== track && blurProcessorRef.current) {
|
|
166
|
+
try { previouslyAttached.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
167
|
+
blurAttachedTrackRef.current = null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isBlurEnabled) {
|
|
171
|
+
if (!blurProcessorRef.current) {
|
|
172
|
+
setIsBlurLoading(true)
|
|
173
|
+
try {
|
|
174
|
+
const { GaussianBlurBackgroundProcessor } = await loadTwilioVideoProcessorsModule()
|
|
175
|
+
if (cancelled) return
|
|
176
|
+
const processor = new GaussianBlurBackgroundProcessor({
|
|
177
|
+
assetsPath: BLUR_BACKGROUND_ASSETS_PATH,
|
|
178
|
+
})
|
|
179
|
+
await processor.loadModel()
|
|
180
|
+
if (cancelled) return
|
|
181
|
+
blurProcessorRef.current = processor
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('Failed to load Twilio video blur processor:', err)
|
|
184
|
+
if (!cancelled) setIsBlurEnabled(false)
|
|
185
|
+
return
|
|
186
|
+
} finally {
|
|
187
|
+
if (!cancelled) setIsBlurLoading(false)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (blurAttachedTrackRef.current !== track && blurProcessorRef.current) {
|
|
192
|
+
try {
|
|
193
|
+
track.addProcessor(blurProcessorRef.current, {
|
|
194
|
+
inputFrameBufferType: 'videoframe',
|
|
195
|
+
outputFrameBufferContextType: 'bitmaprenderer',
|
|
196
|
+
})
|
|
197
|
+
blurAttachedTrackRef.current = track
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error('Failed to attach blur processor to track:', err)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else if (blurAttachedTrackRef.current === track && blurProcessorRef.current) {
|
|
203
|
+
try { track.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
204
|
+
blurAttachedTrackRef.current = null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
apply()
|
|
209
|
+
|
|
210
|
+
return () => { cancelled = true }
|
|
211
|
+
}, [trackVersion, isBlurEnabled, isBlurSupported])
|
|
212
|
+
|
|
99
213
|
return (
|
|
100
214
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
|
|
101
215
|
{/* Camera selector */}
|
|
@@ -128,6 +242,7 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
128
242
|
display: 'flex',
|
|
129
243
|
alignItems: 'center',
|
|
130
244
|
justifyContent: 'center',
|
|
245
|
+
position: 'relative',
|
|
131
246
|
...style,
|
|
132
247
|
}}
|
|
133
248
|
>
|
|
@@ -145,6 +260,26 @@ export const TwilioLocalPreview: React.FC<TwilioLocalPreviewProps> = ({ style })
|
|
|
145
260
|
display: loading || error ? 'none' : 'block',
|
|
146
261
|
}}
|
|
147
262
|
/>
|
|
263
|
+
{isBlurSupported && !error && (
|
|
264
|
+
<IconButton
|
|
265
|
+
onClick={toggleBlur}
|
|
266
|
+
disabled={isBlurLoading || loading}
|
|
267
|
+
size="small"
|
|
268
|
+
sx={{
|
|
269
|
+
position: 'absolute',
|
|
270
|
+
bottom: 8,
|
|
271
|
+
right: 8,
|
|
272
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
273
|
+
color: isBlurEnabled ? '#4caf50' : 'white',
|
|
274
|
+
'&:hover': { backgroundColor: 'rgba(0,0,0,0.7)' },
|
|
275
|
+
'&.Mui-disabled': { color: 'rgba(255,255,255,0.5)' },
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{isBlurLoading
|
|
279
|
+
? <CircularProgress size={16} sx={{ color: 'white' }} />
|
|
280
|
+
: isBlurEnabled ? <BlurOnIcon fontSize="small" /> : <BlurOffIcon fontSize="small" />}
|
|
281
|
+
</IconButton>
|
|
282
|
+
)}
|
|
148
283
|
</Box>
|
|
149
284
|
</Box>
|
|
150
285
|
)
|
|
@@ -8,9 +8,38 @@ import Video, {
|
|
|
8
8
|
RemoteTrack,
|
|
9
9
|
LocalParticipant,
|
|
10
10
|
} from 'twilio-video'
|
|
11
|
+
import type { GaussianBlurBackgroundProcessor as GaussianBlurBackgroundProcessorType } from '@twilio/video-processors'
|
|
11
12
|
|
|
12
13
|
export const SCREEN_SHARE_TRACK_NAME = 'screen-share'
|
|
13
14
|
|
|
15
|
+
export const BLUR_BACKGROUND_STORAGE_KEY = 'tellescope.twilio.blurBackground'
|
|
16
|
+
export const BLUR_BACKGROUND_ASSETS_PATH = '/twilio-video-processors'
|
|
17
|
+
|
|
18
|
+
let videoProcessorsModulePromise: Promise<typeof import('@twilio/video-processors')> | null = null
|
|
19
|
+
export const loadTwilioVideoProcessorsModule = () => {
|
|
20
|
+
if (!videoProcessorsModulePromise) {
|
|
21
|
+
videoProcessorsModulePromise = import('@twilio/video-processors')
|
|
22
|
+
}
|
|
23
|
+
return videoProcessorsModulePromise
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const readBlurPreference = (): boolean => {
|
|
27
|
+
try {
|
|
28
|
+
return typeof localStorage !== 'undefined'
|
|
29
|
+
&& localStorage.getItem(BLUR_BACKGROUND_STORAGE_KEY) === 'true'
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const writeBlurPreference = (enabled: boolean) => {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof localStorage !== 'undefined') {
|
|
38
|
+
localStorage.setItem(BLUR_BACKGROUND_STORAGE_KEY, enabled ? 'true' : 'false')
|
|
39
|
+
}
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
export interface TwilioVideoState {
|
|
15
44
|
room: Room | null
|
|
16
45
|
isConnecting: boolean
|
|
@@ -25,6 +54,9 @@ export interface TwilioVideoState {
|
|
|
25
54
|
isHost: boolean
|
|
26
55
|
isVideoEnabled: boolean
|
|
27
56
|
isAudioEnabled: boolean
|
|
57
|
+
isBlurSupported: boolean
|
|
58
|
+
isBlurEnabled: boolean
|
|
59
|
+
isBlurLoading: boolean
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
export interface TwilioVideoActions {
|
|
@@ -33,6 +65,7 @@ export interface TwilioVideoActions {
|
|
|
33
65
|
toggleVideo: () => Promise<void>
|
|
34
66
|
toggleAudio: () => void
|
|
35
67
|
toggleScreenShare: () => Promise<void>
|
|
68
|
+
toggleBlur: () => void
|
|
36
69
|
setIsHost: (isHost: boolean) => void
|
|
37
70
|
}
|
|
38
71
|
|
|
@@ -65,13 +98,22 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
65
98
|
const [localScreenTrack, setLocalScreenTrack] = useState<LocalVideoTrack | null>(null)
|
|
66
99
|
const [isScreenSharing, setIsScreenSharing] = useState(false)
|
|
67
100
|
const [screenSharingParticipantSid, setScreenSharingParticipantSid] = useState<string | null>(null)
|
|
101
|
+
const [isBlurSupported, setIsBlurSupported] = useState(false)
|
|
102
|
+
const [isBlurEnabled, setIsBlurEnabled] = useState<boolean>(readBlurPreference)
|
|
103
|
+
const [isBlurLoading, setIsBlurLoading] = useState(false)
|
|
68
104
|
|
|
69
105
|
const localTracksRef = useRef<(LocalVideoTrack | LocalAudioTrack)[]>([])
|
|
106
|
+
const blurProcessorRef = useRef<GaussianBlurBackgroundProcessorType | null>(null)
|
|
107
|
+
const blurAttachedTrackRef = useRef<LocalVideoTrack | null>(null)
|
|
70
108
|
|
|
71
109
|
const connect = useCallback(async (token: string, roomName: string) => {
|
|
72
110
|
setIsConnecting(true)
|
|
73
111
|
setError(null)
|
|
74
112
|
|
|
113
|
+
// Pick up any preference set by the pre-call preview, which lives outside
|
|
114
|
+
// this provider and only persists via localStorage.
|
|
115
|
+
setIsBlurEnabled(readBlurPreference())
|
|
116
|
+
|
|
75
117
|
try {
|
|
76
118
|
// Create local tracks
|
|
77
119
|
const tracks = await Video.createLocalTracks({
|
|
@@ -234,9 +276,89 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
234
276
|
}
|
|
235
277
|
}, [isScreenSharing, stopScreenShare, room])
|
|
236
278
|
|
|
279
|
+
const toggleBlur = useCallback(() => {
|
|
280
|
+
setIsBlurEnabled(prev => {
|
|
281
|
+
const next = !prev
|
|
282
|
+
writeBlurPreference(next)
|
|
283
|
+
return next
|
|
284
|
+
})
|
|
285
|
+
}, [])
|
|
286
|
+
|
|
287
|
+
// Probe video-processors support once on mount
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
let mounted = true
|
|
290
|
+
loadTwilioVideoProcessorsModule()
|
|
291
|
+
.then(({ isSupported }) => { if (mounted) setIsBlurSupported(!!isSupported) })
|
|
292
|
+
.catch(() => { /* leave unsupported */ })
|
|
293
|
+
return () => { mounted = false }
|
|
294
|
+
}, [])
|
|
295
|
+
|
|
296
|
+
// Sync blur processor with the current local video track + enabled state
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!isBlurSupported) return
|
|
299
|
+
const track = localVideoTrack
|
|
300
|
+
|
|
301
|
+
let cancelled = false
|
|
302
|
+
const apply = async () => {
|
|
303
|
+
// Detach from any previously-attached track if it differs from the current one
|
|
304
|
+
const previouslyAttached = blurAttachedTrackRef.current
|
|
305
|
+
if (previouslyAttached && previouslyAttached !== track && blurProcessorRef.current) {
|
|
306
|
+
try { previouslyAttached.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
307
|
+
blurAttachedTrackRef.current = null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!track) return
|
|
311
|
+
|
|
312
|
+
if (isBlurEnabled) {
|
|
313
|
+
if (!blurProcessorRef.current) {
|
|
314
|
+
setIsBlurLoading(true)
|
|
315
|
+
try {
|
|
316
|
+
const { GaussianBlurBackgroundProcessor } = await loadTwilioVideoProcessorsModule()
|
|
317
|
+
if (cancelled) return
|
|
318
|
+
const processor = new GaussianBlurBackgroundProcessor({
|
|
319
|
+
assetsPath: BLUR_BACKGROUND_ASSETS_PATH,
|
|
320
|
+
})
|
|
321
|
+
await processor.loadModel()
|
|
322
|
+
if (cancelled) return
|
|
323
|
+
blurProcessorRef.current = processor
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.error('Failed to load Twilio video blur processor:', err)
|
|
326
|
+
if (!cancelled) setIsBlurEnabled(false)
|
|
327
|
+
return
|
|
328
|
+
} finally {
|
|
329
|
+
if (!cancelled) setIsBlurLoading(false)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (blurAttachedTrackRef.current !== track && blurProcessorRef.current) {
|
|
334
|
+
try {
|
|
335
|
+
track.addProcessor(blurProcessorRef.current, {
|
|
336
|
+
inputFrameBufferType: 'videoframe',
|
|
337
|
+
outputFrameBufferContextType: 'bitmaprenderer',
|
|
338
|
+
})
|
|
339
|
+
blurAttachedTrackRef.current = track
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.error('Failed to attach blur processor to track:', err)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else if (blurAttachedTrackRef.current === track && blurProcessorRef.current) {
|
|
345
|
+
try { track.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
346
|
+
blurAttachedTrackRef.current = null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
apply()
|
|
351
|
+
|
|
352
|
+
return () => { cancelled = true }
|
|
353
|
+
}, [localVideoTrack, isBlurEnabled, isBlurSupported])
|
|
354
|
+
|
|
237
355
|
// Cleanup on unmount
|
|
238
356
|
useEffect(() => {
|
|
239
357
|
return () => {
|
|
358
|
+
if (blurAttachedTrackRef.current && blurProcessorRef.current) {
|
|
359
|
+
try { blurAttachedTrackRef.current.removeProcessor(blurProcessorRef.current) } catch { /* ignore */ }
|
|
360
|
+
}
|
|
361
|
+
blurAttachedTrackRef.current = null
|
|
240
362
|
if (room) {
|
|
241
363
|
room.disconnect()
|
|
242
364
|
}
|
|
@@ -260,11 +382,15 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
260
382
|
isHost,
|
|
261
383
|
isVideoEnabled,
|
|
262
384
|
isAudioEnabled,
|
|
385
|
+
isBlurSupported,
|
|
386
|
+
isBlurEnabled,
|
|
387
|
+
isBlurLoading,
|
|
263
388
|
connect,
|
|
264
389
|
disconnect,
|
|
265
390
|
toggleVideo,
|
|
266
391
|
toggleAudio,
|
|
267
392
|
toggleScreenShare,
|
|
393
|
+
toggleBlur,
|
|
268
394
|
setIsHost,
|
|
269
395
|
}
|
|
270
396
|
|