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 React from 'react';
import { RoomContainer } from '@dailyjs/basic-call/components/Room';
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
import { SpeakerView } from '../SpeakerView'; import { SpeakerView } from '../SpeakerView';
export const Room = () => ( export const Room = () => <SpeakerView />;
<RoomContainer>
<VideoContainer>
<SpeakerView />
</VideoContainer>
</RoomContainer>
);
export default Room; 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 { 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'; import SpeakerTile from './SpeakerTile';
const SIDEBAR_WIDTH = 186;
export const SpeakerView = () => { 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 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 ( return (
<div ref={activeRef} className="active"> <div className="speaker-view">
<SpeakerTile participant={currentSpeaker} screenRef={activeRef} /> <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>{` <style jsx>{`
.speaker-view {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.active { .active {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -9,13 +9,11 @@ import { Button } from '@dailyjs/shared/components/Button';
import Tile from '@dailyjs/shared/components/Tile'; import Tile from '@dailyjs/shared/components/Tile';
import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants'; import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker'; import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions'; import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions';
import usePreferredLayer from '@dailyjs/shared/hooks/usePreferredLayer'; import usePreferredLayer from '@dailyjs/shared/hooks/usePreferredLayer';
import { ReactComponent as IconArrow } from '@dailyjs/shared/icons/raquo-md.svg'; import { ReactComponent as IconArrow } from '@dailyjs/shared/icons/raquo-md.svg';
import sortByKey from '@dailyjs/shared/lib/sortByKey'; import sortByKey from '@dailyjs/shared/lib/sortByKey';
import { debounce } from 'debounce';
import { useDeepCompareMemo } from 'use-deep-compare'; import { useDeepCompareMemo } from 'use-deep-compare';
// --- Constants // --- Constants

View File

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

View File

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

View File

@ -290,6 +290,24 @@ export const Button = forwardRef(
opacity: 0.35; 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 { .button.outline {
background: transparent; background: transparent;
border: 1px solid rgba(255, 255, 255, 0.15); 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 /> <IconPeople />
</TrayButton> </TrayButton>
<TrayButton label="Fake" onClick={() => callObject.addFakeParticipant()}>
+
</TrayButton>
{customTrayComponent} {customTrayComponent}
<span className="divider" /> <span className="divider" />

View File

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

View File

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

View File

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

View File

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