Skip to content

Commit

Permalink
Merge pull request #72 from zoom/1.12.5-demo
Browse files Browse the repository at this point in the history
update demo support spotlight
  • Loading branch information
ylkjick532428 authored Oct 18, 2024
2 parents add80f7 + 93c074e commit 8ee8c38
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 15 deletions.
37 changes: 36 additions & 1 deletion src/feature/video/components/avatar-more.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { useState, useCallback, useContext } from 'react';
import { Slider, Dropdown, Button } from 'antd';
import { AudioMutedOutlined, CheckOutlined, MoreOutlined } from '@ant-design/icons';
import { CheckOutlined, MoreOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import AvatarActionContext from '../context/avatar-context';
import ZoomContext from '../../../context/zoom-context';
import MediaContext from '../../../context/media-context';
import { getAntdDropdownMenu, getAntdItem } from './video-footer-utils';
import { useSpotlightVideo } from '../hooks/useSpotlightVideo';
interface AvatarMoreProps {
className?: string;
userId: number;
isHover: boolean;
}
const isUseVideoPlayer = new URLSearchParams(location.search).get('useVideoPlayer') === '1';
const AvatarMore = (props: AvatarMoreProps) => {
const { userId, isHover } = props;
const { avatarActionState, dispatch } = useContext(AvatarActionContext);
const { mediaStream } = useContext(MediaContext);
const zmClient = useContext(ZoomContext);
const [isDropdownVisible, setIsDropdownVisbile] = useState(false);
const [isControllingRemoteCamera, setIsControllingRemoteCamera] = useState(false);
useSpotlightVideo(zmClient, mediaStream, (participants) => {
dispatch({ type: 'set-spotlighted-videos', payload: participants });
});
const actionItem = avatarActionState[`${userId}`];
const { spotlightedUserList } = avatarActionState;
const menu = [];
if (actionItem) {
if (actionItem.localVolumeAdjust.enabled) {
Expand All @@ -38,6 +45,27 @@ const AvatarMore = (props: AvatarMoreProps) => {
)
);
}
if (isUseVideoPlayer) {
const currentUserId = zmClient.getCurrentUserInfo()?.userId;
const isHostOrManager = zmClient.isHost() || zmClient.isManager();
if (
currentUserId === userId &&
spotlightedUserList?.find((user) => user.userId === currentUserId) &&
spotlightedUserList.length === 1
) {
menu.push(getAntdItem('Remove spotlight', 'removeSpotlight'));
} else if (isHostOrManager) {
if (spotlightedUserList && spotlightedUserList.findIndex((user) => user.userId === userId) > -1) {
menu.push(getAntdItem('Remove spotlight', 'removeSpotlight'));
} else {
const user = zmClient.getUser(userId);
if (user?.bVideoOn) {
menu.push(getAntdItem('Add spotlight', 'addSpotlight'));
menu.push(getAntdItem('Replace spotlight', 'replaceSpotlight'));
}
}
}
}
const onSliderChange = useCallback(
(value: any) => {
mediaStream?.adjustUserAudioVolumeLocally(userId, value);
Expand All @@ -48,6 +76,7 @@ const AvatarMore = (props: AvatarMoreProps) => {
const onDropDownVisibleChange = useCallback((visible: boolean) => {
setIsDropdownVisbile(visible);
}, []);

const onMenuItemClick = useCallback(
({ key }: { key: string }) => {
if (key === 'volume') {
Expand All @@ -63,6 +92,12 @@ const AvatarMore = (props: AvatarMoreProps) => {
setIsControllingRemoteCamera(!isControllingRemoteCamera);
} else if (key === 'subscribeVideoQuality') {
dispatch({ type: 'toggle-video-resolution-adjust', payload: { userId } });
} else if (key === 'removeSpotlight') {
mediaStream?.removeSpotlightedVideo(userId);
} else if (key === 'addSpotlight') {
mediaStream?.spotlightVideo(userId, false);
} else if (key === 'replaceSpotlight') {
mediaStream?.spotlightVideo(userId, true);
}
setIsDropdownVisbile(false);
},
Expand Down
5 changes: 4 additions & 1 deletion src/feature/video/components/call-out-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Modal, Select, Input, Checkbox, Form } from 'antd';
import { useContext } from 'react';
import classNames from 'classnames';
import ZoomContext from '../../../context/zoom-context';
import './call-out-modal.scss';
interface CallOutModalProps {
visible: boolean;
Expand All @@ -13,6 +15,7 @@ interface CallOutModalProps {
const CallOutModal = (props: CallOutModalProps) => {
const { visible, phoneCountryList, phoneCallStatus, onPhoneCallClick, onPhoneCallCancel, setVisible } = props;
const [form] = Form.useForm();
const zmClient = useContext(ZoomContext);
return (
<Modal
open={visible}
Expand All @@ -31,7 +34,7 @@ const CallOutModal = (props: CallOutModalProps) => {
} = data;
const [, code] = countryCode.split('&&');
if (callme) {
onPhoneCallClick?.(code, phoneNumber, '', { callMe: true });
onPhoneCallClick?.(code, phoneNumber, zmClient.getCurrentUserInfo().displayName, { callMe: true });
} else {
onPhoneCallClick?.(code, phoneNumber, name, {
callMe: false,
Expand Down
2 changes: 2 additions & 0 deletions src/feature/video/context/avatar-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Participant } from '../../../index-types';

interface FeatureSwitch {
toggled: boolean;
Expand All @@ -14,5 +15,6 @@ interface AvatarSwitch {
}
export type AvatarContext = {
isControllingRemoteCamera?: boolean;
spotlightedUserList?: Participant[];
} & AvatarSwitch;
export default React.createContext<{ avatarActionState: AvatarContext; dispatch: React.Dispatch<any> }>(null as any);
5 changes: 5 additions & 0 deletions src/feature/video/hooks/useAvatarAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ const avatarActionReducer = produce((draft, action) => {
draft.isControllingRemoteCamera = payload;
break;
}
case 'set-spotlighted-videos': {
const { payload } = action;
draft.spotlightedUserList = payload;
break;
}
case 'toggle-video-resolution-adjust': {
const {
payload: { userId }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { ZoomClient, MediaStream } from '../../../index-types';
import { ApprovedState, RemoteControlAppStatus, RemoteControlSessionStatus } from '@zoom/videosdk';
import { message, Modal } from 'antd';
import { message, Modal, Checkbox } from 'antd';
export function useRemoteControl(
zmClient: ZoomClient,
mediaStream: MediaStream | null,
Expand All @@ -12,6 +12,7 @@ export function useRemoteControl(
const [controllingUser, setControllingUser] = useState<{ userId: number; displayName: string } | null>(null);
const isDownloadAppRef = useRef(false);
const launchModalRef = useRef<any>(null);
const runAsAdminRef = useRef<any>(null);
const onInControllingChange = useCallback((payload: any) => {
const { isControlling } = payload;
setIsControllingUser(isControlling);
Expand Down Expand Up @@ -40,14 +41,26 @@ export function useRemoteControl(
}
Modal.confirm({
title: `${displayName} is requesting remote control of your screen`,
content: isSharingEntireScreen
? 'In order to control your screen, you must install Zoom Remote Control app with a size of 4 MB to continue. You can regain control at any time by clicking on your screen.'
: 'To be controlled, you must share your entire screen instead of a tab or window. After sharing the entire screen, you’ll be requested again.',
content: isSharingEntireScreen ? (
<>
<div>
In order to control your screen, you must install Zoom Remote Control app with a size of 4 MB to continue.
You can regain control at any time by clicking on your screen.
</div>
{navigator.platform?.startsWith('Win') && (
<div style={{ color: '#999', marginTop: '20px' }}>
<Checkbox ref={runAsAdminRef}>Enable the RemoteControl App to control of all applications</Checkbox>
</div>
)}
</>
) : (
'To be controlled, you must share your entire screen instead of a tab or window. After sharing the entire screen, you’ll be requested again.'
),
okText: isSharingEntireScreen ? 'Approve' : 'Select Entire Screen',
cancelText: 'Decline',
onOk: async () => {
if (isSharingEntireScreen) {
mediaStream?.approveRemoteControl(userId);
mediaStream?.approveRemoteControl(userId, !!runAsAdminRef.current?.input?.checked);
} else {
await mediaStream?.stopShareScreen();
if (selfShareView) {
Expand Down
16 changes: 15 additions & 1 deletion src/feature/video/hooks/useShare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect, MutableRefObject } from 'react';
import { useMount, usePrevious, useUnmount } from '../../../hooks';
import { ZoomClient, MediaStream, Participant } from '../../../index-types';
import { Modal } from 'antd';
export function useShare(
zmClient: ZoomClient,
mediaStream: MediaStream | null,
Expand Down Expand Up @@ -49,26 +50,39 @@ export function useShare(
const onShareContentChange = useCallback((payload: any) => {
setActiveSharingId(payload.userId);
}, []);
const onActiveMediaFailed = useCallback(()=>{
Modal.error({
title:'Active media failed',
content:'Something went wrong. An unexpected interruption in media capture or insufficient memory occurred. Try refreshing the page to recover.',
okText:'Refresh',
onOk:()=>{
window.location.reload();
}
})
},[])
useEffect(() => {
zmClient.on('active-share-change', onActiveShareChange);
zmClient.on('share-content-dimension-change', onSharedContentDimensionChange);
zmClient.on('user-updated', onCurrentUserUpdate);
zmClient.on('peer-share-state-change', onPeerShareChange);
zmClient.on('share-content-change', onShareContentChange);
zmClient.on('active-media-failed',onActiveMediaFailed)
return () => {
zmClient.off('active-share-change', onActiveShareChange);
zmClient.off('share-content-dimension-change', onSharedContentDimensionChange);
zmClient.off('user-updated', onCurrentUserUpdate);
zmClient.off('peer-share-state-change', onPeerShareChange);
zmClient.off('share-content-change', onShareContentChange);
zmClient.off('active-media-failed',onActiveMediaFailed)
};
}, [
zmClient,
onActiveShareChange,
onSharedContentDimensionChange,
onCurrentUserUpdate,
onPeerShareChange,
onShareContentChange
onShareContentChange,
onActiveMediaFailed
]);
const previousIsRecieveSharing = usePrevious(isRecieveSharing);
useEffect(() => {
Expand Down
27 changes: 27 additions & 0 deletions src/feature/video/hooks/useSpotlightVideo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useRef, useCallback } from 'react';
import { ZoomClient, Participant, MediaStream } from '../../../index-types';
import { useMount } from '../../../hooks';
export function useSpotlightVideo(
zmClient: ZoomClient,
mediaStream: MediaStream | null,
fn?: (participants: Participant[], updatedUserIDs?: number[]) => void
) {
const fnRef = useRef(fn);
fnRef.current = fn;
const callback = useCallback(
(updatedParticipants?: number[]) => {
const participants = mediaStream?.getSpotlightedUserList() ?? [];
fnRef.current?.(participants, updatedParticipants);
},
[mediaStream]
);
useEffect(() => {
zmClient.on('video-spotlight-change', callback);
return () => {
zmClient.off('video-spotlight-change', callback);
};
}, [zmClient, callback]);
useMount(() => {
callback();
});
}
39 changes: 33 additions & 6 deletions src/feature/video/video-attach.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import React, {
DOMAttributes,
HTMLAttributes,
DetailedHTMLProps,
useCallback
useCallback,
useMemo
} from 'react';
import classnames from 'classnames';
import _ from 'lodash';
Expand All @@ -27,6 +28,7 @@ import { Participant } from '../../index-types';
import { useOrientation, usePrevious } from '../../hooks';
import { useVideoAspect } from './hooks/useVideoAspectRatio';
import { Radio } from 'antd';
import { useSpotlightVideo } from './hooks/useSpotlightVideo';
type CustomElement<T> = Partial<T & DOMAttributes<T> & { children: any }>;

declare global {
Expand All @@ -39,12 +41,18 @@ declare global {
}
}
}

function maxVideoCellWidth(orientation: string, totalParticipants: number, spotlighted?: boolean[]) {
return orientation === 'portrait' ? 'none' : `calc(100vw/${Math.min(totalParticipants, spotlighted ? 2 : 4)})`;
}

const VideoContainer: React.FunctionComponent<RouteComponentProps> = (props) => {
const zmClient = useContext(ZoomContext);
const { mediaStream } = useContext(ZoomMediaContext);
const shareViewRef = useRef<{ selfShareRef: HTMLCanvasElement | HTMLVideoElement | null }>(null);
const videoPlayerListRef = useRef<Record<string, VideoPlayer>>({});
const [isRecieveSharing, setIsRecieveSharing] = useState(false);
const [spotlightUsers, setSpotlightUsers] = useState<Participant[]>();
const [participants, setParticipants] = useState(zmClient.getAllUser());
const [subscribers, setSubscribers] = useState<number[]>([]);
const activeVideo = useActiveVideo(zmClient);
Expand All @@ -59,7 +67,7 @@ const VideoContainer: React.FunctionComponent<RouteComponentProps> = (props) =>
{ label: '90P', value: VideoQuality.Video_90P }
];
const orientation = useOrientation();
const maxVideoCellWidth = orientation === 'portrait' ? 'none' : `calc(100vw/${Math.min(participants.length, 4)})`;

useParticipantsChange(zmClient, (participants) => {
let pageParticipants: Participant[] = [];
if (participants.length > 0) {
Expand All @@ -78,6 +86,9 @@ const VideoContainer: React.FunctionComponent<RouteComponentProps> = (props) =>
setParticipants(pageParticipants);
setSubscribers(pageParticipants.filter((user) => user.bVideoOn).map((u) => u.userId));
});
useSpotlightVideo(zmClient, mediaStream, (p) => {
setSpotlightUsers(p);
});
const setVideoPlayerRef = (userId: number, element: VideoPlayer | null) => {
if (element) {
videoPlayerListRef.current[`${userId}`] = element;
Expand Down Expand Up @@ -107,6 +118,19 @@ const VideoContainer: React.FunctionComponent<RouteComponentProps> = (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 }))
);
}
return participants;
}, [participants, spotlightUsers]);

return (
<div className="viewport" style={{ height: 'auto', width: 'auto', minHeight: '100vh' }}>
Expand All @@ -119,19 +143,22 @@ const VideoContainer: React.FunctionComponent<RouteComponentProps> = (props) =>
<video-player-container class="video-container-wrap">
<AvatarActionContext.Provider value={avatarActionState}>
<ul className="user-list">
{participants.map((user) => {
{sortedParticipants.map((user) => {
const maxWidth = maxVideoCellWidth(orientation, participants.length, (user as any).spotlighted);
return (
<div
className="video-cell"
className={classnames('video-cell', { 'video-cell-spotlight': (user as any).spotlighted })}
key={user.userId}
style={
// Bugs in react, aspectRatio doesn't work. https://github.com/facebook/react/issues/21098
aspectRatio[`${user.userId}`]
? {
aspectRatio: aspectRatio[`${user.userId}`],
maxWidth: maxVideoCellWidth
maxWidth
}
: {
maxWidth
}
: { maxWidth: maxVideoCellWidth }
}
>
{avatarActionState?.avatarActionState[user?.userId]?.videoResolutionAdjust?.toggled && (
Expand Down
5 changes: 4 additions & 1 deletion src/feature/video/video.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
align-items: center;
}
&.video-container-in-sharing {
width: 264px;
width: min(20vw, 264px);
flex-shrink: 0;
}
&.single-video-container {
Expand Down Expand Up @@ -74,6 +74,9 @@
position: relative;
flex: 1;
margin: 12px;
&.video-cell-spotlight{
min-width: 50vw;
}
.change-video-resolution {
position: absolute;
}
Expand Down

0 comments on commit 8ee8c38

Please sign in to comment.