Merge pull request #63 from daily-demos/dev-1133-active-speaker-demo-updates

DEV-1133 active speaker demo updates
This commit is contained in:
Kimberlee Johnson 2022-01-12 10:40:28 -08:00 committed by GitHub
commit 4c60f50425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 44 deletions

View File

@ -12,7 +12,6 @@ import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useTracks } from '@custom/shared/contexts/TracksProvider'; import { useTracks } from '@custom/shared/contexts/TracksProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider'; import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { isLocalId } from '@custom/shared/contexts/participantsState'; import { isLocalId } from '@custom/shared/contexts/participantsState';
import { useActiveSpeaker } from '@custom/shared/hooks/useActiveSpeaker';
import { useCamSubscriptions } from '@custom/shared/hooks/useCamSubscriptions'; import { useCamSubscriptions } from '@custom/shared/hooks/useCamSubscriptions';
import { useResize } from '@custom/shared/hooks/useResize'; import { useResize } from '@custom/shared/hooks/useResize';
import { useScrollbarWidth } from '@custom/shared/hooks/useScrollbarWidth'; import { useScrollbarWidth } from '@custom/shared/hooks/useScrollbarWidth';
@ -40,18 +39,21 @@ export const ParticipantBar = ({
width, width,
}) => { }) => {
const { networkState } = useCallState(); const { networkState } = useCallState();
const { currentSpeaker, screens, swapParticipantPosition } = const {
useParticipants(); currentSpeaker,
screens,
swapParticipantPosition,
} = useParticipants();
const { maxCamSubscriptions } = useTracks(); const { maxCamSubscriptions } = useTracks();
const { pinnedId, showParticipantsBar } = useUIState(); const { pinnedId, showParticipantsBar } = useUIState();
const itemHeight = useMemo( const itemHeight = useMemo(() => width / aspectRatio + GAP, [
() => width / aspectRatio + GAP, aspectRatio,
[aspectRatio, width] width,
); ]);
const paddingTop = useMemo( const paddingTop = useMemo(() => itemHeight * fixed.length, [
() => itemHeight * fixed.length, fixed,
[fixed, itemHeight] itemHeight,
); ]);
const scrollTop = useRef(0); const scrollTop = useRef(0);
const spaceBefore = useRef(null); const spaceBefore = useRef(null);
const spaceAfter = useRef(null); const spaceAfter = useRef(null);
@ -61,14 +63,13 @@ export const ParticipantBar = ({
const [isSidebarScrollable, setIsSidebarScrollable] = useState(false); const [isSidebarScrollable, setIsSidebarScrollable] = useState(false);
const blockScrolling = useBlockScrolling(scrollRef); const blockScrolling = useBlockScrolling(scrollRef);
const scrollbarWidth = useScrollbarWidth(); const scrollbarWidth = useScrollbarWidth();
const activeSpeakerId = useActiveSpeaker();
const hasScreenshares = useMemo(() => screens.length > 0, [screens]); const hasScreenshares = useMemo(() => screens.length > 0, [screens]);
const othersCount = useMemo(() => others.length, [others]); const othersCount = useMemo(() => others.length, [others]);
const visibleOthers = useMemo( const visibleOthers = useMemo(() => others.slice(range[0], range[1]), [
() => others.slice(range[0], range[1]), others,
[others, range] range,
); ]);
const currentSpeakerId = useMemo(() => currentSpeaker?.id, [currentSpeaker]);
/** /**
* Store other ids as string to reduce amount of running useEffects below. * Store other ids as string to reduce amount of running useEffects below.
@ -96,7 +97,7 @@ export const ParticipantBar = ({
if (!showParticipantsBar) { if (!showParticipantsBar) {
setCamSubscriptions({ setCamSubscriptions({
subscribedIds: [ subscribedIds: [
currentSpeaker?.id, currentSpeakerId,
pinnedId, pinnedId,
...fixedRemote.map((p) => p.id), ...fixedRemote.map((p) => p.id),
], ],
@ -117,8 +118,8 @@ export const ParticipantBar = ({
const min = Math.max(0, r[0] - buffer); const min = Math.max(0, r[0] - buffer);
const max = Math.min(otherIds.length, r[1] + buffer); const max = Math.min(otherIds.length, r[1] + buffer);
const ids = otherIds.slice(min, max); const ids = otherIds.slice(min, max);
if (!ids.includes(currentSpeaker?.id) && !isLocalId(currentSpeaker?.id)) { if (!ids.includes(currentSpeakerId) && !isLocalId(currentSpeakerId)) {
ids.push(currentSpeaker?.id); ids.push(currentSpeakerId);
} }
// Calculate paused participant ids by determining their tile position // Calculate paused participant ids by determining their tile position
const subscribedIds = [...fixedRemote.map((p) => p.id), ...ids]; const subscribedIds = [...fixedRemote.map((p) => p.id), ...ids];
@ -126,7 +127,7 @@ export const ParticipantBar = ({
// ignore unrendered ids, they'll be unsubscribed instead // ignore unrendered ids, they'll be unsubscribed instead
if (!ids.includes(id)) return false; if (!ids.includes(id)) return false;
// ignore current speaker, it should never be paused // ignore current speaker, it should never be paused
if (id === currentSpeaker?.id) return false; if (id === currentSpeakerId) return false;
const top = i * itemHeight; const top = i * itemHeight;
const fixedHeight = fixed.length * itemHeight; const fixedHeight = fixed.length * itemHeight;
const visibleScrollHeight = scrollEl.clientHeight - fixedHeight; const visibleScrollHeight = scrollEl.clientHeight - fixedHeight;
@ -143,7 +144,7 @@ export const ParticipantBar = ({
}); });
}, },
[ [
currentSpeaker?.id, currentSpeakerId,
fixed, fixed,
itemHeight, itemHeight,
maxCamSubscriptions, maxCamSubscriptions,
@ -239,26 +240,27 @@ export const ParticipantBar = ({
const maybePromoteActiveSpeaker = () => { const maybePromoteActiveSpeaker = () => {
const fixedOther = fixed.find((f) => !f.isLocal); const fixedOther = fixed.find((f) => !f.isLocal);
// Ignore when speaker is already at first position or component unmounted // Ignore when speaker is already at first position or component unmounted
if (!fixedOther || fixedOther?.id === activeSpeakerId || !scrollEl) if (!fixedOther || fixedOther?.id === activeSpeakerId || !scrollEl) {
return false; return false;
}
// Active speaker not rendered at all, promote immediately // Active speaker not rendered at all, promote immediately
if ( if (
visibleOthers.every((p) => p.id !== activeSpeakerId) && visibleOthers.every((p) => p.id !== currentSpeakerId) &&
!isLocalId(activeSpeakerId) !isLocalId(currentSpeakerId)
) { ) {
swapParticipantPosition(fixedOther.id, activeSpeakerId); swapParticipantPosition(fixedOther.id, currentSpeakerId);
return false; return false;
} }
const activeTile = othersRef.current?.querySelector( const activeTile = othersRef.current?.querySelector(
`[id="${activeSpeakerId}"]` `[id="${currentSpeakerId}"]`
); );
// Ignore when active speaker is not within "others" // Ignore when active speaker is not within "others"
if (!activeTile) return false; if (!activeTile) return false;
// Ignore when active speaker is already pinned // Ignore when active speaker is already pinned
if (activeSpeakerId === pinnedId) return false; if (currentSpeakerId === pinnedId) return false;
const { height: tileHeight } = activeTile.getBoundingClientRect(); const { height: tileHeight } = activeTile.getBoundingClientRect();
const othersVisibleHeight = const othersVisibleHeight =
@ -270,10 +272,11 @@ export const ParticipantBar = ({
if ( if (
scrolledOffsetTop + tileHeight / 2 < othersVisibleHeight && scrolledOffsetTop + tileHeight / 2 < othersVisibleHeight &&
scrolledOffsetTop > -tileHeight / 2 scrolledOffsetTop > -tileHeight / 2
) ) {
return false; return false;
}
return swapParticipantPosition(fixedOther.id, activeSpeakerId); return swapParticipantPosition(fixedOther.id, currentSpeakerId);
}; };
maybePromoteActiveSpeaker(); maybePromoteActiveSpeaker();
const throttledHandler = debounce(maybePromoteActiveSpeaker, 100); const throttledHandler = debounce(maybePromoteActiveSpeaker, 100);
@ -283,7 +286,7 @@ export const ParticipantBar = ({
scrollEl?.removeEventListener('scroll', throttledHandler); scrollEl?.removeEventListener('scroll', throttledHandler);
}; };
}, [ }, [
activeSpeakerId, currentSpeakerId,
fixed, fixed,
hasScreenshares, hasScreenshares,
pinnedId, pinnedId,

View File

@ -43,8 +43,10 @@ export const ParticipantsProvider = ({ children }) => {
initialParticipantsState initialParticipantsState
); );
const { viewMode } = useUIState(); const { viewMode } = useUIState();
const [participantMarkedForRemoval, setParticipantMarkedForRemoval] = const [
useState(null); participantMarkedForRemoval,
setParticipantMarkedForRemoval,
] = useState(null);
/** /**
* ALL participants (incl. shared screens) in a convenient array * ALL participants (incl. shared screens) in a convenient array
@ -95,31 +97,37 @@ export const ParticipantsProvider = ({ children }) => {
[allParticipants] [allParticipants]
); );
const isOwner = useMemo( const isOwner = useMemo(() => !!localParticipant?.isOwner, [
() => !!localParticipant?.isOwner, localParticipant,
[localParticipant] ]);
);
/** /**
* The participant who should be rendered prominently right now * The participant who should be rendered prominently right now
*/ */
const currentSpeaker = useMemo(() => { const currentSpeaker = useMemo(() => {
/** /**
* Ensure activeParticipant is still present in the call. * If the activeParticipant is still in the call, return the activeParticipant.
* The activeParticipant only updates to a new active participant so * The activeParticipant only updates to a new active participant so
* if everyone else is muted when AP leaves, the value will be stale. * if everyone else is muted when AP leaves, the value will be stale.
*/ */
const isPresent = participants.some((p) => p?.id === activeParticipant?.id); const isPresent = participants.some((p) => p?.id === activeParticipant?.id);
if (isPresent) {
return activeParticipant;
}
/**
* If the activeParticipant has left, calculate the remaining displayable participants
*/
const displayableParticipants = participants.filter((p) => !p?.isLocal); const displayableParticipants = participants.filter((p) => !p?.isLocal);
/**
* If nobody ever unmuted, return the first participant with a camera on
* Or, if all cams are off, return the first remote participant
*/
if ( if (
!isPresent &&
displayableParticipants.length > 0 && displayableParticipants.length > 0 &&
displayableParticipants.every((p) => p.isMicMuted && !p.lastActiveDate) displayableParticipants.every((p) => p.isMicMuted && !p.lastActiveDate)
) { ) {
// Return first cam on participant in case everybody is muted and nobody ever talked
// or first remote participant, in case everybody's cam is muted, too.
return ( return (
displayableParticipants.find((p) => !p.isCamMuted) ?? displayableParticipants.find((p) => !p.isCamMuted) ??
displayableParticipants?.[0] displayableParticipants?.[0]
@ -130,7 +138,9 @@ export const ParticipantsProvider = ({ children }) => {
.sort((a, b) => sortByKey(a, b, 'lastActiveDate')) .sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
.reverse(); .reverse();
return isPresent ? activeParticipant : sorted?.[0] ?? localParticipant; const lastActiveSpeaker = sorted?.[0];
return lastActiveSpeaker || localParticipant;
}, [activeParticipant, localParticipant, participants]); }, [activeParticipant, localParticipant, participants]);
/** /**
@ -296,11 +306,12 @@ export const ParticipantsProvider = ({ children }) => {
* Our UX doesn't ever highlight the local user as the active speaker. * Our UX doesn't ever highlight the local user as the active speaker.
*/ */
const localId = callObject.participants().local.session_id; const localId = callObject.participants().local.session_id;
if (localId === activeSpeaker?.peerId) return; const activeSpeakerId = activeSpeaker?.peerId;
if (localId === activeSpeakerId) return;
dispatch({ dispatch({
type: ACTIVE_SPEAKER, type: ACTIVE_SPEAKER,
id: activeSpeaker?.peerId, id: activeSpeakerId,
}); });
}; };
callObject.on('active-speaker-change', handleActiveSpeakerChange); callObject.on('active-speaker-change', handleActiveSpeakerChange);