updates from ENG-2175
This commit is contained in:
parent
e01c70c33e
commit
f2bc235cc1
|
|
@ -12,6 +12,10 @@ Send messages to other participants using sendAppMessage
|
|||
|
||||
Broadcast call to a custom RTMP endpoint using a variety of difference layout modes
|
||||
|
||||
### [📃 Pagination](./pagination)
|
||||
|
||||
Demonstrates using manual track management to support larger call sizes
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import Room from '../Room';
|
|||
import { Asides } from './Asides';
|
||||
import { Modals } from './Modals';
|
||||
|
||||
export const App = () => {
|
||||
export const App = ({ customComponentForState }) => {
|
||||
const { state } = useCallState();
|
||||
|
||||
const componentForState = useCallUI({
|
||||
state,
|
||||
room: () => <Room />,
|
||||
...customComponentForState,
|
||||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
|
|
@ -41,6 +42,7 @@ export const App = () => {
|
|||
|
||||
App.propTypes = {
|
||||
asides: PropTypes.arrayOf(PropTypes.func),
|
||||
customComponentsForState: PropTypes.any,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
|||
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
||||
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { VideoGrid } from '../VideoGrid';
|
||||
import { Header } from './Header';
|
||||
|
||||
export const Room = () => {
|
||||
export const Room = ({ MainComponent = VideoGrid }) => {
|
||||
const { setShowModal, showModal } = useWaitingRoom();
|
||||
const { localParticipant } = useParticipants();
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ export const Room = () => {
|
|||
<Header />
|
||||
|
||||
<main>
|
||||
<VideoGrid />
|
||||
<MainComponent />
|
||||
</main>
|
||||
|
||||
{/* Show waiting room notification & modal if call owner */}
|
||||
|
|
@ -61,4 +62,8 @@ export const Room = () => {
|
|||
);
|
||||
};
|
||||
|
||||
Room.propTypes = {
|
||||
MainComponent: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Room;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ yarn
|
|||
yarn workspace @dailyjs/live-streaming dev
|
||||
```
|
||||
|
||||
Note that this example uses a env `MANUAL_TRACK_SUBS=1` which will disable [automatic track management](https://docs.daily.co/reference#%EF%B8%8F-setsubscribetotracksautomatically).
|
||||
|
||||
## How does this example work?
|
||||
|
||||
When call sizes exceed a certain volume (~12 or more particpants) it's important to start optimising for both bandwidth and CPU. Using manual track subscriptions allows each client to specify which participants they want to receive video and/or audio from, reducing how much data needs to be downloaded as well as the number of connections our servers maintain (subsequently supporting increased participant counts.)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@dailyjs/basic-call/components/App';
|
||||
import Room from '@dailyjs/basic-call/components/Room';
|
||||
|
||||
const Test = () => <div>Hello</div>;
|
||||
|
||||
/**
|
||||
* Rather than create an entirely new Room component we'll
|
||||
* pass use the one in basic call with a custom MainComponent
|
||||
*/
|
||||
export const AppWithPagination = () => (
|
||||
<App
|
||||
customComponentForState={{
|
||||
room: () => <Room MainComponent={Test} />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default AppWithPagination;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithPagination as default } from './App';
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
/* global rtcpeers */
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} 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 { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
|
||||
import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
|
||||
import sortByKey from '@dailyjs/shared/lib/sortByKey';
|
||||
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
const MIN_TILE_WIDTH = 280;
|
||||
const MAX_TILES_PER_PAGE = 12;
|
||||
|
||||
export const PaginatedVideoGrid = () => {
|
||||
const {
|
||||
activeParticipant,
|
||||
participantCount,
|
||||
participants,
|
||||
swapParticipantPosition,
|
||||
} = useParticipants();
|
||||
const activeSpeakerId = useActiveSpeaker();
|
||||
|
||||
const { maxCamSubscriptions, updateCamSubscriptions } = useTracks();
|
||||
|
||||
const displayableParticipantCount = useMemo(
|
||||
() => participantCount,
|
||||
[participantCount]
|
||||
);
|
||||
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [pages, setPages] = useState(1);
|
||||
const [maxTilesPerPage] = useState(MAX_TILES_PER_PAGE);
|
||||
|
||||
const gridRef = useRef(null);
|
||||
|
||||
// Update width and height of grid when window is resized
|
||||
useEffect(() => {
|
||||
let frame;
|
||||
const handleResize = () => {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
const width = gridRef.current?.clientWidth;
|
||||
const height = gridRef.current?.clientHeight;
|
||||
setDimensions({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
});
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [maxColumns, maxRows] = useMemo(() => {
|
||||
const { width, height } = dimensions;
|
||||
|
||||
const columns = Math.max(1, Math.floor(width / MIN_TILE_WIDTH));
|
||||
const widthPerTile = width / columns;
|
||||
const rows = Math.max(1, Math.floor(height / (widthPerTile * (9 / 16))));
|
||||
|
||||
return [columns, rows];
|
||||
}, [dimensions]);
|
||||
|
||||
const pageSize = useMemo(
|
||||
() => Math.min(maxColumns * maxRows, maxTilesPerPage),
|
||||
[maxColumns, maxRows, maxTilesPerPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPages(Math.ceil(displayableParticipantCount / pageSize));
|
||||
}, [pageSize, displayableParticipantCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page <= pages) return;
|
||||
setPage(pages);
|
||||
}, [page, pages]);
|
||||
|
||||
const [tileWidth, tileHeight] = useMemo(() => {
|
||||
const { width, height } = dimensions;
|
||||
const n = Math.min(pageSize, displayableParticipantCount);
|
||||
if (n === 0) return [width, height];
|
||||
const dims = [];
|
||||
for (let i = 1; i <= n; i + 1) {
|
||||
let maxWidthPerTile = (width - (i - 1)) / i;
|
||||
let maxHeightPerTile = maxWidthPerTile / DEFAULT_ASPECT_RATIO;
|
||||
const rows = Math.ceil(n / i);
|
||||
if (rows * maxHeightPerTile > height) {
|
||||
maxHeightPerTile = (height - (rows - 1)) / rows;
|
||||
maxWidthPerTile = maxHeightPerTile * DEFAULT_ASPECT_RATIO;
|
||||
dims.push([maxWidthPerTile, maxHeightPerTile]);
|
||||
} else {
|
||||
dims.push([maxWidthPerTile, maxHeightPerTile]);
|
||||
}
|
||||
}
|
||||
return dims.reduce(
|
||||
([rw, rh], [w, h]) => {
|
||||
if (w * h < rw * rh) return [rw, rh];
|
||||
return [w, h];
|
||||
},
|
||||
[0, 0]
|
||||
);
|
||||
}, [dimensions, pageSize, displayableParticipantCount]);
|
||||
|
||||
const visibleParticipants = useMemo(
|
||||
() =>
|
||||
participants.length - page * pageSize > 0
|
||||
? participants.slice((page - 1) * pageSize, page * pageSize)
|
||||
: participants.slice(-pageSize),
|
||||
[page, pageSize, participants]
|
||||
);
|
||||
|
||||
/**
|
||||
* Play / pause tracks based on pagination
|
||||
*/
|
||||
const camSubscriptions = useMemo(() => {
|
||||
const maxSubs = maxCamSubscriptions
|
||||
? // avoid subscribing to only a portion of a page
|
||||
Math.max(maxCamSubscriptions, pageSize)
|
||||
: // if no maximum is set, subscribe to adjacent pages
|
||||
3 * pageSize;
|
||||
|
||||
// Determine participant ids to subscribe to, based on page.
|
||||
let subscribedIds = [];
|
||||
switch (page) {
|
||||
// First page
|
||||
case 1:
|
||||
subscribedIds = participants
|
||||
.slice(0, Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
// Last page
|
||||
case Math.ceil(participants.length / pageSize):
|
||||
subscribedIds = participants
|
||||
.slice(-Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
// Any other page
|
||||
default:
|
||||
{
|
||||
const buffer = (maxSubs - pageSize) / 2;
|
||||
const min = (page - 1) * pageSize - buffer;
|
||||
const max = page * pageSize + buffer;
|
||||
subscribedIds = participants.slice(min, max).map((p) => p.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine subscribed, but invisible (= paused) video tracks.
|
||||
const invisibleSubscribedIds = subscribedIds.filter(
|
||||
(id) => id !== 'local' && !visibleParticipants.some((vp) => vp.id === id)
|
||||
);
|
||||
return {
|
||||
subscribedIds: subscribedIds.filter((id) => id !== 'local'),
|
||||
pausedIds: invisibleSubscribedIds,
|
||||
};
|
||||
}, [maxCamSubscriptions, page, pageSize, participants, visibleParticipants]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
updateCamSubscriptions(
|
||||
camSubscriptions?.subscribedIds,
|
||||
camSubscriptions?.pausedIds
|
||||
);
|
||||
}, 50);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [
|
||||
camSubscriptions?.subscribedIds,
|
||||
camSubscriptions?.pausedIds,
|
||||
updateCamSubscriptions,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Set bandwidth layer based on amount of visible participants
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (typeof rtcpeers === 'undefined' || rtcpeers?.getCurrentType() !== 'sfu')
|
||||
return;
|
||||
|
||||
const sfu = rtcpeers.soup;
|
||||
const count = visibleParticipants.length;
|
||||
|
||||
visibleParticipants.forEach(({ id }) => {
|
||||
if (count < 5) {
|
||||
// High quality video for calls with < 5 people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 2);
|
||||
} else if (count < 10) {
|
||||
// Medium quality video for calls with < 10 people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 1);
|
||||
} else {
|
||||
// Low quality video for calls with 10 or more people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 0);
|
||||
}
|
||||
});
|
||||
}, [visibleParticipants]);
|
||||
|
||||
/**
|
||||
* Handle position updates based on active speaker events
|
||||
*/
|
||||
const handleActiveSpeakerChange = useCallback(
|
||||
(peerId) => {
|
||||
if (!peerId) return;
|
||||
// active participant is already visible
|
||||
if (visibleParticipants.some(({ id }) => id === peerId)) return;
|
||||
// ignore repositioning when viewing page > 1
|
||||
if (page > 1) return;
|
||||
|
||||
/**
|
||||
* We can now assume that
|
||||
* a) the user is looking at page 1
|
||||
* b) the most recent active participant is not visible on page 1
|
||||
* c) we'll have to promote the most recent participant's position to page 1
|
||||
*
|
||||
* To achieve that, we'll have to
|
||||
* - find the least recent active participant on page 1
|
||||
* - swap least & most recent active participant's position via setParticipantPosition
|
||||
*/
|
||||
const sortedVisibleRemoteParticipants = visibleParticipants
|
||||
.filter(({ isLocal }) => !isLocal)
|
||||
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'));
|
||||
|
||||
if (!sortedVisibleRemoteParticipants.length) return;
|
||||
|
||||
swapParticipantPosition(sortedVisibleRemoteParticipants[0].id, peerId);
|
||||
},
|
||||
[page, swapParticipantPosition, visibleParticipants]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (page > 1 || !activeSpeakerId) return;
|
||||
handleActiveSpeakerChange(activeSpeakerId);
|
||||
}, [activeSpeakerId, handleActiveSpeakerChange, page]);
|
||||
|
||||
const tiles = useDeepCompareMemo(
|
||||
() =>
|
||||
visibleParticipants.map((p) => (
|
||||
<Tile
|
||||
participant={p}
|
||||
mirrored
|
||||
style={{
|
||||
maxHeight: tileHeight,
|
||||
maxWidth: tileWidth,
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
[
|
||||
activeParticipant,
|
||||
participantCount,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
visibleParticipants,
|
||||
]
|
||||
);
|
||||
|
||||
const handlePrevClick = () => setPage((p) => p - 1);
|
||||
const handleNextClick = () => setPage((p) => p + 1);
|
||||
|
||||
return (
|
||||
<div ref={gridRef} className="grid">
|
||||
{pages > 1 && page > 1 && (
|
||||
<button type="button" onClick={handlePrevClick}>
|
||||
«
|
||||
</button>
|
||||
)}
|
||||
<div>{tiles}</div>
|
||||
{pages > 1 && page < pages && (
|
||||
<button type="button" onClick={handleNextClick}>
|
||||
»
|
||||
</button>
|
||||
)}
|
||||
<style jsx>{`
|
||||
.grid {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.tiles {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 1px;
|
||||
max-height: 100%;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginatedVideoGrid;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { PaginatedVideoGrid as default } from './PaginatedVideoGrid';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
DAILY_DOMAIN=
|
||||
DAILY_API_KEY=
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
DAILY_ROOM=
|
||||
MANUAL_TRACK_SUBS=1
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@dailyjs/basic-call/pages/_app';
|
||||
import AppWithPagination from '../components/App';
|
||||
|
||||
App.customAppComponent = <AppWithPagination />;
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
REMOVE_TRACKS,
|
||||
TRACK_STARTED,
|
||||
TRACK_STOPPED,
|
||||
UPDATE_SUBSCRIPTIONS,
|
||||
tracksReducer,
|
||||
} from './tracksState';
|
||||
|
||||
|
|
@ -54,9 +53,7 @@ export const TracksProvider = ({ children }) => {
|
|||
);
|
||||
|
||||
const pauseVideoTrack = useCallback((id) => {
|
||||
/**
|
||||
* Ignore undefined, local or screenshare.
|
||||
*/
|
||||
// Ignore undefined, local or screenshare
|
||||
if (!id || isLocalId(id) || isScreenId(id)) return;
|
||||
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
|
||||
return;
|
||||
|
|
@ -68,9 +65,7 @@ export const TracksProvider = ({ children }) => {
|
|||
|
||||
const resumeVideoTrack = useCallback(
|
||||
(id) => {
|
||||
/**
|
||||
* Ignore undefined, local or screenshare.
|
||||
*/
|
||||
// Ignore undefined, local or screenshare
|
||||
if (!id || isLocalId(id) || isScreenId(id)) return;
|
||||
|
||||
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
|
||||
|
|
@ -108,16 +103,34 @@ export const TracksProvider = ({ children }) => {
|
|||
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
||||
? [...remoteParticipantIds]
|
||||
: [...ids, ...recentSpeakerIds];
|
||||
|
||||
const updates = remoteParticipantIds.reduce((u, id) => {
|
||||
const shouldSubscribe = subscribedIds.includes(id);
|
||||
const shouldPause = pausedIds.includes(id);
|
||||
const isSubscribed =
|
||||
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
||||
|
||||
// Set resume state for newly subscribed tracks
|
||||
if (shouldSubscribe) {
|
||||
rtcpeers.soup.setResumeOnSubscribeForTrack(
|
||||
id,
|
||||
'cam-video',
|
||||
!pausedIds.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
// Pause already subscribed tracks
|
||||
if (shouldSubscribe && shouldPause) {
|
||||
pauseVideoTrack(id);
|
||||
}
|
||||
|
||||
if (
|
||||
isLocalId(id) ||
|
||||
isScreenId(id) ||
|
||||
(shouldSubscribe && isSubscribed)
|
||||
)
|
||||
return u;
|
||||
|
||||
const result = {
|
||||
setSubscribedTracks: {
|
||||
audio: true,
|
||||
|
|
@ -129,19 +142,7 @@ export const TracksProvider = ({ children }) => {
|
|||
return { ...u, [id]: result };
|
||||
}, {});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_SUBSCRIPTIONS,
|
||||
subscriptions: {
|
||||
video: subscribedIds.reduce((v, id) => {
|
||||
const result = {
|
||||
id,
|
||||
paused: pausedIds.includes(id) || !ids.includes(id),
|
||||
};
|
||||
return { ...v, ...result };
|
||||
}, {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Fast resume already subscribed videos
|
||||
ids
|
||||
.filter((id) => !pausedIds.includes(id))
|
||||
.forEach((id) => {
|
||||
|
|
@ -153,23 +154,24 @@ export const TracksProvider = ({ children }) => {
|
|||
|
||||
callObject.updateParticipants(updates);
|
||||
},
|
||||
[callObject, remoteParticipantIds, recentSpeakerIds, resumeVideoTrack]
|
||||
[
|
||||
callObject,
|
||||
remoteParticipantIds,
|
||||
recentSpeakerIds,
|
||||
pauseVideoTrack,
|
||||
resumeVideoTrack,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
if (!callObject) return false;
|
||||
|
||||
const trackStoppedQueue = [];
|
||||
|
||||
const handleTrackStarted = ({ participant, track }) => {
|
||||
if (state.subscriptions.video?.[participant.session_id]?.paused) {
|
||||
pauseVideoTrack(participant.session_id);
|
||||
}
|
||||
/**
|
||||
* If track for participant was recently stopped, remove it from queue,
|
||||
* so we don't run into a stale state.
|
||||
* so we don't run into a stale state
|
||||
*/
|
||||
const stoppingIdx = trackStoppedQueue.findIndex(
|
||||
([p, t]) =>
|
||||
|
|
@ -186,9 +188,7 @@ export const TracksProvider = ({ children }) => {
|
|||
};
|
||||
|
||||
const trackStoppedBatchInterval = setInterval(() => {
|
||||
if (!trackStoppedQueue.length) {
|
||||
return;
|
||||
}
|
||||
if (!trackStoppedQueue.length) return;
|
||||
dispatch({
|
||||
type: TRACK_STOPPED,
|
||||
items: trackStoppedQueue.splice(0, trackStoppedQueue.length),
|
||||
|
|
@ -248,25 +248,17 @@ export const TracksProvider = ({ children }) => {
|
|||
callObject.off('participant-joined', handleParticipantJoined);
|
||||
callObject.off('participant-left', handleParticipantLeft);
|
||||
};
|
||||
}, [callObject, pauseVideoTrack, state.subscriptions.video]);
|
||||
|
||||
useEffect(() => {
|
||||
Object.values(state.subscriptions.video).forEach(({ id, paused }) => {
|
||||
if (paused) {
|
||||
pauseVideoTrack(id);
|
||||
}
|
||||
});
|
||||
}, [pauseVideoTrack, state.subscriptions.video]);
|
||||
}, [callObject, pauseVideoTrack]);
|
||||
|
||||
return (
|
||||
<TracksContext.Provider
|
||||
value={{
|
||||
audioTracks: state.audioTracks,
|
||||
videoTracks: state.videoTracks,
|
||||
pauseVideoTrack,
|
||||
resumeVideoTrack,
|
||||
remoteParticipantIds,
|
||||
updateCamSubscriptions,
|
||||
videoTracks: state.videoTracks,
|
||||
remoteParticipantIds,
|
||||
recentSpeakerIds,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ import { getId, getScreenId } from './participantsState';
|
|||
const initialTracksState = {
|
||||
audioTracks: {},
|
||||
videoTracks: {},
|
||||
subscriptions: {
|
||||
video: {},
|
||||
},
|
||||
};
|
||||
|
||||
// --- Actions ---
|
||||
|
|
@ -21,7 +18,6 @@ const initialTracksState = {
|
|||
const TRACK_STARTED = 'TRACK_STARTED';
|
||||
const TRACK_STOPPED = 'TRACK_STOPPED';
|
||||
const REMOVE_TRACKS = 'REMOVE_TRACKS';
|
||||
const UPDATE_SUBSCRIPTIONS = 'UPDATE_SUBSCRIPTIONS';
|
||||
|
||||
// --- Reducer and helpers --
|
||||
|
||||
|
|
@ -65,11 +61,11 @@ function tracksReducer(prevState, action) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
case TRACK_STOPPED: {
|
||||
const { audioTracks, subscriptions, videoTracks } = prevState;
|
||||
const { audioTracks, videoTracks } = prevState;
|
||||
|
||||
const newAudioTracks = { ...audioTracks };
|
||||
const newSubscriptions = { ...subscriptions };
|
||||
const newVideoTracks = { ...videoTracks };
|
||||
|
||||
action.items.forEach(([participant, track]) => {
|
||||
|
|
@ -94,13 +90,12 @@ function tracksReducer(prevState, action) {
|
|||
|
||||
return {
|
||||
audioTracks: newAudioTracks,
|
||||
subscriptions: newSubscriptions,
|
||||
videoTracks: newVideoTracks,
|
||||
};
|
||||
}
|
||||
|
||||
case REMOVE_TRACKS: {
|
||||
const { audioTracks, subscriptions, videoTracks } = prevState;
|
||||
const { audioTracks, videoTracks } = prevState;
|
||||
const id = getId(action.participant);
|
||||
const screenId = getScreenId(id);
|
||||
|
||||
|
|
@ -111,17 +106,10 @@ function tracksReducer(prevState, action) {
|
|||
|
||||
return {
|
||||
audioTracks,
|
||||
subscriptions,
|
||||
videoTracks,
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_SUBSCRIPTIONS:
|
||||
return {
|
||||
...prevState,
|
||||
subscriptions: action.subscriptions,
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
|
@ -133,5 +121,4 @@ export {
|
|||
REMOVE_TRACKS,
|
||||
TRACK_STARTED,
|
||||
TRACK_STOPPED,
|
||||
UPDATE_SUBSCRIPTIONS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { useCallState } from '../contexts/CallProvider';
|
||||
import { useParticipants } from '../contexts/ParticipantsProvider';
|
||||
|
||||
/**
|
||||
* Convenience hook to contain all logic on determining the active speaker
|
||||
* (= the current one and only actively speaking person)
|
||||
*/
|
||||
export const useActiveSpeaker = () => {
|
||||
const { showLocalVideo } = useCallState();
|
||||
const { activeParticipant, localParticipant, participantCount } =
|
||||
useParticipants();
|
||||
|
||||
// we don't show active speaker indicators EVER in a 1:1 call or when the user is alone in-call
|
||||
if (participantCount <= 2) return null;
|
||||
|
||||
if (!activeParticipant?.isMicMuted) {
|
||||
return activeParticipant?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the local video is displayed and the last known active speaker
|
||||
* is muted, we can only fall back to the local participant.
|
||||
*/
|
||||
return localParticipant?.isMicMuted || !showLocalVideo
|
||||
? null
|
||||
: localParticipant?.id;
|
||||
};
|
||||
|
||||
export default useActiveSpeaker;
|
||||
Loading…
Reference in New Issue