@tellescope/react-components 1.249.0 → 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.
Files changed (80) hide show
  1. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  2. package/lib/cjs/Forms/forms.js +13 -5
  3. package/lib/cjs/Forms/forms.js.map +1 -1
  4. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  5. package/lib/cjs/Forms/forms.v2.js +13 -5
  6. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  7. package/lib/cjs/Forms/hooks.d.ts +2 -1
  8. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  9. package/lib/cjs/Forms/hooks.js +50 -27
  10. package/lib/cjs/Forms/hooks.js.map +1 -1
  11. package/lib/cjs/Forms/inputs.d.ts +19 -4
  12. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  13. package/lib/cjs/Forms/inputs.js +224 -173
  14. package/lib/cjs/Forms/inputs.js.map +1 -1
  15. package/lib/cjs/Forms/inputs.v2.d.ts +7 -3
  16. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  17. package/lib/cjs/Forms/inputs.v2.js +42 -32
  18. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  19. package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
  20. package/lib/cjs/TwilioVideo/TwilioControls.js +12 -2
  21. package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
  22. package/lib/cjs/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
  23. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js +154 -2
  24. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js.map +1 -1
  25. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +7 -0
  26. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  27. package/lib/cjs/TwilioVideo/TwilioVideoContext.js +148 -1
  28. package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
  29. package/lib/esm/CMS/components.d.ts +0 -1
  30. package/lib/esm/CMS/components.d.ts.map +1 -1
  31. package/lib/esm/Forms/forms.d.ts +3 -3
  32. package/lib/esm/Forms/forms.d.ts.map +1 -1
  33. package/lib/esm/Forms/forms.js +13 -5
  34. package/lib/esm/Forms/forms.js.map +1 -1
  35. package/lib/esm/Forms/forms.v2.d.ts +3 -3
  36. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  37. package/lib/esm/Forms/forms.v2.js +13 -5
  38. package/lib/esm/Forms/forms.v2.js.map +1 -1
  39. package/lib/esm/Forms/hooks.d.ts +2 -1
  40. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  41. package/lib/esm/Forms/hooks.js +50 -27
  42. package/lib/esm/Forms/hooks.js.map +1 -1
  43. package/lib/esm/Forms/inputs.d.ts +21 -6
  44. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  45. package/lib/esm/Forms/inputs.js +69 -20
  46. package/lib/esm/Forms/inputs.js.map +1 -1
  47. package/lib/esm/Forms/inputs.native.d.ts +0 -1
  48. package/lib/esm/Forms/inputs.native.d.ts.map +1 -1
  49. package/lib/esm/Forms/inputs.v2.d.ts +7 -3
  50. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  51. package/lib/esm/Forms/inputs.v2.js +27 -17
  52. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  53. package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
  54. package/lib/esm/TwilioVideo/TwilioControls.js +14 -4
  55. package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
  56. package/lib/esm/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
  57. package/lib/esm/TwilioVideo/TwilioLocalPreview.js +155 -3
  58. package/lib/esm/TwilioVideo/TwilioLocalPreview.js.map +1 -1
  59. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +7 -0
  60. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  61. package/lib/esm/TwilioVideo/TwilioVideoContext.js +146 -0
  62. package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
  63. package/lib/esm/TwilioVideo/hooks.d.ts +1 -1
  64. package/lib/esm/controls.d.ts +2 -2
  65. package/lib/esm/inputs.d.ts +1 -1
  66. package/lib/esm/inputs.native.d.ts +0 -1
  67. package/lib/esm/inputs.native.d.ts.map +1 -1
  68. package/lib/esm/state.d.ts +330 -330
  69. package/lib/esm/theme.native.d.ts +0 -1
  70. package/lib/esm/theme.native.d.ts.map +1 -1
  71. package/lib/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +11 -10
  73. package/src/Forms/forms.tsx +18 -2
  74. package/src/Forms/forms.v2.tsx +18 -2
  75. package/src/Forms/hooks.tsx +69 -32
  76. package/src/Forms/inputs.tsx +143 -18
  77. package/src/Forms/inputs.v2.tsx +58 -8
  78. package/src/TwilioVideo/TwilioControls.tsx +27 -1
  79. package/src/TwilioVideo/TwilioLocalPreview.tsx +136 -1
  80. 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 { Box, Typography, CircularProgress, FormControl, InputLabel, Select, MenuItem } from '@mui/material'
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