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
|
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
|
## Getting started
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ import Room from '../Room';
|
||||||
import { Asides } from './Asides';
|
import { Asides } from './Asides';
|
||||||
import { Modals } from './Modals';
|
import { Modals } from './Modals';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = ({ customComponentForState }) => {
|
||||||
const { state } = useCallState();
|
const { state } = useCallState();
|
||||||
|
|
||||||
const componentForState = useCallUI({
|
const componentForState = useCallUI({
|
||||||
state,
|
state,
|
||||||
room: () => <Room />,
|
room: () => <Room />,
|
||||||
|
...customComponentForState,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize children to avoid unnecassary renders from HOC
|
// Memoize children to avoid unnecassary renders from HOC
|
||||||
|
|
@ -41,6 +42,7 @@ export const App = () => {
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
asides: PropTypes.arrayOf(PropTypes.func),
|
asides: PropTypes.arrayOf(PropTypes.func),
|
||||||
|
customComponentsForState: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||||
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
||||||
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
|
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { VideoGrid } from '../VideoGrid';
|
import { VideoGrid } from '../VideoGrid';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
||||||
export const Room = () => {
|
export const Room = ({ MainComponent = VideoGrid }) => {
|
||||||
const { setShowModal, showModal } = useWaitingRoom();
|
const { setShowModal, showModal } = useWaitingRoom();
|
||||||
const { localParticipant } = useParticipants();
|
const { localParticipant } = useParticipants();
|
||||||
|
|
||||||
|
|
@ -23,7 +24,7 @@ export const Room = () => {
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<VideoGrid />
|
<MainComponent />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Show waiting room notification & modal if call owner */}
|
{/* Show waiting room notification & modal if call owner */}
|
||||||
|
|
@ -61,4 +62,8 @@ export const Room = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Room.propTypes = {
|
||||||
|
MainComponent: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
export default Room;
|
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
|
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?
|
## 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.)
|
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 App from '@dailyjs/basic-call/pages/_app';
|
||||||
|
import AppWithPagination from '../components/App';
|
||||||
|
|
||||||
|
App.customAppComponent = <AppWithPagination />;
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
REMOVE_TRACKS,
|
REMOVE_TRACKS,
|
||||||
TRACK_STARTED,
|
TRACK_STARTED,
|
||||||
TRACK_STOPPED,
|
TRACK_STOPPED,
|
||||||
UPDATE_SUBSCRIPTIONS,
|
|
||||||
tracksReducer,
|
tracksReducer,
|
||||||
} from './tracksState';
|
} from './tracksState';
|
||||||
|
|
||||||
|
|
@ -54,9 +53,7 @@ export const TracksProvider = ({ children }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const pauseVideoTrack = useCallback((id) => {
|
const pauseVideoTrack = useCallback((id) => {
|
||||||
/**
|
// Ignore undefined, local or screenshare
|
||||||
* Ignore undefined, local or screenshare.
|
|
||||||
*/
|
|
||||||
if (!id || isLocalId(id) || isScreenId(id)) return;
|
if (!id || isLocalId(id) || isScreenId(id)) return;
|
||||||
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
|
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -68,9 +65,7 @@ export const TracksProvider = ({ children }) => {
|
||||||
|
|
||||||
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 || isLocalId(id) || isScreenId(id)) return;
|
||||||
|
|
||||||
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
|
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
|
||||||
|
|
@ -108,16 +103,34 @@ export const TracksProvider = ({ children }) => {
|
||||||
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
||||||
? [...remoteParticipantIds]
|
? [...remoteParticipantIds]
|
||||||
: [...ids, ...recentSpeakerIds];
|
: [...ids, ...recentSpeakerIds];
|
||||||
|
|
||||||
const updates = remoteParticipantIds.reduce((u, id) => {
|
const updates = remoteParticipantIds.reduce((u, id) => {
|
||||||
const shouldSubscribe = subscribedIds.includes(id);
|
const shouldSubscribe = subscribedIds.includes(id);
|
||||||
|
const shouldPause = pausedIds.includes(id);
|
||||||
const isSubscribed =
|
const isSubscribed =
|
||||||
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
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 (
|
if (
|
||||||
isLocalId(id) ||
|
isLocalId(id) ||
|
||||||
isScreenId(id) ||
|
isScreenId(id) ||
|
||||||
(shouldSubscribe && isSubscribed)
|
(shouldSubscribe && isSubscribed)
|
||||||
)
|
)
|
||||||
return u;
|
return u;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
setSubscribedTracks: {
|
setSubscribedTracks: {
|
||||||
audio: true,
|
audio: true,
|
||||||
|
|
@ -129,19 +142,7 @@ export const TracksProvider = ({ children }) => {
|
||||||
return { ...u, [id]: result };
|
return { ...u, [id]: result };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
dispatch({
|
// Fast resume already subscribed videos
|
||||||
type: UPDATE_SUBSCRIPTIONS,
|
|
||||||
subscriptions: {
|
|
||||||
video: subscribedIds.reduce((v, id) => {
|
|
||||||
const result = {
|
|
||||||
id,
|
|
||||||
paused: pausedIds.includes(id) || !ids.includes(id),
|
|
||||||
};
|
|
||||||
return { ...v, ...result };
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
ids
|
ids
|
||||||
.filter((id) => !pausedIds.includes(id))
|
.filter((id) => !pausedIds.includes(id))
|
||||||
.forEach((id) => {
|
.forEach((id) => {
|
||||||
|
|
@ -153,23 +154,24 @@ export const TracksProvider = ({ children }) => {
|
||||||
|
|
||||||
callObject.updateParticipants(updates);
|
callObject.updateParticipants(updates);
|
||||||
},
|
},
|
||||||
[callObject, remoteParticipantIds, recentSpeakerIds, resumeVideoTrack]
|
[
|
||||||
|
callObject,
|
||||||
|
remoteParticipantIds,
|
||||||
|
recentSpeakerIds,
|
||||||
|
pauseVideoTrack,
|
||||||
|
resumeVideoTrack,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!callObject) {
|
if (!callObject) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackStoppedQueue = [];
|
const trackStoppedQueue = [];
|
||||||
|
|
||||||
const handleTrackStarted = ({ participant, track }) => {
|
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,
|
* 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(
|
const stoppingIdx = trackStoppedQueue.findIndex(
|
||||||
([p, t]) =>
|
([p, t]) =>
|
||||||
|
|
@ -186,9 +188,7 @@ export const TracksProvider = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackStoppedBatchInterval = setInterval(() => {
|
const trackStoppedBatchInterval = setInterval(() => {
|
||||||
if (!trackStoppedQueue.length) {
|
if (!trackStoppedQueue.length) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TRACK_STOPPED,
|
type: TRACK_STOPPED,
|
||||||
items: trackStoppedQueue.splice(0, trackStoppedQueue.length),
|
items: trackStoppedQueue.splice(0, trackStoppedQueue.length),
|
||||||
|
|
@ -248,25 +248,17 @@ 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, state.subscriptions.video]);
|
}, [callObject, pauseVideoTrack]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Object.values(state.subscriptions.video).forEach(({ id, paused }) => {
|
|
||||||
if (paused) {
|
|
||||||
pauseVideoTrack(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [pauseVideoTrack, state.subscriptions.video]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TracksContext.Provider
|
<TracksContext.Provider
|
||||||
value={{
|
value={{
|
||||||
audioTracks: state.audioTracks,
|
audioTracks: state.audioTracks,
|
||||||
|
videoTracks: state.videoTracks,
|
||||||
pauseVideoTrack,
|
pauseVideoTrack,
|
||||||
resumeVideoTrack,
|
resumeVideoTrack,
|
||||||
remoteParticipantIds,
|
|
||||||
updateCamSubscriptions,
|
updateCamSubscriptions,
|
||||||
videoTracks: state.videoTracks,
|
remoteParticipantIds,
|
||||||
recentSpeakerIds,
|
recentSpeakerIds,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ import { getId, getScreenId } from './participantsState';
|
||||||
const initialTracksState = {
|
const initialTracksState = {
|
||||||
audioTracks: {},
|
audioTracks: {},
|
||||||
videoTracks: {},
|
videoTracks: {},
|
||||||
subscriptions: {
|
|
||||||
video: {},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
@ -21,7 +18,6 @@ const initialTracksState = {
|
||||||
const TRACK_STARTED = 'TRACK_STARTED';
|
const TRACK_STARTED = 'TRACK_STARTED';
|
||||||
const TRACK_STOPPED = 'TRACK_STOPPED';
|
const TRACK_STOPPED = 'TRACK_STOPPED';
|
||||||
const REMOVE_TRACKS = 'REMOVE_TRACKS';
|
const REMOVE_TRACKS = 'REMOVE_TRACKS';
|
||||||
const UPDATE_SUBSCRIPTIONS = 'UPDATE_SUBSCRIPTIONS';
|
|
||||||
|
|
||||||
// --- Reducer and helpers --
|
// --- Reducer and helpers --
|
||||||
|
|
||||||
|
|
@ -65,11 +61,11 @@ function tracksReducer(prevState, action) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case TRACK_STOPPED: {
|
case TRACK_STOPPED: {
|
||||||
const { audioTracks, subscriptions, videoTracks } = prevState;
|
const { audioTracks, videoTracks } = prevState;
|
||||||
|
|
||||||
const newAudioTracks = { ...audioTracks };
|
const newAudioTracks = { ...audioTracks };
|
||||||
const newSubscriptions = { ...subscriptions };
|
|
||||||
const newVideoTracks = { ...videoTracks };
|
const newVideoTracks = { ...videoTracks };
|
||||||
|
|
||||||
action.items.forEach(([participant, track]) => {
|
action.items.forEach(([participant, track]) => {
|
||||||
|
|
@ -94,13 +90,12 @@ function tracksReducer(prevState, action) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audioTracks: newAudioTracks,
|
audioTracks: newAudioTracks,
|
||||||
subscriptions: newSubscriptions,
|
|
||||||
videoTracks: newVideoTracks,
|
videoTracks: newVideoTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case REMOVE_TRACKS: {
|
case REMOVE_TRACKS: {
|
||||||
const { audioTracks, subscriptions, videoTracks } = prevState;
|
const { audioTracks, videoTracks } = prevState;
|
||||||
const id = getId(action.participant);
|
const id = getId(action.participant);
|
||||||
const screenId = getScreenId(id);
|
const screenId = getScreenId(id);
|
||||||
|
|
||||||
|
|
@ -111,17 +106,10 @@ function tracksReducer(prevState, action) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audioTracks,
|
audioTracks,
|
||||||
subscriptions,
|
|
||||||
videoTracks,
|
videoTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case UPDATE_SUBSCRIPTIONS:
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
subscriptions: action.subscriptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
@ -133,5 +121,4 @@ export {
|
||||||
REMOVE_TRACKS,
|
REMOVE_TRACKS,
|
||||||
TRACK_STARTED,
|
TRACK_STARTED,
|
||||||
TRACK_STOPPED,
|
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