ParticipantBar

This commit is contained in:
Jon 2021-07-23 14:10:35 +01:00
parent 0123e01487
commit dc2dc63a98
15 changed files with 682 additions and 102 deletions

View File

@ -1,14 +1,7 @@
import React from 'react';
import { RoomContainer } from '@dailyjs/basic-call/components/Room';
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
import { SpeakerView } from '../SpeakerView';
export const Room = () => (
<RoomContainer>
<VideoContainer>
<SpeakerView />
</VideoContainer>
</RoomContainer>
);
export const Room = () => <SpeakerView />;
export default Room;

View File

@ -1,15 +1,106 @@
import React, { useRef } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import RoomContainer from '@dailyjs/basic-call/components/Room/RoomContainer';
import ParticipantBar from '@dailyjs/shared/components/ParticipantBar/ParticipantBar';
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import { isScreenId } from '@dailyjs/shared/contexts/participantsState';
import SpeakerTile from './SpeakerTile';
const SIDEBAR_WIDTH = 186;
export const SpeakerView = () => {
const { currentSpeaker } = useParticipants();
const { currentSpeaker, localParticipant, participants, screens } =
useParticipants();
const { updateCamSubscriptions } = useTracks();
const { showLocalVideo } = useCallState();
const { pinnedId } = useUIState();
const activeRef = useRef();
const screensAndPinned = useMemo(
() => [...screens, ...participants.filter(({ id }) => id === pinnedId)],
[participants, pinnedId, screens]
);
const otherParticipants = useMemo(
() => participants.filter(({ isLocal }) => !isLocal),
[participants]
);
const showSidebar = useMemo(() => {
const hasScreenshares = screens.length > 0;
if (isScreenId(pinnedId)) {
return false;
}
return participants.length > 1 || hasScreenshares;
}, [participants, pinnedId, screens]);
/* const screenShareTiles = useMemo(
() => <ScreensAndPins items={screensAndPinned} />,
[screensAndPinned]
); */
const hasScreenshares = useMemo(() => screens.length > 0, [screens]);
const fixedItems = useMemo(() => {
const items = [];
if (showLocalVideo) {
items.push(localParticipant);
}
if (hasScreenshares && otherParticipants.length > 0) {
items.push(otherParticipants[0]);
}
return items;
}, [hasScreenshares, localParticipant, otherParticipants, showLocalVideo]);
const otherItems = useMemo(() => {
if (otherParticipants.length > 1) {
return otherParticipants.slice(hasScreenshares ? 1 : 0);
}
return [];
}, [hasScreenshares, otherParticipants]);
/**
* Update cam subscriptions, in case ParticipantBar is not shown.
*/
useEffect(() => {
// Sidebar takes care of cam subscriptions for all displayed participants.
if (showSidebar) return;
updateCamSubscriptions([
currentSpeaker?.id,
...screensAndPinned.map((p) => p.id),
]);
}, [currentSpeaker, screensAndPinned, showSidebar, updateCamSubscriptions]);
return (
<div ref={activeRef} className="active">
<SpeakerTile participant={currentSpeaker} screenRef={activeRef} />
<div className="speaker-view">
<RoomContainer>
<VideoContainer>
<div ref={activeRef} className="active">
<SpeakerTile participant={currentSpeaker} screenRef={activeRef} />
</div>
</VideoContainer>
</RoomContainer>
{showSidebar && (
<ParticipantBar
fixed={fixedItems}
others={otherItems}
width={SIDEBAR_WIDTH}
/>
)}
<style jsx>{`
.speaker-view {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.active {
display: flex;
align-items: center;

View File

@ -9,13 +9,11 @@ import { Button } from '@dailyjs/shared/components/Button';
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 { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions';
import usePreferredLayer from '@dailyjs/shared/hooks/usePreferredLayer';
import { ReactComponent as IconArrow } from '@dailyjs/shared/icons/raquo-md.svg';
import sortByKey from '@dailyjs/shared/lib/sortByKey';
import { debounce } from 'debounce';
import { useDeepCompareMemo } from 'use-deep-compare';
// --- Constants

View File

@ -11,7 +11,7 @@ export const Aside = ({ onClose, children }) => (
<div className="close">
<Button
size="small-square"
variant="dark"
variant="white"
className="closeButton"
onClick={onClose}
>
@ -42,9 +42,15 @@ export const Aside = ({ onClose, children }) => (
.call-aside .close {
position: absolute;
top: var(--spacing-xxs);
left: calc(-48px - var(--spacing-xxs));
left: calc(-43px);
border-right: 1px solid var(--gray-wash);
z-index: 99;
}
.call-aside :global(.closeButton) {
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
height: 48px;
}
`}</style>
</aside>
);

View File

@ -58,6 +58,7 @@ const PersonRow = ({ participant, isOwner = false }) => (
margin-bottom: var(--spacing-xxxs);
justify-content: space-between;
align-items: center;
flex: 1;
}
.person-row .name {
@ -104,49 +105,57 @@ export const PeopleAside = () => {
return (
<Aside onClose={() => setShowAside(false)}>
{isOwner && (
<div className="owner-actions">
<Button
fullWidth
size="tiny"
variant="outline-gray"
onClick={() =>
callObject.updateParticipants({ '*': { setAudio: false } })
}
>
Mute all mics
</Button>
<Button
fullWidth
size="tiny"
variant="outline-gray"
onClick={() =>
callObject.updateParticipants({ '*': { setVideo: false } })
}
>
Mute all cams
</Button>
<div className="people-aside">
{isOwner && (
<div className="owner-actions">
<Button
fullWidth
size="tiny"
variant="outline-gray"
onClick={() =>
callObject.updateParticipants({ '*': { setAudio: false } })
}
>
Mute all mics
</Button>
<Button
fullWidth
size="tiny"
variant="outline-gray"
onClick={() =>
callObject.updateParticipants({ '*': { setVideo: false } })
}
>
Mute all cams
</Button>
</div>
)}
<div className="rows">
{allParticipants.map((p) => (
<PersonRow participant={p} key={p.id} isOwner={isOwner} />
))}
</div>
)}
<div className="rows">
{allParticipants.map((p) => (
<PersonRow participant={p} key={p.id} isOwner={isOwner} />
))}
</div>
<style jsx>
{`
.owner-actions {
display: flex;
align-items: center;
gap: var(--spacing-xxxs);
margin: var(--spacing-xs) var(--spacing-xxs);
}
<style jsx>
{`
.people-aside {
display: block;
}
.rows {
margin: var(--spacing-xxs);
}
`}
</style>
.owner-actions {
display: flex;
align-items: center;
gap: var(--spacing-xxxs);
margin: var(--spacing-xs) var(--spacing-xxs);
flex: 1;
}
.rows {
margin: var(--spacing-xxs);
flex: 1;
}
`}
</style>
</div>
</Aside>
);
};

View File

@ -290,6 +290,24 @@ export const Button = forwardRef(
opacity: 0.35;
}
.button.white {
background: white;
border: 0px;
}
.button.white:hover,
.button.white:focus,
.button.white:active {
background: var(--gray-wash);
border: 0px;
color: var(--primary-default);
}
.button.white:focus {
box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.15);
}
.button.white:disabled {
opacity: 0.35;
}
.button.outline {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.15);

View File

@ -0,0 +1,382 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Tile } from '@dailyjs/shared/components/Tile';
import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import { isLocalId } from '@dailyjs/shared/contexts/participantsState';
import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions';
import { useResize } from '@dailyjs/shared/hooks/useResize';
import { useScrollbarWidth } from '@dailyjs/shared/hooks/useScrollbarWidth';
import classnames from 'classnames';
import debounce from 'debounce';
import PropTypes from 'prop-types';
import { useBlockScrolling } from './useBlockScrolling';
/**
* Gap between tiles in pixels.
*/
const GAP = 1;
/**
* Maximum amount of buffered video tiles, in addition
* to the visible on-screen tiles.
*/
const MAX_SCROLL_BUFFER = 10;
export const ParticipantBar = ({
aspectRatio = DEFAULT_ASPECT_RATIO,
fixed = [],
others = [],
width,
}) => {
const { networkState } = useCallState();
const { currentSpeaker, screens, swapParticipantPosition } =
useParticipants();
const { maxCamSubscriptions } = useTracks();
const { pinnedId, showParticipantsBar } = useUIState();
const itemHeight = useMemo(
() => width / aspectRatio + GAP,
[aspectRatio, width]
);
const paddingTop = useMemo(
() => itemHeight * fixed.length,
[fixed, itemHeight]
);
const scrollTop = useRef(0);
const spaceBefore = useRef(null);
const spaceAfter = useRef(null);
const scrollRef = useRef(null);
const othersRef = useRef(null);
const [range, setRange] = useState([0, 20]);
const [isSidebarScrollable, setIsSidebarScrollable] = useState(false);
const blockScrolling = useBlockScrolling(scrollRef);
const scrollbarWidth = useScrollbarWidth();
const activeSpeakerId = useActiveSpeaker();
const hasScreenshares = useMemo(() => screens.length > 0, [screens]);
const othersCount = useMemo(() => others.length, [others]);
const visibleOthers = useMemo(
() => others.slice(range[0], range[1]),
[others, range]
);
/**
* Store other ids as string to reduce amount of running useEffects below.
*/
const otherIds = useMemo(() => others.map((o) => o?.id), [others]);
const [camSubscriptions, setCamSubscriptions] = useState({
subscribedIds: [],
pausedIds: [],
});
useCamSubscriptions(
camSubscriptions?.subscribedIds,
camSubscriptions?.pausedIds
);
/**
* Determines subscribed and paused participant ids,
* based on rendered range, scroll position and viewport.
*/
const updateCamSubscriptions = useCallback(
(r) => {
const scrollEl = scrollRef.current;
const fixedRemote = fixed.filter((p) => !isLocalId(p.id));
if (!showParticipantsBar) {
setCamSubscriptions({
subscribedIds: [
currentSpeaker?.id,
pinnedId,
...fixedRemote.map((p) => p.id),
],
pausedIds: [],
});
return;
}
if (!scrollEl) return;
/**
* Make sure we don't accidentally end up with a negative buffer,
* in case maxCamSubscriptions is lower than the amount of displayable
* participants.
*/
const buffer =
Math.max(0, maxCamSubscriptions - (r[1] - r[0]) - fixedRemote.length) /
2;
const min = Math.max(0, r[0] - buffer);
const max = Math.min(otherIds.length, r[1] + buffer);
const ids = otherIds.slice(min, max);
if (!ids.includes(currentSpeaker?.id) && !isLocalId(currentSpeaker?.id)) {
ids.push(currentSpeaker?.id);
}
// Calculate paused participant ids by determining their tile position
const subscribedIds = [...fixedRemote.map((p) => p.id), ...ids];
const pausedIds = otherIds.filter((id, i) => {
// ignore unrendered ids, they'll be unsubscribed instead
if (!ids.includes(id)) return false;
// ignore current speaker, it should never be paused
if (id === currentSpeaker?.id) return false;
const top = i * itemHeight;
const fixedHeight = fixed.length * itemHeight;
const visibleScrollHeight = scrollEl.clientHeight - fixedHeight;
const paused =
// bottom video edge above top viewport edge
top + itemHeight < scrollEl.scrollTop ||
// top video edge below bottom viewport edge
top > scrollEl.scrollTop + visibleScrollHeight;
return paused;
});
setCamSubscriptions({
subscribedIds,
pausedIds,
});
},
[
currentSpeaker?.id,
fixed,
itemHeight,
maxCamSubscriptions,
otherIds,
pinnedId,
showParticipantsBar,
]
);
/**
* Updates
* 1. the range of rendered others tiles
* 2. the spacing boxes before and after the visible tiles
*/
const updateVisibleRange = useCallback(
(st) => {
const visibleHeight = scrollRef.current.clientHeight - paddingTop;
const scrollBuffer = Math.min(
MAX_SCROLL_BUFFER,
(2 * visibleHeight) / itemHeight
);
const visibleItemCount = Math.ceil(
visibleHeight / itemHeight + scrollBuffer
);
let start = Math.floor(
Math.max(0, st - (scrollBuffer / 2) * itemHeight) / itemHeight
);
const end = Math.min(start + visibleItemCount, othersCount);
if (end - visibleItemCount < start) {
// Make sure we always render the same amount of tiles and buffered tiles
start = Math.max(0, end - visibleItemCount);
}
/**
* updateVisibleRange is called while scrolling for every frame!
* We're updating only one state to cause exactly one re-render.
* Heights for spacer elements are set directly on the DOM to stay fast.
*/
setRange([start, end]);
if (!spaceBefore.current || !spaceAfter.current) return [start, end];
spaceBefore.current.style.height = `${start * itemHeight}px`;
spaceAfter.current.style.height = `${(othersCount - end) * itemHeight}px`;
return [start, end];
},
[itemHeight, othersCount, paddingTop, spaceAfter, spaceBefore]
);
useResize(() => {
const scrollEl = scrollRef.current;
if (!showParticipantsBar || !scrollEl) return;
setIsSidebarScrollable(scrollEl?.scrollHeight > scrollEl?.clientHeight);
const r = updateVisibleRange(scrollEl.scrollTop);
updateCamSubscriptions(r);
}, [
scrollRef,
showParticipantsBar,
updateCamSubscriptions,
updateVisibleRange,
]);
/**
* Setup optimized scroll listener.
*/
useEffect(() => {
const scrollEl = scrollRef.current;
if (!scrollEl) return false;
let frame;
const handleScroll = () => {
scrollTop.current = scrollEl.scrollTop;
if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
if (!scrollEl) return;
const r = updateVisibleRange(scrollEl.scrollTop);
updateCamSubscriptions(r);
});
};
scrollEl.addEventListener('scroll', handleScroll);
return () => {
scrollEl.removeEventListener('scroll', handleScroll);
};
}, [scrollRef, updateCamSubscriptions, updateVisibleRange]);
/**
* Move out-of-view active speakers to position right after presenters.
*/
useEffect(() => {
const scrollEl = scrollRef.current;
// Ignore promoting, when no screens are being shared
// because the active participant will be shown in the SpeakerTile anyway
if (!hasScreenshares || !scrollEl) return false;
const maybePromoteActiveSpeaker = () => {
const fixedOther = fixed.find((f) => !f.isLocal);
// Ignore when speaker is already at first position or component unmounted
if (!fixedOther || fixedOther?.id === activeSpeakerId || !scrollEl)
return false;
// Active speaker not rendered at all, promote immediately
if (
visibleOthers.every((p) => p.id !== activeSpeakerId) &&
!isLocalId(activeSpeakerId)
) {
swapParticipantPosition(fixedOther.id, activeSpeakerId);
return false;
}
const activeTile = othersRef.current?.querySelector(
`[id="${activeSpeakerId}"]`
);
// Ignore when active speaker is not within "others"
if (!activeTile) return false;
// Ignore when active speaker is already pinned
if (activeSpeakerId === pinnedId) return false;
const { height: tileHeight } = activeTile.getBoundingClientRect();
const othersVisibleHeight =
scrollEl?.clientHeight - othersRef.current?.offsetTop;
const scrolledOffsetTop = activeTile.offsetTop - scrollEl?.scrollTop;
// Ignore when speaker is already visible (< 50% cut off)
if (
scrolledOffsetTop + tileHeight / 2 < othersVisibleHeight &&
scrolledOffsetTop > -tileHeight / 2
)
return false;
return swapParticipantPosition(fixedOther.id, activeSpeakerId);
};
maybePromoteActiveSpeaker();
const throttledHandler = debounce(maybePromoteActiveSpeaker, 100);
scrollEl.addEventListener('scroll', throttledHandler);
return () => {
scrollEl?.removeEventListener('scroll', throttledHandler);
};
}, [
activeSpeakerId,
fixed,
hasScreenshares,
pinnedId,
swapParticipantPosition,
visibleOthers,
]);
const otherTiles = useMemo(
() =>
visibleOthers.map((callItem) => (
<Tile
aspectRatio={aspectRatio}
key={callItem.id}
participant={callItem}
/>
)),
[aspectRatio, visibleOthers]
);
return (
<div
ref={scrollRef}
className={classnames('sidebar', {
blockScrolling,
scrollable: isSidebarScrollable,
scrollbarOutside: scrollbarWidth > 0,
})}
>
<div className="fixed">
{fixed.map((item, i) => {
// reduce setting up & tearing down tiles as much as possible
const key = i;
return (
<Tile
key={key}
aspectRatio={aspectRatio}
participant={item}
network={networkState}
/>
);
})}
</div>
{showParticipantsBar && (
<div ref={othersRef} className="participants">
<div ref={spaceBefore} style={{ width }} />
{otherTiles}
<div ref={spaceAfter} style={{ width }} />
</div>
)}
<style jsx>{`
.sidebar {
border-left: 1px solid var(--blue-dark);
flex: none;
margin-left: 1px;
overflow-x: hidden;
overflow-y: auto;
}
.sidebar.blockScrolling {
overflow-y: hidden;
}
.sidebar.blockScrolling.scrollbarOutside {
background-color: red;
padding-right: 12px;
}
.sidebar.scrollable:not(.scrollbarOutside) :global(.tile-actions) {
right: 20px;
}
.sidebar .fixed {
background: red;
position: sticky;
top: 0;
z-index: 3;
}
.sidebar .participants {
position: relative;
}
.sidebar :global(.tile) {
border-top: ${GAP}px solid var(--blue-dark);
width: ${width}px;
}
.sidebar .fixed :global(.tile:first-child) {
border: none;
}
`}</style>
</div>
);
};
ParticipantBar.propTypes = {
aspectRatio: PropTypes.number,
fixed: PropTypes.array.isRequired,
others: PropTypes.array.isRequired,
width: PropTypes.number,
};
export default ParticipantBar;

