diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js index d931cb0..88f8dc9 100644 --- a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js @@ -59,6 +59,7 @@ export const SpeakerTile = ({ participant, screenRef }) => { participant={participant} style={style} videoFit={videoFit} + showActiveSpeaker={false} onVideoResize={handleNativeAspectRatio} /> ); diff --git a/dailyjs/basic-call/components/VideoGrid/VideoGrid.js b/dailyjs/basic-call/components/VideoGrid/VideoGrid.js index 744f185..97a2b3c 100644 --- a/dailyjs/basic-call/components/VideoGrid/VideoGrid.js +++ b/dailyjs/basic-call/components/VideoGrid/VideoGrid.js @@ -2,7 +2,7 @@ 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 usePreferredLayer from '@dailyjs/shared/hooks/usePreferredLayer'; +import usePreferredLayerByCount from '@dailyjs/shared/hooks/usePreferredLayerByCount'; import { useDeepCompareMemo } from 'use-deep-compare'; /** @@ -106,7 +106,7 @@ export const VideoGrid = React.memo( // Optimise performance by reducing video quality // when more participants join (if in SFU mode) - usePreferredLayer(allParticipants); + usePreferredLayerByCount(allParticipants); if (!participants.length) { return null; diff --git a/dailyjs/pagination/components/PaginatedVideoGrid/PaginatedVideoGrid.js b/dailyjs/pagination/components/PaginatedVideoGrid/PaginatedVideoGrid.js index d5caa9c..80648e0 100644 --- a/dailyjs/pagination/components/PaginatedVideoGrid/PaginatedVideoGrid.js +++ b/dailyjs/pagination/components/PaginatedVideoGrid/PaginatedVideoGrid.js @@ -11,7 +11,7 @@ import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker'; import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions'; -import usePreferredLayer from '@dailyjs/shared/hooks/usePreferredLayer'; +import usePreferredLayerByCount from '@dailyjs/shared/hooks/usePreferredLayerByCount'; import { ReactComponent as IconArrow } from '@dailyjs/shared/icons/raquo-md.svg'; import sortByKey from '@dailyjs/shared/lib/sortByKey'; import { useDeepCompareMemo } from 'use-deep-compare'; @@ -181,7 +181,7 @@ export const PaginatedVideoGrid = () => { ); // Set bandwidth layer based on amount of visible participants - usePreferredLayer(visibleParticipants); + usePreferredLayerByCount(visibleParticipants); /** * Handle position updates based on active speaker events diff --git a/dailyjs/shared/components/Tile/Tile.js b/dailyjs/shared/components/Tile/Tile.js index ac49960..5014940 100644 --- a/dailyjs/shared/components/Tile/Tile.js +++ b/dailyjs/shared/components/Tile/Tile.js @@ -13,6 +13,7 @@ export const Tile = React.memo( mirrored = true, showName = true, showAvatar = true, + showActiveSpeaker = true, aspectRatio = DEFAULT_ASPECT_RATIO, onVideoResize, videoFit = 'contain', @@ -22,6 +23,8 @@ export const Tile = React.memo( const videoEl = useRef(null); const [tileAspectRatio, setTileAspectRatio] = useState(aspectRatio); + const [layer, setLayer] = useState(); + /** * Add optional event listener for resize event so the parent component * can know the video's native aspect ratio. @@ -52,10 +55,26 @@ export const Tile = React.memo( setTileAspectRatio(aspectRatio); }, [aspectRatio, tileAspectRatio]); + useEffect(() => { + if ( + typeof rtcpeers === 'undefined' || + rtcpeers?.getCurrentType() !== 'sfu' + ) + return false; + + const i = setInterval(() => { + setLayer( + rtcpeers.sfu.consumers[`${participant.id}/cam-video`]?._preferredLayer + ); + }, 1500); + + return () => clearInterval(i); + }, [participant]); + const cx = classNames('tile', videoFit, { mirrored, avatar: showAvatar && !videoTrack, - active: participant.isActiveSpeaker, + active: showActiveSpeaker && participant.isActiveSpeaker, }); return ( @@ -64,7 +83,7 @@ export const Tile = React.memo( {showName && (
{participant.isMicMuted && } - {participant.name} + {participant.name} - {layer}
)} {videoTrack ? ( @@ -92,8 +111,17 @@ export const Tile = React.memo( box-sizing: border-box; } - .tile.active { + .tile.active:before { + content: ''; + position: absolute; + top: 0px; + right: 0px; + left: 0px; + bottom: 0px; border: 2px solid var(--primary-default); + box-sizing: border-box; + pointer-events: none; + z-index: 2; } .tile .name { @@ -104,10 +132,11 @@ export const Tile = React.memo( left: 0px; z-index: 2; line-height: 1; + font-size: 0.875rem; color: white; font-weight: var(--weight-medium); padding: var(--spacing-xxs); - text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.35); + text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.45); gap: var(--spacing-xxs); } @@ -159,6 +188,7 @@ Tile.propTypes = { aspectRatio: PropTypes.number, onVideoResize: PropTypes.func, videoFit: PropTypes.string, + showActiveSpeaker: PropTypes.bool, }; export default Tile; diff --git a/dailyjs/shared/contexts/ParticipantsProvider.js b/dailyjs/shared/contexts/ParticipantsProvider.js index 3e31b45..c26db9e 100644 --- a/dailyjs/shared/contexts/ParticipantsProvider.js +++ b/dailyjs/shared/contexts/ParticipantsProvider.js @@ -1,3 +1,5 @@ +/* global rtcpeers */ + import React, { createContext, useCallback, @@ -7,11 +9,16 @@ import React, { useState, useMemo, } from 'react'; +import { + useUIState, + VIEW_MODE_SPEAKER, +} from '@dailyjs/shared/contexts/UIStateProvider'; import PropTypes from 'prop-types'; import { sortByKey } from '../lib/sortByKey'; import { useCallState } from './CallProvider'; + import { initialParticipantsState, isLocalId, @@ -31,6 +38,7 @@ export const ParticipantsProvider = ({ children }) => { participantsReducer, initialParticipantsState ); + const { viewMode } = useUIState(); const [participantMarkedForRemoval, setParticipantMarkedForRemoval] = useState(null); @@ -47,6 +55,14 @@ export const ParticipantsProvider = ({ children }) => { */ const participants = useMemo(() => state.participants, [state.participants]); + /** + * Array of participant IDs + */ + const participantIds = useMemo( + () => participants.map((p) => p.id).join(','), + [participants] + ); + /** * The number of participants, who are not a shared screen * (technically a shared screen counts as a participant, but we shouldn't tell humans) @@ -218,6 +234,40 @@ export const ParticipantsProvider = ({ children }) => { ); }, [callObject, handleNewParticipantsState]); + /** + * Adjust video quality from the 3 simulcast layers based + * on active speaker status. Note: this currently uses + * undocumented internal methods (we'll be adding support + * for this into our API soon!) + */ + const setBandWidthControls = useCallback(() => { + if (typeof rtcpeers === 'undefined') return; + const sfu = rtcpeers?.soup; + const isSFU = rtcpeers?.currentlyPreferred?.typeName?.() === 'sfu'; + if (!isSFU) return; + + const ids = participantIds.split(','); + + ids.forEach((id) => { + if (isLocalId(id)) return; + + // Speaker view settings based on speaker status or pinned user + if (viewMode === VIEW_MODE_SPEAKER) { + if (currentSpeaker?.id === id) { + sfu.setPreferredLayerForTrack(id, 'cam-video', 2); + } else { + sfu.setPreferredLayerForTrack(id, 'cam-video', 0); + } + } + + // Note: grid view settings are handled by the grid view component + }); + }, [currentSpeaker?.id, participantIds, viewMode]); + + useEffect(() => { + setBandWidthControls(); + }, [setBandWidthControls]); + useEffect(() => { if (!callObject) return false; const handleActiveSpeakerChange = ({ activeSpeaker }) => { diff --git a/dailyjs/shared/hooks/usePreferredLayer.js b/dailyjs/shared/hooks/usePreferredLayerByCount.js similarity index 92% rename from dailyjs/shared/hooks/usePreferredLayer.js rename to dailyjs/shared/hooks/usePreferredLayerByCount.js index 3da474d..4e74c9c 100644 --- a/dailyjs/shared/hooks/usePreferredLayer.js +++ b/dailyjs/shared/hooks/usePreferredLayerByCount.js @@ -14,7 +14,7 @@ import { useEffect } from 'react'; * * Note: this will have no effect when not in SFU mode */ -export const usePreferredLayer = (participants) => { +export const usePreferredLayerByCount = (participants) => { /** * Set bandwidth layer based on amount of visible participants */ @@ -40,4 +40,4 @@ export const usePreferredLayer = (participants) => { }, [participants]); }; -export default usePreferredLayer; +export default usePreferredLayerByCount;