I am having trouble rendering the user (self) video for my meeting page for my web app using Video sdk 2.2.5. Can anyone check this code to see what I am doing wrong?
src/feature/zoom/video/VideoContainer.tsx
‘use client’;
import {
useContext,
useEffect,
useRef,
useState,
useMemo,
} from ‘react’;
import ZoomContext from ‘@/contexts/zoom-context’;
import ZoomMediaContext from ‘@/contexts/media-context’;
import { usePagination } from ‘@/feature/zoom/hooks/usePagination’;
import { useActiveVideo } from ‘@/feature/zoom/hooks/useActiveVideo’;
import { useAvatarAction } from ‘@/feature/zoom/hooks/useAvatarAction’;
import { useNetworkQuality } from ‘@/feature/zoom/hooks/useNetworkQuality’;
import { useCleanUp } from ‘@/feature/zoom/hooks/useCleanUp’;
import VideoFooter from ‘@/feature/zoom/video/video-footer’;
import Pagination from ‘@/feature/zoom/components/pagination’;
import RemoteCameraControlPanel from ‘@/feature/zoom/components/remote-camera-control’;
import AvatarActionContext from ‘@/contexts/avatar-context’;
import styles from ‘./VideoContainer.module.css’; // Use CSS module
import clsx from ‘clsx’;
interface Participant {
userId: number;
displayName?: string;
bVideoOn?: boolean;
}
interface ZoomClientMinimal {
getAllUser: () => Participant;
on: (event: string, callback: (…args: any) => void) => void;
off: (event: string, callback: (…args: any) => void) => void;
}
interface ZoomClientExtended {
getSessionInfo?: () => { isInMeeting: boolean };
getCurrentUserInfo?: () => { userId: number };
}
interface ZoomClientFull extends ZoomClientMinimal, ZoomClientExtended {}
type NetworkQuality = Record<number | string, { uplink: number; downlink: number }>;
export default function VideoContainer(): JSX.Element | null {
const zmClient = useContext(ZoomContext) as unknown as ZoomClientFull;
const {
mediaStream,
video: { decode: isVideoDecodeReady },
} = useContext(ZoomMediaContext);
const videoRef = useRef(null);
const [isRecieveSharing, setIsRecieveSharing] = useState(false);
const [visibleParticipants, setVisibleParticipants] = useState<Participant>();
useEffect(() => {
if (!zmClient) return;
const updateUsers = () => {
const updated = zmClient.getAllUser?.() ?? ;
setVisibleParticipants(updated);
};
zmClient.on(‘user-added’, updateUsers);
zmClient.on(‘user-removed’, updateUsers);
// Initial trigger
updateUsers();
return () => {
zmClient.off(‘user-added’, updateUsers);
zmClient.off(‘user-removed’, updateUsers);
};
}, [zmClient]);
const activeVideo = useActiveVideo(zmClient);
const rawNetworkQuality = useNetworkQuality(zmClient);
const networkQuality: NetworkQuality = useMemo(() => rawNetworkQuality ?? {}, [rawNetworkQuality]);
const currentUserId = zmClient.getCurrentUserInfo?.()?.userId ?? -1;
const avatarActionState = useAvatarAction(zmClient, visibleParticipants);
const isInMeeting = Boolean(zmClient.getSessionInfo?.()?.isInMeeting);
const { page, totalPage, setPage } = usePagination(zmClient, { width: 1, height: 1 });
useCleanUp(null, zmClient, mediaStream);
// Determine column layout class
const getGridColumnClass = (count: number): string => {
if (count <= 1) return styles[‘cols-1’];
if (count === 2) return styles[‘cols-2’];
if (count <= 6) return styles[‘cols-3’];
return styles[‘cols-3’];
};
// Always run this useEffect
useEffect(() => {
if (!mediaStream || !isVideoDecodeReady) return;
const renderStreams = () => {
visibleParticipants.forEach((user: Participant) => {
const canvasEl = document.getElementById(zoom-video-${user.userId}
) as HTMLCanvasElement | null;
if (!canvasEl) return;
// Always set dimensions fresh
canvasEl.width = canvasEl.clientWidth;
canvasEl.height = canvasEl.clientHeight;
try {
if (user.userId === currentUserId) {
// ✅ Self view
mediaStream.attachVideo(user.userId, canvasEl);
} else if (user.bVideoOn) {
// ✅ Remote participants
mediaStream.renderVideo(canvasEl, user.userId, canvasEl.width, canvasEl.height, 0, 0, 3);
}
} catch (err) {
console.error(`❌ Failed to render video for user ${user.userId}:`, err);
}
});
};
renderStreams();
return () => {
visibleParticipants.forEach((user: Participant) => {
const canvasEl = document.getElementById(zoom-video-${user.userId}
) as HTMLCanvasElement | null;
if (!canvasEl) return;
try {
if (user.userId === currentUserId) {
mediaStream.detachVideo(user.userId);
} else {
mediaStream.stopRenderVideo(canvasEl, user.userId).catch((err: unknown) => {
console.error(`❌ Failed to stop render for user ${user.userId}:`, err);
});
}
} catch (err) {
console.warn(`⚠️ Cleanup error for user ${user.userId}:`, err);
}
});
};
}, [mediaStream, isVideoDecodeReady, visibleParticipants, currentUserId]);
useEffect(() => {
const update = () => zmClient.getAllUser?.();
zmClient.on(‘user-added’, update);
zmClient.on(‘user-removed’, update);
return () => {
zmClient.off(‘user-added’, update);
zmClient.off(‘user-removed’, update);
};
}, [zmClient]);
if (!mediaStream || !isVideoDecodeReady) return null;
return (
{/* Video Grid */}
<AvatarActionContext.Provider value={avatarActionState}>
<div
ref={videoRef}
className={clsx(
styles.gridWrapper,
getGridColumnClass(visibleParticipants.length)
)}
>
{visibleParticipants.map((user: Participant) => {
const isSelf = user.userId === currentUserId;
return (
<canvas
id={
zoom-video-${user.userId}
}className=“w-full h-full object-cover”
/>
{/* Name label */}
<div className="absolute bottom-2 left-2 text-white text-xs bg-black/60 px-2 py-1 rounded">
{isSelf ? 'You' : user.displayName || user.userId}
</div>
</div>
);
})}
</div>
</AvatarActionContext.Provider>
</div>
{/* Footer */}
<VideoFooter className="video-operations" sharing />
{/* Pagination */}
{totalPage > 1 && (
<Pagination
page={page}
totalPage={totalPage}
setPage={setPage}
inSharing={isRecieveSharing}
/>
)}
</div>
);
}