@fraku/video 0.0.1
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/README.md +2 -0
- package/package.json +75 -0
- package/src/App.tsx +7 -0
- package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.stories.tsx +50 -0
- package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.tsx +253 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/ButtonsDock.tsx +353 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/DockButton.tsx +90 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/MenuItemTemplate.tsx +35 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/MobileIconButton/MobileIconButton.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/MobileIconButton/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/Overlay/Overlay.tsx +74 -0
- package/src/components/ZoomVideoPlugin/components/Overlay/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/ParticipantsList.tsx +52 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsContent.tsx +19 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsMenu.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsOverlay.tsx +52 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/AudioSettings.tsx +191 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/BackgroundSettings.tsx +47 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownItemTemplate.tsx +20 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownValueTemplate.tsx +12 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/VideoSettings.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/context.ts +20 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/Video.tsx +35 -0
- package/src/components/ZoomVideoPlugin/constants.ts +4 -0
- package/src/components/ZoomVideoPlugin/context.ts +86 -0
- package/src/components/ZoomVideoPlugin/hooks/useClientMessages.ts +142 -0
- package/src/components/ZoomVideoPlugin/hooks/useDeviceSize.ts +24 -0
- package/src/components/ZoomVideoPlugin/hooks/useStartVideoOptions.ts +14 -0
- package/src/components/ZoomVideoPlugin/hooks/useZoomVideoPlayer.tsx +142 -0
- package/src/components/ZoomVideoPlugin/index.ts +2 -0
- package/src/components/ZoomVideoPlugin/lib/platforms.ts +17 -0
- package/src/components/ZoomVideoPlugin/pages/AfterSession.tsx +14 -0
- package/src/components/ZoomVideoPlugin/pages/MainSession.tsx +53 -0
- package/src/components/ZoomVideoPlugin/pages/PanelistsSession.tsx +97 -0
- package/src/components/ZoomVideoPlugin/pages/PreSessionConfiguration.tsx +154 -0
- package/src/components/ZoomVideoPlugin/types.global.d.ts +15 -0
- package/src/components/ZoomVideoPlugin/types.ts +23 -0
- package/src/global.d.ts +46 -0
- package/src/index.css +4 -0
- package/src/index.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { Breakpoint } from '../../hooks/useDeviceSize'
|
|
3
|
+
import Overlay from '../Overlay'
|
|
4
|
+
import { SETTINGS_OVERLAY_WIDTH } from '../../constants'
|
|
5
|
+
import SettingsMenu from './SettingsMenu'
|
|
6
|
+
import SettingsContent from './SettingsContent'
|
|
7
|
+
import { settingsOverlayContext, SettingsOverlayContextProps, SettingsTab } from './context'
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
breakpoint: Breakpoint
|
|
11
|
+
visible: boolean
|
|
12
|
+
onHide: () => void
|
|
13
|
+
defaultTab?: SettingsTab
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SettingsOverlay = ({ visible, onHide, breakpoint, defaultTab = SettingsTab.Audio }: Props) => {
|
|
17
|
+
const [selectedSettingsTab, setSelectedSettingsTab] = useState<SettingsTab>(defaultTab)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (visible) setSelectedSettingsTab(defaultTab)
|
|
21
|
+
}, [defaultTab, visible])
|
|
22
|
+
|
|
23
|
+
const contextVal = useMemo<SettingsOverlayContextProps>(
|
|
24
|
+
() => ({
|
|
25
|
+
selectedSettingsTab,
|
|
26
|
+
setSelectedSettingsTab
|
|
27
|
+
}),
|
|
28
|
+
[selectedSettingsTab]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Overlay
|
|
33
|
+
visible={visible}
|
|
34
|
+
onHide={onHide}
|
|
35
|
+
header="Settings"
|
|
36
|
+
breakpoint={breakpoint}
|
|
37
|
+
width={SETTINGS_OVERLAY_WIDTH}
|
|
38
|
+
className="min-w-[30vw] min-h-[30vh] max-h-[60vh] h-full [&_.p-dialog-content]:overflow-hidden"
|
|
39
|
+
>
|
|
40
|
+
<settingsOverlayContext.Provider value={contextVal}>
|
|
41
|
+
<div className="flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x divide-neutral-80 h-full">
|
|
42
|
+
<SettingsMenu />
|
|
43
|
+
<div className="flex-1 overflow-y-auto p-12">
|
|
44
|
+
<SettingsContent />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</settingsOverlayContext.Provider>
|
|
48
|
+
</Overlay>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default SettingsOverlay
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { ProgressBar } from 'primereact/progressbar'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import { Dropdown } from 'primereact/dropdown'
|
|
5
|
+
import { useZoomVideoContext } from '../../../context'
|
|
6
|
+
import DropdownValueTemplate from './DropdownValueTemplate'
|
|
7
|
+
import DropdownItemTemplate from './DropdownItemTemplate'
|
|
8
|
+
|
|
9
|
+
const UPDATE_INTERVAL_MS = 300
|
|
10
|
+
|
|
11
|
+
const AudioSettings = () => {
|
|
12
|
+
const {
|
|
13
|
+
activeAudioOutput,
|
|
14
|
+
activeMicrophone,
|
|
15
|
+
audioOutputList,
|
|
16
|
+
localAudio,
|
|
17
|
+
micList,
|
|
18
|
+
switchActiveAudioOutput,
|
|
19
|
+
switchActiveMicrophone
|
|
20
|
+
} = useZoomVideoContext()
|
|
21
|
+
|
|
22
|
+
const [micLevel, setMicLevel] = useState<number>(0)
|
|
23
|
+
const [speakerLevel, setSpeakerLevel] = useState<number>(0)
|
|
24
|
+
const [micState, setMicState] = useState<'idle' | 'recording' | 'playing'>('idle')
|
|
25
|
+
|
|
26
|
+
const micListOptions = micList.map((mic) => ({
|
|
27
|
+
label: mic.label || `Micrófono ${mic.deviceId}`,
|
|
28
|
+
value: mic.deviceId
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
const speakerOptions = audioOutputList.map((spk) => ({
|
|
32
|
+
label: spk.label || `Altavoz ${spk.deviceId}`,
|
|
33
|
+
value: spk.deviceId
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const micTesterRef = useRef<any>(null)
|
|
37
|
+
const lastUpdateMicRef = useRef<number>(0)
|
|
38
|
+
const handleMicTest = useCallback(async () => {
|
|
39
|
+
if (!localAudio) return
|
|
40
|
+
|
|
41
|
+
if (micTesterRef.current) {
|
|
42
|
+
micTesterRef.current.stop?.()
|
|
43
|
+
micTesterRef.current = null
|
|
44
|
+
setMicState('idle')
|
|
45
|
+
setMicLevel(0)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tester = localAudio.testMicrophone({
|
|
50
|
+
microphoneId: activeMicrophone,
|
|
51
|
+
speakerId: activeAudioOutput,
|
|
52
|
+
recordAndPlay: true,
|
|
53
|
+
onAnalyseFrequency: (v: number) => {
|
|
54
|
+
const now = Date.now()
|
|
55
|
+
// only update every UPDATE_INTERVAL_MS ms
|
|
56
|
+
if (now - lastUpdateMicRef.current > UPDATE_INTERVAL_MS) {
|
|
57
|
+
lastUpdateMicRef.current = now
|
|
58
|
+
setMicLevel(v)
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
onStartRecording: () => setMicState('recording'),
|
|
62
|
+
onStartPlayRecording: () => setMicState('playing'),
|
|
63
|
+
onStopPlayRecording: () => {
|
|
64
|
+
setMicState('idle')
|
|
65
|
+
micTesterRef.current = null
|
|
66
|
+
setMicLevel(0)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
micTesterRef.current = tester
|
|
71
|
+
setMicState('recording')
|
|
72
|
+
}, [localAudio, activeMicrophone, activeAudioOutput])
|
|
73
|
+
|
|
74
|
+
const speakerTesterRef = useRef<any>(null)
|
|
75
|
+
const lastUpdateSpeakerRef = useRef<number>(0)
|
|
76
|
+
const handleSpeakerTest = useCallback(async () => {
|
|
77
|
+
if (!localAudio) return
|
|
78
|
+
if (speakerTesterRef.current) {
|
|
79
|
+
speakerTesterRef.current.destroy?.()
|
|
80
|
+
speakerTesterRef.current = null
|
|
81
|
+
setSpeakerLevel(0)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const tester = localAudio.testSpeaker({
|
|
85
|
+
speakerId: activeAudioOutput,
|
|
86
|
+
onAnalyseFrequency: (v: number) => {
|
|
87
|
+
const now = Date.now()
|
|
88
|
+
// only update every UPDATE_INTERVAL_MS ms
|
|
89
|
+
if (now - lastUpdateSpeakerRef.current > UPDATE_INTERVAL_MS) {
|
|
90
|
+
lastUpdateSpeakerRef.current = now
|
|
91
|
+
setSpeakerLevel(v)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
speakerTesterRef.current = tester
|
|
96
|
+
}, [localAudio, activeAudioOutput])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return () => {
|
|
100
|
+
micTesterRef.current?.stop?.()
|
|
101
|
+
speakerTesterRef.current?.destroy?.()
|
|
102
|
+
}
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex flex-col">
|
|
107
|
+
{/* Microphone Section */}
|
|
108
|
+
<p className="text-s font-bold">Micrófono</p>
|
|
109
|
+
<p className="text-s mb-8 text-neutral-40">Selecciona un método de entrada de audio</p>
|
|
110
|
+
<Dropdown
|
|
111
|
+
className="mb-16 w-full"
|
|
112
|
+
itemTemplate={(option) => <DropdownItemTemplate option={option} isActive={option.value === activeMicrophone} />}
|
|
113
|
+
placeholder="Selecciona un micrófono"
|
|
114
|
+
options={micListOptions}
|
|
115
|
+
onChange={async (e) => await switchActiveMicrophone(e.value)}
|
|
116
|
+
value={activeMicrophone}
|
|
117
|
+
valueTemplate={(option) => <DropdownValueTemplate option={option} icon="fa-regular fa-microphone" />}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
{/* Microphone Test Header */}
|
|
121
|
+
<div className="mt-4 mb-8">
|
|
122
|
+
<p className="text-s font-bold">Prueba de micrófono</p>
|
|
123
|
+
<div className="mt-4 self-start">
|
|
124
|
+
<p className="text-s text-neutral-40 text-start">
|
|
125
|
+
{micState === 'idle' && 'Graba una muestra de tu voz y escúchela para verificar el funcionamiento.'}
|
|
126
|
+
{micState === 'recording' && 'Grabando la voz... espera unos segundos.'}
|
|
127
|
+
{micState === 'playing' && 'Reproduciendo grabación...'}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={handleMicTest}
|
|
135
|
+
className={cn(
|
|
136
|
+
'w-fit rounded-l p-8 font-semibold transition-all duration-200 border hover:border-neutral-70 text-white',
|
|
137
|
+
{
|
|
138
|
+
'bg-neutral-60': micState === 'recording' || micState === 'playing',
|
|
139
|
+
'bg-neutral-50': micState === 'idle'
|
|
140
|
+
}
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{micState === 'recording' && 'Detener grabación'}
|
|
144
|
+
{micState === 'playing' && 'Detener reproducción'}
|
|
145
|
+
{micState === 'idle' && 'Probar micrófono'}
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
{/* Status message */}
|
|
149
|
+
<ProgressBar
|
|
150
|
+
value={micState === 'idle' ? 0 : micLevel}
|
|
151
|
+
showValue={false}
|
|
152
|
+
className="w-full mt-4 h-8 rounded-m border border-neutral-80"
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{/* Speaker Section */}
|
|
156
|
+
<div className="mt-32">
|
|
157
|
+
<p className="text-s font-bold">Altavoz</p>
|
|
158
|
+
<p className="text-s mb-8 text-neutral-40">Selecciona un método de salida de audio</p>
|
|
159
|
+
<Dropdown
|
|
160
|
+
className="mb-16 w-full"
|
|
161
|
+
itemTemplate={(option) => (
|
|
162
|
+
<DropdownItemTemplate option={option} isActive={option.value === activeAudioOutput} />
|
|
163
|
+
)}
|
|
164
|
+
options={speakerOptions}
|
|
165
|
+
placeholder="Selecciona un altavoz"
|
|
166
|
+
onChange={async (e) => await switchActiveAudioOutput(e.value)}
|
|
167
|
+
value={activeAudioOutput}
|
|
168
|
+
valueTemplate={(option) => <DropdownValueTemplate option={option} icon="fa-regular fa-volume" />}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={handleSpeakerTest}
|
|
174
|
+
className={cn(
|
|
175
|
+
'w-fit rounded-l p-8 font-semibold transition-all duration-200 border hover:border-neutral-70 text-white',
|
|
176
|
+
speakerTesterRef.current ? 'bg-neutral-60' : 'bg-neutral-50'
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
{speakerTesterRef.current ? 'Detener prueba de altavoz' : 'Probar altavoz'}
|
|
180
|
+
</button>
|
|
181
|
+
<ProgressBar
|
|
182
|
+
value={speakerLevel}
|
|
183
|
+
showValue={false}
|
|
184
|
+
className="w-full mt-8 h-8 rounded-m border border-neutral-80"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default AudioSettings
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import cn from 'classnames'
|
|
2
|
+
import { useZoomVideoContext } from '../../../context'
|
|
3
|
+
|
|
4
|
+
const BackgroundSettings = () => {
|
|
5
|
+
const { isBlurred, setIsBlurred } = useZoomVideoContext()
|
|
6
|
+
|
|
7
|
+
const options = [
|
|
8
|
+
{
|
|
9
|
+
label: 'Ninguno',
|
|
10
|
+
icon: 'fa-ban',
|
|
11
|
+
isActive: !isBlurred
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
label: 'Desenfocar',
|
|
15
|
+
icon: 'fa-droplet',
|
|
16
|
+
isActive: isBlurred
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-col">
|
|
22
|
+
<p className="text-s font-bold mb-8">Fondo</p>
|
|
23
|
+
<div className="flex gap-12">
|
|
24
|
+
{options.map(({ label, icon, isActive }) => {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
key={label}
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={() => setIsBlurred((prev: boolean) => (isActive ? prev : !prev))}
|
|
30
|
+
className={cn(
|
|
31
|
+
'w-full h-80 flex flex-col items-center justify-center gap-8 border rounded-l bg-white font-bold transition-colors ease-in-out',
|
|
32
|
+
isActive ? '!border-neutral-60 border-2' : 'border-neutral-80 hover:border-neutral-70'
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<span className="flex items-center justify-center w-24 h-24">
|
|
36
|
+
<i className={cn('fa-regular fa-xl', icon)} aria-hidden="true" />
|
|
37
|
+
</span>
|
|
38
|
+
<span>{label}</span>
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default BackgroundSettings
|
package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownItemTemplate.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const DropdownItemTemplate = ({
|
|
2
|
+
option,
|
|
3
|
+
isActive
|
|
4
|
+
}: {
|
|
5
|
+
option: { label: string; value: string }
|
|
6
|
+
isActive: boolean
|
|
7
|
+
}) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center gap-4">
|
|
10
|
+
{isActive && (
|
|
11
|
+
<span className="flex items-center justify-center w-24 h-24">
|
|
12
|
+
<i className="fa-regular fa-check" aria-hidden="true" />
|
|
13
|
+
</span>
|
|
14
|
+
)}
|
|
15
|
+
<span className="mr-2">{option.label}</span>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default DropdownItemTemplate
|
package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownValueTemplate.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const DropdownValueTemplate = ({ option, icon }: { option: { label: string; value: string }; icon: string }) => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center gap-4 font-semibold text-neutral-50">
|
|
4
|
+
<span className="flex items-center justify-center w-24 h-24">
|
|
5
|
+
<i className={icon} aria-hidden="true" />
|
|
6
|
+
</span>
|
|
7
|
+
<span className="truncate">{option.label}</span>
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default DropdownValueTemplate
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Dropdown } from 'primereact/dropdown'
|
|
2
|
+
import { useZoomVideoContext } from '../../../context'
|
|
3
|
+
import DropdownValueTemplate from './DropdownValueTemplate'
|
|
4
|
+
import DropdownItemTemplate from './DropdownItemTemplate'
|
|
5
|
+
|
|
6
|
+
const VideoSettings = () => {
|
|
7
|
+
const { activeCamera, cameraList, setActiveCamera } = useZoomVideoContext()
|
|
8
|
+
|
|
9
|
+
const cameraOptions = cameraList.map((camera) => ({
|
|
10
|
+
label: camera.label || `Cámara ${camera.deviceId}`,
|
|
11
|
+
value: camera.deviceId
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-col">
|
|
16
|
+
<p className="text-s font-bold mb-8">Cámara</p>
|
|
17
|
+
<Dropdown
|
|
18
|
+
itemTemplate={(option) => <DropdownItemTemplate option={option} isActive={option.value === activeCamera} />}
|
|
19
|
+
className="mb-16"
|
|
20
|
+
onChange={(e) => setActiveCamera(e.value)}
|
|
21
|
+
options={cameraOptions}
|
|
22
|
+
placeholder="Selecciona una cámara"
|
|
23
|
+
value={activeCamera ?? ''}
|
|
24
|
+
valueTemplate={(option) => <DropdownValueTemplate option={option} icon="fa-regular fa-video" />}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default VideoSettings
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useContext, createContext } from 'react'
|
|
2
|
+
import { ReactSetter } from '../../types'
|
|
3
|
+
|
|
4
|
+
export enum SettingsTab {
|
|
5
|
+
Audio = 'Audio',
|
|
6
|
+
Video = 'Video',
|
|
7
|
+
Background = 'Background'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SettingsOverlayContextProps = {
|
|
11
|
+
selectedSettingsTab: SettingsTab
|
|
12
|
+
setSelectedSettingsTab: ReactSetter<SettingsTab>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const settingsOverlayContext = createContext<SettingsOverlayContextProps>({
|
|
16
|
+
selectedSettingsTab: SettingsTab.Audio,
|
|
17
|
+
setSelectedSettingsTab: () => {}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const useSettingsOverlayContext = () => useContext(settingsOverlayContext)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './SettingsOverlay'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
|
|
3
|
+
type VideoProps = {
|
|
4
|
+
fullRef: React.RefObject<HTMLVideoElement> | null
|
|
5
|
+
}
|
|
6
|
+
const Video = ({ fullRef }: VideoProps) => {
|
|
7
|
+
return (
|
|
8
|
+
<StyledVideoContainer>
|
|
9
|
+
<video-player-container className="video-player-container">
|
|
10
|
+
<video-player ref={fullRef} className="video-player" />
|
|
11
|
+
</video-player-container>
|
|
12
|
+
</StyledVideoContainer>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const StyledVideoContainer = styled.div`
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: auto;
|
|
19
|
+
aspect-ratio: 16/9;
|
|
20
|
+
|
|
21
|
+
& video-player-container {
|
|
22
|
+
margin-left: auto;
|
|
23
|
+
margin-right: auto;
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: auto;
|
|
26
|
+
|
|
27
|
+
video-player {
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: auto;
|
|
30
|
+
aspect-ratio: 16/9;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
export default Video
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import ZoomVideo, {
|
|
3
|
+
ActiveSpeaker,
|
|
4
|
+
ConnectionState,
|
|
5
|
+
LocalAudioTrack,
|
|
6
|
+
LocalVideoTrack,
|
|
7
|
+
MediaDevice,
|
|
8
|
+
Participant,
|
|
9
|
+
Stream,
|
|
10
|
+
VideoClient
|
|
11
|
+
} from '@zoom/videosdk'
|
|
12
|
+
import { Breakpoint } from './hooks/useDeviceSize'
|
|
13
|
+
|
|
14
|
+
export const zmClient = ZoomVideo.createClient()
|
|
15
|
+
|
|
16
|
+
export type ZoomVideoContextType = {
|
|
17
|
+
activeAudioOutput: string | undefined
|
|
18
|
+
activeCamera: string | undefined
|
|
19
|
+
activeMicrophone: string | undefined
|
|
20
|
+
activeSpeakers: ActiveSpeaker[]
|
|
21
|
+
activeVideoId: number | null
|
|
22
|
+
audioOutputList: MediaDevice[]
|
|
23
|
+
breakpoint: Breakpoint
|
|
24
|
+
cameraList: MediaDevice[]
|
|
25
|
+
closeParentContainer: () => void
|
|
26
|
+
connectionState: ConnectionState | null
|
|
27
|
+
isBlurred: boolean
|
|
28
|
+
isCamOn: boolean
|
|
29
|
+
isMicOn: boolean
|
|
30
|
+
localAudio: LocalAudioTrack | undefined
|
|
31
|
+
localVideo: LocalVideoTrack | undefined
|
|
32
|
+
mediaStream: typeof Stream | null
|
|
33
|
+
micList: MediaDevice[]
|
|
34
|
+
participants: Participant[]
|
|
35
|
+
setActiveCamera: ReactSetter<string | undefined>
|
|
36
|
+
setIsBlurred: ReactSetter<boolean>
|
|
37
|
+
setIsCamOn: ReactSetter<boolean>
|
|
38
|
+
setIsMicOn: ReactSetter<boolean>
|
|
39
|
+
setLocalAudio: ReactSetter<LocalAudioTrack>
|
|
40
|
+
setLocalVideo: ReactSetter<LocalVideoTrack>
|
|
41
|
+
setMediaStream: ReactSetter<typeof Stream | null>
|
|
42
|
+
setParticipants: ReactSetter<Participant[]>
|
|
43
|
+
stopSession: () => Promise<void>
|
|
44
|
+
switchActiveAudioOutput: (deviceId: string) => Promise<void>
|
|
45
|
+
switchActiveMicrophone: (deviceId: string) => Promise<void>
|
|
46
|
+
zmClient: typeof VideoClient
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const ZoomVideoContext = createContext<ZoomVideoContextType>({
|
|
50
|
+
activeAudioOutput: undefined,
|
|
51
|
+
activeCamera: undefined,
|
|
52
|
+
activeMicrophone: undefined,
|
|
53
|
+
activeSpeakers: [],
|
|
54
|
+
activeVideoId: null,
|
|
55
|
+
audioOutputList: [],
|
|
56
|
+
breakpoint: Breakpoint.Mobile,
|
|
57
|
+
cameraList: [],
|
|
58
|
+
closeParentContainer: () => {},
|
|
59
|
+
connectionState: null,
|
|
60
|
+
isBlurred: false,
|
|
61
|
+
isCamOn: false,
|
|
62
|
+
isMicOn: false,
|
|
63
|
+
localAudio: undefined,
|
|
64
|
+
localVideo: undefined,
|
|
65
|
+
mediaStream: null,
|
|
66
|
+
micList: [],
|
|
67
|
+
participants: [],
|
|
68
|
+
setActiveCamera: () => {},
|
|
69
|
+
setIsBlurred: () => {},
|
|
70
|
+
setIsCamOn: () => {},
|
|
71
|
+
setIsMicOn: () => {},
|
|
72
|
+
setLocalAudio: () => {},
|
|
73
|
+
setLocalVideo: () => {},
|
|
74
|
+
setMediaStream: () => {},
|
|
75
|
+
setParticipants: () => {},
|
|
76
|
+
stopSession: async () => {},
|
|
77
|
+
switchActiveAudioOutput: () => Promise.resolve(),
|
|
78
|
+
switchActiveMicrophone: () => Promise.resolve(),
|
|
79
|
+
zmClient
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const useZoomVideoContext = () => {
|
|
83
|
+
const ctx = useContext(ZoomVideoContext)
|
|
84
|
+
if (!ctx) throw new Error('useZoomVideo must be used within ZoomVideoProvider')
|
|
85
|
+
return ctx
|
|
86
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
type ActiveSpeaker,
|
|
4
|
+
CommandChannelMsg,
|
|
5
|
+
ConnectionChangePayload,
|
|
6
|
+
ConnectionState,
|
|
7
|
+
MediaDevice,
|
|
8
|
+
type Participant,
|
|
9
|
+
type Stream,
|
|
10
|
+
VideoActiveState,
|
|
11
|
+
type VideoClient
|
|
12
|
+
} from '@zoom/videosdk'
|
|
13
|
+
import { isAndroidOrIOSBrowser } from '../lib/platforms'
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
zmClient: typeof VideoClient | null
|
|
17
|
+
mediaStream: typeof Stream | null
|
|
18
|
+
setActiveMicrophone: ReactSetter<string | undefined>
|
|
19
|
+
setActiveAudioOutput: ReactSetter<string | undefined>
|
|
20
|
+
setActiveCamera: ReactSetter<string | undefined>
|
|
21
|
+
setMicList: ReactSetter<MediaDevice[]>
|
|
22
|
+
setAudioOutputList: ReactSetter<MediaDevice[]>
|
|
23
|
+
setCameraList: ReactSetter<MediaDevice[]>
|
|
24
|
+
setConnectionState: ReactSetter<ConnectionState | null>
|
|
25
|
+
setIsCamOn: ReactSetter<boolean>
|
|
26
|
+
setIsMicOn: ReactSetter<boolean>
|
|
27
|
+
setActiveSpeakers: ReactSetter<ActiveSpeaker[]>
|
|
28
|
+
setParticipants: ReactSetter<Participant[]>
|
|
29
|
+
setActiveVideoId: ReactSetter<number | null>
|
|
30
|
+
setHandRaises: ReactSetter<Record<string, boolean>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const useClientMessages = ({
|
|
34
|
+
zmClient,
|
|
35
|
+
mediaStream,
|
|
36
|
+
setActiveMicrophone,
|
|
37
|
+
setActiveAudioOutput,
|
|
38
|
+
setActiveCamera,
|
|
39
|
+
setMicList,
|
|
40
|
+
setAudioOutputList,
|
|
41
|
+
setCameraList,
|
|
42
|
+
setConnectionState,
|
|
43
|
+
setIsCamOn,
|
|
44
|
+
setIsMicOn,
|
|
45
|
+
setActiveSpeakers,
|
|
46
|
+
setActiveVideoId,
|
|
47
|
+
setParticipants,
|
|
48
|
+
setHandRaises
|
|
49
|
+
}: Props) => {
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!mediaStream || !zmClient) return
|
|
52
|
+
|
|
53
|
+
setActiveVideoId(mediaStream?.getActiveVideoId() ?? null)
|
|
54
|
+
|
|
55
|
+
const onCommandChannelMessage = (payload: CommandChannelMsg) => {
|
|
56
|
+
console.log('onCommandChannelMessage payload: ', payload)
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(payload?.text)
|
|
59
|
+
if (data.type === 'RAISE_HAND') {
|
|
60
|
+
setHandRaises((prev) => ({ ...prev, [data.userId]: true }))
|
|
61
|
+
} else if (data.type === 'LOWER_HAND') {
|
|
62
|
+
setHandRaises((prev) => ({ ...prev, [data.userId]: false }))
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.warn('Invalid command message', e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const onConnectionChange = (payload: ConnectionChangePayload) => {
|
|
70
|
+
setConnectionState(payload.state)
|
|
71
|
+
console.log(`Connection state changed to ${payload.state}, reason: ${payload.reason}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const onParticipantChange = () => {
|
|
75
|
+
const allUsers = zmClient.getAllUser()
|
|
76
|
+
setParticipants(allUsers)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onActiveVideoChange = (payload: { state: VideoActiveState; userId: number }) => {
|
|
80
|
+
const { state, userId } = payload
|
|
81
|
+
if (state === 'Active') {
|
|
82
|
+
setActiveVideoId(userId)
|
|
83
|
+
console.log(`User ${userId} video is active`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onActiveSpeakerChange = (payload: ActiveSpeaker[]) => {
|
|
88
|
+
if (Array.isArray(payload)) {
|
|
89
|
+
setActiveSpeakers(payload)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const onDeviceChange = () => {
|
|
94
|
+
if (mediaStream) {
|
|
95
|
+
console.log('onDeviceChange: update device list')
|
|
96
|
+
setMicList(mediaStream.getMicList())
|
|
97
|
+
setAudioOutputList(mediaStream.getSpeakerList())
|
|
98
|
+
if (!isAndroidOrIOSBrowser()) setCameraList(mediaStream.getCameraList())
|
|
99
|
+
setActiveMicrophone(mediaStream.getActiveMicrophone())
|
|
100
|
+
setActiveAudioOutput(mediaStream.getActiveSpeaker())
|
|
101
|
+
setActiveCamera(mediaStream.getActiveCamera())
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* https://marketplacefront.zoom.us/sdk/custom/web/modules/ZoomVideo.VideoClient.html#on */
|
|
106
|
+
zmClient.on('active-speaker', onActiveSpeakerChange)
|
|
107
|
+
zmClient.on('command-channel-message', onCommandChannelMessage)
|
|
108
|
+
zmClient.on('connection-change', onConnectionChange)
|
|
109
|
+
zmClient.on('device-change', onDeviceChange)
|
|
110
|
+
zmClient.on('user-added', onParticipantChange)
|
|
111
|
+
zmClient.on('user-removed', onParticipantChange)
|
|
112
|
+
zmClient.on('user-updated', onParticipantChange)
|
|
113
|
+
zmClient.on('video-active-change', onActiveVideoChange) // It fires when the active speaker is talking for more than one second, allowing for a smoother user experience than the Audio active-speaker event. Event fires for active speakers who have their video on or off
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
zmClient.off('active-speaker', onActiveSpeakerChange)
|
|
117
|
+
zmClient.off('command-channel-message', onCommandChannelMessage)
|
|
118
|
+
zmClient.off('connection-change', onConnectionChange)
|
|
119
|
+
zmClient.off('device-change', onDeviceChange)
|
|
120
|
+
zmClient.off('user-added', onParticipantChange)
|
|
121
|
+
zmClient.off('user-removed', onParticipantChange)
|
|
122
|
+
zmClient.off('user-updated', onParticipantChange)
|
|
123
|
+
zmClient.off('video-active-change', onActiveVideoChange)
|
|
124
|
+
}
|
|
125
|
+
}, [
|
|
126
|
+
mediaStream,
|
|
127
|
+
setActiveCamera,
|
|
128
|
+
setActiveMicrophone,
|
|
129
|
+
setActiveAudioOutput,
|
|
130
|
+
setActiveSpeakers,
|
|
131
|
+
setActiveVideoId,
|
|
132
|
+
setCameraList,
|
|
133
|
+
setConnectionState,
|
|
134
|
+
setHandRaises,
|
|
135
|
+
setIsCamOn,
|
|
136
|
+
setIsMicOn,
|
|
137
|
+
setMicList,
|
|
138
|
+
setParticipants,
|
|
139
|
+
zmClient,
|
|
140
|
+
setAudioOutputList
|
|
141
|
+
])
|
|
142
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useMeasure } from 'react-use'
|
|
3
|
+
|
|
4
|
+
export enum Breakpoint {
|
|
5
|
+
Mobile,
|
|
6
|
+
Tablet,
|
|
7
|
+
Desktop
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// TODO: Use tailwind screens when created a new project with zoom video
|
|
11
|
+
const MOBILE_MAX = 768
|
|
12
|
+
const TABLET_MAX = 1024
|
|
13
|
+
|
|
14
|
+
export const useContainerSize = () => {
|
|
15
|
+
const [ref, { width }] = useMeasure<HTMLDivElement>()
|
|
16
|
+
|
|
17
|
+
const breakpoint = useMemo(() => {
|
|
18
|
+
if (width < MOBILE_MAX) return Breakpoint.Mobile
|
|
19
|
+
if (width < TABLET_MAX) return Breakpoint.Tablet
|
|
20
|
+
return Breakpoint.Desktop
|
|
21
|
+
}, [width])
|
|
22
|
+
|
|
23
|
+
return { ref, width, breakpoint }
|
|
24
|
+
}
|