From ffffd929bdca866098c4c24d33bc35648f67cdf2 Mon Sep 17 00:00:00 2001 From: "vic.yang" Date: Mon, 30 Dec 2024 10:12:32 +0800 Subject: [PATCH 1/3] Video SDK Web 2.1.0 update --- .eslintrc | 17 +- package.json | 23 +- src/App.tsx | 40 ++- src/feature/chat/chat.scss | 2 +- src/feature/chat/chat.tsx | 4 +- src/feature/command/command.scss | 4 +- src/feature/command/command.tsx | 10 +- src/feature/preview/preview.scss | 19 +- src/feature/preview/preview.tsx | 88 +++++-- .../subsession/component/broadcast-panel.tsx | 4 +- .../component/broadcast-voice-panel.tsx | 2 +- .../subsession/component/draggable-modal.tsx | 4 +- .../component/subsession-create.tsx | 2 +- .../subsession/component/subsession-item.tsx | 2 +- .../component/subsession-manage.tsx | 6 +- .../component/subsession-options.tsx | 2 +- src/feature/subsession/hooks/useAskForHelp.ts | 2 +- .../subsession/hooks/useBroadcastMessage.ts | 2 +- .../subsession/hooks/useInviteJoinRoom.ts | 2 +- .../subsession/hooks/useParticipantsChange.ts | 4 +- src/feature/subsession/hooks/useSubsession.ts | 8 +- .../hooks/useSubsessionClosingCountdown.ts | 2 +- .../hooks/useSubsessionCountdown.ts | 2 +- .../subsession/hooks/useSubsessionOptions.ts | 2 +- .../subsession/hooks/useSubsessionTimeup.ts | 2 +- src/feature/subsession/subsession-types.d.ts | 2 +- src/feature/subsession/subsession.tsx | 1 - .../components/audio-video-statistic.tsx | 6 +- src/feature/video/components/camera.tsx | 6 +- src/feature/video/components/draggable.tsx | 119 +++++++++ src/feature/video/components/leave.tsx | 4 +- src/feature/video/components/live-stream.tsx | 2 +- .../video/components/live-transcription.tsx | 3 +- src/feature/video/components/microphone.tsx | 21 +- src/feature/video/components/pagination.scss | 20 +- src/feature/video/components/pagination.tsx | 2 +- .../components/remote-camera-control.tsx | 1 - src/feature/video/components/report-btn.tsx | 4 +- src/feature/video/components/screen-share.tsx | 3 +- src/feature/video/components/share-bar.tsx | 7 +- src/feature/video/components/share-view.tsx | 14 +- src/feature/video/components/video-footer.tsx | 100 ++++++-- .../video/hooks/useAttachPagination.ts | 28 +++ src/feature/video/video-attach.tsx | 234 +++++++++++------- src/feature/video/video-layout-helper.ts | 2 +- src/feature/video/video-single.tsx | 5 +- src/feature/video/video.scss | 53 +++- src/utils/platform.ts | 4 +- 48 files changed, 654 insertions(+), 242 deletions(-) create mode 100644 src/feature/video/components/draggable.tsx create mode 100644 src/feature/video/hooks/useAttachPagination.ts diff --git a/.eslintrc b/.eslintrc index a9acfa4..6d32aeb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,7 +45,7 @@ "no-unused-vars": 0, "no-unreachable": 2, "no-duplicate-imports": 2, - "max-params":["error", 8], + "max-params": ["error", 8], "react/display-name": 0, "react-hooks/rules-of-hooks": 2, "react-hooks/exhaustive-deps": 1, @@ -53,11 +53,24 @@ "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off", "complexity": "off", + "prefer-object-has-own": "off", "prettier/prettier": [ "error", { "endOfLine": "auto" } - ] + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "none", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true, + }, + ], } } diff --git a/package.json b/package.json index 17a91ef..ab96ed8 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "react-video-sdk-demo", - "version": "1.12.14", + "version": "2.1.0", "private": true, "dependencies": { "@ant-design/icons": "4.7.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.3", - "@zoom/videosdk": "^1.12.14", + "@zoom/videosdk": "^2.1.0", "antd": "4.24.3", "classnames": "^2.2.6", "crypto-js": "^4.0.0", @@ -59,18 +59,23 @@ "@types/react-dom": "18.2.18", "@types/react-router-dom": "^5.3.3", "@types/uuid": "^9.0.7", - "@typescript-eslint/eslint-plugin": "^5.31.0", - "@typescript-eslint/parser": "^5.31.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "copy-webpack-plugin": "6.2.1", "customize-cra": "1.0.0", - "eslint-config-alloy": "^4.6.2", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.6.2", + "eslint-config-alloy": "^5.1.2", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "^3.3.3", "react-app-rewired": "2.2.1", "sass": "1.32.8", "sass-loader": "10.1.1", - "typescript": "^4.3.5", + "typescript": "^5.5.4", "write-file-webpack-plugin": "4.5.1" + }, + "overrides": { + "react-scripts": { + "typescript": "^5" + } } } diff --git a/src/App.tsx b/src/App.tsx index 738be79..a75096c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,21 @@ -import { useEffect, useContext, useState, useCallback, useReducer, useMemo } from 'react'; +import { + type HTMLAttributes, + type DetailedHTMLProps, + type DOMAttributes, + useEffect, + useContext, + useState, + useCallback, + useReducer, + useMemo +} from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; -import ZoomVideo, { ConnectionState, ReconnectReason } from '@zoom/videosdk'; +import ZoomVideo, { + type VideoPlayerContainer, + type VideoPlayer, + ConnectionState, + ReconnectReason +} from '@zoom/videosdk'; import { message, Modal } from 'antd'; import 'antd/dist/antd.min.css'; import produce from 'immer'; @@ -15,8 +30,9 @@ import LoadingLayer from './component/loading-layer'; import Chat from './feature/chat/chat'; import Command from './feature/command/command'; import Subsession from './feature/subsession/subsession'; -import { MediaStream } from './index-types'; +import type { MediaStream } from './index-types'; import './App.css'; +type CustomElement = Partial & { children: any }>; interface AppProps { meetingArgs: { @@ -91,6 +107,12 @@ declare global { ltClient: any | undefined; logClient: any | undefined; } + namespace JSX { + interface IntrinsicElements { + ['video-player']: DetailedHTMLProps, VideoPlayer> & { class?: string }; + ['video-player-container']: CustomElement & { class?: string }; + } + } } function App(props: AppProps) { @@ -167,7 +189,9 @@ function App(props: AppProps) { }; init(); return () => { - ZoomVideo.destroyClient(); + if (zmClient.getSessionInfo()?.isInMeeting) { + ZoomVideo.destroyClient(); + } }; }, [ sdkKey, @@ -205,9 +229,15 @@ function App(props: AppProps) { window.mediaStream = zmClient.getMediaStream(); console.log('getSessionInfo', zmClient.getSessionInfo()); - } else if (payload.state === ConnectionState.Closed) { + } else if (payload.state === ConnectionState.Closed || payload.state === ConnectionState.Fail) { setStatus('closed'); dispatch({ type: 'reset-media' }); + if (payload.state === ConnectionState.Fail) { + Modal.error({ + title: 'Join meeting failed', + content: `Join meeting failed. reason:${payload.reason ?? ''}` + }); + } if (payload.reason === 'ended by host') { Modal.warning({ title: 'Meeting ended', diff --git a/src/feature/chat/chat.scss b/src/feature/chat/chat.scss index b3dc97a..86aeed8 100644 --- a/src/feature/chat/chat.scss +++ b/src/feature/chat/chat.scss @@ -6,7 +6,7 @@ align-items: center; justify-content: center; .chat-wrap { - width: 60vw; + width: max(340px,60vw); max-width: 800px; height: 80vh; background: #fff; diff --git a/src/feature/chat/chat.tsx b/src/feature/chat/chat.tsx index bf171d0..30a09e5 100644 --- a/src/feature/chat/chat.tsx +++ b/src/feature/chat/chat.tsx @@ -1,4 +1,6 @@ -import React, { useCallback, useContext, useEffect, useState, useRef, useLayoutEffect } from 'react'; +import type React from 'react'; +// eslint-disable-next-line no-duplicate-imports +import { useCallback, useContext, useState, useRef, useLayoutEffect } from 'react'; import { Input } from 'antd'; import { ChatPrivilege } from '@zoom/videosdk'; import ZoomContext from '../../context/zoom-context'; diff --git a/src/feature/command/command.scss b/src/feature/command/command.scss index b3dc97a..c49dad1 100644 --- a/src/feature/command/command.scss +++ b/src/feature/command/command.scss @@ -1,4 +1,4 @@ -.chat-container { +.cmd-container { width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); @@ -6,7 +6,7 @@ align-items: center; justify-content: center; .chat-wrap { - width: 60vw; + width: max(340px,60vw); max-width: 800px; height: 80vh; background: #fff; diff --git a/src/feature/command/command.tsx b/src/feature/command/command.tsx index e3473ac..4493859 100644 --- a/src/feature/command/command.tsx +++ b/src/feature/command/command.tsx @@ -1,14 +1,16 @@ -import React, { useCallback, useContext, useEffect, useState, useRef } from 'react'; +import type React from 'react'; +// eslint-disable-next-line no-duplicate-imports +import { useCallback, useContext, useEffect, useState, useRef } from 'react'; import produce from 'immer'; import { Input } from 'antd'; import ZoomContext from '../../context/zoom-context'; -import { CommandReceiver, CommandRecord } from './cmd-types'; +import type { CommandReceiver, CommandRecord } from './cmd-types'; import { useParticipantsChange } from './hooks/useParticipantsChange'; import ChatMessageItem from './component/cmd-message-item'; import CommandReceiverContainer from './component/cmd-receiver'; import { useMount } from '../../hooks'; import './command.scss'; -import { CommandChannelMsg } from '@zoom/videosdk'; +import type { CommandChannelMsg } from '@zoom/videosdk'; const { TextArea } = Input; const oneToAllUser = { @@ -140,7 +142,7 @@ const CommandContainer = () => { setCurrentUserId(zmClient.getSessionInfo().userId); }); return ( -
+

Command Channel Chat

diff --git a/src/feature/preview/preview.scss b/src/feature/preview/preview.scss index 9ffa70b..72887e3 100644 --- a/src/feature/preview/preview.scss +++ b/src/feature/preview/preview.scss @@ -16,23 +16,30 @@ background: rgba(0, 0, 0, 1); border-radius: 14px; overflow: hidden; - &>video, &>canvas { + & > video, + .video-player-container { position: absolute; - top:0; + top: 0; left: 0; width: 100%; height: 100%; display: none; } - & > .preview-video-show{ + & .video-player { + height: 100%; + } + .preview-video-show { + z-index: 1; display: block; + width: 100%; + height: 100%; } } -.video-operations-preview > div{ +.video-operations-preview > div { display: flex; padding: 15px 20px; - background-color: rgba(0,0,0,0.7); + background-color: rgba(0, 0, 0, 0.7); border-radius: 14px; } .join-button { @@ -91,7 +98,7 @@ width: 800px; margin: 0 auto; text-align: left; - h3{ + h3 { margin-top: 30px; } .speaker-action { diff --git a/src/feature/preview/preview.tsx b/src/feature/preview/preview.tsx index 49f9ded..68da804 100644 --- a/src/feature/preview/preview.tsx +++ b/src/feature/preview/preview.tsx @@ -1,12 +1,15 @@ -import React, { useCallback, useState, useRef } from 'react'; -import ZoomVideo, { TestMicrophoneReturn, TestSpeakerReturn } from '@zoom/videosdk'; +import { useCallback, useState, useRef, useContext } from 'react'; +import type { TestMicrophoneReturn, TestSpeakerReturn, VideoPlayer } from '@zoom/videosdk'; +// eslint-disable-next-line no-duplicate-imports +import ZoomVideo from '@zoom/videosdk'; import { useMount, useUnmount } from '../../hooks'; import './preview.scss'; import MicrophoneButton from '../video/components/microphone'; import CameraButton from '../video/components/camera'; -import { message, Button, Progress, Select } from 'antd'; -import { MediaDevice } from '../video/video-types'; +import { Button, Progress, Select } from 'antd'; +import type { MediaDevice } from '../video/video-types'; import classNames from 'classnames'; +import ZoomContext from '../../context/zoom-context'; // label: string; // deviceId: string; @@ -17,6 +20,10 @@ let localAudio = ZoomVideo.createLocalAudioTrack(); let localVideo = ZoomVideo.createLocalVideoTrack(); let allDevices; +interface AppProps { + useVideoPlayer?: string; +} + const mountDevices: () => Promise<{ mics: MediaDevice[]; speakers: MediaDevice[]; @@ -78,7 +85,8 @@ const updateMicFeedbackStyle = () => { const { Option } = Select; -const PreviewContainer = () => { +const PreviewContainer = (props: AppProps) => { + const isUseVideoPlayer = props.useVideoPlayer === '1'; const [isStartedAudio, setIsStartedAudio] = useState(false); const [isMuted, setIsMuted] = useState(true); const [isStartedVideo, setIsStartedVideo] = useState(false); @@ -99,6 +107,11 @@ const PreviewContainer = () => { const microphoneTesterRef = useRef(); const videoRef = useRef(null); const canvasRef = useRef(null); + const videoPlayerRef = useRef(null); + const zmClient = useContext(ZoomContext); + // useEffect(() => { + // zmClient.leave(); + // }, []); const onCameraClick = useCallback(async () => { if (isStartedVideo) { @@ -150,9 +163,30 @@ const PreviewContainer = () => { if (localVideo) { if (activeCamera !== key) { await localVideo.switchCamera(key); + setActiveCamera(key); } } }; + const onSwitchMicrophone = (key: string) => { + setActiveMicrophone(key); + if (speakerTesterRef.current) { + speakerTesterRef.current.destroy(); + speakerTesterRef.current = undefined; + } + if (isRecordingVoice || isPlayingRecording) { + microphoneTesterRef.current?.stop(); + setIsRecordingVoice(false); + setIsPlayingRecording(false); + } + }; + const onSwitchSpeaker = (key: string) => { + setActiveSpeaker(key); + if (isPlayingAudio) { + speakerTesterRef.current?.stop(); + setIsPlayingAudio(false); + setOutputLevel(0); + } + }; const onBlurBackground = useCallback(async () => { if (isInVBMode) { if (isBlur) { @@ -164,9 +198,16 @@ const PreviewContainer = () => { } else { if (!isBlur) { localVideo.stop(); - if (canvasRef.current) { - localVideo.start(canvasRef.current, { imageUrl: 'blur' }); + if (isUseVideoPlayer) { + if (videoPlayerRef.current) { + localVideo.start(videoPlayerRef.current, { imageUrl: 'blur' }); + } + } else { + if (canvasRef.current) { + localVideo.start(canvasRef.current, { imageUrl: 'blur' }); + } } + setIsInVBMode(true); setIsBlur(!isBlur); } @@ -189,6 +230,8 @@ const PreviewContainer = () => { if (microphoneTesterRef.current) { microphoneTesterRef.current.destroy(); microphoneTesterRef.current = undefined; + setIsRecordingVoice(false); + setIsPlayingRecording(false); } if (isPlayingAudio) { speakerTesterRef.current?.stop(); @@ -208,6 +251,7 @@ const PreviewContainer = () => { if (speakerTesterRef.current) { speakerTesterRef.current.destroy(); speakerTesterRef.current = undefined; + setIsPlayingAudio(false); } if (!isPlayingRecording && !isRecordingVoice) { microphoneTesterRef.current = localAudio.testMicrophone({ @@ -265,12 +309,22 @@ const PreviewContainer = () => { playsInline ref={videoRef} /> - + {isUseVideoPlayer ? ( + + + + ) : ( + + )}
@@ -279,8 +333,8 @@ const PreviewContainer = () => { isMuted={isMuted} onMicrophoneClick={onMicrophoneClick} onMicrophoneMenuClick={onMicrophoneMenuClick} - microphoneList={micList} - speakerList={speakerList} + // microphoneList={micList} + // speakerList={speakerList} activeMicrophone={activeMicrophone} activeSpeaker={activeSpeaker} /> @@ -306,7 +360,7 @@ const PreviewContainer = () => { { - setActiveMicrophone(value); + onSwitchMicrophone(value); }} value={activeMicrophone} className="speaker-list" diff --git a/src/feature/subsession/component/broadcast-panel.tsx b/src/feature/subsession/component/broadcast-panel.tsx index 4ba2a5c..53f24bc 100644 --- a/src/feature/subsession/component/broadcast-panel.tsx +++ b/src/feature/subsession/component/broadcast-panel.tsx @@ -1,4 +1,6 @@ -import React, { useState, useCallback, useContext } from 'react'; +import type React from 'react'; +// eslint-disable-next-line no-duplicate-imports +import { useState, useCallback, useContext } from 'react'; import { Input, Button } from 'antd'; import './broadcast-panel.scss'; import ZoomContext from '../../../context/zoom-context'; diff --git a/src/feature/subsession/component/broadcast-voice-panel.tsx b/src/feature/subsession/component/broadcast-voice-panel.tsx index da03757..e52f5d7 100644 --- a/src/feature/subsession/component/broadcast-voice-panel.tsx +++ b/src/feature/subsession/component/broadcast-voice-panel.tsx @@ -6,7 +6,7 @@ import ZoomContext from '../../../context/zoom-context'; interface BroadcastPanelProps { afterBroadcast?: () => void; } -const BroadcastVoicePanel = (props: BroadcastPanelProps) => { +const BroadcastVoicePanel = (_props: BroadcastPanelProps) => { const [isStarted, setIsStarted] = useState(false); const buttonRef = useRef(null); const isHover = useHover(buttonRef); diff --git a/src/feature/subsession/component/draggable-modal.tsx b/src/feature/subsession/component/draggable-modal.tsx index d0f3b7c..679d6b3 100644 --- a/src/feature/subsession/component/draggable-modal.tsx +++ b/src/feature/subsession/component/draggable-modal.tsx @@ -1,4 +1,6 @@ -import React, { useState, useRef, useCallback } from 'react'; +import type React from 'react'; +// eslint-disable-next-line no-duplicate-imports +import { useState, useRef, useCallback } from 'react'; import Draggable from 'react-draggable'; import { Modal } from 'antd'; interface DraggableModalProps { diff --git a/src/feature/subsession/component/subsession-create.tsx b/src/feature/subsession/component/subsession-create.tsx index bf76c28..16282cf 100644 --- a/src/feature/subsession/component/subsession-create.tsx +++ b/src/feature/subsession/component/subsession-create.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { InputNumber, Radio, Button } from 'antd'; import { SubsessionAllocationPattern } from '@zoom/videosdk'; import './subsession-create.scss'; diff --git a/src/feature/subsession/component/subsession-item.tsx b/src/feature/subsession/component/subsession-item.tsx index ad69fa0..724469c 100644 --- a/src/feature/subsession/component/subsession-item.tsx +++ b/src/feature/subsession/component/subsession-item.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import { useContext } from 'react'; import classNames from 'classnames'; import { RollbackOutlined } from '@ant-design/icons'; import { IconFont } from '../../../component/icon-font'; diff --git a/src/feature/subsession/component/subsession-manage.tsx b/src/feature/subsession/component/subsession-manage.tsx index 2868fcd..0b4d71d 100644 --- a/src/feature/subsession/component/subsession-manage.tsx +++ b/src/feature/subsession/component/subsession-manage.tsx @@ -1,8 +1,8 @@ -import React, { useState, useCallback, useContext, useEffect } from 'react'; +import { useState, useCallback, useContext } from 'react'; import { Collapse, Button, Popover } from 'antd'; import { SubsessionUserStatus, SubsessionStatus } from '@zoom/videosdk'; -import { Participant } from '../../../index-types'; -import { CurrentSubsession, Subsession } from '../subsession-types'; +import type { Participant } from '../../../index-types'; +import type { CurrentSubsession, Subsession } from '../subsession-types'; import SubsessionItem from './subsession-item'; import SubsessionOptions from './subsession-options'; import BroadcastMessagePanel from './broadcast-panel'; diff --git a/src/feature/subsession/component/subsession-options.tsx b/src/feature/subsession/component/subsession-options.tsx index 7532991..33652b1 100644 --- a/src/feature/subsession/component/subsession-options.tsx +++ b/src/feature/subsession/component/subsession-options.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { Checkbox, InputNumber, Menu } from 'antd'; import './subsession-options.scss'; const { Item: MenuItem } = Menu; diff --git a/src/feature/subsession/hooks/useAskForHelp.ts b/src/feature/subsession/hooks/useAskForHelp.ts index 05b340d..686f3f3 100644 --- a/src/feature/subsession/hooks/useAskForHelp.ts +++ b/src/feature/subsession/hooks/useAskForHelp.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react'; import { Modal, message } from 'antd'; -import { SubsessionClient, ZoomClient } from '../../../index-types'; +import type { SubsessionClient, ZoomClient } from '../../../index-types'; import { AskHostHelpResponse } from '@zoom/videosdk'; const { confirm } = Modal; export function useAskForHelp(zmClient: ZoomClient, ssClient: SubsessionClient | null) { diff --git a/src/feature/subsession/hooks/useBroadcastMessage.ts b/src/feature/subsession/hooks/useBroadcastMessage.ts index 4a14cfc..fd08678 100644 --- a/src/feature/subsession/hooks/useBroadcastMessage.ts +++ b/src/feature/subsession/hooks/useBroadcastMessage.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react'; import { notification, message } from 'antd'; -import { ZoomClient } from '../../../index-types'; +import type { ZoomClient } from '../../../index-types'; const broadcastNotificationKey = 'BoNotification'; export function useBroadcastMessage(zmClient: ZoomClient) { const onBroadcastMessage = useCallback( diff --git a/src/feature/subsession/hooks/useInviteJoinRoom.ts b/src/feature/subsession/hooks/useInviteJoinRoom.ts index 2477f64..aa3b721 100644 --- a/src/feature/subsession/hooks/useInviteJoinRoom.ts +++ b/src/feature/subsession/hooks/useInviteJoinRoom.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect } from 'react'; -import { ZoomClient } from '../../../index-types'; +import type { ZoomClient } from '../../../index-types'; interface SubsessionInvite { subsessionId: string; subsessionName: string; diff --git a/src/feature/subsession/hooks/useParticipantsChange.ts b/src/feature/subsession/hooks/useParticipantsChange.ts index b84a1e8..bed8fda 100644 --- a/src/feature/subsession/hooks/useParticipantsChange.ts +++ b/src/feature/subsession/hooks/useParticipantsChange.ts @@ -1,7 +1,7 @@ -import { ParticipantPropertiesPayload } from '@zoom/videosdk'; +import type { ParticipantPropertiesPayload } from '@zoom/videosdk'; import { useEffect, useRef, useCallback } from 'react'; import { useMount } from '../../../hooks'; -import { ZoomClient, Participant } from '../../../index-types'; +import type { ZoomClient, Participant } from '../../../index-types'; export function useParticipantsChange( zmClient: ZoomClient, fn: (participants: Participant[], currentUpdates?: ParticipantPropertiesPayload[]) => void diff --git a/src/feature/subsession/hooks/useSubsession.ts b/src/feature/subsession/hooks/useSubsession.ts index a084d36..e22e609 100644 --- a/src/feature/subsession/hooks/useSubsession.ts +++ b/src/feature/subsession/hooks/useSubsession.ts @@ -1,14 +1,14 @@ import { + type ParticipantPropertiesPayload, SubsessionAllocationPattern, SubsessionUserStatus, - SubsessionStatus, - ParticipantPropertiesPayload + SubsessionStatus } from '@zoom/videosdk'; import produce from 'immer'; import { useState, useEffect, useCallback } from 'react'; import { useMount, usePrevious } from '../../../hooks'; -import { SubsessionClient, Participant, ZoomClient } from '../../../index-types'; -import { CurrentSubsession, Subsession } from '../subsession-types'; +import type { SubsessionClient, Participant, ZoomClient } from '../../../index-types'; +import type { CurrentSubsession, Subsession } from '../subsession-types'; import { useParticipantsChange } from './useParticipantsChange'; import { useSubsessionOptions } from './useSubsessionOptions'; export function useSubsession(zmClient: ZoomClient, ssClient: SubsessionClient | null) { diff --git a/src/feature/subsession/hooks/useSubsessionClosingCountdown.ts b/src/feature/subsession/hooks/useSubsessionClosingCountdown.ts index f9ae1e7..1dc468e 100644 --- a/src/feature/subsession/hooks/useSubsessionClosingCountdown.ts +++ b/src/feature/subsession/hooks/useSubsessionClosingCountdown.ts @@ -1,6 +1,6 @@ import { SubsessionStatus } from '@zoom/videosdk'; import { useState, useEffect, useCallback } from 'react'; -import { ZoomClient } from '../../../index-types'; +import type { ZoomClient } from '../../../index-types'; export function useSubsessionClosingCountdown(zmClient: ZoomClient, subsessionStatus: SubsessionStatus) { const [closingCountdown, setClosingCountdown] = useState(-1); diff --git a/src/feature/subsession/hooks/useSubsessionCountdown.ts b/src/feature/subsession/hooks/useSubsessionCountdown.ts index 12e745e..e6831ed 100644 --- a/src/feature/subsession/hooks/useSubsessionCountdown.ts +++ b/src/feature/subsession/hooks/useSubsessionCountdown.ts @@ -1,6 +1,6 @@ import { SubsessionStatus } from '@zoom/videosdk'; import { useState, useEffect, useCallback } from 'react'; -import { SubsessionClient, ZoomClient } from '../../../index-types'; +import type { SubsessionClient, ZoomClient } from '../../../index-types'; import { formatCountdown } from '../subsession-utils'; export function useSubsessionCountdown(zmClient: ZoomClient, ssClient: SubsessionClient | null) { diff --git a/src/feature/subsession/hooks/useSubsessionOptions.ts b/src/feature/subsession/hooks/useSubsessionOptions.ts index 89c0512..c20486d 100644 --- a/src/feature/subsession/hooks/useSubsessionOptions.ts +++ b/src/feature/subsession/hooks/useSubsessionOptions.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react'; -import { SubsessionOptions } from '../subsession-types'; +import type { SubsessionOptions } from '../subsession-types'; export function useSubsessionOptions() { const [isAutoJoinSubsession, setIsAutoJoinSubsession] = useState(false); const [isBackToMainSessionEnabled, setIsBackToMainSessionEnabled] = useState(true); diff --git a/src/feature/subsession/hooks/useSubsessionTimeup.ts b/src/feature/subsession/hooks/useSubsessionTimeup.ts index 1d72a5c..1d900e7 100644 --- a/src/feature/subsession/hooks/useSubsessionTimeup.ts +++ b/src/feature/subsession/hooks/useSubsessionTimeup.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react'; import { Modal } from 'antd'; -import { SubsessionClient, ZoomClient } from '../../../index-types'; +import type { SubsessionClient, ZoomClient } from '../../../index-types'; const { confirm } = Modal; export function useSubsessionTimeUp( zmClient: ZoomClient, diff --git a/src/feature/subsession/subsession-types.d.ts b/src/feature/subsession/subsession-types.d.ts index 73874ed..206268a 100644 --- a/src/feature/subsession/subsession-types.d.ts +++ b/src/feature/subsession/subsession-types.d.ts @@ -1,4 +1,4 @@ -import { SubsessionUserStatus, Participant } from '@zoom/videosdk'; +import type { SubsessionUserStatus } from '@zoom/videosdk'; interface SubsessionParticipant { isInSubsession?: boolean; userId: number; diff --git a/src/feature/subsession/subsession.tsx b/src/feature/subsession/subsession.tsx index b651a88..de09fff 100644 --- a/src/feature/subsession/subsession.tsx +++ b/src/feature/subsession/subsession.tsx @@ -129,7 +129,6 @@ const SubsessionContainer: React.FunctionComponent = (props currentSubsession.userStatus === SubsessionUserStatus.Invited; const isUseVideoPlayer = new URLSearchParams(props.location.search).get('useVideoPlayer') === '1'; - return (
{mediaStream?.isSupportMultipleVideos() ? ( diff --git a/src/feature/video/components/audio-video-statistic.tsx b/src/feature/video/components/audio-video-statistic.tsx index 77542df..dbf0e48 100644 --- a/src/feature/video/components/audio-video-statistic.tsx +++ b/src/feature/video/components/audio-video-statistic.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback, useContext, useRef } from 'react'; import { Modal, Table, Tabs } from 'antd'; -import { AudioQosData, VideoQosData } from '@zoom/videosdk'; +import type { AudioQosData, VideoQosData } from '@zoom/videosdk'; import ZoomContext from '../../../context/zoom-context'; import MediaContext from '../../../context/media-context'; -import { useMount, useUnmount } from '../../../hooks'; +import { useMount } from '../../../hooks'; interface AudioVideoStatisticModalProps { visible: boolean; defaultTab?: string; @@ -30,7 +30,7 @@ const AudioMetrics = [ { title: 'Bitrate', value: ['bitrate'], - format: (value: number) => `${(value / 1024).toFixed(1)} kps` + format: (value: number) => `${(value / 1024).toFixed(1)} kb/s` }, { title: 'Latency', diff --git a/src/feature/video/components/camera.tsx b/src/feature/video/components/camera.tsx index 059be30..86b15bb 100644 --- a/src/feature/video/components/camera.tsx +++ b/src/feature/video/components/camera.tsx @@ -1,10 +1,10 @@ import { useContext } from 'react'; -import { Button, Tooltip, Menu, Dropdown } from 'antd'; +import { Button, Tooltip, Dropdown } from 'antd'; import { CheckOutlined, UpOutlined, VideoCameraAddOutlined, VideoCameraOutlined } from '@ant-design/icons'; import ZoomMediaContext from '../../../context/media-context'; import classNames from 'classnames'; -import { MediaDevice } from '../video-types'; -import { getAntdDropdownMenu, getAntdItem, MenuItem } from './video-footer-utils'; +import type { MediaDevice } from '../video-types'; +import { type MenuItem, getAntdDropdownMenu, getAntdItem } from './video-footer-utils'; interface CameraButtonProps { isStartedVideo: boolean; isMirrored?: boolean; diff --git a/src/feature/video/components/draggable.tsx b/src/feature/video/components/draggable.tsx new file mode 100644 index 0000000..1e2935a --- /dev/null +++ b/src/feature/video/components/draggable.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useRef } from 'react'; +import { isAndroidOrIOSBrowser } from '../.../../../../utils/platform'; +interface WrapperProps { + children: React.ReactNode | undefined; + className?: string; + customstyle?: Record; +} +function Draggable({ children, className, customstyle }: WrapperProps) { + const selfViewRef = useRef(null); + let active = false; + let currentX: number; + let currentY: number; + let initialX: number; + let initialY: number; + let xOffset = 0; + let yOffset = 0; + function touchStart(e: TouchEvent) { + // e.preventDefault(); + // 获取触摸开始时的位置 + initialX = e.touches[0].clientX - xOffset; + initialY = e.touches[0].clientY - yOffset; + active = true; + } + function touchEnd(_e: TouchEvent) { + if (active) { + active = false; + } + } + function touchMove(e: TouchEvent) { + if (active) { + // e.preventDefault(); + // 获取触摸移动时的位置 + currentX = e.touches[0].clientX - initialX; + currentY = e.touches[0].clientY - initialY; + xOffset = currentX; + yOffset = currentY; + // 设置元素新位置 + if (selfViewRef.current) { + selfViewRef.current.style.transform = 'translate3d(' + currentX + 'px, ' + currentY + 'px, 0)'; + } + } + } + function dragStart(e: MouseEvent) { + if (e.type === 'mousedown') { + active = true; + // 获取鼠标指针位置 + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + } + } + + function dragEnd(_e: MouseEvent) { + if (active) { + active = false; + } + } + + function drag(e: MouseEvent) { + if (active) { + // e.preventDefault(); + // 计算元素新位置 + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + // 设置元素新位置 + if (selfViewRef.current) { + selfViewRef.current.style.transform = 'translate3d(' + currentX + 'px, ' + currentY + 'px, 0)'; + } + } + } + useEffect(() => { + let tempRef = selfViewRef.current; + if (isAndroidOrIOSBrowser()) { + // 触摸开始时触发的事件 + if (tempRef) { + tempRef.addEventListener('touchstart', touchStart, false); + } + // 触摸移动时触发的事件 + window.addEventListener('touchmove', touchMove, false); + // 触摸结束时触发的事件 + window.addEventListener('touchend', touchEnd, false); + } else { + if (tempRef) { + // 鼠标按下时触发的事件 + tempRef.addEventListener('mousedown', dragStart, false); + } + // 鼠标释放时触发的事件 + window.addEventListener('mouseup', dragEnd, false); + // 鼠标移动时触发的事件 + window.addEventListener('mousemove', drag, false); + } + + return () => { + if (isAndroidOrIOSBrowser()) { + if (tempRef) { + tempRef.removeEventListener('touchstart', touchStart); + } + // 触摸移动时触发的事件 + window.removeEventListener('touchmove', touchMove); + // 触摸结束时触发的事件 + window.removeEventListener('touchend', touchEnd); + } else { + if (tempRef) { + tempRef.removeEventListener('mousedown', dragStart); + } + window.removeEventListener('mouseup', dragEnd); + window.removeEventListener('mousemove', drag); + } + }; + }, []); + return ( +
+ {children} +
+ ); +} + +export default React.memo(Draggable); diff --git a/src/feature/video/components/leave.tsx b/src/feature/video/components/leave.tsx index 7279cf5..1bf69b1 100644 --- a/src/feature/video/components/leave.tsx +++ b/src/feature/video/components/leave.tsx @@ -1,11 +1,9 @@ -import React from 'react'; -import { Button, Dropdown, Menu } from 'antd'; +import { Button, Dropdown } from 'antd'; import classNames from 'classnames'; import { UpOutlined } from '@ant-design/icons'; import { IconFont } from '../../../component/icon-font'; import { getAntdDropdownMenu, getAntdItem } from './video-footer-utils'; const { Button: DropdownButton } = Dropdown; -const { Item: MenuItem } = Menu; interface LeaveButtonProps { onLeaveClick: () => void; onEndClick: () => void; diff --git a/src/feature/video/components/live-stream.tsx b/src/feature/video/components/live-stream.tsx index 3ec66b0..80a1835 100644 --- a/src/feature/video/components/live-stream.tsx +++ b/src/feature/video/components/live-stream.tsx @@ -1,4 +1,4 @@ -import { Button, Tooltip, Modal, Input, Form } from 'antd'; +import { Button, Modal, Input, Form } from 'antd'; import classNames from 'classnames'; import { IconFont } from '../../../component/icon-font'; interface LiveStreamButtonProps { diff --git a/src/feature/video/components/live-transcription.tsx b/src/feature/video/components/live-transcription.tsx index ed98229..ffced12 100644 --- a/src/feature/video/components/live-transcription.tsx +++ b/src/feature/video/components/live-transcription.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Button, Tooltip, Dropdown } from 'antd'; +import { Button, Dropdown } from 'antd'; import classNames from 'classnames'; import { IconFont } from '../../../component/icon-font'; import { UpOutlined } from '@ant-design/icons'; diff --git a/src/feature/video/components/microphone.tsx b/src/feature/video/components/microphone.tsx index b99dbff..4b8c3c9 100644 --- a/src/feature/video/components/microphone.tsx +++ b/src/feature/video/components/microphone.tsx @@ -4,10 +4,9 @@ import { Tooltip, Dropdown, Button } from 'antd'; import classNames from 'classnames'; import { CheckOutlined, UpOutlined } from '@ant-design/icons'; import { IconFont } from '../../../component/icon-font'; -import { MediaDevice } from '../video-types'; +import type { MediaDevice } from '../video-types'; import CallOutModal from './call-out-modal'; import { getAntdDropdownMenu, getAntdItem } from './video-footer-utils'; -// import { useCurrentAudioLevel } from '../hooks/useCurrentAudioLevel'; import CRCCallOutModal from './crc-call-out-modal'; import { AudoiAnimationIcon } from '../../../component/audio-animation-icon'; import { useAudioLevel } from '../hooks/useAudioLevel'; @@ -31,6 +30,7 @@ interface MicrophoneButtonProps { activeSpeaker?: string; phoneCallStatus?: { text: string; type: string }; isSecondaryAudioStarted?: boolean; + isPreview?: boolean; } const MicrophoneButton = (props: MicrophoneButtonProps) => { const { @@ -48,6 +48,7 @@ const MicrophoneButton = (props: MicrophoneButtonProps) => { disabled, isMicrophoneForbidden, isSecondaryAudioStarted, + isPreview, onMicrophoneClick, onMicrophoneMenuClick, onPhoneCallClick, @@ -87,13 +88,19 @@ const MicrophoneButton = (props: MicrophoneButtonProps) => { ); menuItems.push(getAntdItem('', 'd2', undefined, undefined, 'divider')); } - menuItems.push( - getAntdItem(isSecondaryAudioStarted ? 'Stop secondary audio' : 'Start secondary audio', 'secondary audio') - ); + if (!isPreview) { + menuItems.push( + getAntdItem(isSecondaryAudioStarted ? 'Stop secondary audio' : 'Start secondary audio', 'secondary audio') + ); + } + menuItems.push(getAntdItem('', 'd3', undefined, undefined, 'divider')); - if (audio !== 'phone') { - menuItems.push(getAntdItem('Audio Statistic', 'statistic')); + if (!isPreview) { + if (audio !== 'phone') { + menuItems.push(getAntdItem('Audio Statistic', 'statistic')); + } } + menuItems.push(getAntdItem(audio === 'phone' ? 'Hang Up' : 'Leave Audio', 'leave audio')); const onMenuItemClick = (payload: { key: any }) => { diff --git a/src/feature/video/components/pagination.scss b/src/feature/video/components/pagination.scss index 29edbb8..c06cbb2 100644 --- a/src/feature/video/components/pagination.scss +++ b/src/feature/video/components/pagination.scss @@ -1,6 +1,6 @@ .pagination { position: absolute; - top:50%; + top: 50%; left: 0; transform: translateY(-50%); height: 100px; @@ -9,7 +9,8 @@ justify-content: space-between; pointer-events: none; z-index: 2; - .previous-page-button,.next-page-button{ + .previous-page-button, + .next-page-button { display: flex; justify-content: center; align-items: center; @@ -19,24 +20,27 @@ pointer-events: auto; border: 0; background: #000; - > .anticon{ + > .anticon { font-size: 56px !important; } } - &.in-sharing{ + &.in-sharing { top: 0; - right:0; + right: 0; left: auto; width: 264px; flex-direction: column; transform: none; height: 100%; align-items: center; - .previous-page-button,.next-page-button{ + .previous-page-button, + .next-page-button { .anticon { transform: rotate(90deg); } } - + .next-page-button { + margin-bottom: 10vh; + } } -} \ No newline at end of file +} diff --git a/src/feature/video/components/pagination.tsx b/src/feature/video/components/pagination.tsx index 5e00568..17e1971 100644 --- a/src/feature/video/components/pagination.tsx +++ b/src/feature/video/components/pagination.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-boolean-value */ -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import classnames from 'classnames'; diff --git a/src/feature/video/components/remote-camera-control.tsx b/src/feature/video/components/remote-camera-control.tsx index 9f1212a..9f331ce 100644 --- a/src/feature/video/components/remote-camera-control.tsx +++ b/src/feature/video/components/remote-camera-control.tsx @@ -17,7 +17,6 @@ import './remote-camera-control.scss'; import { IconFont } from '../../../component/icon-font'; import { getAntdDropdownMenu, getAntdItem } from './video-footer-utils'; import classNames from 'classnames'; -import { PTZCameraCapability } from '@zoom/videosdk'; import { useCameraControl } from '../hooks/useCameraControl'; import AvatarContext from '../context/avatar-context'; diff --git a/src/feature/video/components/report-btn.tsx b/src/feature/video/components/report-btn.tsx index 4690734..da505b4 100644 --- a/src/feature/video/components/report-btn.tsx +++ b/src/feature/video/components/report-btn.tsx @@ -1,6 +1,6 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { Button, message, Modal, List, Typography } from 'antd'; -import { useState, useContext, useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import ZoomVideo from '@zoom/videosdk'; import ZoomContext from '../../../context/zoom-context'; import './report-btn.scss'; @@ -66,8 +66,6 @@ const ReportBtn = () => { width: 520 }); }; - // @ts-ignore - let meetingArgs: any = Object.fromEntries(new URLSearchParams(location.search)); return ( <> {msgContextHolder} diff --git a/src/feature/video/components/screen-share.tsx b/src/feature/video/components/screen-share.tsx index e4e66d6..a773ca3 100644 --- a/src/feature/video/components/screen-share.tsx +++ b/src/feature/video/components/screen-share.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Button, Tooltip, Menu, Dropdown } from 'antd'; +import { Button, Tooltip, Dropdown } from 'antd'; import classNames from 'classnames'; import { IconFont } from '../../../component/icon-font'; import { LockOutlined, UnlockOutlined, UpOutlined, CheckOutlined } from '@ant-design/icons'; diff --git a/src/feature/video/components/share-bar.tsx b/src/feature/video/components/share-bar.tsx index bb984b2..3300c13 100644 --- a/src/feature/video/components/share-bar.tsx +++ b/src/feature/video/components/share-bar.tsx @@ -10,7 +10,6 @@ import { ShareStatus } from '@zoom/videosdk'; import { SHARE_CANVAS_ID } from '../video-constants'; import { getAntdDropdownMenu, getAntdItem } from './video-footer-utils'; import './share-bar.scss'; -import { Participant } from '../../../index-types'; const { Button: DropdownButton } = Dropdown; interface ShareBarProps { @@ -34,11 +33,11 @@ const ShareBar = forwardRef((props: ShareBarProps, ref: any) => { if (shareAudioStatus?.isShareAudioEnabled) { if (shareAudioStatus.isShareAudioMuted) { mediaStream?.unmuteShareAudio().then(() => { - setShareAudioStatus(mediaStream.getShareAudioStatus()); + setShareAudioStatus(mediaStream?.getShareAudioStatus()); }); } else { mediaStream?.muteShareAudio().then(() => { - setShareAudioStatus(mediaStream.getShareAudioStatus()); + setShareAudioStatus(mediaStream?.getShareAudioStatus()); }); } } @@ -87,6 +86,7 @@ const ShareBar = forwardRef((props: ShareBarProps, ref: any) => { ); return (
+ (
+ )
); }); diff --git a/src/feature/video/components/share-view.tsx b/src/feature/video/components/share-view.tsx index 3003d9a..851229f 100644 --- a/src/feature/video/components/share-view.tsx +++ b/src/feature/video/components/share-view.tsx @@ -117,15 +117,11 @@ const ShareView = forwardRef((props: ShareViewProps, ref: any) => { mediaStream?.updateSharingCanvasDimension(shareViewSize.width * pixelRatio, shareViewSize.height * pixelRatio); } }, [mediaStream, previousShareViewSize, shareViewSize, viewType, previousViewType]); - useImperativeHandle( - ref, - () => { - return { - selfShareRef: selfShareViewRef.current - }; - }, - [] - ); + useImperativeHandle(ref, () => { + return { + selfShareRef: selfShareViewRef.current + }; + }, []); useEffect(() => { onRecieveSharingChange(isRecieveSharing); }, [isRecieveSharing, onRecieveSharingChange]); diff --git a/src/feature/video/components/video-footer.tsx b/src/feature/video/components/video-footer.tsx index 138fe05..ddc3fc5 100644 --- a/src/feature/video/components/video-footer.tsx +++ b/src/feature/video/components/video-footer.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useContext, useEffect, MutableRefObject } from 'react'; +import { useState, useCallback, useContext, useEffect } from 'react'; import classNames from 'classnames'; import { message, Modal, Form, Select, Checkbox, Tooltip } from 'antd'; import { SoundOutlined } from '@ant-design/icons'; @@ -9,17 +9,17 @@ import { ScreenShareButton } from './screen-share'; import AudioVideoStatisticModal from './audio-video-statistic'; import ZoomMediaContext from '../../../context/media-context'; import { useUnmount, useMount } from '../../../hooks'; -import { MediaDevice } from '../video-types'; +import type { MediaDevice } from '../video-types'; import './video-footer.scss'; -import { isAndroidOrIOSBrowser, isIOSMobile } from '../../../utils/platform'; +import { isAndroidOrIOSBrowser } from '../../../utils/platform'; import { getPhoneCallStatusDescription, SELF_VIDEO_ID } from '../video-constants'; -import { getRecordingButtons, RecordButtonProps, RecordingButton } from './recording'; +import { type RecordButtonProps, getRecordingButtons, RecordingButton } from './recording'; import { + type DialOutOption, DialoutState, RecordingStatus, MutedSource, AudioChangeAction, - DialOutOption, VideoCapturingState, SharePrivilege, MobileVideoFacingMode, @@ -33,6 +33,7 @@ import IsoRecordingModal from './recording-ask-modal'; import { LiveStreamButton, LiveStreamModal } from './live-stream'; import { IconFont } from '../../../component/icon-font'; import { VideoMaskModel } from './video-mask-modal'; +import { useParticipantsChange } from '../hooks/useParticipantsChange'; interface VideoFooterProps { className?: string; selfShareCanvas?: HTMLCanvasElement | HTMLVideoElement | null; @@ -58,7 +59,7 @@ const VideoFooter = (props: VideoFooterProps) => { const [isStartedLiveTranscription, setIsStartedLiveTranscription] = useState(false); const [isDisableCaptions, setIsDisableCaptions] = useState(false); const [isMirrored, setIsMirrored] = useState(false); - const [isBlur, setIsBlur] = useState(false); + const [isBlur, setIsBlur] = useState(mediaStream?.getVirtualbackgroundStatus().imageSrc === 'blur'); const [isMuted, setIsMuted] = useState(!!zmClient.getCurrentUserInfo()?.muted); const [activeMicrophone, setActiveMicrophone] = useState(mediaStream?.getActiveMicrophone()); const [activeSpeaker, setActiveSpeaker] = useState(mediaStream?.getActiveSpeaker()); @@ -84,10 +85,18 @@ const VideoFooter = (props: VideoFooterProps) => { const [isSecondaryAudioStarted, setIsSecondaryAudioStarted] = useState(false); const [secondaryMicForm] = Form.useForm(); + useParticipantsChange(zmClient, () => { + setIsMuted(!!zmClient.getCurrentUserInfo()?.muted); + }); const onCameraClick = useCallback(async () => { if (isStartedVideo) { await mediaStream?.stopVideo(); setIsStartedVideo(false); + if (activePlaybackUrl) { + await mediaStream?.switchMicrophone('default'); + setActiveMicrophone(mediaStream?.getActiveMicrophone()); + setActivePlaybackUrl(''); + } } else { const startVideoOptions = { hd: true, @@ -99,10 +108,22 @@ const VideoFooter = (props: VideoFooterProps) => { Object.assign(startVideoOptions, { virtualBackground: { imageUrl: 'blur' } }); } await mediaStream?.startVideo(startVideoOptions); + if (!mediaStream?.isSupportMultipleVideos()) { + const canvasElement = document.querySelector(`#${SELF_VIDEO_ID}`) as HTMLCanvasElement; + mediaStream?.renderVideo( + canvasElement, + zmClient.getSessionInfo().userId, + canvasElement.width, + canvasElement.height, + 0, + 0, + 3 + ); + } setIsStartedVideo(true); } - }, [mediaStream, isStartedVideo, isBlur]); + }, [mediaStream, isStartedVideo, zmClient, isBlur, activePlaybackUrl]); const onMicrophoneClick = useCallback(async () => { if (isStartedAudio) { if (isMuted) { @@ -117,6 +138,7 @@ const VideoFooter = (props: VideoFooterProps) => { } else { await mediaStream?.startAudio({ highBitrate: true }); } + setActiveMicrophone(mediaStream?.getActiveMicrophone()); } catch (e: any) { if (e.type === 'INSUFFICIENT_PRIVILEGES' && e.reason === 'USER_FORBIDDEN_MICROPHONE') { setIsMicrophoneForbidden(true); @@ -125,7 +147,7 @@ const VideoFooter = (props: VideoFooterProps) => { } // setIsStartedAudio(true); } - }, [mediaStream, isStartedAudio, isMuted]); + }, [mediaStream, isStartedAudio, isMuted, activePlaybackUrl]); const onMicrophoneMenuClick = async (key: string) => { if (mediaStream) { const [type, deviceId] = key.split('|'); @@ -142,6 +164,7 @@ const VideoFooter = (props: VideoFooterProps) => { } else if (type === 'leave audio') { if (audio === 'computer') { await mediaStream.stopAudio(); + setIsMicrophoneForbidden(false); } else if (audio === 'phone') { await mediaStream.hangup(); setPhoneCallStatus(undefined); @@ -154,6 +177,10 @@ const VideoFooter = (props: VideoFooterProps) => { await mediaStream.stopSecondaryAudio(); setIsSecondaryAudioStarted(false); } else { + const selectedMic = secondaryMicForm.getFieldValue('mic'); + if (!mediaStream.getMicList().some((item) => item.deviceId === selectedMic)) { + secondaryMicForm.setFieldValue('mic', undefined); + } Modal.confirm({ title: 'Start secondary audio', content: ( @@ -163,7 +190,11 @@ const VideoFooter = (props: VideoFooterProps) => { options={mediaStream.getMicList().map((item) => ({ value: item.deviceId, label: item.label, - disabled: item.deviceId === mediaStream.getActiveMicrophone() + disabled: + item.deviceId === mediaStream.getActiveMicrophone() || + item.label === + mediaStream.getMicList()?.find((item) => item.deviceId === mediaStream.getActiveMicrophone()) + ?.label }))} /> @@ -188,7 +219,7 @@ const VideoFooter = (props: VideoFooterProps) => { try { const data = await secondaryMicForm.validateFields(); const { mic, constraints } = data; - const option = {}; + const option = { autoGainControl: false, noiseSuppression: false, echoCancellation: false }; if (constraints) { constraints.forEach((key: string) => { Object.assign(option, { [`${key}`]: true }); @@ -210,7 +241,11 @@ const VideoFooter = (props: VideoFooterProps) => { if (activeCamera !== key) { await mediaStream.switchCamera(key); setActiveCamera(mediaStream.getActiveCamera()); - setActivePlaybackUrl(''); + if (activePlaybackUrl) { + await mediaStream.switchMicrophone('default'); + setActiveMicrophone(mediaStream.getActiveMicrophone()); + setActivePlaybackUrl(''); + } } } }; @@ -249,21 +284,19 @@ const VideoFooter = (props: VideoFooterProps) => { const onHostAudioMuted = useCallback( (payload: any) => { const { action, source, type } = payload; + const currentUser = zmClient.getCurrentUserInfo(); + setIsStartedAudio(currentUser.audio === 'computer' || currentUser.audio === 'phone'); + setAudio(currentUser.audio); if (action === AudioChangeAction.Join) { setIsStartedAudio(true); setAudio(type); - setTimeout(() => { - setIsMuted(!!zmClient.getCurrentUserInfo()?.muted); - }, 1000); } else if (action === AudioChangeAction.Leave) { setIsStartedAudio(false); } else if (action === AudioChangeAction.Muted) { - setIsMuted(true); if (source === MutedSource.PassiveByMuteOne) { message.info('Host muted you'); } } else if (action === AudioChangeAction.Unmuted) { - setIsMuted(false); if (source === 'passive') { message.info('Host unmuted you'); } @@ -390,10 +423,22 @@ const VideoFooter = (props: VideoFooterProps) => { }, [mediaStream] ); - const onHostAskToUnmute = useCallback((payload: any) => { - const { reason } = payload; - console.log(`Host ask to unmute the audio.`, reason); - }, []); + const onHostAskToUnmute = useCallback( + (payload: any) => { + const { reason } = payload; + Modal.confirm({ + title: 'Unmute audio', + content: `Host ask to unmute audio, reason: ${reason}`, + onOk: async () => { + await mediaStream?.unmuteAudio(); + }, + onCancel() { + console.log('cancel'); + } + }); + }, + [mediaStream] + ); const onCaptionStatusChange = useCallback((payload: any) => { const { autoCaption } = payload; @@ -520,7 +565,20 @@ const VideoFooter = (props: VideoFooterProps) => { } } }); - + useEffect(() => { + if (mediaStream && zmClient.getSessionInfo().isInMeeting && statisticVisible) { + mediaStream.subscribeAudioStatisticData(); + mediaStream.subscribeVideoStatisticData(); + mediaStream.subscribeShareStatisticData(); + } + return () => { + if (zmClient.getSessionInfo().isInMeeting) { + mediaStream?.unsubscribeAudioStatisticData(); + mediaStream?.unsubscribeVideoStatisticData(); + mediaStream?.unsubscribeShareStatisticData(); + } + }; + }, [mediaStream, zmClient, statisticVisible]); const recordingButtons: RecordButtonProps[] = getRecordingButtons(recordingStatus, zmClient.isHost()); return (
diff --git a/src/feature/video/hooks/useAttachPagination.ts b/src/feature/video/hooks/useAttachPagination.ts new file mode 100644 index 0000000..d93eaa4 --- /dev/null +++ b/src/feature/video/hooks/useAttachPagination.ts @@ -0,0 +1,28 @@ +import { useState } from 'react'; +import type { ZoomClient } from '../../../index-types'; +import { isAndroidOrIOSBrowser, isIPad } from '../../../utils/platform'; +import { useParticipantsChange } from './useParticipantsChange'; + +export function usePagination(zmClient: ZoomClient, preferVideoCount: number) { + let pageSize = preferVideoCount; + if (isAndroidOrIOSBrowser()) { + pageSize = Math.min(preferVideoCount, 4); + } + if (isIPad()) { + pageSize = Math.min(preferVideoCount, 9); + } + const [page, setPage] = useState(0); + const [totalSize, setTotalSize] = useState(0); + + useParticipantsChange(zmClient, (allParticipants) => { + setTotalSize(allParticipants.length - 1); + }); + + return { + page, + totalPage: Math.ceil(totalSize / pageSize), + pageSize, + totalSize, + setPage + }; +} diff --git a/src/feature/video/video-attach.tsx b/src/feature/video/video-attach.tsx index be795b9..bd69d68 100644 --- a/src/feature/video/video-attach.tsx +++ b/src/feature/video/video-attach.tsx @@ -1,22 +1,16 @@ -import React, { - useState, - useContext, - useRef, - useEffect, - DOMAttributes, - HTMLAttributes, - DetailedHTMLProps, - useCallback, - useMemo -} from 'react'; +import { useState, useContext, useRef, useEffect, useCallback, useMemo } from 'react'; +// eslint-disable-next-line no-duplicate-imports +import type React from 'react'; import classnames from 'classnames'; import _ from 'lodash'; -import { RouteComponentProps } from 'react-router-dom'; -import { type VideoPlayerContainer, type VideoPlayer, VideoQuality } from '@zoom/videosdk'; +import type { RouteComponentProps } from 'react-router-dom'; +import { type VideoPlayer, VideoQuality } from '@zoom/videosdk'; import ZoomContext from '../../context/zoom-context'; import ZoomMediaContext from '../../context/media-context'; import AvatarActionContext from './context/avatar-context'; import ShareView from './components/share-view'; +import Pagination from './components/pagination'; +import { usePagination } from './hooks/useAttachPagination'; import VideoFooter from './components/video-footer'; import ReportBtn from './components/report-btn'; import Avatar from './components/avatar'; @@ -24,37 +18,35 @@ import { useActiveVideo } from './hooks/useAvtiveVideo'; import { useAvatarAction } from './hooks/useAvatarAction'; import { useNetworkQuality } from './hooks/useNetworkQuality'; import { useParticipantsChange } from './hooks/useParticipantsChange'; -import { Participant } from '../../index-types'; -import { useOrientation, usePrevious } from '../../hooks'; +import type { Participant } from '../../index-types'; +import { usePrevious } from '../../hooks'; import { useVideoAspect } from './hooks/useVideoAspectRatio'; import { Radio } from 'antd'; +// import Draggable from 'react-draggable'; +import Draggable from './components/draggable'; import { useSpotlightVideo } from './hooks/useSpotlightVideo'; import RemoteCameraControlPanel from './components/remote-camera-control'; -type CustomElement = Partial & { children: any }>; +import { isAndroidOrIOSBrowser } from '../../utils/platform'; -declare global { - namespace JSX { - interface IntrinsicElements { - ['video-player']: DetailedHTMLProps, VideoPlayer> & { class?: string }; - ['video-player-container']: CustomElement & { class?: string }; - // ['zoom-video']: DetailedHTMLProps, VideoPlayer> & { class?: string }; - // ['zoom-video-container']: CustomElement & { class?: string }; - } - } +interface ExtendedParticipant extends Participant { + spotlighted?: boolean; } -function maxVideoCellWidth(orientation: string, totalParticipants: number, spotlighted?: boolean[]) { - return orientation === 'portrait' ? 'none' : `calc(100vw/${Math.min(totalParticipants, spotlighted ? 2 : 4)})`; -} +type VideoAttachProps = RouteComponentProps; -const VideoContainer: React.FunctionComponent = (props) => { +const VideoContainer: React.FunctionComponent = (props) => { + const preferPageCount = Number(new URLSearchParams(props.location.search).get('videoCount') || 4); const zmClient = useContext(ZoomContext); + const { page, pageSize, totalPage, setPage } = usePagination(zmClient, preferPageCount); const { mediaStream } = useContext(ZoomMediaContext); const shareViewRef = useRef<{ selfShareRef: HTMLCanvasElement | HTMLVideoElement | null }>(null); + const videoPlayerListRef = useRef>({}); const [isRecieveSharing, setIsRecieveSharing] = useState(false); - const [spotlightUsers, setSpotlightUsers] = useState(); - const [participants, setParticipants] = useState(zmClient.getAllUser()); + const [spotlightUsers, setSpotlightUsers] = useState([]); + const [participants, setParticipants] = useState([]); + const [currentPageParticipants, setCurrentPageParticipants] = useState([]); + const [currentUser, setCurrentUser] = useState(zmClient.getCurrentUserInfo()); const [subscribers, setSubscribers] = useState([]); const activeVideo = useActiveVideo(zmClient); const avatarActionState = useAvatarAction(zmClient, participants, true); @@ -67,29 +59,45 @@ const VideoContainer: React.FunctionComponent = (props) => { label: '180P', value: VideoQuality.Video_180P }, { label: '90P', value: VideoQuality.Video_90P } ]; - const orientation = useOrientation(); - useParticipantsChange(zmClient, (participants) => { - let pageParticipants: Participant[] = []; - if (participants.length > 0) { - if (participants.length === 1) { - pageParticipants = participants; - } else { - pageParticipants = participants - .filter((user) => user.userId !== zmClient.getSessionInfo().userId) - .sort((user1, user2) => Number(user2.bVideoOn) - Number(user1.bVideoOn)); - const currentUser = zmClient.getCurrentUserInfo(); - if (currentUser) { - pageParticipants.splice(1, 0, currentUser); - } + useSpotlightVideo(zmClient, mediaStream, (p) => { + setSpotlightUsers(p.map((user) => user.userId)); + }); + + useParticipantsChange(zmClient, (allParticipants, updatedParticipants) => { + let participants: ExtendedParticipant[] = []; + let tempCurUserInfo: Participant | null = null; + // enhance failover logic + if (!currentUser?.userId) { + tempCurUserInfo = zmClient.getCurrentUserInfo(); + setCurrentUser(tempCurUserInfo); + } + updatedParticipants?.forEach((p) => { + if (p?.userId === currentUser?.userId) { + setCurrentUser((c) => ({ + ...c, + ...p + })); } + }); + + if (allParticipants.length > 0) { + participants = allParticipants + .filter((p) => p.userId !== (currentUser?.userId || tempCurUserInfo?.userId)) + .sort((user1, user2) => Number(user2.bVideoOn) - Number(user1.bVideoOn)); } - setParticipants(pageParticipants); - setSubscribers(pageParticipants.filter((user) => user.bVideoOn).map((u) => u.userId)); - }); - useSpotlightVideo(zmClient, mediaStream, (p) => { - setSpotlightUsers(p); + setParticipants(participants); }); + + useEffect(() => { + let currPageParticipants: ExtendedParticipant[] = participants + .map((p) => ({ ...p, spotlighted: spotlightUsers.includes(p?.userId) })) + .sort((user1, user2) => Number(user2.spotlighted) - Number(user1.spotlighted)) + .slice(page * pageSize, page * pageSize + pageSize); + setCurrentPageParticipants(currPageParticipants); + setSubscribers(currPageParticipants.filter((user) => user.bVideoOn).map((u) => u.userId)); + }, [pageSize, page, participants, spotlightUsers]); + const setVideoPlayerRef = (userId: number, element: VideoPlayer | null) => { if (element) { videoPlayerListRef.current[`${userId}`] = element; @@ -112,6 +120,17 @@ const VideoContainer: React.FunctionComponent = (props) => }); } }, [subscribers, previousSubscribers, mediaStream]); + useEffect(() => { + if (currentUser?.bVideoOn) { + const attachment = videoPlayerListRef.current[`${currentUser?.userId}`]; + if (attachment) { + mediaStream?.attachVideo(currentUser?.userId, VideoQuality.Video_720P, attachment); + } + } else { + mediaStream?.detachVideo(currentUser?.userId); + } + }, [currentUser, mediaStream]); + const onVideoResolutionChange = useCallback( ({ target: { value } }: any, userId: number) => { const attachment = videoPlayerListRef.current[`${userId}`]; @@ -119,23 +138,62 @@ const VideoContainer: React.FunctionComponent = (props) => }, [videoPlayerListRef, mediaStream] ); - const sortedParticipants = useMemo(() => { - if (spotlightUsers?.length) { - const splightUserIds = spotlightUsers.map((u) => u.userId); - return participants - .filter((user) => !splightUserIds.includes(user.userId)) - .concat( - participants - .filter((user) => splightUserIds.includes(user.userId)) - .map((user) => ({ spotlighted: true, ...user })) - ); + const gridColumns = useMemo(() => { + if (isRecieveSharing) return 1; + if (spotlightUsers.length) { + return Math.sqrt(pageSize) * 2; } - return participants; - }, [participants, spotlightUsers]); - + return Math.sqrt(pageSize); + }, [spotlightUsers, isRecieveSharing, pageSize]); + const gridRows = useMemo(() => { + if (isRecieveSharing) { + return pageSize; + } + if (spotlightUsers.length) { + return Math.sqrt(pageSize) * 2; + } + return Math.sqrt(pageSize); + }, [spotlightUsers, isRecieveSharing, pageSize]); return ( -
+
+ {/*
*/} + + + + {currentUser?.bVideoOn && ( +
+ { + setVideoPlayerRef(currentUser?.userId, element); + }} + /> +
+ )} + {currentUser && ( + + )} +
+
+
+ {/*
*/} +
= (props) => > -
    - {sortedParticipants.map((user) => { - const maxWidth = maxVideoCellWidth(orientation, participants.length, (user as any).spotlighted); +
      + {currentPageParticipants.map((user) => { return (
      {avatarActionState?.avatarActionState[user?.userId]?.videoResolutionAdjust?.toggled && (
      @@ -175,31 +227,35 @@ const VideoContainer: React.FunctionComponent = (props) => />
      )} - {user.bVideoOn && ( -
      +
      + {user.bVideoOn && ( { setVideoPlayerRef(user.userId, element); }} /> -
      - )} - + )} + +
      ); })}
    - + {zmClient.getSessionInfo()?.isInMeeting && }
+ {totalPage > 1 && }
); diff --git a/src/feature/video/video-layout-helper.ts b/src/feature/video/video-layout-helper.ts index 06134ae..4052348 100644 --- a/src/feature/video/video-layout-helper.ts +++ b/src/feature/video/video-layout-helper.ts @@ -1,4 +1,4 @@ -import { CellLayout, Position } from './video-types'; +import type { CellLayout } from './video-types'; import { VideoQuality } from '@zoom/videosdk'; interface Grid { row: number; diff --git a/src/feature/video/video-single.tsx b/src/feature/video/video-single.tsx index a3ca446..d0ce4d5 100644 --- a/src/feature/video/video-single.tsx +++ b/src/feature/video/video-single.tsx @@ -69,6 +69,7 @@ const VideoContainer: React.FunctionComponent = (_props) => zmClient.off('video-capturing-change', onCurrentVideoChange); }; }, [zmClient, onActiveVideoChange, onCurrentVideoChange]); + // active user = regard as `video-active-change` payload, excluding the case where it is self and the video is turned on. // In this case, the self video is rendered seperately. const activeUser = useMemo( @@ -78,11 +79,13 @@ const VideoContainer: React.FunctionComponent = (_props) => ), [participants, activeVideo, zmClient] ); + const isCurrentUserStartedVideo = zmClient.getCurrentUserInfo()?.bVideoOn; useEffect(() => { if (mediaStream && videoRef.current) { if (activeUser?.bVideoOn !== previousActiveUser.current?.bVideoOn) { if (activeUser?.bVideoOn) { + mediaStream.stopRenderVideo(videoRef.current, activeUser.userId); mediaStream.renderVideo( videoRef.current, activeUser.userId, @@ -196,7 +199,7 @@ const VideoContainer: React.FunctionComponent = (_props) => networkQuality={networkQuality[`${activeUser.userId}`]} /> )} - + {zmClient.getSessionInfo()?.isInMeeting && }
diff --git a/src/feature/video/video.scss b/src/feature/video/video.scss index 52db0cb..c4775ab 100644 --- a/src/feature/video/video.scss +++ b/src/feature/video/video.scss @@ -10,11 +10,14 @@ .video-container { position: relative; width: 100%; - height: 100%; &.video-container-attech { display: flex; align-items: center; + overflow-y: hidden; + height: 100%; + flex-direction: column; } + &.video-container-in-sharing { width: min(20vw, 264px); flex-shrink: 0; @@ -59,23 +62,27 @@ } .video-container-wrap { flex: 1; + width: 100%; height: 100%; } + .user-list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-around; - align-items: center; + height: 100%; + display: grid; .video-cell { - min-width: 256px; - aspect-ratio: 16/9; position: relative; - flex: 1; margin: 12px; - &.video-cell-spotlight{ - min-width: 50vw; + &.video-cell-spotlight { + grid-row-start: span 2; + grid-column-start: span 2; + } + .aspact-ratio { + position: relative; + max-width: 100%; + max-height: 100%; + height: 100%; + overflow: hidden; } .change-video-resolution { position: absolute; @@ -91,6 +98,30 @@ } } } + .unified-self-view { + position: fixed; + aspect-ratio: 4/3; + top: 50px; + right: 50px; + z-index: 2; + cursor: all-scroll; + touch-action: none; + } + + .unified-self-view-container { + width: 100%; + aspect-ratio: 4/3; + background-color: black; + } + .video-player { + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + .avatar-list { position: absolute; top: 0; diff --git a/src/utils/platform.ts b/src/utils/platform.ts index 3fad199..9c1aeb1 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -27,8 +27,8 @@ export function getExploreName() { export function isSupportWebCodecs() { return typeof (window as any).MediaStreamTrackProcessor === 'function'; } -const isIPad = () => { - return /MacIntel/i.test(navigator.platform) && navigator?.maxTouchPoints > 2; +export const isIPad = () => { + return /iPad/i.test(navigator.userAgent) || (/MacIntel/i.test(navigator.platform) && navigator?.maxTouchPoints > 2); }; export const isIOSMobile = () => { const { userAgent } = navigator; From 4c4e896f006f669273cfdb2126f216d9cef7ff66 Mon Sep 17 00:00:00 2001 From: "vic.yang" Date: Mon, 30 Dec 2024 10:16:15 +0800 Subject: [PATCH 2/3] remove comments --- src/feature/video/components/draggable.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/feature/video/components/draggable.tsx b/src/feature/video/components/draggable.tsx index 1e2935a..5733c38 100644 --- a/src/feature/video/components/draggable.tsx +++ b/src/feature/video/components/draggable.tsx @@ -15,8 +15,6 @@ function Draggable({ children, className, customstyle }: WrapperProps) { let xOffset = 0; let yOffset = 0; function touchStart(e: TouchEvent) { - // e.preventDefault(); - // 获取触摸开始时的位置 initialX = e.touches[0].clientX - xOffset; initialY = e.touches[0].clientY - yOffset; active = true; @@ -28,13 +26,10 @@ function Draggable({ children, className, customstyle }: WrapperProps) { } function touchMove(e: TouchEvent) { if (active) { - // e.preventDefault(); - // 获取触摸移动时的位置 currentX = e.touches[0].clientX - initialX; currentY = e.touches[0].clientY - initialY; xOffset = currentX; yOffset = currentY; - // 设置元素新位置 if (selfViewRef.current) { selfViewRef.current.style.transform = 'translate3d(' + currentX + 'px, ' + currentY + 'px, 0)'; } @@ -57,13 +52,10 @@ function Draggable({ children, className, customstyle }: WrapperProps) { function drag(e: MouseEvent) { if (active) { - // e.preventDefault(); - // 计算元素新位置 currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; - // 设置元素新位置 if (selfViewRef.current) { selfViewRef.current.style.transform = 'translate3d(' + currentX + 'px, ' + currentY + 'px, 0)'; } @@ -72,22 +64,16 @@ function Draggable({ children, className, customstyle }: WrapperProps) { useEffect(() => { let tempRef = selfViewRef.current; if (isAndroidOrIOSBrowser()) { - // 触摸开始时触发的事件 if (tempRef) { tempRef.addEventListener('touchstart', touchStart, false); } - // 触摸移动时触发的事件 window.addEventListener('touchmove', touchMove, false); - // 触摸结束时触发的事件 window.addEventListener('touchend', touchEnd, false); } else { if (tempRef) { - // 鼠标按下时触发的事件 tempRef.addEventListener('mousedown', dragStart, false); } - // 鼠标释放时触发的事件 window.addEventListener('mouseup', dragEnd, false); - // 鼠标移动时触发的事件 window.addEventListener('mousemove', drag, false); } @@ -96,9 +82,7 @@ function Draggable({ children, className, customstyle }: WrapperProps) { if (tempRef) { tempRef.removeEventListener('touchstart', touchStart); } - // 触摸移动时触发的事件 window.removeEventListener('touchmove', touchMove); - // 触摸结束时触发的事件 window.removeEventListener('touchend', touchEnd); } else { if (tempRef) { From 172eb54144b1a6be260d8b2a2e010a65f6eeb93b Mon Sep 17 00:00:00 2001 From: "vic.yang" Date: Mon, 30 Dec 2024 13:20:45 +0800 Subject: [PATCH 3/3] Video SDK Web 2.1.0 update --- src/feature/video/components/video-footer.tsx | 13 ------------- src/feature/video/video-single.tsx | 5 +---- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/feature/video/components/video-footer.tsx b/src/feature/video/components/video-footer.tsx index ddc3fc5..d9309ff 100644 --- a/src/feature/video/components/video-footer.tsx +++ b/src/feature/video/components/video-footer.tsx @@ -108,19 +108,6 @@ const VideoFooter = (props: VideoFooterProps) => { Object.assign(startVideoOptions, { virtualBackground: { imageUrl: 'blur' } }); } await mediaStream?.startVideo(startVideoOptions); - if (!mediaStream?.isSupportMultipleVideos()) { - const canvasElement = document.querySelector(`#${SELF_VIDEO_ID}`) as HTMLCanvasElement; - mediaStream?.renderVideo( - canvasElement, - zmClient.getSessionInfo().userId, - canvasElement.width, - canvasElement.height, - 0, - 0, - 3 - ); - } - setIsStartedVideo(true); } }, [mediaStream, isStartedVideo, zmClient, isBlur, activePlaybackUrl]); diff --git a/src/feature/video/video-single.tsx b/src/feature/video/video-single.tsx index d0ce4d5..2e762bb 100644 --- a/src/feature/video/video-single.tsx +++ b/src/feature/video/video-single.tsx @@ -69,7 +69,6 @@ const VideoContainer: React.FunctionComponent = (_props) => zmClient.off('video-capturing-change', onCurrentVideoChange); }; }, [zmClient, onActiveVideoChange, onCurrentVideoChange]); - // active user = regard as `video-active-change` payload, excluding the case where it is self and the video is turned on. // In this case, the self video is rendered seperately. const activeUser = useMemo( @@ -79,13 +78,11 @@ const VideoContainer: React.FunctionComponent = (_props) => ), [participants, activeVideo, zmClient] ); - const isCurrentUserStartedVideo = zmClient.getCurrentUserInfo()?.bVideoOn; useEffect(() => { if (mediaStream && videoRef.current) { if (activeUser?.bVideoOn !== previousActiveUser.current?.bVideoOn) { if (activeUser?.bVideoOn) { - mediaStream.stopRenderVideo(videoRef.current, activeUser.userId); mediaStream.renderVideo( videoRef.current, activeUser.userId, @@ -199,7 +196,7 @@ const VideoContainer: React.FunctionComponent = (_props) => networkQuality={networkQuality[`${activeUser.userId}`]} /> )} - {zmClient.getSessionInfo()?.isInMeeting && } + {zmClient.getSessionInfo()?.isInMeeting && }