import React, { useState, useMemo, useEffect, useRef } from 'react'; import Tile from '@dailyjs/shared/components/Tile'; import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useTracks } from '@dailyjs/shared/contexts/TracksProvider'; import { useDeepCompareMemo } from 'use-deep-compare'; export const VideoGrid = React.memo( () => { const containerRef = useRef(); const { allParticipants } = useParticipants(); const { resumeVideoTrack } = useTracks(); const [dimensions, setDimensions] = useState({ width: 1, height: 1, }); useEffect(() => { let frame; const handleResize = () => { if (frame) cancelAnimationFrame(frame); frame = requestAnimationFrame(() => setDimensions({ width: containerRef.current?.clientWidth, height: containerRef.current?.clientHeight, }) ); }; handleResize(); window.addEventListener('resize', handleResize); window.addEventListener('orientationchange', handleResize); return () => { window.removeEventListener('resize', handleResize); window.removeEventListener('orientationchange', handleResize); }; }, []); const layout = useMemo(() => { const aspectRatio = DEFAULT_ASPECT_RATIO; const tileCount = allParticipants.length || 0; const w = dimensions.width; const h = dimensions.height; // brute-force search layout where video occupy the largest area of the container let bestLayout = { area: 0, cols: 0, rows: 0, width: 0, height: 0, }; for (let cols = 0; cols <= tileCount; cols += 1) { const rows = Math.ceil(tileCount / cols); const hScale = w / (cols * aspectRatio); const vScale = h / rows; let width; let height; if (hScale <= vScale) { width = Math.floor(w / cols); height = Math.floor(width / aspectRatio); } else { height = Math.floor(h / rows); width = Math.floor(height * aspectRatio); } const area = width * height; if (area > bestLayout.area) { bestLayout = { area, width, height, rows, cols, }; } } return bestLayout; }, [dimensions, allParticipants]); const tiles = useDeepCompareMemo( () => allParticipants.map((p) => ( )), [layout, allParticipants] ); /** * Set bandwidth layer based on amount of visible participants */ useEffect(() => { if ( typeof rtcpeers === 'undefined' || // eslint-disable-next-line no-undef rtcpeers?.getCurrentType() !== 'sfu' ) return; // eslint-disable-next-line no-undef const sfu = rtcpeers.soup; const count = allParticipants.length; allParticipants.forEach(({ id }) => { if (count < 5) { // High quality video for calls with < 5 people per page sfu.setPreferredLayerForTrack(id, 'cam-video', 2); } else if (count < 10) { // Medium quality video for calls with < 10 people per page sfu.setPreferredLayerForTrack(id, 'cam-video', 1); } else { // Low quality video for calls with 10 or more people per page sfu.setPreferredLayerForTrack(id, 'cam-video', 0); } }); }, [allParticipants]); useEffect(() => { allParticipants.forEach( (p) => p.id !== 'local' && resumeVideoTrack(p.id) ); }, [allParticipants, resumeVideoTrack]); if (!allParticipants.length) { return null; } return (
{tiles}
); }, () => true ); export default VideoGrid;