Merge pull request #63 from daily-demos/dev-1133-active-speaker-demo-updates
DEV-1133 active speaker demo updates
This commit is contained in:
commit
4c60f50425
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue