@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,14 @@
|
|
|
1
|
+
import { Stream } from '@zoom/videosdk'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
export const useStartVideoOptions = ({ mediaStream }: { mediaStream: typeof Stream | null }) => {
|
|
5
|
+
return useMemo(
|
|
6
|
+
() => ({
|
|
7
|
+
hd: true,
|
|
8
|
+
fullHd: true,
|
|
9
|
+
ptz: mediaStream?.isBrowserSupportPTZ(),
|
|
10
|
+
originalRatio: true
|
|
11
|
+
}),
|
|
12
|
+
[mediaStream]
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from 'react'
|
|
2
|
+
import { useEffectOnce } from 'react-use'
|
|
3
|
+
import { type VideoClient, type Participant, VideoQuality, VideoPlayer, ConnectionState } from '@zoom/videosdk'
|
|
4
|
+
import { VIDEO_PLACEHOLDER } from './../constants'
|
|
5
|
+
import { useZoomVideoContext } from '../context'
|
|
6
|
+
import { useStartVideoOptions } from './useStartVideoOptions'
|
|
7
|
+
|
|
8
|
+
type UseZoomVideoPlayerProps = {
|
|
9
|
+
fullRef: RefObject<HTMLVideoElement | null>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Create a name tag for the video */
|
|
13
|
+
const createNameTag = ({ name, isMicOff }: { name: string; isMicOff: boolean }): HTMLDivElement => {
|
|
14
|
+
const container = document.createElement('div')
|
|
15
|
+
container.className = 'absolute top-[2px] px-4 py-4 opacity-[0.6] w-full h-fit flex items-center justify-between'
|
|
16
|
+
const nameTag = document.createElement('div')
|
|
17
|
+
container.appendChild(nameTag)
|
|
18
|
+
nameTag.textContent = name
|
|
19
|
+
nameTag.className = 'bg-neutral-40 text-s font-bold text-white px-4 py-4 rounded-m max-w-[60%] truncate'
|
|
20
|
+
|
|
21
|
+
const micOffSpan = document.createElement('i')
|
|
22
|
+
micOffSpan.className = 'fa-regular fa-microphone-slash w-24 h-24 !text-white'
|
|
23
|
+
isMicOff ? micOffSpan.classList.remove('hidden') : micOffSpan.classList.add('hidden')
|
|
24
|
+
container.appendChild(micOffSpan)
|
|
25
|
+
|
|
26
|
+
return container
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Determine which user video to show */
|
|
30
|
+
const getRenderTargetUserId = (
|
|
31
|
+
zmClient: typeof VideoClient,
|
|
32
|
+
participants: Participant[],
|
|
33
|
+
activeVideoId: number | null
|
|
34
|
+
): number | null => {
|
|
35
|
+
if (!participants?.length) return null
|
|
36
|
+
const selfId = zmClient?.getCurrentUserInfo()?.userId ?? null
|
|
37
|
+
if (participants.length === 1) return selfId
|
|
38
|
+
if (participants.length === 2) {
|
|
39
|
+
return participants.find((p) => p.userId !== selfId)?.userId ?? selfId
|
|
40
|
+
}
|
|
41
|
+
return activeVideoId ?? selfId
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const useZoomVideoPlayer = ({ fullRef }: UseZoomVideoPlayerProps) => {
|
|
45
|
+
const {
|
|
46
|
+
activeAudioOutput,
|
|
47
|
+
activeMicrophone,
|
|
48
|
+
activeVideoId,
|
|
49
|
+
connectionState,
|
|
50
|
+
isCamOn,
|
|
51
|
+
isMicOn,
|
|
52
|
+
mediaStream,
|
|
53
|
+
participants,
|
|
54
|
+
zmClient
|
|
55
|
+
} = useZoomVideoContext()
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const containerEl = fullRef.current
|
|
59
|
+
if (!mediaStream || !containerEl || !zmClient || connectionState === ConnectionState.Closed) return
|
|
60
|
+
|
|
61
|
+
const userId = getRenderTargetUserId(zmClient, participants, activeVideoId)
|
|
62
|
+
if (!userId) return
|
|
63
|
+
|
|
64
|
+
let videoElement: VideoPlayer | null = null
|
|
65
|
+
let wrapperEl: HTMLDivElement | null = null
|
|
66
|
+
|
|
67
|
+
const attach = async () => {
|
|
68
|
+
const participant = participants.find((p) => p.userId === userId)
|
|
69
|
+
const displayName = participant?.displayName ?? ''
|
|
70
|
+
const nameTag = createNameTag({ name: displayName, isMicOff: participant?.muted || !participant?.audio })
|
|
71
|
+
|
|
72
|
+
// Clear previous content
|
|
73
|
+
containerEl.innerHTML = ''
|
|
74
|
+
|
|
75
|
+
// Create wrapper for video and name tag
|
|
76
|
+
wrapperEl = document.createElement('div')
|
|
77
|
+
wrapperEl.className = 'relative w-full h-full'
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Create or reuse the video-player element
|
|
81
|
+
const element = await mediaStream.attachVideo(userId, VideoQuality.Video_720P)
|
|
82
|
+
videoElement = element as VideoPlayer
|
|
83
|
+
if (!videoElement) throw new Error('No video element')
|
|
84
|
+
|
|
85
|
+
// Add video
|
|
86
|
+
wrapperEl.appendChild(videoElement)
|
|
87
|
+
// Add name tag
|
|
88
|
+
wrapperEl.appendChild(nameTag)
|
|
89
|
+
// Append to container
|
|
90
|
+
containerEl.appendChild(wrapperEl)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.warn('Failed to attach video, using placeholder:', err)
|
|
93
|
+
containerEl.innerHTML = VIDEO_PLACEHOLDER
|
|
94
|
+
containerEl.appendChild(nameTag)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
attach()
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
if (videoElement && userId) {
|
|
102
|
+
try {
|
|
103
|
+
mediaStream.detachVideo(userId)
|
|
104
|
+
videoElement.remove()
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.warn('Failed to detach video:', err)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (wrapperEl) wrapperEl.remove()
|
|
110
|
+
}
|
|
111
|
+
}, [activeVideoId, connectionState, fullRef, mediaStream, participants, zmClient])
|
|
112
|
+
|
|
113
|
+
// Manage local camera
|
|
114
|
+
const startVideoOptions = useStartVideoOptions({ mediaStream })
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!mediaStream || connectionState === ConnectionState.Closed) return
|
|
117
|
+
|
|
118
|
+
const toggleCam = async () => {
|
|
119
|
+
try {
|
|
120
|
+
const isCapturing = mediaStream.isCapturingVideo()
|
|
121
|
+
|
|
122
|
+
if (isCamOn && !isCapturing) {
|
|
123
|
+
await mediaStream.startVideo(startVideoOptions)
|
|
124
|
+
} else if (!isCamOn && isCapturing) {
|
|
125
|
+
await mediaStream.stopVideo()
|
|
126
|
+
}
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
console.warn('Video toggle failed:', err)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
toggleCam()
|
|
133
|
+
}, [mediaStream, isCamOn, startVideoOptions, connectionState])
|
|
134
|
+
|
|
135
|
+
// Initialize microphone on join if mic is on
|
|
136
|
+
useEffectOnce(() => {
|
|
137
|
+
if (!mediaStream || !isMicOn) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
mediaStream.startAudio({ microphoneId: activeMicrophone, speakerId: activeAudioOutput }).catch(console.warn)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const isIPad = (): boolean => {
|
|
2
|
+
const { userAgent, maxTouchPoints } = navigator
|
|
3
|
+
const isiPadClassic = /iPad/i.test(userAgent)
|
|
4
|
+
const isiPadOS = /Macintosh/i.test(userAgent) && maxTouchPoints > 1
|
|
5
|
+
return isiPadClassic || isiPadOS
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const isIOSMobile = (): boolean => {
|
|
9
|
+
const { userAgent } = navigator
|
|
10
|
+
return /iPhone|iPad|iPod/i.test(userAgent) || isIPad()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const isAndroidBrowser = (): boolean => /Android/i.test(navigator.userAgent)
|
|
14
|
+
|
|
15
|
+
export const isAndroidOrIOSBrowser = (): boolean => {
|
|
16
|
+
return isAndroidBrowser() || isIOSMobile()
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Button } from 'primereact/button'
|
|
2
|
+
|
|
3
|
+
const AfterSession = ({ restartSession }: { restartSession: () => void }) => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-col gap-24 py-16 justify-center items-center w-full">
|
|
6
|
+
<h2>After Session</h2>
|
|
7
|
+
<Button className="w-fit" onClick={restartSession}>
|
|
8
|
+
Restart Session
|
|
9
|
+
</Button>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default AfterSession
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { useUnmount } from 'react-use'
|
|
3
|
+
import Video from '../components/Video'
|
|
4
|
+
import { Button } from 'primereact/button'
|
|
5
|
+
import { useZoomVideoContext } from '../context'
|
|
6
|
+
import { useZoomVideoPlayer } from '../hooks/useZoomVideoPlayer'
|
|
7
|
+
import ParticipantsList from '../components/ParticipantsList'
|
|
8
|
+
|
|
9
|
+
const MainSession = () => {
|
|
10
|
+
const { activeVideoId, zmClient, mediaStream, stopSession, setIsCamOn, setIsMicOn, participants } =
|
|
11
|
+
useZoomVideoContext()
|
|
12
|
+
|
|
13
|
+
const fullRef = useRef<HTMLVideoElement>(null)
|
|
14
|
+
|
|
15
|
+
useZoomVideoPlayer({ fullRef })
|
|
16
|
+
|
|
17
|
+
useUnmount(() => {
|
|
18
|
+
if (zmClient?.getSessionInfo().isInMeeting) {
|
|
19
|
+
if (zmClient?.getCurrentUserInfo().audio) {
|
|
20
|
+
mediaStream?.stopAudio().catch(console.warn)
|
|
21
|
+
}
|
|
22
|
+
if (zmClient?.getCurrentUserInfo()?.bVideoOn) {
|
|
23
|
+
mediaStream?.stopVideo().catch(console.warn)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
mediaStream?.stopShareScreen().catch(console.warn)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-24 py-16 justify-start w-full">
|
|
32
|
+
<div className="flex flex-col gap-16 items-center justify-center w-full">
|
|
33
|
+
<div className="flex flex-col gap-16 items-center justify-center w-full @sm:w-[640px] @md:w-[680px] mx-auto">
|
|
34
|
+
<Video fullRef={fullRef} />
|
|
35
|
+
|
|
36
|
+
<p>Active video id: {activeVideoId}</p>
|
|
37
|
+
<ParticipantsList
|
|
38
|
+
zmClient={zmClient}
|
|
39
|
+
setIsCamOn={setIsCamOn}
|
|
40
|
+
setIsMicOn={setIsMicOn}
|
|
41
|
+
participants={participants}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<Button className="w-fit" onClick={stopSession}>
|
|
45
|
+
Stop Session
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default MainSession
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react'
|
|
2
|
+
import { Button } from 'primereact/button'
|
|
3
|
+
import { useUnmount } from 'react-use'
|
|
4
|
+
import Video from '../components/Video'
|
|
5
|
+
import { useZoomVideoContext } from '../context'
|
|
6
|
+
import { useZoomVideoPlayer } from '../hooks/useZoomVideoPlayer'
|
|
7
|
+
import ParticipantsList from '../components/ParticipantsList'
|
|
8
|
+
import ButtonsDock from '../components/ButtonsDock'
|
|
9
|
+
import { SessionStep } from '../types'
|
|
10
|
+
|
|
11
|
+
const PanelistsSession = ({ initMainSession }: { initMainSession: () => void }) => {
|
|
12
|
+
const {
|
|
13
|
+
activeMicrophone,
|
|
14
|
+
activeAudioOutput,
|
|
15
|
+
activeVideoId,
|
|
16
|
+
mediaStream,
|
|
17
|
+
stopSession,
|
|
18
|
+
setIsCamOn,
|
|
19
|
+
setIsMicOn,
|
|
20
|
+
participants,
|
|
21
|
+
switchActiveMicrophone,
|
|
22
|
+
setActiveCamera,
|
|
23
|
+
switchActiveAudioOutput,
|
|
24
|
+
zmClient
|
|
25
|
+
} = useZoomVideoContext()
|
|
26
|
+
|
|
27
|
+
const fullRef = useRef<HTMLVideoElement>(null)
|
|
28
|
+
|
|
29
|
+
useZoomVideoPlayer({ fullRef })
|
|
30
|
+
|
|
31
|
+
const switchActiveCamera = useCallback(
|
|
32
|
+
(deviceId: string) => {
|
|
33
|
+
setActiveCamera(deviceId)
|
|
34
|
+
mediaStream?.switchCamera(deviceId)
|
|
35
|
+
},
|
|
36
|
+
[mediaStream, setActiveCamera]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const toggleMicrophone = useCallback(async () => {
|
|
40
|
+
if (!mediaStream || !zmClient) return
|
|
41
|
+
if (zmClient.getCurrentUserInfo().audio) {
|
|
42
|
+
await mediaStream.stopAudio().catch(console.warn)
|
|
43
|
+
setIsMicOn(false)
|
|
44
|
+
} else {
|
|
45
|
+
await mediaStream.startAudio({ microphoneId: activeMicrophone, speakerId: activeAudioOutput }).catch(console.warn)
|
|
46
|
+
setIsMicOn(true)
|
|
47
|
+
}
|
|
48
|
+
}, [activeMicrophone, activeAudioOutput, mediaStream, setIsMicOn, zmClient])
|
|
49
|
+
|
|
50
|
+
useUnmount(() => {
|
|
51
|
+
if (zmClient?.getSessionInfo().isInMeeting) {
|
|
52
|
+
if (zmClient?.getCurrentUserInfo().audio) {
|
|
53
|
+
mediaStream?.stopAudio().catch(console.warn)
|
|
54
|
+
}
|
|
55
|
+
if (zmClient?.getCurrentUserInfo()?.bVideoOn) {
|
|
56
|
+
mediaStream?.stopVideo().catch(console.warn)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
mediaStream?.stopShareScreen().catch(console.warn)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-col gap-24 py-16 justify-start w-full">
|
|
65
|
+
<div className="flex flex-col @sm:flex-row gap-8 p-16 items-center justify-between bg-warning-90 w-full">
|
|
66
|
+
<p>Estás en una conferencia de prueba, abre sesión para invitar a todos el listado de asistentes.</p>
|
|
67
|
+
<Button onClick={initMainSession} className="whitespace-pre-line">
|
|
68
|
+
Abrir videoconferencia
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="flex flex-col gap-16 items-center justify-center w-full @sm:w-[640px] @md:w-[680px] mx-auto">
|
|
73
|
+
<Video fullRef={fullRef} />
|
|
74
|
+
|
|
75
|
+
<ButtonsDock
|
|
76
|
+
exit={stopSession}
|
|
77
|
+
sessionStep={SessionStep.OnlyPanelistsSession}
|
|
78
|
+
setIsCamOn={() => setIsCamOn((prev) => !prev)}
|
|
79
|
+
setIsMicOn={toggleMicrophone}
|
|
80
|
+
setActiveMicrophone={switchActiveMicrophone}
|
|
81
|
+
setActiveCamera={switchActiveCamera}
|
|
82
|
+
setActiveAudioOutput={switchActiveAudioOutput}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<p>Active video id: {activeVideoId}</p>
|
|
86
|
+
<ParticipantsList
|
|
87
|
+
zmClient={zmClient}
|
|
88
|
+
setIsCamOn={setIsCamOn}
|
|
89
|
+
setIsMicOn={setIsMicOn}
|
|
90
|
+
participants={participants}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default PanelistsSession
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useRef, useState, useCallback } from 'react'
|
|
2
|
+
import ZoomVideo from '@zoom/videosdk'
|
|
3
|
+
import { Button } from 'primereact/button'
|
|
4
|
+
import { useEffectOnce, useUnmount } from 'react-use'
|
|
5
|
+
import { useZoomVideoContext } from '../context'
|
|
6
|
+
import Video from '../components/Video'
|
|
7
|
+
import ButtonsDock from '../components/ButtonsDock'
|
|
8
|
+
import { VIDEO_PLACEHOLDER } from '../constants'
|
|
9
|
+
import { SessionStep } from '../types'
|
|
10
|
+
|
|
11
|
+
const PreSessionConfiguration = ({ joinSession }: { joinSession: () => Promise<void> }) => {
|
|
12
|
+
const {
|
|
13
|
+
closeParentContainer,
|
|
14
|
+
localAudio,
|
|
15
|
+
localVideo,
|
|
16
|
+
setIsCamOn,
|
|
17
|
+
setIsMicOn,
|
|
18
|
+
setActiveCamera,
|
|
19
|
+
switchActiveAudioOutput,
|
|
20
|
+
switchActiveMicrophone,
|
|
21
|
+
setLocalVideo
|
|
22
|
+
} = useZoomVideoContext()
|
|
23
|
+
const [isVideoStarted, setIsVideoStarted] = useState(false)
|
|
24
|
+
const [isAudioStarted, setIsAudioStarted] = useState(false)
|
|
25
|
+
const videoRef = useRef<HTMLVideoElement | null>(null)
|
|
26
|
+
|
|
27
|
+
const stopLocalVideo = useCallback(async () => {
|
|
28
|
+
const containerEl = videoRef.current
|
|
29
|
+
if (!containerEl || !localVideo) return
|
|
30
|
+
try {
|
|
31
|
+
await localVideo.stop()
|
|
32
|
+
setIsVideoStarted(false)
|
|
33
|
+
containerEl.innerHTML = VIDEO_PLACEHOLDER
|
|
34
|
+
setIsCamOn(false)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error stopping local video:', error)
|
|
37
|
+
await localVideo.start(containerEl)
|
|
38
|
+
setIsVideoStarted(true)
|
|
39
|
+
}
|
|
40
|
+
}, [localVideo, setIsCamOn])
|
|
41
|
+
|
|
42
|
+
const stopLocalAudio = useCallback(async () => {
|
|
43
|
+
if (!localAudio) return
|
|
44
|
+
try {
|
|
45
|
+
await localAudio.stop()
|
|
46
|
+
setIsAudioStarted(false)
|
|
47
|
+
setIsMicOn(false)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error stopping local audio:', error)
|
|
50
|
+
await localAudio.start()
|
|
51
|
+
}
|
|
52
|
+
}, [localAudio, setIsMicOn])
|
|
53
|
+
|
|
54
|
+
const toggleLocalVideo = useCallback(async () => {
|
|
55
|
+
if (!localVideo) return
|
|
56
|
+
if (isVideoStarted) {
|
|
57
|
+
await stopLocalVideo()
|
|
58
|
+
} else if (videoRef.current) {
|
|
59
|
+
try {
|
|
60
|
+
await localVideo.start(videoRef.current)
|
|
61
|
+
setIsVideoStarted(true)
|
|
62
|
+
setIsCamOn(true)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error starting local video:', error)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [localVideo, isVideoStarted, stopLocalVideo, setIsCamOn])
|
|
68
|
+
|
|
69
|
+
const toggleAudioMuteStatus = useCallback(async () => {
|
|
70
|
+
if (!localAudio) return
|
|
71
|
+
if (isAudioStarted) {
|
|
72
|
+
await stopLocalAudio()
|
|
73
|
+
} else {
|
|
74
|
+
try {
|
|
75
|
+
await localAudio.start()
|
|
76
|
+
setIsAudioStarted(true)
|
|
77
|
+
setIsMicOn(true)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Error starting local audio:', error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [isAudioStarted, localAudio, stopLocalAudio, setIsMicOn])
|
|
83
|
+
|
|
84
|
+
const switchActiveCamera = async (deviceId: string) => {
|
|
85
|
+
if (!localVideo) return
|
|
86
|
+
setActiveCamera(deviceId)
|
|
87
|
+
if (!isVideoStarted) return
|
|
88
|
+
await localVideo.stop()
|
|
89
|
+
setLocalVideo(ZoomVideo.createLocalVideoTrack(deviceId))
|
|
90
|
+
if (!videoRef.current) return
|
|
91
|
+
await localVideo.start(videoRef.current)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const exit = useCallback(async () => {
|
|
95
|
+
if (isVideoStarted) await stopLocalVideo()
|
|
96
|
+
if (isAudioStarted) await stopLocalAudio()
|
|
97
|
+
closeParentContainer()
|
|
98
|
+
}, [isVideoStarted, isAudioStarted, stopLocalVideo, stopLocalAudio, closeParentContainer])
|
|
99
|
+
|
|
100
|
+
useEffectOnce(() => {
|
|
101
|
+
const startLocalMedia = async () => {
|
|
102
|
+
if (!videoRef.current || !localVideo || !localAudio) return
|
|
103
|
+
try {
|
|
104
|
+
await localVideo.start(videoRef.current).catch(console.error)
|
|
105
|
+
await localAudio.start().catch(console.error)
|
|
106
|
+
setIsVideoStarted(true)
|
|
107
|
+
setIsAudioStarted(true)
|
|
108
|
+
setIsCamOn(true)
|
|
109
|
+
setIsMicOn(true)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error starting local media:', error)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
startLocalMedia()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
useUnmount(async () => {
|
|
119
|
+
if (isAudioStarted) {
|
|
120
|
+
await stopLocalAudio().catch(console.error)
|
|
121
|
+
}
|
|
122
|
+
if (isVideoStarted) {
|
|
123
|
+
await stopLocalVideo().catch(console.error)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex flex-col gap-24 py-16 items-center justify-between w-full">
|
|
129
|
+
<section className="flex flex-col gap-8 py-16 items-center text-center">
|
|
130
|
+
<h3>Acceso a la videoconferencia</h3>
|
|
131
|
+
<p>Comprueba que todo funciona correctamente para una mejor experiencia en la videollamada.</p>
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
<div className="flex flex-col gap-16 items-center justify-center w-full sm:w-[640px] md:w-[680px] mx-auto">
|
|
135
|
+
<Video fullRef={videoRef} />
|
|
136
|
+
<ButtonsDock
|
|
137
|
+
exit={exit}
|
|
138
|
+
sessionStep={SessionStep.LocalSettingsConfiguration}
|
|
139
|
+
setIsCamOn={toggleLocalVideo}
|
|
140
|
+
setIsMicOn={toggleAudioMuteStatus}
|
|
141
|
+
setActiveMicrophone={switchActiveMicrophone}
|
|
142
|
+
setActiveCamera={switchActiveCamera}
|
|
143
|
+
setActiveAudioOutput={switchActiveAudioOutput}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<section className="flex flex-col gap-8">
|
|
148
|
+
<Button onClick={joinSession} label="Acceder a la videoconferencia" />
|
|
149
|
+
</section>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default PreSessionConfiguration
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Global type declarations for Zoom Video SDK custom elements
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
webEndpoint?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
namespace JSX {
|
|
8
|
+
interface IntrinsicElements {
|
|
9
|
+
'video-player-container': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
|
|
10
|
+
'video-player': React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export enum SessionStep {
|
|
4
|
+
LocalSettingsConfiguration,
|
|
5
|
+
OnlyPanelistsSession,
|
|
6
|
+
MainSession,
|
|
7
|
+
AfterSession
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Credentials = {
|
|
11
|
+
sessionName: string
|
|
12
|
+
signature: string
|
|
13
|
+
userName: string
|
|
14
|
+
sessionPasscode: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ReactSetter<T> = React.Dispatch<React.SetStateAction<T>>
|
|
18
|
+
|
|
19
|
+
export type ZoomVideoPluginProps = {
|
|
20
|
+
credentials: Credentials | null
|
|
21
|
+
closeParentContainer: () => void
|
|
22
|
+
setIsCloseButtonVisible: ReactSetter<boolean>
|
|
23
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type PromiseType<T extends (..._: any) => any> = Awaited<ReturnType<T>>
|
|
2
|
+
|
|
3
|
+
type DeepPartial<T> = {
|
|
4
|
+
[P in keyof T]?: DeepPartial<T[P]>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type ReactSetter<T> = React.Dispatch<React.SetStateAction<T>>
|
|
8
|
+
|
|
9
|
+
interface AwsWafCaptcha {
|
|
10
|
+
renderCaptcha: (
|
|
11
|
+
container: HTMLElement,
|
|
12
|
+
options: {
|
|
13
|
+
apiKey: string
|
|
14
|
+
onSuccess: (wafToken: string) => void
|
|
15
|
+
onLoad?: () => void
|
|
16
|
+
onError?: () => void
|
|
17
|
+
onPuzzleTimeout?: () => void
|
|
18
|
+
onPuzzleIncorrect?: () => void
|
|
19
|
+
onPuzzleCorrect?: () => void
|
|
20
|
+
defaultLocale?: string
|
|
21
|
+
disableLanguageSelector?: boolean
|
|
22
|
+
dynamicWidth?: boolean
|
|
23
|
+
skipTitle?: boolean
|
|
24
|
+
}
|
|
25
|
+
) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AwsWafIntegration {
|
|
29
|
+
getToken: () => Promise<string>
|
|
30
|
+
hasToken: () => boolean
|
|
31
|
+
forceRefreshToken: () => Promise<void>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Window {
|
|
35
|
+
dataLayer: Record<string, unknown>[]
|
|
36
|
+
AwsWafCaptcha: AwsWafCaptcha
|
|
37
|
+
AwsWafIntegration: AwsWafIntegration
|
|
38
|
+
awsWafCookieDomainList: string[]
|
|
39
|
+
|
|
40
|
+
documentPictureInPicture: {
|
|
41
|
+
requestWindow: (options: { width: number; height: number }) => Promise<Window>
|
|
42
|
+
addEventListener: (type: 'enter' | 'leave', listener: (event: any) => void) => void
|
|
43
|
+
window: Window | null
|
|
44
|
+
}
|
|
45
|
+
webEndpoint: string | undefined
|
|
46
|
+
}
|
package/src/index.css
ADDED
package/src/index.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_ZOOM_SESSION_NAME?: string
|
|
5
|
+
readonly VITE_ZOOM_SIGNATURE?: string
|
|
6
|
+
readonly VITE_ZOOM_USER_NAME?: string
|
|
7
|
+
readonly VITE_ZOOM_SESSION_PASSCODE?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ImportMeta {
|
|
11
|
+
readonly env: ImportMetaEnv
|
|
12
|
+
}
|