View File

@ -0,0 +1,2 @@
export { ParticipantBar as default } from './ParticipantBar';
export { ParticipantBar } from './ParticipantBar';

View File

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
/**
* Takes a ref to the scrolling element in ParticipantBar.
* Observes DOM changes and returns true, if a TileActions menu is opened.
* @returns boolean
*/
export const useBlockScrolling = (scrollRef) => {
const [blockScrolling, setBlockScrolling] = useState(false);
useEffect(() => {
const scrollEl = scrollRef.current;
if (!scrollEl || typeof MutationObserver === 'undefined') return false;
const observer = new MutationObserver((mutations) => {
if (!scrollEl) return;
mutations.forEach((m) => {
const { target } = m;
if (
m.attributeName === 'class' &&
target.classList.contains('tile-actions') &&
scrollEl.scrollHeight > scrollEl.clientHeight
) {
setBlockScrolling(target.classList.contains('showMenu'));
}
});
});
observer.observe(scrollEl, {
attributes: true,
subtree: true,
});
return () => observer.disconnect();
}, [scrollRef]);
return blockScrolling;
};
export default useBlockScrolling;

View File

@ -52,6 +52,10 @@ export const BasicTray = () => {
<IconPeople />
</TrayButton>
<TrayButton label="Fake" onClick={() => callObject.addFakeParticipant()}>
+
</TrayButton>
{customTrayComponent}
<span className="divider" />

View File

@ -31,6 +31,7 @@ export const CallProvider = ({
subscribeToTracksAutomatically = true,
}) => {
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
const [showLocalVideo, setShowLocalVideo] = useState(true);
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
const [enableRecording, setEnableRecording] = useState(null);
const [startCloudRecording, setStartCloudRecording] = useState(false);
@ -106,12 +107,14 @@ export const CallProvider = ({
addFakeParticipant,
preJoinNonAuthorized,
leave,
showLocalVideo,
roomExp,
videoQuality,
enableRecording,
setVideoQuality,
setBandwidth,
setRedirectOnLeave,
setShowLocalVideo,
startCloudRecording,
subscribeToTracksAutomatically,
}}

View File

@ -37,7 +37,7 @@ const SUBSCRIBE_ALL_VIDEO_THRESHOLD = 9;
const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => {
const { callObject } = useCallState();
const { callObject, subscribeToTracksAutomatically } = useCallState();
const { participants } = useParticipants();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
@ -52,37 +52,42 @@ export const TracksProvider = ({ children }) => {
[participants]
);
const pauseVideoTrack = useCallback((id) => {
/**
* Ignore undefined, local or screenshare.
*/
if (
!id ||
isLocalId(id) ||
isScreenId(id) ||
rtcpeers.getCurrentType() !== 'sfu'
) {
return;
}
const pauseVideoTrack = useCallback(
(id) => {
/**
* Ignore undefined, local or screenshare.
*/
if (
!id ||
subscribeToTracksAutomatically ||
isLocalId(id) ||
isScreenId(id) ||
rtcpeers.getCurrentType() !== 'sfu'
) {
return;
}
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
return;
}
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
return;
}
const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video');
if (!consumer) {
rtcpeers.soup.setResumeOnSubscribeForTrack(id, 'cam-video', false);
} else {
rtcpeers.soup.pauseConsumer(consumer);
}
}, []);
rtcpeers.soup.pauseTrack(id, 'cam-video');
},
[subscribeToTracksAutomatically]
);
const resumeVideoTrack = useCallback(
(id) => {
/**
* Ignore undefined, local or screenshare.
*/
if (!id || isLocalId(id) || isScreenId(id)) return;
if (
!id ||
subscribeToTracksAutomatically ||
isLocalId(id) ||
isScreenId(id)
)
return;
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
const subscribe = () => {
@ -98,20 +103,14 @@ export const TracksProvider = ({ children }) => {
break;
case 'sfu': {
if (!rtcpeers.soup.implementationIsAcceptingCalls) return;
const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video');
if (!(consumer && consumer.appData)) {
rtcpeers.soup.setResumeOnSubscribeForTrack(id, 'cam-video', true);
subscribe();
} else {
rtcpeers.soup.resumeConsumer(consumer);
}
rtcpeers.soup.resumeTrack(id, 'cam-video');
break;
}
default:
break;
}
},
[callObject]
[callObject, subscribeToTracksAutomatically]
);
const remoteParticipantIds = useMemo(
@ -127,7 +126,7 @@ export const TracksProvider = ({ children }) => {
*/
const updateCamSubscriptions = useCallback(
(ids, pausedIds = []) => {
if (!callObject) return;
if (!callObject || subscribeToTracksAutomatically) return;
const subscribedIds =
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
? [...remoteParticipantIds]
@ -176,6 +175,7 @@ export const TracksProvider = ({ children }) => {
},
[
callObject,
subscribeToTracksAutomatically,
remoteParticipantIds,
recentSpeakerIds,
pauseVideoTrack,
@ -255,7 +255,10 @@ export const TracksProvider = ({ children }) => {
return { [id]: result };
}, {});
callObject.updateParticipants(updates);
if (!subscribeToTracksAutomatically) {
callObject.updateParticipants(updates);
}
}, 100);
callObject.on('track-started', handleTrackStarted);
@ -270,7 +273,7 @@ export const TracksProvider = ({ children }) => {
callObject.off('participant-joined', handleParticipantJoined);
callObject.off('participant-left', handleParticipantLeft);
};
}, [callObject, pauseVideoTrack]);
}, [callObject, subscribeToTracksAutomatically, pauseVideoTrack]);
return (
<TracksContext.Provider

View File

@ -20,9 +20,11 @@ export const UIStateProvider = ({
customTrayComponent,
children,
}) => {
const [pinnedId, setPinnedId] = useState(null);
const [preferredViewMode, setPreferredViewMode] = useState(VIEW_MODE_SPEAKER);
const [viewMode, setViewMode] = useState(preferredViewMode);
const [isShowingScreenshare, setIsShowingScreenshare] = useState(false);
const [showParticipantsBar, setShowParticipantsBar] = useState(true);
const [showAside, setShowAside] = useState();
const [activeModals, setActiveModals] = useState({});
const [customCapsule, setCustomCapsule] = useState();
@ -56,11 +58,12 @@ export const UIStateProvider = ({
}, []);
useEffect(() => {
if (isShowingScreenshare) {
if (pinnedId || isShowingScreenshare) {
setViewMode(VIEW_MODE_SPEAKER);
} else {
setViewMode(preferredViewMode);
}
setViewMode(preferredViewMode);
}, [isShowingScreenshare, preferredViewMode]);
}, [isShowingScreenshare, pinnedId, preferredViewMode]);
return (
<UIStateContext.Provider
@ -72,12 +75,16 @@ export const UIStateProvider = ({
openModal,
closeModal,
closeAside,
showParticipantsBar,
currentModals,
toggleAside,
pinnedId,
showAside,
setShowAside,
setIsShowingScreenshare,
setPreferredViewMode,
setPinnedId,
setShowParticipantsBar,
customCapsule,
setCustomCapsule,
}}

View File

@ -1,5 +1,3 @@
import equal from 'fast-deep-equal';
/**
* Call State
* ---
@ -17,10 +15,6 @@ export const initialCallState = {
fatalError: false,
};
function getId(participant) {
return participant.local ? 'local' : participant.user_id;
}
export function isLocal(id) {
return id === 'local';
}

View File

@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';
export const useScrollbarWidth = () => {
const [scrollbarWidth, setScrollbarWidth] = useState(0);
useEffect(() => {
// Create fake div to determine if scrollbar is rendered inside or outside
const div = document.createElement('div');
div.style.width = '100px';
div.style.height = '1px';
div.style.position = 'absolute';
div.style.left = '-9999em';
div.style.top = '-9999em';
div.style.overflow = 'auto';
div.setAttribute('aria-hidden', 'true');
const child = document.createElement('div');
child.textContent = 'This is a test div.';
div.appendChild(child);
document.body.appendChild(div);
const autoWidth = child.clientWidth;
div.style.overflow = 'hidden';
const hiddenWidth = child.clientWidth;
setScrollbarWidth(hiddenWidth - autoWidth);
div.remove();
}, []);
return scrollbarWidth;
};
export default useScrollbarWidth;